最近在拜讀《你不知道的js》,而此篇是對於《你不知道的js》中this部分的筆記整理,希望能有效的梳理,並且鞏固關於this的知識點
一、呼叫位置
1、什麼呼叫位置?
呼叫位置就是函式在程式碼中被呼叫的位置(而不是宣告的位置)
2、如何尋找函式被呼叫的位置?
關鍵:分析呼叫棧,即為了到達當前執行位置所呼叫的所有函式。而我們關心的呼叫位置就在當前正在執行的函式的前一個呼叫中
先來看一段程式碼:
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的呼叫位置複製程式碼
我們可以把呼叫棧想象成一個函式呼叫鏈,但這種方法麻煩且易出錯。
但我們可以使用另一種方式:使用瀏覽器的除錯工具,設立斷點,或直接在程式碼中插入debugger。執行程式碼時,偵錯程式會在那個位置暫停,同時會展示當前位置的函式呼叫列表,這就是你的呼叫棧。真正的呼叫位置是棧中的第二個元素
二、繫結規則
1、預設繫結
最常用的函式呼叫型別是獨立函式呼叫。可把這規則看做是無法應用其他規則時的預設規則。
先看一段程式碼:
function foo() {
//當前呼叫棧是:baz // 因此,當前呼叫位置是全域性作用域 console.log(this.a);
}var a = 2;
foo();
// 2複製程式碼
從程式碼中發現this指向了全域性物件,而且函式呼叫時應用了this的預設繫結。
如何判斷是預設繫結?
可從分析呼叫位置來看看foo()是如何呼叫的。在程式碼中,foo()是直接使用不帶任何修飾的函式引用進行呼叫的,因此只能是預設繫結,無法應用其他規則
但如果是在嚴格模式下,又會有怎樣的結果呢?請看如下程式碼:
function foo() {
"use strict" console.log(this.a);
}var a = 2;
foo();
// TypeError:this is undefined複製程式碼
這段程式碼表示:雖然this的繫結規則完全取決於呼叫位置,但只有在非嚴格模式下,預設繫結才繫結全域性物件;在嚴格模式下則會繫結到undefined。
但是在嚴格模式下呼叫則不影響預設繫結:
function foo() {
console.log(this.a);
}var a = 2;
(function() {
"use strict" foo();
// 2
})();
複製程式碼
注意:通常來說不應該在程式碼中混合使用strict模式與非strict模式
2、隱式繫結
這條規則是指呼叫位置是否有上下文物件,或者是否被某個物件擁有或包含
先看以下程式碼:
function foo() {
console.log(this.a);
}var obj = {
a: 2, foo: foo
};
obj.foo();
// 2複製程式碼
該呼叫位置使用了obj上下文來引用函式,或者說函式被呼叫時obj物件“擁有”或“包含”它。
因此當函式引用有上下文物件時,隱式繫結規則會把函式呼叫中的this繫結到這個上下文物件
上述程式碼呼叫foo()時,this被繫結到obj,因此this指向了obj,this.a 與 obj.a 是一樣的。
另外物件屬性引用鏈中只有上一層或最後一層在呼叫位置中起作用。例如:
function foo() {
console.log(this.a);
}var obj2 = {
a: 42, foo: foo
};
var obj1 = {
a: 2, obj2: obj2
};
obj1.obj2.foo();
// 42複製程式碼
隱式丟失
被隱式繫結的函式會丟失繫結物件這是一個常見的this繫結問題,也就是說丟失後它會應用預設繫結,從而把this繫結到全域性物件或undefined上,取決於是否是嚴格模式。
例1:
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()是不帶任何修飾的函式呼叫,因此使用了預設繫結
例2:
function foo() {
console.log(this.a);
}function doFoo(fn) {
// fn其實引用的是foo fn();
// 呼叫位置
}var obj = {
a: 2, foo: foo
};
var a = "oops, global";
// a是全域性物件的屬性doFoo(obj.foo);
// "oops, global"複製程式碼
這裡使用了引數傳遞,也是隱式賦值,所以結果和例1一樣
例3:
function foo() {
console.log(this.a);
}var obj = {
a: 2, foo: foo
};
var a = "oops, global";
// a是全域性物件的屬性setTimeout(obj.foo, 100);
// oops, global複製程式碼
回撥函式丟失this繫結是常見的,呼叫回撥函式的函式可能會修改this
總結: 分析隱式繫結時,我們必須在一個物件內部包含一個指向函式的屬性,並通過這個屬性間接引用函式,從而把this間接(隱式)繫結到這個物件上
3、顯式繫結
方法:可以使用call或apply直接指定this的繫結物件
缺點:無法解決丟失繫結的問題
例:
function foo() {
console.log(this.a);
}var obj = {
a: 2
};
foo.call(obj);
// 2複製程式碼
如果你傳入了一個原始值作為this繫結物件,這個原始值會被轉換成它的物件形式(new xxx()),這叫裝箱
(1)、硬繫結
此為顯式繫結的一個變種,可以解決丟失繫結問題缺點:會大大降低函式的靈活性,使用繫結之後就無法使用隱式繫結或者顯式繫結來修改this
例:
function foo() {
console.log(this.a);
}var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
}bar();
// 2setTimeout(bar, 100);
// 2// 硬繫結的bar不可能再修改它的thisbar.call(window);
// 2複製程式碼
foo.call(obj)強制把this繫結到了obj,之後呼叫函式bar,它總會在obj上呼叫foo,這是顯式的強制繫結,叫做硬繫結
典型應用場景一:建立一個包裹函式,負責接收引數並返回值
function foo(something) {
console.log(this.a, something);
return this.a + something;
}var obj = {
a: 2
};
var bar = function() {
return foo.apply(obj, arguments);
}var b = bar(3);
// 2 3console.log(b);
// 5複製程式碼
典型應用場景二:建立一個可以重複使用的輔助函式
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 3console.log(b);
// 5複製程式碼
由於硬繫結是一種常用模式,所以ES5提供了內建方法Function.prototype.bind:
function foo(something) {
console.log(this.a, something);
return this.a + something;
}var obj = {
a: 2
};
var bar = foo.bind(obj);
var b = bar(3);
// 2 3console.log(b);
// 5複製程式碼
bind會返回一個硬編碼的新函式,會把你指定的引數位置為this的上下文並呼叫原始函式
(2)、API呼叫“上下文”
通過 call() 或 apply() 實現
4、new繫結
使用new來呼叫函式,或者說發生建構函式呼叫時,會自動執行下面操作a、建立一個全新物件b、新物件會被執行[[Prototype]]連結c、新物件被繫結到函式呼叫的thisd、如果函式沒有返回其他物件,則自動返回新物件程式碼:
var obj = {
};
obj.__proto__ = Base.prototype;
var result = Base.call(obj);
return typeof result === 'obj' ? result : obj;
複製程式碼
三、優先順序
1、隱式繫結與顯式繫結
function foo() {
console.log(this.a);
}var obj1 = {
a: 2, foo: foo
};
var obj2 = {
a: 3, foo: foo
};
obj1.foo();
// 2obj2.foo();
// 3obj1.foo.call(obj2);
// 3obj2.foo.call(obj1);
// 2複製程式碼
顯然:顯式繫結 >
隱式繫結
2、new繫結與隱式繫結
function foo(something) {
this.a = something;
}var obj1 = {
foo: foo
};
var obj2 = {
};
obj1.foo(2);
console.log(obj1.a);
// 2obj1.foo.call(obj2, 3);
// 3console.log(obj2.a);
// 3var bar = new obj1.foo(4);
console.log(obj1.a);
// 2console.log(bar.a);
// 4複製程式碼
new繫結 >
隱式繫結
3、new繫結與顯式繫結
new和call/apply無法一起使用,因此無法通過new foo.call(obj1) 來直接測試,但我們可以使用硬繫結來測試
function foo(something) {
this.a = something;
}var obj1 = {
};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);
// 2var baz = new bar(3);
console.log(obj1.a);
// 2console.log(bar.a);
// 3複製程式碼
這裡bar被硬繫結在了obj1上,但new bar(3)並沒有把obj1.a修改為3。相反,new修改了硬繫結(到obj1的)呼叫bar()中的this。因為使用了new繫結,我們得到了一個名為baz的新物件,並且baz.a的值為3new繫結 >
硬繫結(顯式繫結)
4、判斷this
(1)、由new呼叫? 繫結到新建立的物件(new繫結)
var bar = new foo();
複製程式碼
(2)、由call或apply或bind呼叫?繫結到指定物件(顯式繫結)
var bar = foo.call(obj2);
複製程式碼
(3)、由上下文物件呼叫?繫結到那個上下文物件(隱式繫結)
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複製程式碼
function foo(a, b) {
console.log("a:"+ a + ", b:" + b);
}foo.apply(null, [2, 3]);
// a:2, b:3var bar = foo.bind(null, 2);
bar(3);
// a:2, b:3複製程式碼
總是用null來忽略this繫結可能會產生一些副作用。如果某個函式使用了this(如第三方庫中的一個函式),那預設繫結規則會把this繫結到全域性物件(瀏覽器中為window),這會導致不可預計的後果(如修改全域性物件),或者導致更多難以分析和追蹤的bug
更安全的this
一種更安全的做法是傳入一個特殊物件,把this繫結到這個物件不會對你的程式產生任何副作用。
可建立一個”DMZ”非軍事區物件,一個空的非委託的物件,任何對於this的使用都會被限制在這個空物件中,不會對全域性物件產生任何影響
function foo(a, b) {
console.log("a:"+ a + ", b:" + b);
}// 我們的DMZ空物件var __null = Object.create(null);
foo.apply(__null, [2, 3]);
// a:2, b:3var bar = foo.bind(__null, 2);
bar(3);
// a:2, b:3複製程式碼
2、間接引用
間接引用的情況下,呼叫這個函式會應用預設繫結規則,並且最容易在賦值時發生:
function foo(a, b) {
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(),這裡會使用預設繫結
對於預設繫結來說,決定this繫結物件的並不是呼叫位置是否處於嚴格模式,而是函式體是否處於嚴格模式
3、軟繫結
給預設繫結指定一個全域性物件和undefined以外的值,可實現和硬繫結相同的效果,同時保留隱式繫結或顯式繫結修改this的能力
if(!Function.prototype.softBind) {
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(a, b) {
console.log("name: " + this.name);
}var obj = {
name: 'obj'
}, obj2 = {
name: 'obj2'
}, obj3 = {
name: 'obj3'
};
var fooOBJ = foo.softBind(obj);
fooOBJ();
// name: objobj2.foo = softBind(obj);
obj2.foo();
// name: obj2fooOBJ.call(obj3);
// name: obj3setTimeout(obj2.foo, 100);
// name: obj 使用了軟繫結複製程式碼
從上述程式碼中可以看到軟繫結版本的foo()可以手動將this繫結到obj2或obj3上,但如果應用預設繫結,則會將this繫結到obj
五、箭頭函式
箭頭函式不使用this的四種標準規則,而是根據外層(函式或全域性)作用域來決定this
function foo() {
// 返回一個箭頭函式 return (a) =>
{
// this繼承自foo();
console.log(this.a);
};
}var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2);
// 2, 不是3複製程式碼
foo()內部建立的箭頭函式會捕獲呼叫時foo()的this。由於foo()的this繫結到obj1,bar(引用箭頭函式)的this也會繫結到obj1,箭頭函式的繫結無法被修改(new也不行)
function foo() {
var self = this;
setTimeout(function(){
console.log(self.a);
}, 100);
}var obj = {
a: 2
};
foo.call(obj);
// 2複製程式碼
self=this與箭頭函式都可以取代bind,但本質上是替代了this機制
經常編寫this風格程式碼,但絕大部分時候會使用self=this或箭頭函式來否定this機制,應當注意以下兩點:
a、只是用詞法作用域並完全拋棄錯誤this風格的程式碼
b、完全採用this風格,在必要時使用bind(),儘量避免使用self=this和箭頭函式
兩種風格混用通常會使程式碼更難維護,並且可能也會更難編寫