讀《你不知道的JavaScript》
讀《你不知道的 JavaScript》
Part 1 作用域與閉包
詞法作用域
定義在詞法階段的作用域。換句話說,詞法作用域就是由你在寫程式碼時將變數和塊作用域寫在哪裡來決定的。
-
詞法作用域只由函式宣告所在之處決定
-
只會查詢一級識別符號,二級三級...都不會找下去
函式表示式&函式宣告
-
(function(){...}()) 這是函式表示式,這個等同於 (function foo(){...})()
立即執行函式表示式
-
function 關鍵字如果是宣告中的第一個詞,那麼就是一個函式宣告,否則就是一個函式表示式
塊作用域
- try/catch catch 語句中有塊作用域
提升
-
變數和函式在內的所有宣告都任何程式碼被在執行前首先被處理(先有聲明後有賦值)
-
函式宣告比變數先被提升(重複的 var 宣告會被忽略,重複的函式宣告會覆蓋原來的)
閉包
-
當函式可記住並訪問所在的詞法作用域時,就產生了閉包,即使函式是在當前詞法作用域之外執行的
-
模組有兩個主要特徵:
(1)為建立內部作用域而呼叫了一個包裝函式
(2)包裝函式的返回值必須至少包括一個對內部函式的引用
這樣子就建立了涵蓋整個包裝函式內部作用域的閉包
Part 2 this
- 具名函式可以使用名稱識別符號來引用自身; 而匿名函式沒有名稱識別符號,無法從函式內部引用自身
function foo(num) { console.log("foo: " + num); this.count++; } foo.count = 0; var i; for (i = 0; i < 10; i++) { if (i > 5) { foo.call(foo, i); } } console.log(foo.count);
使用 call(...)可以確保 this 指向函式物件 foo 本身
- this 在任何情況下都不指向函式的詞法作用域。this 實際上是在函式呼叫時發生的繫結。
this 既不指向函式自身也不指向函式的詞法作用域,它的指向完全取決於函式在哪被呼叫(誰呼叫就指向誰)。
預設繫結
可以當作是無法應用其他規則時的預設規則。
獨立函式呼叫
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2 (嚴格模式下, 是undefined)
細節: 雖然 this 的繫結規則完全取決於呼叫位置,但是隻有 foo()執行在非 strict mode 下時, this 預設繫結才能繫結到全域性物件;嚴格模式下,與 foo()的呼叫位置無關
隱式繫結
- 當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的 this 繫結到這個上下文物件。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
obj.foo();
- 隱式丟失
顯式繫結
- 可以通過 call(...) 和 apply(...) 方法直接指定 this 的繫結物件。
call(...) 和 apply(...) 的第一個引數是一個物件,會把這個物件繫結到 this , 接著在呼叫函式時指定這個 this
如果傳入的是一個原始值(字串型別、布林型別或者數字型別)來當作 this 的繫結物件,這個原始值會被轉換成其物件形式(也就是 new String(...)、new Boolean(...)、new Number(...))。這通常稱為“裝箱”。
call(...) 和 apply(...) 的區別在於第二個引數:apply(...) 接受陣列形式的引數,call(...) 接受的是引數列表
- 顯示的強制繫結,稱為硬繫結
ES5 提供了內建的方法 Function.prototype.bind , 用法如下:
function foo(value) {
console.log(this.a, value); // 2 3
return this.a + value;
}
var obj = {
a: 2,
};
var bar = foo.bind(obj);
var b = bar(3);
console.log(b); // 5
bind(...) 會返回一個硬編碼的新函式,它會把引數設定為 this 的上下文並呼叫原始函式
new 繫結
- 使用 new 來呼叫函式,或者說發生建構函式呼叫時,會自動執行下面操作:
- 1.建立(或者說構造)一個全新的物件
- 2.這個新物件會被執行原型連線
- 3.這個新物件會繫結到函式呼叫的 this
- 4.如果函式沒有返回其他物件,那麼 new 表示式中的函式呼叫會自動返回這個新物件
優先順序
顯示繫結 > 隱式繫結
new 繫結 > 隱式繫結
- 在 new 中使用硬繫結函式:主要目的是預先設定函式的一些引數,這樣子在使用 new 進行初始化時就可以只傳其餘的引數。
bind(...) 的功能之一就是可以把除了第一個引數(第一個引數用於繫結 this)之外的其他引數都傳給下層的函式(這種技術稱為“部分應用”,是“柯里化”的一種)。
function foo(p1, p2) {
this.val = p1 + p2;
}
// 使用 null 是因為我們不關心 this 綁定了誰
var bar = foo.bind(null, "p1");
// 使用 new 時, this 會被修改了
var baz = new bar("p2");
console.log(baz.val); // p1p2
判斷 this
可以按以下順序判斷:
- 1.函式是否在 new 中呼叫( new 繫結)?YES- this 繫結的是新建立的物件
var bar = new foo();
- 2.函式是否通過 call 、apply(顯式繫結)或者硬繫結(bind)呼叫?YES- this 繫結的是指定的物件
var bar = foo.call(obj2);
- 3.函式是否在某個上下文物件中呼叫(隱式繫結)?YES- this 繫結的是那個上下文物件
var bar = obj1.foo();
- 4.以上都不是的話,使用預設繫結。嚴格模式下,就繫結到 undefined, 否則繫結到全域性物件。
var bar = foo();
繫結例外
- 如果說,把 null 或者 undefined 作為 this 的繫結物件傳入 call、apply 或者 bind ,這些值在呼叫時會被忽略,實際應用的還是預設繫結。
- 常見的做法是使用 apply(...) 來展開一個數組,並作為引數傳入一個函式中:
function foo(a, b) {
console.log(a, b);
}
// 把陣列“展開”成引數
foo.apply(null, [2, 3]); // 2 3
- bind(...) 可以對引數進行柯里化(預先設定一些引數):
function foo(a, b) {
console.log(a, b);
}
// 使用 bind(...) 進行柯里化
var bar = foo.bind(null, 2);
bar(3); // 2 3
箭頭函式
-
箭頭函式不使用 this 的四種標準規則,而是根據外層(函式或者全域性)作用域來決定 this
-
箭頭函式的繫結無法修改( new 也不行!)
物件
- 簡單基本型別(string number boolean undefined null)本身不是物件
typeof null 會返回 'object', 但實際上 null 是基本型別
-
函式就是物件的一個子型別(可呼叫的物件)
-
訪問屬性時,引擎實際上會呼叫內部預設的[[Get]]操作(在設定屬性值時是[[Put]]),[[Get]] 操作會檢查物件本身是否包含這個屬性,如果沒找到的話還會查詢 [[Prototype]]鏈
內建物件
String Number Boolean Object Function Array Date RegExp Error
- 實際上,這只是一些內建函式,可以當作建構函式來使用,從而構造一個對應子型別的新物件
var strObject = new String("I am a string");
-
null 和 undefined 沒有對應的構造形式,只要文字(宣告)形式。 Date 只要構造形式,沒有文字形式。
-
Object Array Function RegExp 無論是使用文字形式還是構造形式,他們都是物件,不是字面量。
複製物件
- Object.assign() 淺拷貝,是使用 = 操作符來賦值
存在性
-
in 操作符會檢查屬性是否在物件及其 [[Prototype]] 原型鏈中, hasOwnProperty(..) 只會檢查屬性是否在 myObject 物件中,不會檢查 [[Prototype]] 鏈。
-
propertyIsEnumerable(..) 會檢查給定的屬性名是否直接存在於物件中(而不是在原型鏈上)並且滿足 enumerable:true
-
Object.getOwnPropertyNames(..) 會返回一個數組,包含所有屬性,無論它們是否可列舉
遍歷
-
for...in 遍歷的是物件中的所有可列舉屬性,不是屬性值
-
可以使用 ES6 的 for..of 語法來遍歷資料結構(陣列、物件,等等)中的值,for..of 會尋找內建或者自定義的 @@iterator 物件並呼叫它的 next() 方法來遍歷資料值。
類
- 類意味著複製???
顯式混入
- JS 中的函式無法真正的複製,只能複製對共享函式物件的引用(函式就是物件)
原型
當試圖引用物件的屬性時會觸發[[Get]]操作。對於預設的[[Get]]操作來說,第一步是檢查物件本身是否有這個屬性,有的話就使用它;沒有的話就需要使用物件的[[Prototype]]鏈了。
-
[[Prototype]] 對於其他物件的引用,幾乎所有的物件在建立時[[Prototype]]屬性會被賦予一個非空的值
-
Object.create(..) 它會建立一個物件並把這個物件的 [[Prototype]] 關聯到指定的物件,有個缺點是:需要建立一個新物件然後把就物件拋棄掉,不能直接修改已有的預設物件。第二個引數指定了需要新增到新物件中的屬性名以及這些屬性的屬性描述符(enumerable writable configurable 等等)
Object.create(null) 會建立一個擁有空 [[prototype]] 連結的物件,這個物件無法進行委託。由於這個物件沒有原型鏈,所以 instanceof 操作符無法進行判斷,因此總會返回 false
-
所有普通的[[Prototype]]鏈最終都會指向內建的 Object.prototype
-
實際上,物件的.constructor 會預設指向一個函式,這個函式可以通過物件的.prototype 引用。constructor 並不表示被構造
-
Object.setPrototypeOf(...) 可以用標準且可靠的方法來修改關聯
// ES6之前需要拋棄預設的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);
// ES6開始可以直接修改現有的Bar.prototype
Bar.setPrototypeOf(Bar, prototype, Foo.prototype);
屬性設定和遮蔽
obj.foo = 2;
分析:
-
如果 foo 不是直接存在於 obj 中,[[Prototype]] 鏈就會被遍歷,類似 [[Get]] 操作。如果原型鏈上找不到 foo,foo 就會被直接新增到 obj 上。
-
如果說,foo 既出現在 obj 中,也出現在 myObject 的 [[Prototype]] 鏈上層,那麼就會發生遮蔽:obj 中包含的 foo 屬性會遮蔽原型鏈上層的所有 foo 屬性(因為 obj.foo 總會選擇原型鏈中最底層的 foo 屬性)
-
遮蔽屬性???
(原型)繼承
-
內省/反射:檢查一個例項(JavaScript 中的物件)的繼承祖先(JavaScript 中的委託關聯)
-
判斷[[Prototype]]反射的方法:isPrototypeOf(...)
Foo.prototype.isPrototypeOf(a); // true(在 a 的整條 [[Prototype]] 鏈中是否出現過 Foo.prototype?)
b.prototype.isPrototypeOf(c); // 判斷 b 是否出現在 c 的 [[Prototype]] 中
- 獲取一個物件的 [[Prototype]] 鏈:
// 在 ES5 中,標準方法:
Object.getPrototypeOf(a);
// 絕大多數瀏覽器也支援一種非標準的方法來訪問內部 [[Prototype]] 屬性:
a.__proto__; // .__proto__ 實際上並不存在與你正在使用的物件中(.constructor 也是一樣的),實際上是存在於 Object.prototype 中
- instanceof 用於檢測建構函式的 prototype 屬性是否出現在某個例項物件的原型鏈上 ???
物件關聯
- Object.create()的 polyfill 程式碼:
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
- 關聯兩個物件最常用的方法是使用 new 關鍵詞進行函式呼叫,在呼叫的 4 個步驟中會建立一個關聯其他物件的新物件。使用 new 呼叫函式時會把新物件的 .prototype 屬性關聯到“其他物件”
行為委託
-
[[prototype]] 機制就是指物件中的一個內部連結引用另一個物件
-
在委託行為中,我們會盡量避免在[[Prototype]]鏈的不同級別中使用相同的命名
-
委託行為意味著某些物件在找不到屬性或者方法引用時,會把這個請求委託給另一個物件
-
在 API 介面的設計中,委託最好在內部實現,不要直接暴露出去
互相委託(禁止)
- 無法在兩個或兩個以上互相(雙向)委託的物件之間建立迴圈委託。如果把 B 關聯到 A 然後試著吧 A 關聯到 B 就會出錯