this 之謎揭底:從淺入深理解 JavaScript 中的 this 關(guān)鍵字(二)

前言

this 之謎揭底:從淺入深理解 JavaScript 中的 this 關(guān)鍵字(二)

調(diào)用位置

  • 在理解 this 的綁定過程之前,首先要理解調(diào)用位置調(diào)用位置就是函數(shù)在代碼中被調(diào)用的位置(而不是聲明的位置)
  • 通常來說,尋找調(diào)用位置就是尋找"函數(shù)被調(diào)用的位置", 最重要的要分析調(diào)用棧(就是為了到達(dá)當(dāng)前執(zhí)行位置所調(diào)用的所有函數(shù))。運(yùn)行代碼時(shí),調(diào)用器會(huì)在那個(gè)位置暫停,同時(shí)會(huì)在展示當(dāng)前位置的函數(shù)調(diào)用列表,這就是調(diào)用棧。

綁定規(guī)則

  • 函數(shù)的調(diào)用位置決定了 this 的綁定對(duì)象,通常情況下分為以下幾種規(guī)則:

默認(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
  • 當(dāng)調(diào)用 foo() 時(shí),this.a 被解析成了全局變量 a。為什么?
    • 因?yàn)樵谏鲜龃a中,函數(shù)調(diào)用時(shí)應(yīng)用了this 的默認(rèn)綁定,因此 this 指向全局對(duì)象。(要理解 this,就要先理解調(diào)用位置)
  • 如果使用嚴(yán)格模式(strict mode),那全局對(duì)象將無法使用默認(rèn)綁定,因此 this 會(huì)綁定到 undefined。
function foo(){
    "use strict";
    console.log(this.a);
}
var a = 2;
foo(); // Type: this is undefined
  • 雖然 this 的綁定規(guī)則完全取決于調(diào)用位置,但是只有 foo() 運(yùn)行在非 strict mode下時(shí),默認(rèn)綁定才能綁定到全局對(duì)象; 嚴(yán)格模式下與 foo() 的調(diào)用位置無關(guān)。
function foo(){
    console.log(this.a);
}

var a = 2;

(function (){
    "use strict";

    foo(); // 2
})
  • 通常情況下,盡量減少在代碼中混合使用 strict modenon-strict mode,盡量減少在代碼中混合使用 strict mode 和 non-strict mode。

隱式綁定

  • 另一條規(guī)則是調(diào)用位置是否有上下文對(duì)象,或者說是否被某個(gè)對(duì)象擁有或包裹。
  • 考慮以下代碼:
function foo() {
    console.log(this.a); // 2
}

var obj = {
    a: 2,
    foo: foo
}

obj.foo();
  • 上述代碼中,調(diào)用位置使用 obj 的上下文來引用函數(shù),可以說函數(shù)被調(diào)用時(shí) obj 對(duì)象擁有或包含它。
  • 當(dāng)函數(shù)引用有上下文對(duì)象時(shí),隱式綁定規(guī)則會(huì)把函數(shù)調(diào)用中的 this 綁定到這個(gè)上下文對(duì)象上,因此在調(diào)用 foo() 時(shí) this 被綁定到了 obj 上,所以 this.a 與 obj.a 是一樣的。
  • 注意:對(duì)象屬性引用鏈中只有最頂層或最后一層會(huì)影響調(diào)用位置。
  • 如下代碼:
function foo() {
    console.log( this.a );
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42
  • 隱式丟失:在被隱式綁定的函數(shù)會(huì)丟失綁定對(duì)象,也就是說它會(huì)默認(rèn)綁定,從而把 this 綁定到全局對(duì)象或 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 是全局對(duì)象的屬性

bar(); // "oops, global"
  • 還有一種奇怪的方式,就是在傳入回調(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 是全局對(duì)象的屬性

doFoo( obj.foo ); // "oops, global"
  • 在我們傳入函數(shù)時(shí)也會(huì)被隱式賦值。
  • 那如果傳入的函數(shù)不是自定義的函數(shù),而是語言內(nèi)置的函數(shù)呢?結(jié)果還是一樣的,沒有區(qū)別
function foo() {
    console.log( this.a );
}

var obj = {
    a: 2,
    foo: foo
};

var a = "oops, global"; // a 是全局對(duì)象的屬性

setTimeout( obj.foo, 100 ); // "oops, global"

顯示綁定

  • 那我們不想在對(duì)象內(nèi)部包含函數(shù)引用,而是想在某個(gè)對(duì)象上強(qiáng)制調(diào)用函數(shù),該如何操作?
    • 那就必須要使用 call() 和 apply()。第一個(gè)參數(shù)是一個(gè)對(duì)象,也就是需要綁定的對(duì)象,第二個(gè)參數(shù)傳入的參數(shù),而兩者之間的區(qū)別就在于第二個(gè)參數(shù),call 的第二個(gè)參數(shù)是一個(gè)個(gè)參數(shù),而 apply 則是一個(gè)參數(shù)數(shù)組。
// call()
function foo() {
    console.log( this.a );
}

var obj = {
    a:2
};

foo.call( obj ); // 2


// apply()
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

new綁定

  • 在傳統(tǒng)的語言中,構(gòu)造函數(shù)時(shí)一個(gè)特殊方法,使用 new 初始化需要調(diào)用的類,通常形式下是 let something = new MyClass();。
  • 在使用 new 來調(diào)用函數(shù),會(huì)自動(dòng)執(zhí)行以下操作:
    1. 創(chuàng)建一個(gè)新對(duì)象
    2. 讓新對(duì)象的 __proto__(隱式原型) 等于函數(shù)的 prototype(顯式原型)
    3. 綁定 this, 讓新象綁定于函數(shù)的 this 指向
    4. 判斷返回值,如果返回值不是一個(gè)對(duì)象,則返回剛新建的新對(duì)象。

優(yōu)先級(jí)

  • 如果在某個(gè)調(diào)用位置應(yīng)用多條規(guī)則該如何?那為了解決此問題,那就引申出了優(yōu)先級(jí)問題。
  • 毫無疑問,默認(rèn)綁定的優(yōu)先級(jí)是四條規(guī)則中最低的,可以先不考慮它。
  • 先來看看隱式綁定和顯式綁定那個(gè)優(yōu)先級(jí)更高?
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
  • 可以看出,顯式綁定的優(yōu)先級(jí)更高,也就是說在判斷時(shí)應(yīng)當(dāng)考慮是否可以應(yīng)用顯式綁定。
  • 再來看看new綁定和隱式綁定的優(yōu)先級(jí)?
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綁定
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
  • 可以看出,new 綁定比隱式綁定的優(yōu)先級(jí)更高,但 new 綁定和顯式綁定誰的優(yōu)先級(jí)更高呢?
  • new 與 call/apply 無法一起使用,因此無法通過 new foo.call(obj1) 來進(jìn)行測(cè)試,但可以通過硬綁定來測(cè)試他兩的優(yōu)先級(jí)。
  • 硬綁定:Function.prototype.bind(...) 會(huì)創(chuàng)建一個(gè)新的包裝函數(shù),這個(gè)函數(shù)會(huì)忽略當(dāng)前的this綁定(無論綁定的對(duì)象是什么),并把我們提供的對(duì)象綁定到this上。
  • 這樣看起來硬綁定(也是顯式綁定的一種)似乎比 new 綁定的優(yōu)先級(jí)更高,無法使用 new 來控制 this 綁定。
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) 并沒有像我們預(yù)計(jì)的那樣把 obj1.a 修改為 3。相反, new 修改了硬綁定(到 obj1 的)調(diào)用 bar(..) 中的 this。因?yàn)槭褂昧?new 綁定,我們得到了一個(gè)名字為 baz 的新對(duì)象,并且 baz.a 的值是 3。
  • 硬綁定中的bind(...) 的功能之一就是可以把除了第一個(gè)參數(shù)(第一個(gè)參數(shù)用于綁定this)之外的其他參數(shù)傳遞給下層的函數(shù)(這種技術(shù)稱為"部分應(yīng)用",是"柯里化"的一種)。
function foo(p1,p2) {
    this.val = p1 + p2;
}

// 之所以使用 null 是因?yàn)樵诒纠形覀儾⒉魂P(guān)心硬綁定的 this 是什么
// 反正使用 new 時(shí) this 會(huì)被修改
var bar = foo.bind( null, "p1" );

var baz = new bar( "p2" );

baz.val; // p1p2
  • 判斷this
    1. 是否在 new 中調(diào)用(new 綁定), this 指向新創(chuàng)建的對(duì)象
    2. 是否通過 call、apply(顯示綁定),this 指向綁定的對(duì)象
    3. 是否在某個(gè)對(duì)象中調(diào)用(隱式綁定),this 指向綁定的上下文對(duì)象
    4. 如果都不是,則是默認(rèn)綁定,在嚴(yán)格模式下,this 指向 undefined, 非嚴(yán)格模式下,this 指向全局對(duì)象。
  • 優(yōu)先級(jí)問題
    • 顯式綁定:call()、apply()。(硬綁定也是顯式綁定的其中一種: bind())
    • new 綁定: new Foo()
    • 隱式綁定: obj.foo();
    • 默認(rèn)綁定: foo();
  • 排序:顯式綁定 > new 綁定 > 隱式綁 定 > 默認(rèn)綁定

綁定例子

被忽略的this

  • 如果你把 null 或者 undefined 作為 this 的綁定對(duì)象傳入 call、apply 或者 bind,這些值在調(diào)用時(shí)會(huì)被忽略,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:
function foo() {
    console.log( this.a );
}

var a = 2;

foo.call( null ); // 2
  • 那在什么情況下會(huì)傳入 null 呢?
    • 一種非常常見的做法是使用 apply(..) 來“展開”一個(gè)數(shù)組,并當(dāng)作參數(shù)傳入一個(gè)函數(shù)。
function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}

// 把數(shù)組“展開”成參數(shù)
foo.apply( null, [2, 3] ); // a:2, b:3

// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
  • 但總是用 null 來忽略 this 綁定可能會(huì)產(chǎn)生一些副作用。
  • 更安全的this
    • DMZ(demilitarized zone)空委托對(duì)象
  • 在 JavaScript 中創(chuàng)建一個(gè)空對(duì)象最簡(jiǎn)單的方法都是 Object.create(null)。Object.create(null) 和 {} 很 像, 但 是 并 不 會(huì) 創(chuàng) 建 Object.prototype 這個(gè)委托,所以它比 {}“更空”:
function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}

// 我們的 DMZ 空對(duì)象
var ? = Object.create( null );

// 把數(shù)組展開成參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3

// 使用 bind(..) 進(jìn)行柯里化
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
  • 賦值表達(dá)式 p.foo = o.foo 的返回值是目標(biāo)函數(shù)的引用,因此調(diào)用位置是 foo() 而不是 p.foo() 或者 o.foo()。根據(jù)我們之前說過的,這里會(huì)應(yīng)用默認(rèn)綁定。
  • 注意:對(duì)于默認(rèn)綁定來說,決定 this 綁定對(duì)象的并不是調(diào)用位置是否處于嚴(yán)格模式,而是函數(shù)體是否處于嚴(yán)格模式。如果函數(shù)體處于嚴(yán)格模式,this 會(huì)被綁定到 undefined,否則this 會(huì)被綁定到全局對(duì)象。

軟綁定

  • 硬綁定這種方式可以把 this 強(qiáng)制綁定到指定的對(duì)象(除了使用 new 時(shí)),防止函數(shù)調(diào)用應(yīng)用默認(rèn)綁定規(guī)則。使用硬綁定會(huì)大大降低函數(shù)的靈活性,使用硬綁定之后就無法使用隱式綁定或顯示綁定來修改 this。
  • 可通過一種軟綁定的方法來實(shí)現(xiàn):
if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕獲所有 curried 參數(shù)
        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;
    };
}
  • 實(shí)現(xiàn)軟綁定功能:
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   <---- 應(yīng)用了軟綁定
  • 可以看到,軟綁定的 foo() 可手動(dòng)將 this 綁定到 obj2 或 obj3 上,但如果應(yīng)用默認(rèn)綁定,則會(huì)將 this 綁定到 obj。

this 詞法

  • 在 ES6 中出現(xiàn)了一種無法使用這些規(guī)則的特殊函數(shù)類型:箭頭函數(shù)
  • 箭頭函數(shù)不適用 this 的四種標(biāo)準(zhǔn)規(guī)則,而是根據(jù)外層(函數(shù)或全局)的作用域來決定 this
function foo() {
    // 返回一個(gè)箭頭函數(shù)
    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() 內(nèi)部創(chuàng)建的箭頭函數(shù)會(huì)捕獲調(diào)用時(shí) foo() 的 this。由于 foo() 的 this 綁定到 obj1, bar(引用箭頭函數(shù))的 this 也會(huì)綁定到 obj1,箭頭函數(shù)的綁定無法被修改。(new 也不行?。?/li>
  • 在 ES6 之前,我們也有使用和箭頭函數(shù)一樣的模式,如下代碼:
function foo() {
    var self = this; // this 快照
    setTimeout( function(){
        console.log( self.a );
    }, 100 );
}

var obj = {
    a: 2
};

foo.call( obj ); // 2
  • 雖然 self = this 和箭頭函數(shù)看起來都可以取代 bind(..),但是從本質(zhì)上來說,它們想替代的是 this 機(jī)制。

小結(jié)

  1. 判斷 this 指向
    1. 是否在 new 中調(diào)用(new 綁定), this 指向新創(chuàng)建的對(duì)象
    2. 是否通過 call、apply(顯示綁定),this 指向綁定的對(duì)象
    3. 是否在某個(gè)對(duì)象中調(diào)用(隱式綁定),this 指向綁定對(duì)象的上下文
    4. 如果都不是,則是默認(rèn)綁定,在嚴(yán)格模式下,this 指向 undefined, 非嚴(yán)格模式下,this 指向全局對(duì)象。
  2. 箭頭函數(shù)不會(huì)使用上述的四條規(guī)則,而是根據(jù)當(dāng)前的詞法作用域來決定 this 的。箭頭函數(shù)會(huì)繼承外層函數(shù)調(diào)用的 this 綁定(無論 this 綁定到什么)。與 ES6 之前的 self = this 的機(jī)制一樣。
  3. 注意:對(duì)于默認(rèn)綁定來說,決定 this 綁定對(duì)象的并不是調(diào)用位置是否處于嚴(yán)格模式,而是函數(shù)體是否處于嚴(yán)格模式。如果函數(shù)體處于嚴(yán)格模式,this 會(huì)被綁定到 undefined,否則this 會(huì)被綁定到全局對(duì)象。

特殊字符描述:

  1. 問題標(biāo)注 Q:(question)
  2. 答案標(biāo)注 R:(result)
  3. 注意事項(xiàng)標(biāo)準(zhǔn):A:(attention matters)
  4. 詳情描述標(biāo)注:D:(detail info)
  5. 總結(jié)標(biāo)注:S:(summary)
  6. 分析標(biāo)注:Ana:(analysis)
  7. 提示標(biāo)注:T:(tips)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容