this
是JavaScript
中比較複雜的機制之一,本篇文章希望可以帶大家瞭解this
相關的知識。本文內容來自書籍《你不知道的JavaScript(上卷)》,只是自己稍微整理一下。
☕️為什麼使用this
問題來了,既然this
比較複雜,我們為什麼還要使用呢?看一段程式碼:
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call( this );
console.log( greeting );
}
var me = {
name: "Kyle"
};
var you = {
name: "Reader"
};
identify.call( me ); // KYLE
identify.call( you ); // READER
speak.call( me ); // Hello, 我是 KYLE
speak.call( you ); // Hello, 我是 READER
複製程式碼
這段程式碼可以在不同的上下文物件(me
和you
)複用函式,並且程式碼中使用了this
,如果不使用this
程式碼會是這個樣子
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify( context );
console.log( greeting );
}
identify( you ); // READER
speak( me ); //hello, 我是 KYLE
複製程式碼
可以看出來,比起顯示地傳遞上下文物件,使用this
這種隱式的傳遞一個物件的引用,更加方便
⬇️this
的誤區
關於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( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被呼叫了多少次?
console.log( foo.count ); // 0
複製程式碼
執行後我們發現foo.count
仍然是0,說明this
並沒有指向foo
自身。
誤區二:指向它的作用域
在某種情況下這個說法是正確的,而在某些情況下這個說法又是錯誤的,但是要注意!!this
在任何情況下都不指向函式的詞法作用域!!
為什麼這麼說呢?
function bar() {
console.log(1);
}
this.bar(); // 1
複製程式碼
在上例中,this
指向了全域性作用域,但是隻是特殊情況,因此會有這個說法是正確的,而在某些情況下這個說法又是錯誤的結論
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo();
複製程式碼
上文this.a
檢視引用foo
詞法作用域定義的變數a
,這是永遠也不可能實現的
❤️this
到底是什麼
說了它的使用方式以及誤區,那麼this
到底是什麼呢?首先明確一點:this
的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。
呼叫位置
this
是在呼叫時被繫結的,完全取決於函式的呼叫位置,因此要搞清楚函式的呼叫位置,但是某些程式設計模式會隱藏函式的呼叫位置,最重要的分析它的呼叫棧(就是為了達到當前執行位置的所有呼叫函式)
function baz() {
// 當前呼叫棧是:baz
// 因此,當前呼叫位置是全域性作用域
bar(); // <-- bar 的呼叫位置
}
function bar() {
// 當前呼叫棧是 baz -> bar
// 因此,當前呼叫位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的呼叫位置
}
function foo() {
// 當前呼叫棧是 baz -> bar -> foo
// 因此,當前呼叫位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的呼叫位置
複製程式碼
☕️繫結規則
下面介紹this
繫結的4種規則,下次看到this
出現時,便可以使用這些規則
預設繫結
這是比較常見的函式呼叫型別:獨立函式呼叫
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
複製程式碼
如何判斷應用了預設繫結呢?foo
是直接使用不帶任何修飾符的函式進行引用呼叫的
注意:如果使用了嚴格模式,this
會繫結到undefined
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
複製程式碼
隱式繫結
當函式引用有上下文物件時(嚴格來說函式被物件“擁有”或者“包含”),隱式繫結規則會把函式呼叫中的this
繫結到這個上下文物件
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
複製程式碼
嚴格來說,foo
不屬於obj
物件,但是落腳點卻指向obj
物件,因此你可以說函式被呼叫時 obj
物件“擁 有”或者“包含”它。
隱式丟失
一個最常見的問題就是:隱式繫結會丟失繫結物件,從而執行預設繫結
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
其實是一個不帶任何修飾的函式呼叫
另外一種情況就是引數傳遞。
引數傳遞其實就是一種隱式賦值,我們在傳入函式時也是隱式賦值
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"
複製程式碼
綜上所述:有兩種情況會導致隱式繫結的繫結丟失。
- 進行引用賦值
var bar = obj.foo;
- 進行傳遞引數
doFoo( obj.foo );
顯式繫結
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
複製程式碼
通過foo.call(..)
可以在呼叫時強制把this
繫結到obj
上,但是這樣的方式也無法解決掉丟失繫結問題
var a = 0;
function foo() {
console.log(this.a);
}
var obj1 = {
a:1
};
var obj2 = {
a:2
};
foo.call(obj1);// 1
foo.call(obj2);// 2
複製程式碼
我們發現this
隨著呼叫一直在改變,即this
丟失。
我們可以通過以下方式解決:
硬繫結
建立一個包裹函式,傳入所有的引數並返回接收到的所有值
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
複製程式碼
API
呼叫的上下文
許多內建函式都提供了一個可選引數,通常被稱為上下文context
,其作用和bind
一樣,確保你的回撥 函式使用指定的 this。
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
複製程式碼
new
繫結
JavaScript
中的new
機制與物件導向的語言完全不同,實際上,在JavaScript
中並不存在所謂的"建構函式",只有對與函式的"構造呼叫"
使用new
來呼叫函式,或者說發生建構函式呼叫時的流程:
- 建立(構造一個全新的物件)
- 這個新物件會被執行
[[原型]]
連線 - 這個新物件會被繫結到函式呼叫的
this
- 如果函式沒有返回其他物件,那麼
new
表示式中的函式呼叫會自動返回這個新物件
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
複製程式碼
❤️優先順序
上面介紹了this
的4種繫結規則,那麼它們的優先順序誰高誰低呢,首先,確認一點的是預設繫結的優先順序最低
比較隱式繫結和顯示繫結
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
// new 和 隱式繫結同時存在,obj1的a是2,而this指向了bar
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
複製程式碼
可以看出來new
繫結高於隱式繫結
比較new
繫結和顯示繫結
由於new
和 call/apply
無法一起使用,我們可以使用硬繫結測試優先順序
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
的優先順序大於硬繫結。
但是使用剛開始的裸bind
function foo(something) {
this.a = something;
}
function bind(obj, fn) {
return function() {
fn.apply(obj. arguments);
}
}
var obj1 = {};
var bar = bind( obj1, foo );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 3
console.log( baz.a ); // undefined
複製程式碼
會驚奇地發現,new bar(3)
把obj1.a
修改為 3
因此內建bind
的實現是非常複雜的,不在此進行研究,既然這麼複雜,為什麼還要使用呢?
這種做法稱為“部 分應用”,是“柯里化”的一種,它的主要目的是預設函式的一些引數,這樣在使用new
進行初始化時就可以只傳入其餘的引數。
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
複製程式碼
從上我們可以總結出可以通過以下順序判斷this
:
- 函式是否在
new
中呼叫(new
繫結)? - 函式是否通過
call、apply
(顯式繫結)或者硬繫結呼叫 - 函式是否在某個上下文物件中呼叫(隱式繫結)
- 如果都不是的話,使用預設繫結
☕️繫結例外
規則總有例外,當你認為應用了其他規則時,有可能只應用了預設規則
被忽略的this
如果我們把null
或者undefined
作為this
的繫結物件傳遞入call
、apply
、或者bind
,會使用預設繫結規則
function foo() {
console.log( this.a );
}
var a = 2;
foo.call( null ); // 2
複製程式碼
那麼什麼情況下會使用這種方式呢?利用apply
展開陣列或者bind
實現函式柯里化的部分應用
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把陣列“展開”成引數
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
複製程式碼
es6
中可以使用...
來代替``apply(...)```,但是ES6中沒有柯里化的相關方法
忽略this
會存在一個問題,比如第三方庫的函式真的使用了this
,我們這種方式把this
繫結到了全域性作用域,會存在問題,需要使用更安全的this
,建立空的非委託的物件Object.create( null )
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空物件
var ø = Object.create( null );
// 把陣列展開成引數
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3
複製程式碼
間接引用
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()
,因此還是會呼叫預設規則
軟繫結
硬繫結可以把this
強制繫結到指定的物件上,防止函式呼叫應用預設規則繫結,但是有一個弊端就是無法通過隱式或者顯示繫結來修改this
如果可以給預設繫結指定一個全域性物件和undefined
以外的值,就可以實現和硬繫結相同的效果,同時保留隱式繫結或者顯式繫結修改 this
的能力
這種叫做軟繫結。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
console.log('fn', this);
// 捕獲所有 curried 引數
var curried = [].slice.call( arguments, 1 );
var bound = function() {
console.log('this', this);
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 <---- 應用了軟繫結
複製程式碼
☕️this
詞法
最後介紹es6
中的箭頭函式,箭頭函式不使用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
的this
繫結到了obj1
,bar
引用箭頭函式的this
也會繫結到obj1
,箭頭函式的繫結無法被修改