Javascript 最難理解的源頭,始於 function

First Class Functions

https://pjchender.blogspot.tw/2016/03/javascriptfunctionobjects.html First Class Functions(一級函式)的概念,是指其他類型(Objects, String, Boolean, Numbers)做的事, Function都能做,包括將Function指定成一個變數等等 (assign them to variables, pass them around, create them on the fly)。

  • 函式只是物件的一種
  • 可以將 function 儲存成變數
  • 可以將 function 當成參數代入另一個 function 中
  • 可以在一個 function 中回傳另一個 function
  • function 跟物件一樣有屬性(property)

function只是一種特殊的物件,它可以被當作物件來使用

所有函式都是物件中的"方法"

function 本身有幾個scope(作用域):

  1. 函式本身
  2. 外部函式內容(如果是函式中的函式,可存取外部函式內容)
  3. 全域

接下來介紹function時,會同時以scope、closure、變數、this 這幾個方向來探討

Execution Context

Execution Context (執行上下文),是一種結構,

所有 javascript 都是在某個上下文中被執行,並且包含三個東西:

  • Variable object(變數物件,簡稱VO): 集合執行上下文會用到的變數資料與函式定義。
  • Scope chain(作用域鏈,或作用域連鎖): 上層VO與自己的VO形成的作用域連鎖。
  • this值

函式的上下文,則是在呼叫時作為區分。 函式中,Activation object(啓動物件,簡稱AO) 可以用來建立 Scope chain

Scope chain(作用域鏈) 函式執行上下文時,AO裡面會有一個屬性,用來指向上一層(父母層)的AO or VO,這個鏈結會一直串到全域的VO上,形成scope chain。 函式執行時,尋找變數時會用Scope chain 尋找。

Context

函式被某物件呼叫執行時,所處的物件環境,就稱為Context

Context 通常稱為 this Context,

也就是函式執行時,會用 this 值來存取Context (呼叫函式的物件所處的環境)

也就是說,this 通常會指向呼叫函式的物件,如果物件不存在,就會指向 global object

Scope

Scope 通常稱為 Variable Scope(變數作用域),代表變數存取的範圍

Scope 決定了變數(變數、物件、函式)的可訪問性(可見性)

JavaScript 擁有 function scope(local scope): 所有的 function 都可以建立自己的 scpoe

在Javascript 可以分成兩種 scope 類型:

  • global scope 全域作用域
  • local scope 本地作用域

Global scope

在函式外部所宣告(declared)的變數,會具有Global,Global 擁有全域特性,讓所有 scripts 及 function 都能訪問

Local scope

呼叫一個函式時,函式內部會自己形成一個 local scope

<function scope>

在這個 scopoe 裡面宣告(declared)的 Local Variables 擁有本地作用域特性,僅能在function 內部被使用

且local scope 裡面包含了幾個東西

  • Closure
  • global scope

scope 內部第一層都會包含一個 Closure(封包)

宣告(declared)

使用 var 來宣告變數,如果沒有,則會變成全域變數

function myFunction(){
  // 沒有經過宣告的a,會自動被宣告成global variable
  a = 'this is a'
  //宣告(declared) b = 2
  var b = 'this is b'
}

myFunction();

console.log(a)
console.log(b)

當我們宣告變數時即啟動他的生命週期, Global 變數生命週期要等到關閉瀏覽器才會清除。 Local 變數生命週期會在function 執行完畢之後就清除。

Closure

由於 Closure-“閉包” 這個字詞有多層意義,你可以說它是一種技術,或是一種資料結構,或是這種有記憶環境值的函式。 Closure 能以 by reference 記住函式本身以及被建立時當下環境。

( Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure ‘remembers’ the environment in which it was created.)

因此,才可以在Closure改變被綁定變數值。

並且,在 Closure 裡可以存取到 Scope chain中所有變數,這也是一大賣點

Scope 決定了變數(變數、物件、函式)的可訪問性(可見性) 因此,在函式Scope裡的Closure所宣告的變數也擁有local scope特性,因此會被限制僅能在function 內部被使用

Closure 裡的 this

Closure 本身也是函式,所以裡面也包含了 this 資訊,但是不會去記憶、封閉住 this (因為this不再scope chain裡),這也是為什麼我們有時要另外透過變數來指定 this

this 會指向呼叫他的物件,如果不知道是誰呼叫他,就會直接指向最外層 windows 物件

使用 closure 模擬私有方法

每個閉包,都有各自的環境

例如,IIEF (立即呼叫函式表達式) IIEF 常用來建立 Module (模組), 因此,在IIEF所建立的模組內,就有類似物件的私有屬性、方法 我們常見jQuery Plugin或其他模組功能的程式設計,都可以見到IIEF

留意 closure 的效能問題

定義過多層的巢狀function,會造成搜尋的時間越長, 每當使用或呼叫函式時,scope chain都會一層層搜索直到找到該物件,越多層負擔越重

常見錯誤 : 在Closure 裡使用迴圈+延遲執行的function

迴圈 + 具有延遲執行的function 例如 setTimeout( callback_function, 時間)

如果再 closure 裡面使用這樣的方式,當 closure 內文執行完畢, 迴圈也跑完了,所有迴圈 i 都已經跳出迴圈執行,

而延遲執行的 function 卻還未執行,

當 callback function 終於準備執行時,所有 i 都已經變成迴圈執行完畢的最終i值

要避免這種情況,就要把這個延遲 callback 建立新的作用域環境

變數的傳遞 (值、物件、函式…)

function 內部與外部有形成一個 scope chain (作用域鏈) 可以透過 AO 來指向外部AO或VO,一直延續到Global VO, 因此,可以從 function 內部,透過 scope chain 取得外部的資源(By Reference)

如下圖所示:

所以,在 function 內部定義的變數,只有他自己可以使用

同時,function 可以取得外層的變數

Currying(柯里化)及 IIEF (立即呼叫函式表達式) 都是一個透過這樣的原則來實作。

this

  • 一般情況,this都代表著最外層 global 物件 (瀏覽器預設為window物件)
  • 函式裡面如果宣告 this,this 指向呼叫他的物件
  • 物件裡面的方法如果宣告 this,this 會代表物件本身
  • 物件的建構式(constructor) 裡宣告 this,當物件被 new 運算符宣告成新的物件,this就會代表新物件

函式中的 this 就會指向呼叫他的物件

當 this 不確定是誰呼叫他時,就會指向最外部的global物件

this 是函式呼叫執行時產生

call(呼叫) 與 bind(綁定)

在開始了解 call 跟 bind 之前,先了解他們呼叫的方式

call 可以提供this或參數值傳入原有函式,且立即呼叫
bind 則是建立一個新函式,在提供this或參數傳入這個新函式,但不會立即呼叫

另外,apply與call的原理一樣,差別在於後面的參數要用array的方式帶入

call(呼叫)

使用 call,會將第一個參數當作為 this,其次才是相關參數 並且在 call的當下會立即執行 以下範例: myFn call 所傳入的第一個參數,就會變成this 因此,就能在myFn使用this物件中的值 其他參數則傳入一般數值 這時再myFn就要定義相對應的變數來接這些參數

const myFn = function(x, y){
  console.log(this.a + this.b + x + y)
}

myFn.call({a:'a', b:'b'}, 'c', 'd') //abcd
bind(綁定)

bind 與 call 也很類似, 當函式綁定動作時,第一個參數也是會當作 this,其餘則可以做為對應變數 但是綁定完成後,並不會馬上執行,必須要等到呼叫才會執行 例如:

const myFn = function(x, y){
  console.log(this.a + this.b + x + y)
}

const myExc = myFn.bind({a:'a', b:'b'}, 'c', 'd')

myExc(); //abcd

有趣的是,bind 的參數可以部份套用(Partially applied) 只要依照順序,分批傳入參數也可以成功執行

const myFn = function(x, y){
  console.log(this.a + this.b + x + y)
}

const myExc = myFn.bind({a:'a ', b:'b '}, 'c ')

myExc(); //a b c undefined
myExc('d'); //a b c d

Arrows Function

  • 語法簡單。少打很多字元。
  • 可以讓程式碼的可閱讀性提高。
  • 某些情況下可以綁定this值。

https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/arrow_function.html ES6的箭頭函式(Arrows Function) 解決函式內函式的 this 問題

箭頭函式因為使用了 .bind(this)

箭頭函式會以定義當下的this值為this值

上面 arrows function 等同於下面:

function () {
	this
	function(){
		this
	}.bind(this)

}

舉個例子 這裡我們建立一個 function,用call 將一個值傳入function 當作為 this。 這時,內部再建立一個 IIEF ,IIEF 裡面的 this 不知道是誰在呼叫他,所以就會指向外部 windows 物件

function myfn(){
  console.log(this); //hello
  (function(){
    console.log(this) //undefined or winodws obj
  })()
}

obj = 'hello';

myfn.call(obj)

以前的解法,是透握 var self = this 來處理,讓IIEF能直接使用這個儲存this的變數

function myfn(){
	var self = this;
  console.log(this); //hello
  (function(){
    console.log(self) //hello
  }())
}

obj = 'hello';

myfn.call(obj)

Arror function就解決了這個問題,

function myfn(){
  console.log(this); //hello
  ()=>{
  	console.log(this) //hello
  }
}

obj = 'hello';

myfn.call(obj)

特別提醒! 可以及無法使用Arrows的情況

function 裡可以使用 Arrow function

但是,如果是物件直接呼叫或存取時候,就要留意:

  • 在物件裡的屬性,必須使用function,而不能使用 Arrows function
//作為物件屬性,可以使用function
{
	myfn: function(){ this = 物件 },
}
//不能用 Arrow
{
	myfn: ()=>{ this=undefined or windows object}
}
//使用 Arrow 等同於這樣
this=windows object
{
	myfn: function(){
  				this// windows object
        }.bind(this) //bind 會往上找這個this是誰,最後會找到最外層的this
}
  • 在物件的prototype屬性定義方法,必須使用 function
MyFunction.prototype.sayHello = function(){ }
  • addEventListener 裡面要用function
btn.addEventListener('click', function(){})

整理

所有函式都是物件中的"方法" 函式中的 this 會指向呼叫他的物件 Arrows 裡的 this 會參照當時環境中的 this