這邊記錄一些我覺得身為一位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的例子以及閉包的描述:
myFunc
是 displayName
在 makeFunc
運行時所建立的實例(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 < contents.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 docs對this
的描述:
通常,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
透過apply
、call
和bind
可以達到將this
與特定的物件綁定。不過需要注意的是apply
和call
會直接調用函式,
而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的敘述:
它沒有自己的this
、arguments
、super
、new.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
|