js中this的綁定規則及優先級
一. this綁定規則
函數調用位置決定了this的綁定對象,必須找到正確的調用位置判斷需要應用下面四條規則中的哪一條。
1.1 默認綁定
看下面代碼:
function foo() { console.log(this.a); } var a = 1; foo(); // 2
調用foo的時候,this應用了默認綁定,this指向了全局對象,但是在嚴格模式下,那麽全局對象將無法進行默認綁定,因此this會綁定到undefined
function foo() { ‘use strict‘; console.log(this.a); } var a = 1; foo();// TypeRrror: this is undefined
嚴格模式下與 foo() 的調用位置無關:
function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); // 2 })();
1.2 隱式綁定
另一條需要考慮的規則是調用位置是否有上下文對象
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
但是無論是直接在 obj 中定義還是先定義再添加為引用屬性, 這個函數嚴格來說都不屬於 obj 對象,然而, 調用位置會使用 obj 上下文來引用函數, 因此你可以說函數被調用時 obj 對象“ 擁有” 或者“ 包含” 它
對象屬性引用鏈中只有最頂層或者說最後一層會影響調用位置。 舉例來說:
function foo() { console.log(this.a); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42
1.2.1 隱式丟失
一個最常見的 this 綁定問題就是被隱式綁定的函數會丟失綁定對象, 也就是說它會應用默認綁定, 從而把 this 綁定到全局對象或者 undefined 上, 取決於是否是嚴格模式,
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數別名! var a = "oops, global"; // a 是全局對象的屬性 bar(); // "oops, global"
雖然 bar 是 obj.foo 的一個引用, 但是實際上, 它引用的是 foo 函數本身, 因此此時的 bar() 其實是一個不帶任何修飾的函數調用, 因此應用了默認綁定。在js內置函數中如setTimeout也是如此:
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var a = "oops, global"; // a 是全局對象的屬性 setTimeout(obj.foo, 100); // "oops, global"
和下面偽代碼類似:
function setTimeout(fn, delay) { // 等待 delay 毫秒 fn(); // <-- 調用位置! }
1.3.顯示綁定
call(...),apply(...)可以指定this的綁定對象(前者接收多個參數如call(this, param1, param2, param3...),後者接受一個或兩個參數apply(this, [...]))
function foo() { console.log(this.a); } var obj = { a: 2 }; foo.call(obj); // 2
通過 foo.call(..), 我們可以在調用 foo 時強制把它的 this 綁定到 obj 上。 如果你傳入了一個原始值( 字符串類型、 布爾類型或者數字類型) 來當作 this 的綁定對 象, 這個原始值會被轉換成它的對象形式( 也就是 new String(..)、 new Boolean(..) 或者 new Number(..))。 這通常被稱為“ 裝箱”。
1.3.1 硬綁定
function foo() { console.log(this.a); } var obj = { a: 2 }; var bar = function() { foo.call(obj); }; bar(); // 2 setTimeout(bar, 100); // 2 // 硬綁定的 bar 不可能再修改它的 this bar.call(window); // 2
我們創建了函數 bar(), 並在它的內部手動調用 了 foo.call(obj), 因此強制把 foo 的 this 綁定到了 obj。 無論之後如何調用函數 bar, 它 總會手動在 obj 上調用 foo。 這種綁定是一種顯式的強制綁定, 因此我們稱之為硬綁定。創建一個 i可以重復使用的輔助函數:
function foo(something) { console.log(this.a, something); return this.a + something; } //簡單的輔助綁定函數 function bind(fn, obj) { return function() { return fn.apply(obj, arguments); }; } var obj = { a: 2 }; var bar = bind(foo, obj); var b = bar(3); // 2 3 console.log(b); // 5
由於硬綁定是一種非常常用的模式, 所以在 ES5 中提供了內置的方法 Function.prototype.bind, bind(..) 會返回一個硬編碼的新函數, 它會把參數設置為 this 的上下文並調用原始函數
1.3.2 API調用的“上下文”
function foo(el) { console.log(el, this.id); } var obj = { id: "awesome" }; // 調用 foo(..) 時把 this 綁定到 obj [1, 2, 3].forEach(foo, obj); // 1 awesome 2 awesome 3 awesome
這些函數實際上就是通過 call(..) 或者 apply(..) 實現了顯式綁定, 這樣你可以少些一些代碼。
1.4. new綁定
在 JavaScript 中, 構造函數只是一些 使用 new 操作符時被調用的函數。 它們並不會屬於某個類, 也不會實例化一個類。 實際上, 它們甚至都不能說是一種特殊的函數類型, 它們只是被 new 操作符調用的普通函數而已。使用 new 來調用函數, 或者說發生構造函數調用時, 會自動執行下面的操作:
1. 創建( 或者說構造) 一個全新的對象
2. 這個新對象會被執行 [[ 原型 ]] 連接
3. 這個新對象會綁定到函數調用的 this
4. 如果函數沒有返回其他對象, 那麽 new 表達式中的函數調用會自動返回這個新對象
二. 優先級
毫無疑問, 默認綁定的優先級是四條規則中最低的,來看看隱式綁定和顯示綁定
function foo() { console.log(this.a); } var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call(obj2); // 3 obj2.foo.call(obj1); // 2
顯式綁定優先級更高, new 綁定和隱式綁定的優先級誰高誰低:
function foo(something) { this.a = something; } var obj1 = { foo: foo }; var obj2 = {}; obj1.foo(2); console.log(obj1.a); // 2 obj1.foo.call(obj2, 3); console.log(obj2.a); // 3 var bar = new obj1.foo(4); console.log(obj1.a); // 2 console.log(bar.a); // 4
可以看到 new 綁定比隱式綁定優先級高。 但是 new 綁定和顯式綁定誰的優先級更高呢?
function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind( obj1 ); bar( 2 ); console.log( obj1.a ); // 2 var baz = new bar(3); console.log( obj1.a ); // 2 console.log( baz.a ); // 3
bar 被硬綁定到 obj1 上, 但是 new bar(3) 並沒有像我們預計的那樣把 obj1.a 修改為 3。 相反,new 修改了硬綁定( 到 obj1 的) 調用 bar(..) 中的 this。 因為使用了 new 綁定, 我們得到了一個名字為 baz 的新對象, 並且 baz.a 的值是 3。之所以要在 new 中使用硬綁定函數, 主要目的是預先設置函數的一些參數, 這樣在使用 new 進行初始化時就可以只傳入其余的參數。 bind(..) 的功能之一就是可以把除了第一個 參數( 第一個參數用於綁定 this) 之外的其他參數都傳給下層的函數( 這種技術稱為“ 部 分應用”, 是“ 柯裏化” 的一種)。 舉例來說:
function foo(p1, p2) { this.val = p1 + p2; } //之所以使用 null 是因為在本例中我們並不關心硬綁定的 this 是什麽 // 反正使用 new 時 this 會被修改 var bar = foo.bind(null, "p1"); var baz = new bar("p2"); baz.val; // p1p2
js中this的綁定規則及優先級