最近在讀一本進階的JavaScript的書《你不知道的JavaScript(上卷)》,這次研究了一下“this”。
當一個函數被調用時,會創建一個活動記錄(執行上下文)。
這個記錄會包含函數在哪里被調用(調用棧)、函數的調用方法、傳入的參數等信息。
this就是記錄的其中一個屬性,會在函數執行的過程中用到。
this既不指向函數自身也不指向函數的作用域。
this實際上是在函數被調用時發生的綁定,它指向什么完全取決於函數在哪里被調用。
一、調用位置
調用位置就在當前正在執行的函數的前一個調用中,源碼查看。
function baz() { // 當前調用棧是:baz // 因此,當前調用位置是全局作用域 console.log("baz"); bar(); // <-- bar 的調用位置 } function bar() { // 當前調用棧是 baz -> bar // 因此,當前調用位置在 baz 中 console.log("bar"); foo(); // <-- foo 的調用位置 } function foo() { // 當前調用棧是 baz -> bar -> foo // 因此,當前調用位置在 bar 中 console.log("foo"); } baz(); // <-- baz 的調用位置
二、綁定規則
你必須找到調用位置,然后判斷需要應用下面四條規則中的哪一條。
1)默認綁定
最常用的函數調用類型:獨立函數調用。可以把這條規則看作是無法應用其他規則時的默認規則。
function foo() { console.log(this.a); } var a = 2; foo(); // 2
2)隱式綁定
隱式綁定的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; obj.foo(); // 2
但有時候會出現隱式丟失。
function foo() { console.log(this.a); } var obj = { a: 2, foo: foo }; var bar = obj.foo; // 函數 var a = "oops, global"; // bar(); // "oops, global"
雖然bar是obj.foo的一個引用,但是實際上,它引用的是foo函數本身。
因此此時的bar()其實是一個不帶任何修飾的函數調用,應用了默認綁定。
3)顯式綁定
使用函數的call(..)和apply(..)方法。
function foo() { console.log(this.a); } var obj = { a: 2 }; foo.call(obj); // 2
在很多庫中經常能看到bind方法,這是一種硬綁定,一種顯式的強制綁定,下面是一種bind實現。
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
4)new綁定
使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操作:
1. 創建(或者說構造)一個全新的對象。
2. 這個新對象會被執行[[原型]]連接。
3. 這個新對象會綁定到函數調用的this。
4. 如果函數沒有返回其他對象,那么new表達式中的函數調用會自動返回這個新對象。
function foo(a) { this.a = a; } var bar = new foo(2); console.log(bar.a); // 2
三、優先級
默認綁定的優先級是四條規則中最低的。
1)顯式綁定優先級比隱式綁定要更高
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
2)new綁定比隱式綁定優先級高
function foo(something) { this.a = something; } var obj1 = { foo: foo }; obj1.foo(2); console.log(obj1.a); // 2 var bar = new obj1.foo(4); console.log(obj1.a); // 2 console.log(bar.a); // 4
3)new綁定會修改顯示綁定中this
function bind(fn, obj) { return function() { return fn.apply(obj, arguments); }; } function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind(obj1); //bar被硬綁定到obj1上 bar(2); console.log( obj1.a ); // 2 var baz = new bar(3); // console.log( obj1.a ); // 沒有把obj1.a 修改為3,還是為2 console.log( baz.a ); // 3
4)判斷this
1. 函數是否在new中調用(new綁定)?如果是的話this綁定的是新創建的對象。
var bar = new foo()
2. 函數是否通過call、apply(顯式綁定)或者硬綁定調用?如果是的話,this綁定的是指定的對象。
var bar = foo.call(obj2)
3. 函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this綁定的是那個上下文對象。
var bar = obj1.foo()
4. 如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象。
var bar = foo()
四、綁定例外
1)被忽略的this
如果你把null或者undefined作為this的綁定對象傳入call、apply或者bind。
這些值在調用時會被忽略,實際應用的是默認綁定規則。
function foo() { console.log(this.a); } var a = 2; foo.call(null); // 2
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的返回值是目標函數的引用,因此調用位置是foo()而不是p.foo()或者o.foo()。
3)軟綁定
如果可以給默認綁定指定一個全局對象和undefined以外的值。
那就可以實現和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改this的能力。
可以通過一種被稱為軟綁定的方法來實現我們想要的效果。
Function.prototype.softBind = function(obj) { var fn = this; // 捕獲所有 curried 參數 var curried = [].slice.call(arguments, 1); var bound = function() { return fn.apply((!this || this === (window || global)) ? obj : this, curried.concat.apply(curried, arguments)); }; bound.prototype = Object.create(fn.prototype); return bound; };
function foo() { console.log("name: " + this.name); } var obj = { name: "obj" }, obj2 = { name: "obj2" }, obj3 = { name: "obj3" }; var fooOBJ = foo.softBind( obj ); fooOBJ(); // name: obj <---- 應用了軟綁定 obj2.foo = foo.softBind(obj); obj2.foo(); // name: obj2 fooOBJ.call( obj3 ); // name: obj3 setTimeout( obj2.foo, 10 );// name: obj <---- 應用了軟綁定
軟綁定版本的foo()可以手動將this綁定到obj2或者obj3上。
但如果應用默認綁定,則會將this綁定到obj。