筆者最近在看 你不知道的JavaScript上卷,裡面關於 this
的講解個人覺得非常精彩。JavaScript
中的 this
算是一個核心的概念,有一些同學會對其有點模糊和小恐懼,究其原因,現在對 this
討論的文章很多,讓我們覺得 this
無規律可尋,就像一個幽靈一樣
如果你還沒弄懂 this
,或者對它比較模糊,這篇文章就是專門為你準備的,如果你相對比較熟悉了,那你也可以當做複習鞏固你的知識點
本篇文章,算是一篇讀書筆記,當然也加上了很多我的個人理解,我覺得肯定對大家有所幫助
在理解 this
之前,我們先來看下什麼是執行上下文
簡而言之,執行上下文是評估和執行 JavaScript
程式碼的環境的抽象概念。每當 Javascript 程式碼在執行的時候,它都是在執行上下文中執行
JavaScript 中有三種執行上下文型別
window
物件(瀏覽器的情況下),並且設定 this
的值等於這個全域性物件。一個程式中只會有一個全域性執行上下文eval
函數執行上下文 — 執行在 eval
函數內部的程式碼也會有它屬於自己的執行上下文,但由於 JavaScript 開發者並不經常使用 eval
,所以在這裡我不會討論它這裡我們先得出一個結論,非嚴格模式和嚴格模式中 this 都是指向頂層物件(瀏覽器中是window)
console.log(this === window); // true
'use strict'
console.log(this === window); // true
this.name = 'vnues';
console.log(this.name); // vnues
後面我們的討論更多的是針對函數執行上下文
this
是在執行時進行繫結的,並不是在編寫時繫結,它的上下文取決於函數調 用時的各種條件
牢記:this
的繫結和函數宣告的位置沒有任何關係,只取決於函數的呼叫方式
當一個函數被呼叫時,會建立一個活動記錄(有時候也稱為執行上下文)。這個記錄會包 含函數在哪裡被呼叫(呼叫棧)、函數的呼叫方法、傳入的引數等資訊。this
就是記錄的 其中一個屬性,會在函數執行的過程中用到
看個範例,理解為什麼要用 this
,有時候,我們需要實現類似如下的程式碼:
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = "Hello, I'm " + identify(context);
console.log(greeting);
}
var me = {
name: "Kyle"
};
speak(me); //hello, 我是 KYLE
這段程式碼的問題,在於需要顯示傳遞上下文物件,如果程式碼越來越複雜,這種方式會讓你的程式碼看起來很混亂,用 this
則更加的優雅
var me = {
name: "Kyle"
};
function identify() {
return this.name.toUpperCase();
}
function speak() {
var greeting = "Hello, I'm " + identify.call(this);
console.log(greeting);
}
speak.call(me); // Hello, 我是 KYLE
下面我們來看在函數上下文中的繫結規則,有以下四種
new
繫結最常用的函數呼叫型別:獨立函數呼叫,這個也是優先順序最低的一個,此事 this
指向全域性物件。注意:如果使用嚴格模式(strict mode
),那麼全域性物件將無法使用預設繫結,因此 this
會繫結 到 undefined
,如下所示
var a = 2; // 變數宣告到全域性物件中
function foo() {
console.log(this.a); // 輸出 a
}
function bar() {
'use strict';
console.log(this); // undefined
}
foo();
bar();
還可以我們開頭說的:this
的繫結和函數宣告的位置沒有任何關係,只取決於函數的呼叫方式
先來看一個例子:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
當呼叫 obj.foo()
的時候,this
指向 obj 物件。當函數參照有上下文物件時,隱式繫結規則會把函數呼叫中的 this
繫結到這個上下文物件。因為調 用 foo()
時 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
間接參照
另一個需要注意的是,你有可能(有意或者無意地)建立一個函數的「間接參照」,在這 種情況下,呼叫這個函數會應用預設繫結規則
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
間接(隱式)繫結到這個物件上。 那麼如果我們不想在物件內部包含函數參照,而想在某個物件上強制呼叫函數,該怎麼
做呢?
Javascript
中提供了 apply
、call
和 bind
方法可以讓我們實現
不同之處在於,call()
和 apply()
是立即執行函數,並且接受的引數的形式不同:
call(this, arg1, arg2, ...)
apply(this, [arg1, arg2, ...])
而 bind()
則是建立一個新的包裝函數,並且返回,而不是立刻執行
bind(this, arg1, arg2, ...)
看如下的例子:
function foo(b) {
console.log(this.a + '' + b);
}
var obj = {
a: 2,
foo: foo
};
var a = 1;
foo('Gopal'); // 1Gopal
obj.foo('Gopal'); // 2Gopal
foo.call(obj, 'Gopal'); // 2Gopal
foo.apply(obj, ['Gopal']); // 2Gopal
let bar = foo.bind(obj, 'Gopal');
bar(); // 2Gopal
被忽略的 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
當我們使用建構函式 new
一個範例的時候,這個範例的 this
指向是什麼呢?
我們先來看下使用 new
來呼叫函數,或者說發生建構函式呼叫時,會執行什麼操作,如下:
__proto__
和建構函式的 prototype
繫結this
原理實現類似如下:
function create (ctr) {
// 建立一個空物件
let obj = new Object()
// 連結到建構函式的原型物件中
let Con = [].shift.call(arguments)
obj.__proto__ = Con.prototype
// 繫結this
let result = Con.apply(obj, arguments);
// 如果返回是一個物件,則直接返回這個物件,否則返回範例
return typeof result === 'object'? result : obj;
}
注意:let result = Con.apply(obj, arguments);
實際上就是指的是新物件會繫結到函數呼叫的 this
function Foo(a) {
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); // 2
我們之前介紹的四條規則已經可以包含所有正常的函數。但是 ES6 中介紹了一種無法使用 這些規則的特殊函數型別:箭頭函數
箭頭函數不使用 this
的四種標準規則,而是根據定義時候的外層(函數或者全域性)作用域來決 定 this
。也就是說箭頭函數不會建立自己的 this
,它只會從自己的作用域鏈的上一層繼承 this
function foo() {
// 返回一個箭頭函數
// this 繼承自 foo()
return (a) => {
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
也不 行!)
判斷是否為箭頭函數,是則按照箭頭函數的規則
否則如果要判斷一個執行中函數的 this
繫結,就需要找到這個函數的直接呼叫位置。找到之後就可以順序應用下面這四條規則來判斷 this
的繫結物件
new
呼叫?繫結到新建立的物件call
或者 apply
(或者 bind
)呼叫?繫結到指定的物件undefined
,否則繫結到全域性物件如下圖所示: