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

前言

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

為什么要用 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
  • 這段代碼再不同的上下文對(duì)象(me 和 you) 中重復(fù)使用函數(shù) identify() 和 speak(), 不用針對(duì)每個(gè)對(duì)象編寫(xiě)不同版本的函數(shù)。
  • 若不使用 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

消除對(duì) this 的誤解

  • 在解釋下 this 到底是如何工作的,首先必需消除對(duì) this 的錯(cuò)誤認(rèn)識(shí)。

指向自身

  • 為什么需要從函數(shù)內(nèi)部引用函數(shù)自身呢?
    • 最常見(jiàn)的原因是遞歸。
  • 其實(shí) this 并不像我們所想的那樣指向函數(shù)本身。
  • 考慮以下代碼:
function foo(num) {
    console.log( "foo: " + num );

 // 記錄 foo 被調(diào)用的次數(shù)
    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 被調(diào)用了多少次?
console.log( foo.count ); // 這里會(huì)輸出多少次呢?
  • 先思考,后查看
    <details>
    <summary>查看答案</summary>
    <pre>
function foo(num) {
    console.log( "foo: " + num );
// 記錄 foo 被調(diào)用的次數(shù)
    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 被調(diào)用了多少次?
console.log( foo.count ); // 0
// 為什么會(huì)輸出 0 呢?
// 從字面意思來(lái)看,上面的函數(shù)執(zhí)行了 4 此,理應(yīng)來(lái)說(shuō), foo.count 應(yīng)該是 4 才對(duì)。

</pre>
</details>

  • 當(dāng)執(zhí)行 foo.count = 0; 時(shí),的確向函數(shù)對(duì)象 foo 中添加了一個(gè)屬性 count, 但是函數(shù)內(nèi)部代碼中 this.count 中的 this 并不是指向那個(gè)函數(shù)對(duì)象,雖然屬性名相同,跟對(duì)象卻并不相同,困惑隨之產(chǎn)生。
  • 如果你會(huì)有 “如果我增加的 count 屬性和預(yù)期的不一樣,那我增加的是那個(gè) count?”疑惑。實(shí)際上,如果你讀過(guò)之前的文章,就會(huì)發(fā)現(xiàn)這段代碼會(huì)隱式地創(chuàng)建一個(gè)全局變量 count。它的值為 NaN。如果你發(fā)現(xiàn)為什么是這么個(gè)奇怪的結(jié)果,那你肯定會(huì)有 “為什么它的值是 NaN, 而不是其他值?” 的疑惑。(原理參考:https://mp.weixin.qq.com/s/H1gpn0vfmUwrglMZwk2gzw)
  • 當(dāng)然也有方法對(duì)上述代碼進(jìn)行規(guī)避:
function foo(num) {
    console.log( "foo: " + num );

 // 記錄 foo 被調(diào)用的次數(shù)
    data.count++;
}

var data = {
    count: 0
};

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
        foo( i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// foo 被調(diào)用了多少次?
console.log( data.count ); // 4
  • 雖然從某種角度來(lái)說(shuō),解決了問(wèn)題,但忽略了真正的問(wèn)題——無(wú)法理解 this 的含義和工作原理,上述代碼而是返回了舒適區(qū)——詞法作用域。
  • 上面提到的如果匿名函數(shù)需要引用自身,除了 this 還有已經(jīng)被廢棄的 arguments.callee 來(lái)引用當(dāng)前正在運(yùn)行的函數(shù)對(duì)象。
  • 對(duì)于上述提到的代碼,更進(jìn)階的方式就是使用 foo 標(biāo)識(shí)符來(lái)替代 this 來(lái)引用函數(shù)對(duì)象,如下代碼:
function foo(num) {
    console.log( "foo: " + num );

    // 記錄 foo 被調(diào)用的次數(shù)
    foo.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 被調(diào)用了多少次?
console.log( foo.count ); // 4
  • 這種解決方式依然規(guī)避了 this 問(wèn)題,并且完全依賴(lài)于變量 foo 的詞法作用域。
  • 更進(jìn)階的方式是強(qiáng)制 this 指向 foo 函數(shù)對(duì)象, 使用 call, bind, apply 關(guān)鍵字來(lái)實(shí)現(xiàn)。
function foo(num) {
    console.log( "foo: " + num );

 // 記錄 foo 被調(diào)用的次數(shù)
 // 注意,在當(dāng)前的調(diào)用方式下(參見(jiàn)下方代碼),this 確實(shí)指向 foo
    this.count++;
}

foo.count = 0;

var i;

for (i=0; i<10; i++) {
    if (i > 5) {
 // 使用 call(..) 可以確保 this指向函數(shù)對(duì)象 foo 本身
        foo.call( foo, i );
    }
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9

// foo 被調(diào)用了多少次?
console.log( foo.count ); // 4

它的作用域

  • 常見(jiàn)的誤解:this 指向函數(shù)的作用域,其實(shí)在某種情況下是正確的,但在其他情況下是錯(cuò)誤的。
  • 其實(shí),this 在任何情況下都不指向函數(shù)的詞法作用域。
  • 考慮一下代碼:
function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo();

// 這段代碼你一共能發(fā)現(xiàn)幾處錯(cuò)誤?并且報(bào)錯(cuò)后會(huì)拋出什么?
  • 先思考,后查看
    <details>
    <summary>查看答案</summary>
    <pre>
function foo() {
    var a = 2;
    this.bar();
    // bar(); // 當(dāng)前方式會(huì)根據(jù)詞法作用域的規(guī)則來(lái)查找 bar() 方法,并且調(diào)用它
}
function bar() {
    console.log(this.a); // ReferenceError: a is not defined
    // console.log(a); // 這種方式也不行,因?yàn)楹瘮?shù)會(huì)創(chuàng)建一個(gè)塊作用域,所以無(wú)法通過(guò) bar 的作用域訪(fǎng)問(wèn)到上層 foo 作用域。
}
foo(); // TypeError: this.bar is not a function

</pre>
</details>

  • 首先,這段代碼試圖通過(guò) this.bar() 來(lái)引用 bar() 函數(shù)。這是絕對(duì)不可能成功的,我們之后會(huì)解釋原因。調(diào)用 bar() 最自然的方法是省略前面的 this,直接使用詞法引用標(biāo)識(shí)符。

  • 此外,編寫(xiě)這段代碼的開(kāi)發(fā)者還試圖使用 this 聯(lián)通 foo() 和 bar() 的詞法作用域,從而讓bar() 可以訪(fǎng)問(wèn) foo() 作用域里的變量 a。這是不可能實(shí)現(xiàn)的,你不能使用 this 來(lái)引用一個(gè)詞法作用域內(nèi)部的東西。

this 到底是什么

  • 說(shuō)了這么多,那 this 到底是一個(gè)什么樣的機(jī)制呢?
    • 之前我們說(shuō)過(guò) this 是在運(yùn)行時(shí)進(jìn)行綁定的,而不是在編寫(xiě)時(shí)綁定的,它的上下文取決于函數(shù)調(diào)用時(shí)的各種條件。
    • this 的綁定和函數(shù)聲明的位置沒(méi)有任何關(guān)系,只取決于函數(shù)的調(diào)用方式。
  • 當(dāng)一個(gè)函數(shù)被調(diào)用是,會(huì)創(chuàng)建一個(gè)執(zhí)行上下文,這個(gè)執(zhí)行上下文匯總會(huì)包含函數(shù)在哪里被調(diào)用(也就是調(diào)用棧),函數(shù)的調(diào)用方法, 傳入的參數(shù)等信息。而 this 就是這樣一個(gè)屬性,會(huì)在函數(shù)執(zhí)行的過(guò)程中被用到。

小結(jié)

  • 學(xué)習(xí) this 的第一步要明白 this 既不指向函數(shù)自身也不指向函數(shù)的詞法作用域。
  • this 實(shí)際上是在函數(shù)被調(diào)用時(shí)發(fā)生的綁定,它指向什么完全取決于函數(shù)在哪里被調(diào)用。

特殊字符描述:

  1. 問(wèn)題標(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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