Joey LIU | NANTSOU


這邊記錄一些我覺得身為一位Full Stack工程師應該知道的一些關於javascript的知識與技巧。

這是我個人主觀的心得與想法,僅供參考。

閉包 Closure

作用域

要了解閉包,必須先知道作用域(Scope)。簡言之就是一個變數可以使用的範圍。ES6之前,只有function可以建立作用域和global的作用域。 javascript的作用域是語法作用域(Lexical Scope)或稱靜態作用域(Static Scope)。 也就是說作用域在「宣告」時就確定了不會因為被「執行」的狀況而改變。

1
2
3
4
5
6
7
8
9
var a = 1
function fn() {
  console.log(a); // fn被宣告時,a是global作用域的a。
}
function callFn() {
  var a = 2;
  fn(); // fn被執行時,作用域不會改變。所以callFn作用域的a不會影響到fn裡面的a。
}
callFn(); // 結果是1會被顯示。

閉包

回到閉包的主題,根據MDN web docs的描述:

閉包(Closure)是函式以及該函式被宣告時所在的作用域環境(lexical environment)的組合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

參考MDN web docs的例子以及閉包的描述:

myFuncdisplayNamemakeFunc 運行時所建立的實例(instance)參照。displayName 的實例保有了其作用域環境的參照,作用域裡則內含 name 變數。

實作陷阱:在迴圈建立閉包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function closureMaker() {
    var contents = [
        {id: 1, content: '1'},
        {id: 2, content: '2'},
        {id: 3, content: '3'}
    ];

    closures = [];

    for (var i = 0; i < closures.length; i++) {
        var content = contents[i];
        closures.push(() => content);
    }
    return closures;
}

closures = closureMaker();

closures.forEach(closure => console.log(closure()));

這個例子會建立三個閉包。預期的輸出應該是

1
2
3
{ id: 1, content: '1' }
{ id: 2, content: '2' }
{ id: 3, content: '3' }

但實際上卻是

1
2
3
{ id: 3, content: '3' }
{ id: 3, content: '3' }
{ id: 3, content: '3' }

原因1: 閉包是函式以及該函式被宣告時所在的作用域環境的組合。所以3個閉包共享相同的作用域環境即closureMaker

原因2: var的作用域是function且可以被重複宣吿。

解決方案:1、使用更多閉包來切割每個閉包的作用域環境。2、使用block作用域的let來宣告content

提升 Hoisting

1
2
console.log(abc); // undefined
var abc = 'abc';

上述代碼可以看成下列的代碼,所以輸出undefined

1
2
3
var abc;
console.log(abc); // undefined
abc = 'abc';

簡言之,var的變量提升就是將所有var的宣告挪到該作用域的最上端但卻在原本的地方賦值。

this

根據MDN web docsthis的描述:

通常,this 值由被呼叫的函式來決定。

就我的理解是this通常指向擁有「使用這個this」function的物件。 順帶一提,在嚴格模式下,this會是undefined。瀏覽器的話是window,node的話則是global。 下面的例子,obj.fn的物件是obj,當obj.fn被調用的時候,this就會指向obj所以輸出的結果是'obj'。 而擁有fn的物件就是global,所以直接調用fn的輸出就是'global'

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
global.name = 'global';

function fn () {
    return this.name;
}

var obj = {
    name: 'obj',
    fn
}
console.log(fn()); // global
console.log(obj.fn()); // obj

apply, call, bind

透過applycallbind可以達到將this與特定的物件綁定。不過需要注意的是applycall會直接調用函式, 而bind會返回一個將物件綁定的新函式可供調用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var name = 'global';

function fn () {
    return this.name;
}

var obj = {
    name: 'obj'
}

console.log(fn()); // global
console.log(fn.apply(obj)); // obj
console.log(fn.call(obj)); // obj
console.log(fn.bind(obj)()); // obj

箭頭函式(arrow function)

箭頭函式的內容其實可以獨立寫一個段落,不過根據MDN web docs的敘述:

它沒有自己的thisargumentssupernew.target 等語法。本函式運算式適用於非方法的函式,但不能被用作建構式(constructor)。

箭頭函式並不擁有自己的this變數;使用的this值來自封閉的文本上下文,也就是說,箭頭函式遵循常規變量查找規則。因此,如果在當前範圍中搜索不到this變量時,他們最終會尋找其封閉範圍。

在箭頭函式裡用到的this則在宣告函式時就已被確定,並不會被調用函示的地方而改變。 ES6之後,通常用箭頭函示來建立閉包以避免混淆this所指向的物件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var name = 'global';
var fn = () => this.name;

var obj = {
    name: 'obj',
    fn1() {
        return fn; // fn在global作用域被宣告,所以`this`已跟`global`綁定。
    },
    fn2() {
        return () => this.name;
    }
}
fn1 = obj.fn1();
fn2 = obj.fn2();
console.log(fn1()); // global
console.log(fn2()); // obj

不過需要注意的地方是,建立閉包的函式會因為調用的地方this會有所改變時,建立的箭頭函式的this也會有所改變。 因此調用建立閉包的函式時也要稍微留心。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var name = 'global';
function makeClosure () {
    return () => this.name;
}
var obj = {
    name: 'obj',
    makeClosure
}
fn1 = makeClosure();
fn2 = obj.makeClosure();
console.log(fn1()); // global
console.log(fn2()); // obj