零、序言
本篇是《你不知道的 javascript(上)》讀書筆記。
v1 版本寫得比較亂,傳送門;
注意:如無特殊標註,本篇中的 this 指的是 es5 & 非嚴格模式下的 this。
一、總集
在 js 中, this 的值需要到函數的調用時才能明確,因此完全取決於函數的調用位置(執行 fn(...) 的時候)。
二、綁定規則
1. 默認綁定
這種情況最簡單最常用,函數作爲獨立函數被調用,函數體中的 this 指向 window。同時這條規則也是其他規則不適合時的默認規則。
demo:
function fn() { console.log(this.a); } var a = 'global'; fn(); // global
注意,如果在 fn 函數體中使用了嚴格模式,那麼 fn 中的 this 會被綁定成 undefinded。
function fn() { 'use strict'; console.log(this); // undefined }
2. 隱式綁定
“另一條需要考慮的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含,不過這種說法可能會造成一些誤導。”
這條規則說穿了也很簡單,就是當函數作爲對象的一個屬性,並被這個對象調用時,那麼函數體中的 this 會指向這個對象。
先看個正常的 demo:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
接着看一下會出問題的 demo:
function foo() { console.log( this.a ); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! var a = "oops, global"; // a 是全局對象的屬性 bar(); // "oops, global"
var bar = obj.foo這句,本質上是把函數 foo 的內存地址傳遞給變量 bar,並不是真正地執行了函數 foo,真正的函數執行時 bar() 這一句,因此,在正函數執行的時候,是以獨立函數的規則運行的,所以這裏實際上是走的默認綁定的規則。
因此,在所有的類似這種僅僅傳遞函數的內存地址而不是執行函數的場景,都要注意存在的隱式丟失的問題。同時列舉常見的需要注意的場景:
- setTimeout( obj.foo, 100 );
- function func( fn ) { fn() }; func( obj.foo );
3. 顯式綁定
相對於隱式綁定,顯式綁定時我們可以自主操控的,這就給了大牛們提供了黑操作的控件。也叫硬綁定。
js 中原生綁定的形式只有三個, call/apply/bind,其中 bind 是在 es5 中新加入的函數。對於這三個函數的用法和內部僞代碼,請移步 call,apply 和 bind 方法。
除了我們自己使用上面的函數進行顯式綁定,js 中的某些內置 api 也會實現顯式綁定,例如 forEach:
function fn(el) { console.log(el, this.id); } var obj = { id: 'awesome', } // 調用 [1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome
4. new 綁定
好消息是,這是最後一條綁定規則。
js 中 new 操作符的操作列在其下:
- 創建(或者說構造)一個全新的對象;
- 這個新對象會被執行 [[ 原型 ]] 鏈;
- 這個新對象會綁定到函數調用的 this;
- 如果函數沒有返回其他對象,那麼 new 表達式中的函數調用會自動返回這個新對象;
ps: new 操作符的僞代碼實現請移步 js new 與 return。
當然,真實的實現會更復雜,有興趣的可自信查找資料。
三、注意事項
1. 顯式綁定中, call/apply 綁定後會執行下函數, bind 綁定後會返回一個新的函數, 該函數內部的 this 會指向被綁定的對象。
2. 一般而言,顯式綁定(call/apply/bind)之後的 this 不能夠再次被修改或者綁定。(特殊情況見第 4 節)。
3. new 和 apply/call 不可同時使用。
4. new 和 bind 可以同時使用,bind 的源碼在實現的時候會在內部判斷有沒有與 new 一起使用,如果是的話就會使用新創建的 this 替換硬綁定的 this。
四、例外
規則總有例外。
1. 被忽略的 this
null(undefinded) 作爲一個特殊的 object,也是被允許使用在顯式綁定中的。顯式綁定在遇到這兩個特殊的對象的時候,這些值的調用會被忽略,實際應用的是默認的綁定規則。
function foo(a,b) { console.log( a, b, this ); } foo.apply( null, [2, 3] ); // 2 3 window var bar = foo.bind(null, 2); bar(3); // 2 3 window
當然,使用 null 來忽略 this 的綁定也具有一定的副作用。如果某個函數確實使用了 this (比如第三方庫中的一個函數), 那默認綁定規則會把 this 綁定到全局對象(window 等,當然,嚴格模式下會指向 undefined), 這見導致不可預見的後果(比如修改全局對象)。
因此我們可以使用如下的代碼來防止這個副作用。
function foo(a,b) { console.log(a, b, this); } // 我們自定義的空對象 var ø = Object.create( null ); foo.apply( ø, [2, 3] ); // 2, 3, {} var bar = foo.bind( ø, 2 ) bar(3); // 2, 3, {}
2.間接引用
function foo() { console.log( this.a ); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; o.foo(); // 3 (p.foo = o.foo)(); // 2
放上這段代碼的原因在於最後一句,一開始沒懂,後來看了下執行過程,最後一句代碼前半段是個賦值語句,整體等價於:
p.foo = o.foo; p.foo();
3. es 6 箭頭函數
箭頭函數不會創建自己的 this,箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或全局)作用域來決定 this。
另外, 箭頭函數不能作爲構造器,與 new 一起使用會拋出錯誤。
五、其他資料和鏈接
1.對阮一峯《ES6 入門》中箭頭函數 this 描述的探究;