筆者最近在看 你不知道的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 的系結和函式宣告的位置沒有任何關系,只取決于函式的呼叫方式
當一個函式被呼叫時,會創建一個活動記錄(有時候也稱為執行背景關系),這個記錄會包 含函式在哪里被呼叫(呼叫堆疊)、函式的呼叫方法、傳入的引數等資訊,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
this 的四種系結規則
下面我們來看在函式背景關系中的系結規則,有以下四種
- 默認系結
- 隱式系結
- 顯式系結
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系結
當我們使用建構式 new 一個實體的時候,這個實體的 this 指向是什么呢?
我們先來看下使用 new 來呼叫函式,或者說發生建構式呼叫時,會執行什么操作,如下:
- 創建(或者說構造)一個全新的物件
- 這個新物件會被執行[[原型]]連接,將物件(實體)的
__proto__和建構式的prototype系結 - 這個新物件會系結到函式呼叫的
this - 如果函式沒有回傳其他物件,那么new運算式中的函式呼叫會自動回傳這個新物件
原理實作類似如下:
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 系結,就需要找到這個函式的直接呼叫位置,找到之后就可以順序應用下面這四條規則來判斷 this 的系結物件
- 由
new呼叫?系結到新創建的物件 - 由
call或者apply(或者bind)呼叫?系結到指定的物件 - 由背景關系物件呼叫?系結到那個背景關系物件
- 默認:在嚴格模式下系結到
undefined,否則系結到全域物件
如下圖所示:
參考
-
[譯] 理解 JavaScript 中的執行背景關系和執行堆疊
-
你不知道的JavaScript上卷
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/509144.html
標籤:其他
