深入理解JavaScript this

要說 JavaScript這門語(yǔ)言最容易讓人困惑的知識(shí)點(diǎn),this 關(guān)鍵詞肯定算一個(gè)鞠柄。JavaScript 語(yǔ)言面世多年侦高,一直在進(jìn)化完善,現(xiàn)在在服務(wù)器上還可以通過 node.js 來跑 JavaScript厌杜。顯然奉呛,這門語(yǔ)言還會(huì)活很久。

所以說,我一直相信瞧壮,如果你是一個(gè) JavaScript 開發(fā)者或者說開發(fā)者登馒,學(xué)好 JavaScript 的運(yùn)作原理以及語(yǔ)言特點(diǎn)肯定對(duì)你以后大有好處。

開始之前

在開始正文之前馁痴,我強(qiáng)烈推薦你先掌握好下面的知識(shí):

  • 變量作用域和作用域提升
  • JavaScript 的函數(shù)
  • 閉包

如果沒有對(duì)這些基礎(chǔ)知識(shí)掌握踏實(shí)谊娇,直接討論 JavaScript 的 this 關(guān)鍵詞只會(huì)讓你感到更加地困惑和挫敗。

我為什么要學(xué) this罗晕?

如果上面的簡(jiǎn)單介紹沒有說服你來深入探索 this 關(guān)鍵詞济欢,那我用這節(jié)來講講為什么要學(xué)。

考慮這樣一個(gè)重要問題小渊,假設(shè)開發(fā)者法褥,比如 Douglas Crockford (譯者注:JavaScript 領(lǐng)域必知牛人),不再使用 newthis酬屉,轉(zhuǎn)而使用完完全全的函數(shù)式寫法來做代碼復(fù)用半等,會(huì)怎樣?

事實(shí)上呐萨,基于 JavaScript 內(nèi)置的現(xiàn)成的原型繼承功能杀饵,我們已經(jīng)使用并且將繼續(xù)廣泛使用 newthis 關(guān)鍵詞來實(shí)現(xiàn)代碼復(fù)用。

理由一谬擦,如果只能使用自己寫過的代碼切距,你是沒法工作的。現(xiàn)有的代碼以及你讀到這句話時(shí)別人正在寫的代碼都很有可能包含 this 關(guān)鍵詞惨远。那么學(xué)習(xí)怎么用好它是不是很有用呢谜悟?

因此,即使你不打算在你的代碼庫(kù)中使用它北秽,深入掌握 this 的原理也能讓你在接手別人的代碼理解其邏輯時(shí)事半功倍葡幸。

理由二,拓展你的編碼視野和技能贺氓。使用不同的設(shè)計(jì)模式會(huì)加深你對(duì)代碼的理解蔚叨,怎么去看、怎么去讀辙培、怎么去寫缅叠、怎么去理解。我們寫代碼不僅是給機(jī)器去解析虏冻,還是寫給我們自己看的肤粱。這不僅適用于 JavaScript,對(duì)其他編程語(yǔ)言亦是如此厨相。

隨著對(duì)編程理念的逐步深入理解领曼,它會(huì)逐漸塑造你的編碼風(fēng)格鸥鹉,不管你用的是什么語(yǔ)言什么框架。

就像畢加索會(huì)為了獲得靈感而涉足那些他并不是很贊同很感興趣的領(lǐng)域庶骄,學(xué)習(xí) this 會(huì)拓展你的知識(shí)毁渗,加深對(duì)代碼的理解。

什么是 this 单刁?

在我開始講解前灸异,如果你學(xué)過一門基于類的面向?qū)ο缶幊陶Z(yǔ)言(比如 C#,Java羔飞,C++)肺樟,那請(qǐng)將你對(duì) this 這個(gè)關(guān)鍵詞應(yīng)該是做什么用的先入為主的概念扔到垃圾桶里。JavaScript 的 this 關(guān)鍵詞是很不一樣逻淌,因?yàn)?JavaScript 本來就不是一門基于類的面向?qū)ο缶幊陶Z(yǔ)言么伯。

雖說 ES6 里面 JavaScript 提供了類這個(gè)特性給我們用,但它只是一個(gè)語(yǔ)法糖卡儒,一個(gè)基于原型繼承的語(yǔ)法糖田柔。

this 就是一個(gè)指針,指向我們調(diào)用函數(shù)的對(duì)象骨望。

我難以強(qiáng)調(diào)上一句話有多重要硬爆。請(qǐng)記住,在 Class 添加到 ES6 之前擎鸠,JavaScript 中沒有 Class 這種東西摆屯。Class 只不過是一個(gè)將對(duì)象串在一起表現(xiàn)得像類繼承一樣的語(yǔ)法糖,以一種我們已經(jīng)習(xí)慣的寫法糠亩。所有的魔法背后都是用原型鏈編織起來的。

如果上面的話不好理解准验,那你可以這樣想赎线,this 的上下文跟英語(yǔ)句子的表達(dá)很相似。比如下面的例子

Bob.callPerson(John);

就可以用英語(yǔ)寫成 “Bob called a person named John”糊饱。由于 callPerson() 是 Bob 發(fā)起的垂寥,那 this 就指向 Bob。我們將在下面的章節(jié)深入更多的細(xì)節(jié)另锋。到了這篇文章結(jié)束時(shí)滞项,你會(huì)對(duì) this 關(guān)鍵詞有更好的理解(和信心)。

執(zhí)行上下文

執(zhí)行上下文 是語(yǔ)言規(guī)范中的一個(gè)概念夭坪,用通俗的話講文判,大致等同于函數(shù)的執(zhí)行“環(huán)境”。具體的有:變量作用域(和 作用域鏈條室梅,閉包里面來自外部作用域的變量)戏仓,函數(shù)參數(shù)疚宇,以及 this 對(duì)象的值。

引自: Stackoverflow.com

記住赏殃,現(xiàn)在起敷待,我們專注于查明 this 關(guān)鍵詞到底指向哪。因此仁热,我們現(xiàn)在要思考的就一個(gè)問題:

  • 是什么調(diào)用函數(shù)榜揖?是哪個(gè)對(duì)象調(diào)用了函數(shù)?

為了理解這個(gè)關(guān)鍵概念抗蠢,我們來測(cè)一下下面的代碼举哟。

var person = {
    name: "Jay",
    greet: function() {
        console.log("hello, " + this.name);
    }
};
person.greet();

誰(shuí)調(diào)用了 greet 函數(shù)?是 person 這個(gè)對(duì)象對(duì)吧物蝙?在 greet() 調(diào)用的左邊是一個(gè) person 對(duì)象炎滞,那么 this 關(guān)鍵詞就指向 personthis.name 就等于 "Jay"∥芷颍現(xiàn)在册赛,還是用上面的例子,我加點(diǎn)料:

var greet = person.greet; // 將函數(shù)引用存起來;
greet(); // 調(diào)用函數(shù)

你覺得在這種情況下控制臺(tái)會(huì)輸出什么震嫉?“Jay”森瘪?undefined?還是別的票堵?

正確答案是 undefined扼睬。如果你對(duì)這個(gè)結(jié)果感到驚訝,不必慚愧悴势。你即將學(xué)習(xí)的東西將幫助你在 JavaScript 旅程中打開關(guān)鍵的大門窗宇。

this 的值并不是由函數(shù)定義放在哪個(gè)對(duì)象里面決定,而是函數(shù)執(zhí)行時(shí)由誰(shuí)來喚起決定特纤。

對(duì)于這個(gè)意外的結(jié)果我們暫且壓下军俊,繼續(xù)看下去。(感覺前后銜接得不夠流暢)

帶著這個(gè)困惑捧存,我們接著測(cè)試下 this 三種不同的定義方式粪躬。

找出 this 的指向

上一節(jié)我們已經(jīng)對(duì) this 做了測(cè)試。但是這塊知識(shí)實(shí)在重要昔穴,我們需要再好好琢磨一下镰官。在此之前,我想用下面的代碼給你出個(gè)題:

var name = "Jay Global";
var person = {
    name: 'Jay Person',
    details: {
        name: 'Jay Details',
        print: function() {
            return this.name;
        }
    },
    print: function() {
        return this.name;
    }
};
console.log(person.details.print());  // ?
console.log(person.print());          // ?
var name1 = person.print;
var name2 = person.details;
console.log(name1()); // ?
console.log(name2.print()) // ?

console.log() 將會(huì)輸出什么吗货,把你的答案寫下來泳唠。如果你還想不清楚,復(fù)習(xí)下上一節(jié)宙搬。

準(zhǔn)備好了嗎警检?放松心情孙援,我們來看下面的答案。

答案和解析

person.details.print()

首先扇雕,誰(shuí)調(diào)用了 print 函數(shù)拓售?在 JavaScript 中我們都是從左讀到右。于是 this 指向 details 而不是 person镶奉。這是一個(gè)很重要的區(qū)別础淤,如果你對(duì)這個(gè)感到陌生,那趕緊把它記下哨苛。

print 作為 details 對(duì)象的一個(gè) key鸽凶,指向一個(gè)返回 this.name 的函數(shù)。既然我們已經(jīng)找出 this 指向 details 建峭,那函數(shù)的輸出就應(yīng)該是 'Jay Details'玻侥。

person.print()

再來一次,找出 this 的指向亿蒸。print() 是被 person 對(duì)象調(diào)用的凑兰,沒錯(cuò)吧?

在這種情況边锁,person 里的 print 函數(shù)返回 this.name姑食。this 現(xiàn)在指向 person 了,那 'Jay Person'就是返回值茅坛。

console.log(name1)

這一題就有點(diǎn)狡猾了音半。在上一行有這樣一句代碼:

var name1 = person.print;

如果你是通過這句來思考的,我不會(huì)怪你贡蓖。很遺憾曹鸠,這樣去想是錯(cuò)的。要記住斥铺,this 關(guān)鍵詞是在函數(shù)調(diào)用時(shí)才做綁定的彻桃。name1() 前面是什么?什么都沒有仅父。因此 this 關(guān)鍵詞就將指向全局的 window對(duì)象去。

因此吗蚌,答案是 'Jay Global'喷面。

name2.print()

看一下 name2 指向哪個(gè)對(duì)象宵蛀,是 details 對(duì)象沒錯(cuò)吧?

所以下面這句會(huì)打印出什么呢省容?如果到目前為止的所有小點(diǎn)你都理解了,那這里稍微思考下你就自然有答案了燎字。

console.log(name2.print()) // ??

答案是 'Jay Details'腥椒,因?yàn)?printname2 調(diào)起的阿宅,而 name2 指向 details

詞法作用域

你可能會(huì)問:“什么是詞法作用域笼蛛?

逗我呢洒放,我們不是在探討 this 關(guān)鍵詞嗎,這個(gè)又是哪里冒出來的滨砍?好吧往湿,當(dāng)我們用起 ES6 的箭頭函數(shù),這個(gè)就要考慮了惋戏。如果你已經(jīng)寫了不止一年的 JavaScript领追,那你很可能已經(jīng)碰到箭頭函數(shù)。隨著 ES6 逐漸成為現(xiàn)實(shí)標(biāo)準(zhǔn)响逢,箭頭函數(shù)也變得越來越常用绒窑。

JavaScript 的詞法作用域 并不好懂。如果你 理解閉包舔亭,那要理解這個(gè)概念就容易多了些膨。來看下下面的小段代碼。

// outerFn 的詞法作用域
var outerFn = function() {
    var n = 5;
    console.log(innerItem);
    // innerFn 的詞法作用域
    var innerFn = function() {  
        var innerItem = "inner";    // 錯(cuò)了分歇。只能坐著電梯向上傀蓉,不能向下。
        console.log(n);
    };
    return innerFn;
};
outerFn()();

想象一下一棟樓里面有一架只能向上走的詭異電梯职抡。

建筑的頂層就是全局 windows 對(duì)象葬燎。如果你現(xiàn)在在一樓,你就可以看到并訪問那些放在樓上的東西缚甩,比如放在二樓的 outerFn 和放在三樓的 window 對(duì)象谱净。

這就是為什么我們執(zhí)行代碼 outerFn()(),它在控制臺(tái)打出了 5 而不是 undefined擅威。

然而壕探,當(dāng)我們?cè)囍?outerFn 詞法作用域下打出日志 innerItem,我們遇到了下面的報(bào)錯(cuò)郊丛。請(qǐng)記住李请,JavaScript 的詞法作用域就好像建筑里面那個(gè)只能向上走的詭異電梯。由于 outerFn 的詞法作用域在 innerFn 上面厉熟,所以它不能向下走到 innerFn 的詞法作用域里面并拿到里面的值导盅。這就是觸發(fā)下面報(bào)錯(cuò)的原因:

test.html:304 Uncaught ReferenceError: innerItem is not defined
at outerFn (test.html:304)
at test.html:313

this 和箭頭函數(shù)

在 ES6 里面,不管你喜歡與否揍瑟,箭頭函數(shù)被引入了進(jìn)來白翻。對(duì)于那些還沒用慣箭頭函數(shù)或者新學(xué) JavaScript 的人來說,當(dāng)箭頭函數(shù)和 this 關(guān)鍵詞混合使用時(shí)會(huì)發(fā)生什么绢片,這個(gè)點(diǎn)可能會(huì)給你帶來小小的困惑和淡淡的憂傷滤馍。那這個(gè)小節(jié)就是為你們準(zhǔn)備的岛琼!

當(dāng)涉及到 this 關(guān)鍵詞,箭頭函數(shù)普通函數(shù) 主要的不同是什么巢株?

答案:

箭頭函數(shù)按詞法作用域來綁定它的上下文槐瑞,所以 this 實(shí)際上會(huì)引用到原來的上下文。

引自:hackernoon.com

我實(shí)在沒法給出比這個(gè)更好的總結(jié)纯续。

箭頭函數(shù)保持它當(dāng)前執(zhí)行上下文的詞法作用域不變随珠,而普通函數(shù)則不會(huì)。換句話說猬错,箭頭函數(shù)從包含它的詞法作用域中繼承到了 this 的值窗看。

我們不妨來測(cè)試一些代碼片段,確保你真的理解了倦炒。想清楚這塊知識(shí)點(diǎn)未來會(huì)讓你少點(diǎn)頭痛显沈,因?yàn)槟銜?huì)發(fā)現(xiàn) this 關(guān)鍵詞和箭頭函數(shù)太經(jīng)常一起用了。

示例

仔細(xì)閱讀下面的代碼片段逢唤。

var object = {
    data: [1,2,3],
    dataDouble: [1,2,3],
    double: function() {
        console.log("this inside of outerFn double()");
        console.log(this);
        return this.data.map(function(item) {
            console.log(this);      // 這里的 this 是什么拉讯??
            return item * 2;
        });
    },
    doubleArrow: function() {
        console.log("this inside of outerFn doubleArrow()");
        console.log(this);
        return this.dataDouble.map(item => {
            console.log(this);      // 這里的 this 是什么鳖藕?魔慷?
            return item * 2;
        });
    }
};
object.double();
object.doubleArrow();

如果我們看執(zhí)行上下文,那這兩個(gè)函數(shù)都是被 object 調(diào)用的著恩。所以院尔,就此斷定這兩個(gè)函數(shù)里面的 this 都指向 object 不為過吧?是的喉誊,但我建議你拷貝這段代碼然后自己測(cè)一下邀摆。

這里有個(gè)大問題:

arrow()doubleArrow() 里面的 map 函數(shù)里面的 this 又指向哪里呢?

上一張圖已經(jīng)給了一個(gè)大大的提示伍茄。如果你還不確定栋盹,那請(qǐng)花5分鐘將我們上一節(jié)討論的內(nèi)容再好好想想。然后敷矫,根據(jù)你的理解例获,在實(shí)際執(zhí)行代碼前把你認(rèn)為的 this 應(yīng)該指向哪里寫下來。在下一節(jié)我們將會(huì)回答這個(gè)問題曹仗。

回顧執(zhí)行上下文

這個(gè)標(biāo)題已經(jīng)把答案泄露出來了榨汤。在你看不到的地方,map 函數(shù)對(duì)調(diào)用它的數(shù)組進(jìn)行遍歷整葡,將數(shù)組的每一項(xiàng)傳到回調(diào)函數(shù)里面并把執(zhí)行結(jié)果返回件余。如果你對(duì) JavaScript 的 map 函數(shù)不太了解或有所好奇讥脐,可以讀讀這個(gè)了解更多遭居。

總之啼器,由于 map() 是被 this.data 調(diào)起的,于是 this 將指向那個(gè)存儲(chǔ)在 data 這個(gè) key 里面的數(shù)組俱萍,即 [1,2,3]端壳。同樣的邏輯,this.dataDouble 應(yīng)該指向另一個(gè)數(shù)組枪蘑,值為 [1,2,3]损谦。

現(xiàn)在,如果函數(shù)是 object 調(diào)用的岳颇,我們已經(jīng)確定 this 指向 object 對(duì)吧照捡?好,那來看看下面的代碼片段话侧。

double: function() {
    return this.data.map(function(item) {
        console.log(this);      // 這里的 this 是什么栗精??
        return item * 2;
    });
}

這里有個(gè)很有迷惑性的問題:傳給 map() 的那個(gè)匿名函數(shù)是誰(shuí)調(diào)用的瞻鹏?答案是:這里沒有一個(gè)對(duì)象是悲立。為了看得更明白,這里給出一個(gè) map 函數(shù)的基本實(shí)現(xiàn)新博。

// Array.map polyfill
if (Array.prototype.map === undefined) {
    Array.prototype.map = function(fn) {
        var rv = [];
        for(var i=0, l=this.length; i<l; i++)
            rv.push(fn(this[i]));
        return rv;
    };
}

fn(this[i])); 前面有什么對(duì)象嗎薪夕?沒。因此赫悄,this 關(guān)鍵詞指向全局的 windows 對(duì)象原献。那,為什么 this.dataDouble.map 使用了箭頭函數(shù)會(huì)使得 this 指向 object 呢涩蜘?

我想再說一遍這句話嚼贡,因?yàn)樗鼘?shí)在很重要:

箭頭函數(shù)按詞法作用域?qū)⑺纳舷挛慕壎ǖ?原來的上下文

現(xiàn)在,你可能會(huì)問:原來的上下文是什么同诫?問得好粤策!

誰(shuí)是 doubleArrow() 的初始調(diào)用者?就是 object 對(duì)吧误窖?那它就是原來的上下文

this 和 use strict

為了讓 JavaScript 更加健壯及盡量減少人為出錯(cuò)叮盘,ES5 引進(jìn)了嚴(yán)格模式。一個(gè)典型的例子就是 this 在嚴(yán)格模式下的表現(xiàn)霹俺。你如果想按照嚴(yán)格模式來寫代碼柔吼,你只需要在你正在寫的代碼的作用域最頂端加上這么一行 "use strict;"

記住丙唧,傳統(tǒng)的 JavaScript 只有函數(shù)作用域愈魏,沒有塊作用域。舉個(gè)例子:

function strict() {
    // 函數(shù)級(jí)嚴(yán)格模式寫法
    'use strict';
    function nested() { return 'And so am I!'; }
    return "Hi!  I'm a strict mode function!  " + nested();
}
function notStrict() { return "I'm not strict."; }

代碼片段來自 Mozilla Developer Network。

不過呢培漏,ES6 里面通過 let 關(guān)鍵詞提供了塊作用域的特性溪厘。

現(xiàn)在,來看一段簡(jiǎn)單代碼牌柄,看下 this 在嚴(yán)格模式和非嚴(yán)格模式下會(huì)怎么表現(xiàn)畸悬。在繼續(xù)之前,請(qǐng)將下面的代碼運(yùn)行一下珊佣。

(function() {
    "use strict";
    console.log(this);
})();
(function() {
    // 不使用嚴(yán)格模式
    console.log(this);
})();

正如你看到的蹋宦,this 在嚴(yán)格模式下指向 undefined。相對(duì)的咒锻,非嚴(yán)格模式下 this 指向全局變量 window冷冗。大部分情況下,開發(fā)者使用 this 惑艇,并不希望它指向全局 window 對(duì)象贾惦。嚴(yán)格模式幫我們?cè)谑褂?this 關(guān)鍵詞時(shí),盡量少做搬起石頭砸自己腳的蠢事敦捧。

舉個(gè)例子须板,如果全局的 window 對(duì)象剛好有一個(gè) key 的名字和你希望訪問到的對(duì)象的 key 相同,會(huì)怎樣兢卵?上代碼吧:

(function() {
    // "use strict";
    var item = {
        document: "My document",
        getDoc: function() {
            return this.document;
        }
    }
    var getDoc = item.getDoc;
    console.log(getDoc());
})();

這段代碼有兩個(gè)問題习瑰。

  1. this 將不會(huì)指向 item
  2. 如果程序在非嚴(yán)格模式下運(yùn)行秽荤,將不會(huì)有錯(cuò)誤拋出甜奄,因?yàn)槿值?window 對(duì)象也有一個(gè)名為 document 的屬性。

在這個(gè)簡(jiǎn)單示例中窃款,因?yàn)榇a較短也就不會(huì)形成大問題课兄。

如果你是在生產(chǎn)環(huán)境像上面那樣寫,當(dāng)用到 getDoc 返回的數(shù)據(jù)時(shí)晨继,你將收獲一堆難以定位的報(bào)錯(cuò)烟阐。如果你代碼庫(kù)比較大,對(duì)象間互動(dòng)比較多紊扬,那問題就更嚴(yán)重了蜒茄。

值得慶幸的是,如果我們是在嚴(yán)格模式下跑這段代碼餐屎,由于 this 是 undefined檀葛,于是立刻就有一個(gè)報(bào)錯(cuò)拋給我們:

test.html:312 Uncaught TypeError: Cannot read property 'document' of undefined at getDoc (test.html:312) at test.html:316 at test.html:317

明確設(shè)置執(zhí)行上下文

先前假定大家都對(duì)執(zhí)行上下文不熟,于是我們聊了很多關(guān)于執(zhí)行上下文和 this 的知識(shí)腹缩。

讓人歡喜讓人憂的是屿聋,在 JavaScript 中通過使用內(nèi)置的特性開發(fā)者就可以直接操作執(zhí)行上下文了空扎。這些特性包括:

  • bind():不需要執(zhí)行函數(shù)就可以將 this 的值準(zhǔn)確設(shè)置到你選擇的一個(gè)對(duì)象上。還可以通過逗號(hào)隔開傳遞多個(gè)參數(shù)润讥,如 func.bind(this, param1, param2, ...) 勺卢。
  • apply():將 this 的值準(zhǔn)確設(shè)置到你選擇的一個(gè)對(duì)象上。第二個(gè)參數(shù)是一個(gè)數(shù)組象对,數(shù)組的每一項(xiàng)是你希望傳遞給函數(shù)的參數(shù)。最后宴抚,執(zhí)行函數(shù)勒魔。
  • call():將 this 的值準(zhǔn)確設(shè)置到你選擇的一個(gè)對(duì)象上,然后想 bind 一樣通過逗號(hào)分隔傳遞多個(gè)參數(shù)給函數(shù)菇曲。如:print.call(this, param1, param2, ...)冠绢。最后,執(zhí)行函數(shù)常潮。

上面提到的所有內(nèi)置函數(shù)都有一個(gè)共同點(diǎn)弟胀,就是它們都是用來將 this 關(guān)鍵詞指向到其他地方。這些特性可以讓我們玩一些騷操作喊式。只是呢孵户,這個(gè)話題太廣了都?jí)驅(qū)懞脦灼恼铝耍院?jiǎn)潔起見岔留,這篇文章我不打算展開它的實(shí)際應(yīng)用夏哭。

重點(diǎn):上面那三個(gè)函數(shù),只有 bind() 在設(shè)置好 this 關(guān)鍵詞后不立刻執(zhí)行函數(shù)献联。

什么時(shí)候用 bind竖配、call 和 apply

你可能在想:現(xiàn)在已經(jīng)很亂了,學(xué)習(xí)所有這些的目的是什么里逆?

首先进胯,你會(huì)看到 bind、call 和 apply 這幾個(gè)函數(shù)到處都會(huì)用到原押,特別是在一些大型的庫(kù)和框架胁镐。如果你沒理解它做了些什么,那可憐的你就只用上了 JavaScript 提供的強(qiáng)大能力的一小部分而已诸衔。

如果你不想了解一些可能的用法而想立刻讀下去希停,當(dāng)然了,你可以直接跳過這節(jié)署隘,沒關(guān)系宠能。

下面列出來的應(yīng)用場(chǎng)景都是一些具有深度和廣度的話題(一篇文章基本上是講不完的),所以我放了一些鏈接供你深度閱讀用磁餐。未來我可能會(huì)在這篇終極指南里面繼續(xù)添加新的小節(jié)违崇,這樣大家就可以一次看過癮阿弃。

  1. 方法借用
  2. 柯里化
  3. 偏函數(shù)應(yīng)用
  4. 依賴注入

如果我漏掉了其他實(shí)踐案例,請(qǐng)留言告知羞延。我會(huì)經(jīng)常來優(yōu)化這篇指南渣淳,這樣你作為讀者就可以讀到最豐富的內(nèi)容。

閱讀高質(zhì)量的開源代碼可以升級(jí)你的知識(shí)和技能伴箩。

講真入愧,你會(huì)在一些開源代碼上看到 this 關(guān)鍵詞、call嗤谚、apply 和 bind 的實(shí)際應(yīng)用棺蛛。我會(huì)將這塊結(jié)合著其他能幫你成為更好的程序員的方法一起講。

在我看來巩步,開始閱讀最好的開源代碼是 underscore旁赊。它并不像其他開源項(xiàng)目,如 d3椅野,那樣鐵板一塊终畅,而是內(nèi)部代碼相互比較獨(dú)立,因而它是教學(xué)用的最佳選擇竟闪。另外离福,它代碼簡(jiǎn)潔,文檔詳細(xì)炼蛤,編碼風(fēng)格也是相當(dāng)容易學(xué)習(xí)术徊。

JavaScript 的 this 和 bind

前面提到了,bind 允許你明確設(shè)定 this 的指向而不用實(shí)際去執(zhí)行函數(shù)鲸湃。這里是一個(gè)簡(jiǎn)單示例:

var bobObj = {
    name: "Bob"
};
function print() {
    return this.name;
}
// 將 this 明確指向 "bobObj"
var printNameBob = print.bind(bobObj);
console.log(printNameBob());    // this 會(huì)指向 bob赠涮,于是輸出結(jié)果是 "Bob"

在上面的示例中,如果你把 bind 那行去掉暗挑,那 this 將會(huì)指向全局 window 對(duì)象笋除。

這好像很蠢,但在你想將 this 綁定到具體對(duì)象前你就必須用 bind 來綁定炸裆。在某些場(chǎng)景下垃它,我們可能想從另一個(gè)對(duì)象中借用一些方法。舉個(gè)例子烹看,

var obj1 = {
    data: [1,2,3],
    printFirstData: function() {
        if (this.data.length)
            return this.data[0];
    }
};
var obj2 = {
    data: [4,5,6],
    printSecondData: function() {
        if (this.data.length > 1)
            return this.data[1];
    }
};
// 在 obj1 中借用 obj2 的方法
var getSecondData = obj2.printSecondData.bind(obj1);
console.log(getSecondData());   // 輸出 2

在這個(gè)代碼片段里国拇,obj2 有一個(gè)名為 printSecondData 的方法,而我們想將這個(gè)方法借給 obj1惯殊。在下一行

var getSecondData = obj2.printSecondData.bind(obj1);

通過使用 bind 酱吝,我們讓 obj1 可以訪問 obj2printSecondData 方法。

練習(xí)

在下面的代碼中

var object = {
    data: [1,2,3],
    double: function() {
        this.data.forEach(function() {
            // Get this to point to object.
            console.log(this);
        });
    }
};
object.double();

怎么讓 this 關(guān)鍵詞指向 object土思。提示:你并不需要重寫 this.data.forEach务热。

答案

在上一節(jié)中忆嗜,我們了解了執(zhí)行上下文。如果你對(duì)匿名函數(shù)調(diào)用那部分看得夠細(xì)心崎岂,你就知道它并不會(huì)作為某個(gè)對(duì)象的方法被調(diào)用捆毫。因此,this 關(guān)鍵詞指向了全局 window 對(duì)象冲甘。

于是我們需要將 object 作為上下文綁定到匿名函數(shù)上绩卤,使得里面的 this 指向 object。現(xiàn)在江醇,double 函數(shù)跑起來時(shí)濒憋,是 object 調(diào)用了它,那么 double 里面的 this 指向 object嫁审。

var object = {
    data: [1,2,3],
    double: function() {
        return this.data.forEach(function() {
            // Get this to point to object.
            console.log(this);
        }.bind(this));
    }
};
object.double();

那,如果我們像下面這樣做呢赖晶?

var double = object.double;
double();   // 律适??

double() 的調(diào)用上下文是什么遏插?是全局上下文捂贿。于是,我們就會(huì)看到下面的報(bào)錯(cuò)胳嘲。

Uncaught TypeError: Cannot read property 'forEach' of undefined at double (test.html:282) at test.html:289

所以厂僧,當(dāng)我們用到 this 關(guān)鍵詞時(shí),就要小心在意我們調(diào)用函數(shù)的方式了牛。我們可以在提供 API 給用戶時(shí)固定 this 關(guān)鍵詞颜屠,以此減少這種類型的錯(cuò)誤。但請(qǐng)記住鹰祸,這么做的代價(jià)是犧牲了靈活性甫窟,所以做決定前要考慮清楚。

var double = object.double.bind(object);
double();  // 不再報(bào)錯(cuò)

JavaScript this 和 call

call 方法和 bind 很相似蛙婴,但就如它名字所暗示的粗井,call 會(huì)立刻呼起(執(zhí)行)函數(shù),這是兩個(gè)函數(shù)的最大區(qū)別街图。

var item = {
    name: "I am"
};
function print() {
    return this.name;
}
// 立刻執(zhí)行
var printNameBob = console.log(print.call(item));

call浇衬、applybind 大部分使用場(chǎng)景是重疊的餐济。作為一個(gè)程序員最重要的還是先了解清楚這三個(gè)方法之間的差異耘擂,從而能根據(jù)它們的設(shè)計(jì)和目的的不同來選用。只要你了解清楚了絮姆,你就可以用一種更有創(chuàng)意的方式來使用它們梳星,寫出更獨(dú)到精彩的代碼赞赖。

在參數(shù)數(shù)量固定的場(chǎng)景,callbind 是不錯(cuò)的選擇冤灾。比如說前域,一個(gè)叫 doLogin 的函數(shù)經(jīng)常是接受兩個(gè)參數(shù):usernamepassword。在這個(gè)場(chǎng)景下韵吨,如果你需要將 this 綁定到一個(gè)特定的對(duì)象上匿垄,callbind 會(huì)挺好用的。

如何使用 call

以前一個(gè)最常用的場(chǎng)景是把一個(gè)類數(shù)組對(duì)象归粉,比如 arguments 對(duì)象椿疗,轉(zhuǎn)化成數(shù)組。舉個(gè)例子:

function convertArgs() {
    var convertedArgs = Array.prototype.slice.call(arguments);
    console.log(arguments);
    console.log(Array.isArray(arguments));  // false
    console.log(convertedArgs);
    console.log(Array.isArray(convertedArgs)); // true
}
convertArgs(1,2,3,4);

在上面的例子中糠悼,我們使用 call 將 argument 對(duì)象轉(zhuǎn)化成一個(gè)數(shù)組届榄。在下一個(gè)例子中,我們將會(huì)調(diào)用一個(gè) Array 對(duì)象的方法倔喂,并將 argument 對(duì)象設(shè)置為方法的 this铝条,以此來將傳進(jìn)來參數(shù)加在一起。

function add (a, b) { 
    return a + b; 
}
function sum() {
    return Array.prototype.reduce.call(arguments, add);
}
console.log(sum(1,2,3,4)); // 10

我們?cè)谝粋€(gè)類數(shù)組對(duì)象上調(diào)用了 reduce 函數(shù)席噩。要知道 arguments 不是一個(gè)數(shù)組班缰,但我們給了它調(diào)用 reduce 方法的能力。如果你對(duì) reduce 感興趣悼枢,可以在這里了解更多埠忘。

練習(xí)

現(xiàn)在是時(shí)候鞏固下你新學(xué)到的知識(shí)。

  1. document.querySelectorAll() 返回一個(gè)類數(shù)組對(duì)象 NodeList馒索。請(qǐng)寫一個(gè)函數(shù)莹妒,它接收一個(gè) CSS 選擇器,然后返回一個(gè)選擇到的 DOM 節(jié)點(diǎn)數(shù)組绰上。
  2. 請(qǐng)寫一個(gè)函數(shù)动羽,它接收一個(gè)由鍵值對(duì)組成的數(shù)組,然后將這些鍵值對(duì)設(shè)置到 this 關(guān)鍵詞指向的對(duì)象上渔期,最后將該對(duì)象返回运吓。如果 this 是 nullundefined,那就新建一個(gè) object疯趟。示例:
`set.call( 
            {name: "jay"}, 
            {age: 10, email: '[[email protected]](/cdn-cgi/l/email-protection)'}); 
// return {name: "jay", 
              age: 10, 
              email: '[[email protected]](/cdn-cgi/l/email-protection)'}`拘哨。

JavaScript this 和 apply

apply 就是接受數(shù)組版本的 call。于是當(dāng)使用 apply 時(shí)信峻,多聯(lián)想下數(shù)組倦青。

將一個(gè)方法應(yīng)用(apply)到一個(gè)數(shù)組上。

我用這句話來記住它盹舞,而且還挺管用产镐。apply 為你的現(xiàn)有堆積的軍火庫(kù)又添加了一樣利器隘庄,增加了很多新的可能,你很快就能體會(huì)到這一點(diǎn)癣亚。

當(dāng)你要處理參數(shù)數(shù)量動(dòng)態(tài)變化的場(chǎng)景丑掺,用 apply 吧。將一系列數(shù)據(jù)轉(zhuǎn)化為數(shù)組并用上 apply 能讓你寫出更好用和更具彈性的代碼述雾,會(huì)讓你的工作更輕松街州。

如何使用 apply

Math.min 和 max 都是可以接受多個(gè)參數(shù)并返回最小值和最大值的函數(shù)。除了直接傳 n 個(gè)參數(shù)玻孟,你也可以將這 n 個(gè)參數(shù)放到一個(gè)數(shù)組里然后借助 apply 將它傳到 min 函數(shù)里唆缴。

Math.min(1,2,3,4); // 返回 1
Math.min([1,2,3,4]); // 返回 NaN。只接受數(shù)字
Math.min.apply(null, [1,2,3,4]); // 返回 1

看暈了嗎黍翎?如果真暈了面徽,那我來解釋下。使用 apply 時(shí)我們要傳一個(gè)數(shù)組因?yàn)樗枰獢?shù)組作為第二個(gè)參數(shù)匣掸。而下面

Math.min.apply(null, [1,2,3,4]); // 返回 1

做的事情基本等同于

Math.min(1,2,3,4); // 返回 1

這就是我想指出來的 apply 的神奇之處趟紊。它和 call 工作原理,不過我們只要傳給它一個(gè)數(shù)組而不是 n 個(gè)參數(shù)旺聚。很好玩對(duì)吧织阳?橋豆麻袋眶蕉,這是否意味著 Math.min.call(null, 1,2,3,4); 執(zhí)行起來和 Math.min.apply(null, [1,2,3,4]); 一樣砰粹?

啊,你說對(duì)了造挽!看來你已經(jīng)開始掌握它了

讓我們來看下另一種用法碱璃。

function logArgs() {
    console.log.apply(console, arguments);
}
logArgs(1,3,'I am a string', {name: "jay", age: "1337"}, [4,5,6,7]);

沒錯(cuò),你甚至可以傳一個(gè)類數(shù)組對(duì)象作為 apply 的第二個(gè)參數(shù)饭入。很酷對(duì)吧嵌器?

練習(xí)

  1. 寫一個(gè)函數(shù),它接受一個(gè)由鍵值對(duì)組成的數(shù)組谐丢,然后將這些鍵值對(duì)設(shè)置到 this 關(guān)鍵詞指向的對(duì)象上爽航,最后將該對(duì)象返回。如果 this 是 nullundefined乾忱,那就新建一個(gè) object讥珍。示例:set.apply( {name: "jay"}, [{age: 10}]); // 返回 {name: "jay", age: 10}
  2. 寫一個(gè)類似 Math.maxmin 的函數(shù),不過接收的不是數(shù)字而是運(yùn)算窄瘟。前兩個(gè)參數(shù)必須是數(shù)字衷佃,而后面的參數(shù)你要將其轉(zhuǎn)化為一個(gè)函數(shù)數(shù)組。下面提供一個(gè)方便你上手理解的示例:
function operate() {
    if (arguments.length < 3) {
        throw new Error("至少要三個(gè)參數(shù)");
    }
    if (typeof arguments[0] !== 'number' || typeof arguments[1] !== 'number') {
        throw new Error("前兩個(gè)參數(shù)必須是數(shù)字");
    }
    // 寫代碼
    // 這是一個(gè)由函數(shù)組成的數(shù)組蹄葱。你可以用 call氏义、apply 或者 bind锄列。但不要直接遍歷參數(shù)然后直接塞到一個(gè)數(shù)組里
    var args;
    var result = 0;
    // 好了,開始吧惯悠,祝好運(yùn)
}
function sum(a, b) {
    return a + b;
}
function multiply(a,b) {
    return a * b;
}
console.log(operate(10, 2, sum, multiply));    // 必須返回 32 -> (10 + 2) + (10 * 2) = 32

其他文章和資料

假如我上面的解釋沒能讓你釋疑邻邮,那下面這些額外的資料可以幫你更好地理解 bind 在 JavaScript 里面是怎么運(yùn)作的。

  • 理解 JavaScript 函數(shù) bind 的原型方法
  • Stackoverflow – 使用 JavaScript 的 bind 函數(shù)
  • JavaScript 中 call()吮螺, apply() 和 bind() 如何使用
  • 一看就懂 —— JavaScript 的 .call() .apply() 和 .bind()

我還強(qiáng)烈推薦你去學(xué)習(xí) JavaScript 原型鏈饶囚,不單是因?yàn)槔锩嬗玫酱罅康?this 關(guān)鍵詞,而且它還是 JavaScript 實(shí)現(xiàn)繼承的標(biāo)準(zhǔn)方式鸠补。

下面列出一些幫你了解 this 如何使用的書籍:

  • 編寫高質(zhì)量 JavaScript代碼的68個(gè)有效方法:雖然是本古董萝风,但此書確實(shí)寫得挺好而且還提供了簡(jiǎn)單易懂的示例,教你怎么用好 this紫岩、apply规惰、call 和 bind 來寫出好代碼。書的作者是 TC39 的一個(gè)成員 Dave Hermann泉蝌,所以你大可放心歇万,他對(duì) JavaScript 肯定理解深刻。
  • 你不知道的 JS —— this 和對(duì)象原型:Kyle Simpson 以一種清晰明了勋陪、對(duì)初學(xué)者很友好的方式贪磺,解釋了對(duì)象和原型是怎么相互影響運(yùn)作起來的,寫得很棒诅愚!

總結(jié)

考慮到 this 關(guān)鍵詞已經(jīng)用到了難以計(jì)量的代碼中寒锚,它是 JavaScript 中我們不得不聊的話題。

一個(gè)優(yōu)秀的藝術(shù)家肯定精于工具的使用违孝。作為一個(gè) JavaScript 開發(fā)者刹前,怎么用好它的特性是最最重要的。

如果你想看到一些從特定角度對(duì) this 關(guān)鍵詞深入剖析的文章或者更多的代碼雌桑,請(qǐng)別忘了告訴我喇喉。這些可能的角度可以是(但不限于)下面這些:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末耍目,一起剝皮案震驚了整個(gè)濱河市膏斤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌制妄,老刑警劉巖掸绞,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡衔掸,警方通過查閱死者的電腦和手機(jī)烫幕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敞映,“玉大人较曼,你說我怎么就攤上這事≌裨福” “怎么了捷犹?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)冕末。 經(jīng)常有香客問我萍歉,道長(zhǎng),這世上最難降的妖魔是什么档桃? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任枪孩,我火速辦了婚禮,結(jié)果婚禮上藻肄,老公的妹妹穿的比我還像新娘蔑舞。我一直安慰自己,他們只是感情好嘹屯,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布攻询。 她就那樣靜靜地躺著,像睡著了一般州弟。 火紅的嫁衣襯著肌膚如雪钧栖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天呆馁,我揣著相機(jī)與錄音桐经,去河邊找鬼毁兆。 笑死浙滤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的气堕。 我是一名探鬼主播纺腊,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼茎芭!你這毒婦竟也來了揖膜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤梅桩,失蹤者是張志新(化名)和其女友劉穎壹粟,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡趁仙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年洪添,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雀费。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡干奢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盏袄,到底是詐尸還是另有隱情忿峻,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布辕羽,位于F島的核電站逛尚,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刁愿。R本人自食惡果不足惜黑低,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酌毡。 院中可真熱鬧克握,春花似錦、人聲如沸枷踏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)旭蠕。三九已至停团,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間掏熬,已是汗流浹背佑稠。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旗芬,地道東北人舌胶。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像疮丛,于是被迫代替她去往敵國(guó)和親幔嫂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345