溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript this綁定過程深入詳解

發(fā)布時(shí)間:2020-09-20 09:35:20 來源:腳本之家 閱讀:122 作者:兩面一湯 欄目:web開發(fā)

本文實(shí)例形式詳細(xì)分析了JavaScript this綁定過程。分享給大家供大家參考,具體如下:

在理解this 的綁定過程之前,首先要理解調(diào)用位置:調(diào)用位置就是函數(shù)在代碼中被調(diào)用的位置(而不是聲明的位置)。只有仔細(xì)分析調(diào)用位置才能回答這個(gè)問題:這個(gè)this 到底引用的是什么?通常來說,尋找調(diào)用位置就是尋找“函數(shù)被調(diào)用的位置”,但是做起來并沒有這么簡單,因?yàn)槟承┚幊棠J娇赡軙[藏真正的調(diào)用位置。最重要的是要分析調(diào)用棧(就是為了到達(dá)當(dāng)前執(zhí)行位置所調(diào)用的所有函數(shù))。我們關(guān)心的調(diào)用位置就在當(dāng)前正在執(zhí)行的函數(shù)的前一個(gè)調(diào)用中。

下面我們來看看到底什么是調(diào)用棧和調(diào)用位置:

function baz() {
// 當(dāng)前調(diào)用棧是:baz
// 因此,當(dāng)前調(diào)用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的調(diào)用位置
}
function bar() {
// 當(dāng)前調(diào)用棧是baz -> bar
// 因此,當(dāng)前調(diào)用位置在baz 中
console.log( "bar" );
foo(); // <-- foo 的調(diào)用位置
}
function foo() {
// 當(dāng)前調(diào)用棧是baz -> bar -> foo
// 因此,當(dāng)前調(diào)用位置在bar 中
console.log( "foo" );
}
baz(); // <-- baz 的調(diào)用位置

注意我們是如何(從調(diào)用棧中)分析出真正的調(diào)用位置的,因?yàn)樗鼪Q定了this 的綁定。

你可以把調(diào)用棧想象成一個(gè)函數(shù)調(diào)用鏈,就像我們在前面代碼段的注釋中所寫的一樣。但是這種方法非常麻煩并且容易出錯(cuò)。另一個(gè)查看調(diào)用棧的方法是使用瀏覽器的調(diào)試工具。絕大多數(shù)現(xiàn)代桌面瀏覽器都內(nèi)置了開發(fā)者工具,其中包含JavaScript 調(diào)試器。就本例來說,你可以在工具中給foo() 函數(shù)的第一行代碼設(shè)置一個(gè)斷點(diǎn),或者直接在第一行代碼之前插入一條debugger;語句。運(yùn)行代碼時(shí),調(diào)試器會在那個(gè)位置暫停,同時(shí)會展示當(dāng)前位置的函數(shù)調(diào)用列表,這就是你的調(diào)用棧。因此,如果你想要分析this 的綁定,使用開發(fā)者工具得到調(diào)用棧,然后找到棧中第二個(gè)元素,這就是真正的調(diào)用位置。

綁定規(guī)則

我們來看看在函數(shù)的執(zhí)行過程中調(diào)用位置如何決定this 的綁定對象。你必須找到調(diào)用位置,然后判斷需要應(yīng)用下面四條規(guī)則中的哪一條。

1.默認(rèn)綁定

首先要介紹的是最常用的函數(shù)調(diào)用類型:獨(dú)立函數(shù)調(diào)用??梢园堰@條規(guī)則看作是無法應(yīng)用其他規(guī)則時(shí)的默認(rèn)規(guī)則。

思考一下下面的代碼:

function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2

你應(yīng)該注意到的第一件事是,聲明在全局作用域中的變量(比如var a = 2)就是全局對象的一個(gè)同名屬性。它們本質(zhì)上就是同一個(gè)東西,并不是通過復(fù)制得到的,就像一個(gè)硬幣的兩面一樣。

接下來我們可以看到當(dāng)調(diào)用foo() 時(shí),this.a 被解析成了全局變量a。為什么?因?yàn)樵诒纠?,函?shù)調(diào)用時(shí)應(yīng)用了this 的默認(rèn)綁定,因此this 指向全局對象。

那么我們怎么知道這里應(yīng)用了默認(rèn)綁定呢?可以通過分析調(diào)用位置來看看foo() 是如何調(diào)用的。在代碼中,foo() 是直接使用不帶任何修飾的函數(shù)引用進(jìn)行調(diào)用的,因此只能使用默認(rèn)綁定,無法應(yīng)用其他規(guī)則。

如果使用嚴(yán)格模式(strict mode),那么全局對象將無法使用默認(rèn)綁定,因此this 會綁定到undefined:

function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

這里有一個(gè)微妙但是非常重要的細(xì)節(jié),雖然this 的綁定規(guī)則完全取決于調(diào)用位置,但是只有foo() 運(yùn)行在非strict mode 下時(shí),默認(rèn)綁定才能綁定到全局對象;嚴(yán)格模式下與foo()的調(diào)用位置無關(guān):

function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();

通常來說你不應(yīng)該在代碼中混合使用strict mode 和non-strict mode。整個(gè)程序要么嚴(yán)格要么非嚴(yán)格。然而,有時(shí)候你可能會用到第三方庫,其嚴(yán)格程度和你的代碼有所不同,因此一定要注意這類兼容性細(xì)節(jié)。

2.隱式綁定

另一條需要考慮的規(guī)則是調(diào)用位置是否有上下文對象,或者說是否被某個(gè)對象擁有或者包含,不過這種說法可能會造成一些誤導(dǎo)。

思考下面的代碼:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2

首先需要注意的是foo() 的聲明方式,及其之后是如何被當(dāng)作引用屬性添加到obj 中的。但是無論是直接在obj 中定義還是先定義再添加為引用屬性,這個(gè)函數(shù)嚴(yán)格來說都不屬于obj 對象。然而,調(diào)用位置會使用obj 上下文來引用函數(shù),因此你可以說函數(shù)被調(diào)用時(shí)obj 對象“擁有”或者“包含”它。無論你如何稱呼這個(gè)模式,當(dāng)foo() 被調(diào)用時(shí),它的落腳點(diǎn)確實(shí)指向obj 對象。當(dāng)函數(shù)引用有上下文對象時(shí),隱式綁定規(guī)則會把函數(shù)調(diào)用中的this 綁定到這個(gè)上下文對象。因?yàn)檎{(diào)用foo() 時(shí)this 被綁定到obj,因此this.aobj.a 是一樣的。

對象屬性引用鏈中只有最頂層或者說最后一層會影響調(diào)用位置。舉例來說:

function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42

隱式丟失

一個(gè)最常見的this 綁定問題就是被隱式綁定的函數(shù)會丟失綁定對象,也就是說它會應(yīng)用默認(rèn)綁定,從而把this 綁定到全局對象或者undefined 上,取決于是否是嚴(yán)格模式。

思考下面的代碼:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)別名!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"

雖然bar 是obj.foo 的一個(gè)引用,但是實(shí)際上,它引用的是foo 函數(shù)本身,因此此時(shí)的bar() 其實(shí)是一個(gè)不帶任何修飾的函數(shù)調(diào)用,因此應(yīng)用了默認(rèn)綁定。

一種更微妙、更常見并且更出乎意料的情況發(fā)生在傳入回調(diào)函數(shù)時(shí):

function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其實(shí)引用的是foo
fn(); // <-- 調(diào)用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
doFoo( obj.foo ); // "oops, global"

參數(shù)傳遞其實(shí)就是一種隱式賦值,因此我們傳入函數(shù)時(shí)也會被隱式賦值,所以結(jié)果和上一個(gè)例子一樣。

如果把函數(shù)傳入語言內(nèi)置的函數(shù)而不是傳入你自己聲明的函數(shù),會發(fā)生什么呢?結(jié)果是一樣的,沒有區(qū)別:

function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局對象的屬性
setTimeout( obj.foo, 100 ); // "oops, global"
JavaScript 環(huán)境中內(nèi)置的setTimeout() 函數(shù)實(shí)現(xiàn)和下面的偽代碼類似:
function setTimeout(fn,delay) {
// 等待delay 毫秒
fn(); // <-- 調(diào)用位置!
}

就像我們看到的那樣,回調(diào)函數(shù)丟失this 綁定是非常常見的。除此之外,還有一種情況this 的行為會出乎我們意料:調(diào)用回調(diào)函數(shù)的函數(shù)可能會修改this。在一些流行的JavaScript 庫中事件處理器常會把回調(diào)函數(shù)的this 強(qiáng)制綁定到觸發(fā)事件的DOM 元素上。

這在一些情況下可能很有用,但是有時(shí)它可能會讓你感到非常郁悶。遺憾的是,這些工具通常無法選擇是否啟用這個(gè)行為。

無論是哪種情況,this 的改變都是意想不到的,實(shí)際上你無法控制回調(diào)函數(shù)的執(zhí)行方式,因此就沒有辦法控制會影響綁定的調(diào)用位置。之后我們會介紹如何通過固定this 來修復(fù)(這里是雙關(guān),“修復(fù)”和“固定”的英語單詞都是fixing)這個(gè)問題。

3.顯式綁定

就像我們剛才看到的那樣,在分析隱式綁定時(shí),我們必須在一個(gè)對象內(nèi)部包含一個(gè)指向函數(shù)的屬性,并通過這個(gè)屬性間接引用函數(shù),從而把this 間接(隱式)綁定到這個(gè)對象上。那么如果我們不想在對象內(nèi)部包含函數(shù)引用,而想在某個(gè)對象上強(qiáng)制調(diào)用函數(shù),該怎么做呢?JavaScript 中的“所有”函數(shù)都有一些有用的特性(這和它們的[[ 原型]] 有關(guān)——之后我們會詳細(xì)介紹原型),可以用來解決這個(gè)問題。具體點(diǎn)說,可以使用函數(shù)的call(..) 和apply(..) 方法。嚴(yán)格來說,JavaScript 的宿主環(huán)境有時(shí)會提供一些非常特殊的函數(shù),它們并沒有這兩個(gè)方法。但是這樣的函數(shù)非常罕見,JavaScript 提供的絕大多數(shù)函數(shù)以及你自己創(chuàng)建的所有函數(shù)都可以使用call(..) 和apply(..) 方法。這兩個(gè)方法是如何工作的呢?它們的第一個(gè)參數(shù)是一個(gè)對象,它們會把這個(gè)對象綁定到this,接著在調(diào)用函數(shù)時(shí)指定這個(gè)this。因?yàn)槟憧梢灾苯又付╰his 的綁定對象,因此我們稱之為顯式綁定。

思考下面的代碼:

function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2

通過foo.call(..),我們可以在調(diào)用foo 時(shí)強(qiáng)制把它的this 綁定到obj 上。如果你傳入了一個(gè)原始值(字符串類型、布爾類型或者數(shù)字類型)來當(dāng)作this 的綁定對象,這個(gè)原始值會被轉(zhuǎn)換成它的對象形式(也就是new String(..)new Boolean(..) 或者new Number(..))。這通常被稱為“裝箱”。從this 綁定的角度來說,call(..)apply(..) 是一樣的,它們的區(qū)別體現(xiàn)在其他的參數(shù)上,但是現(xiàn)在我們不用考慮這些??上?,顯式綁定仍然無法解決我們之前提出的丟失綁定問題。

1. 硬綁定

但是顯式綁定的一個(gè)變種可以解決這個(gè)問題。

思考下面的代碼:

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

我們來看看這個(gè)變種到底是怎樣工作的。我們創(chuàng)建了函數(shù)bar(),并在它的內(nèi)部手動(dòng)調(diào)用了foo.call(obj),因此強(qiáng)制把foo 的this 綁定到了obj。無論之后如何調(diào)用函數(shù)bar,它總會手動(dòng)在obj 上調(diào)用foo。這種綁定是一種顯式的強(qiáng)制綁定,因此我們稱之為硬綁定。

硬綁定的典型應(yīng)用場景就是創(chuàng)建一個(gè)包裹函數(shù),傳入所有的參數(shù)并返回接收到的所有值:

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 3
console.log( b ); // 5

另一種使用方法是創(chuàng)建一個(gè)i 可以重復(fù)使用的輔助函數(shù):

function foo(something) {
console.log( this.a, something );
return this.a + something;
}
// 簡單的輔助綁定函數(shù)
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

由于硬綁定是一種非常常用的模式,所以在ES5 中提供了內(nèi)置的方法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 3
console.log( b ); // 5

bind(..) 會返回一個(gè)硬編碼的新函數(shù),它會把參數(shù)設(shè)置為this 的上下文并調(diào)用原始函數(shù)。

2. API調(diào)用的“上下文”

第三方庫的許多函數(shù),以及JavaScript 語言和宿主環(huán)境中許多新的內(nèi)置函數(shù),都提供了一個(gè)可選的參數(shù),通常被稱為“上下文”(context),其作用和bind(..) 一樣,確保你的回調(diào)函數(shù)使用指定的this。

舉例來說:

function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 調(diào)用foo(..) 時(shí)把this 綁定到obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome

這些函數(shù)實(shí)際上就是通過call(..) 或者apply(..) 實(shí)現(xiàn)了顯式綁定,這樣你可以少些一些代碼。

4.new綁定

這是第四條也是最后一條this 的綁定規(guī)則,在講解它之前我們首先需要澄清一個(gè)非常常見的關(guān)于JavaScript 中函數(shù)和對象的誤解。在傳統(tǒng)的面向類的語言中,“構(gòu)造函數(shù)”是類中的一些特殊方法,使用new 初始化類時(shí)會調(diào)用類中的構(gòu)造函數(shù)。通常的形式是這樣的:

something = new MyClass(..);

JavaScript 也有一個(gè)new 操作符,使用方法看起來也和那些面向類的語言一樣,絕大多數(shù)開發(fā)者都認(rèn)為JavaScript 中new 的機(jī)制也和那些語言一樣。然而JavaScript 中new 的機(jī)制實(shí)際上和面向類的語言完全不同。

首先我們重新定義一下JavaScript 中的“構(gòu)造函數(shù)”。在JavaScript 中,構(gòu)造函數(shù)只是一些使用new 操作符時(shí)被調(diào)用的函數(shù)。它們并不會屬于某個(gè)類,也不會實(shí)例化一個(gè)類。實(shí)際上,它們甚至都不能說是一種特殊的函數(shù)類型,它們只是被new 操作符調(diào)用的普通函數(shù)而已。

舉例來說,思考一下Number(..) 作為構(gòu)造函數(shù)時(shí)的行為,ES5.1 中這樣描述它:

Number 構(gòu)造函數(shù)

當(dāng)Number 在new 表達(dá)式中被調(diào)用時(shí),它是一個(gè)構(gòu)造函數(shù):它會初始化新創(chuàng)建的對象。

所以,包括內(nèi)置對象函數(shù)(比如Number(..))在內(nèi)的所有函數(shù)都可以用new 來調(diào)用,這種函數(shù)調(diào)用被稱為構(gòu)造函數(shù)調(diào)用。這里有一個(gè)重要但是非常細(xì)微的區(qū)別:實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”,只有對于函數(shù)的“構(gòu)造調(diào)用”。使用new 來調(diào)用函數(shù),或者說發(fā)生構(gòu)造函數(shù)調(diào)用時(shí),會自動(dòng)執(zhí)行下面的操作。

1. 創(chuàng)建(或者說構(gòu)造)一個(gè)全新的對象。
2. 這個(gè)新對象會被執(zhí)行[[ 原型]] 連接。
3. 這個(gè)新對象會綁定到函數(shù)調(diào)用的this。
4. 如果函數(shù)沒有返回其他對象,那么new 表達(dá)式中的函數(shù)調(diào)用會自動(dòng)返回這個(gè)新對象。

思考下面的代碼:

function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2

使用new 來調(diào)用foo(..) 時(shí),我們會構(gòu)造一個(gè)新對象并把它綁定到foo(..) 調(diào)用中的this上。new 是最后一種可以影響函數(shù)調(diào)用時(shí)this 綁定行為的方法,我們稱之為new 綁定。

更多關(guān)于JavaScript相關(guān)內(nèi)容感興趣的讀者可查看本站專題:《javascript面向?qū)ο笕腴T教程》、《JavaScript常用函數(shù)技巧匯總》、《JavaScript錯(cuò)誤與調(diào)試技巧總結(jié)》、《JavaScript數(shù)據(jù)結(jié)構(gòu)與算法技巧總結(jié)》、《JavaScript遍歷算法與技巧總結(jié)》及《JavaScript數(shù)學(xué)運(yùn)算用法總結(jié)》

希望本文所述對大家JavaScript程序設(shè)計(jì)有所幫助。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI