[譯] 深入淺出 JavaScript 關鍵詞 -- this

老教授發表於2018-05-07

要說 JavaScript 這門語言最容易讓人困惑的知識點,this 關鍵詞肯定算一個。JavaScript 語言面世多年,一直在進化完善,現在在伺服器上還可以通過 node.js 來跑 JavaScript。顯然,這門語言還會活很久。

所以說,我一直相信,如果你是一個 JavaScript 開發者或者說 Web 開發者,學好 JavaScript 的運作原理以及語言特點肯定對你以後大有好處。

開始之前

在開始正文之前,我強烈推薦你先掌握好下面的知識:

如果沒有對這些基礎知識掌握踏實,直接討論 JavaScript 的 this 關鍵詞只會讓你感到更加地困惑和挫敗。

我為什麼要學 this

如果上面的簡單介紹沒有說服你來深入探索 this 關鍵詞,那我用這節來講講為什麼要學。

考慮這樣一個重要問題,假設開發者,比如 Douglas Crockford (譯者注:JavaScript 領域必知牛人),不再使用 newthis,轉而使用完完全全的函式式寫法來做程式碼複用,會怎樣?

事實上,基於 JavaScript 內建的現成的原型繼承功能,我們已經使用並且將繼續廣泛使用 newthis 關鍵詞來實現程式碼複用。

理由一,如果只能使用自己寫過的程式碼,你是沒法工作的。現有的程式碼以及你讀到這句話時別人正在寫的程式碼都很有可能包含 this 關鍵詞。那麼學習怎麼用好它是不是很有用呢?

因此,即使你不打算在你的程式碼庫中使用它,深入掌握 this 的原理也能讓你在接手別人的程式碼理解其邏輯時事半功倍。

理由二,擴充你的編碼視野和技能。使用不同的設計模式會加深你對程式碼的理解,怎麼去看、怎麼去讀、怎麼去寫、怎麼去理解。我們寫程式碼不僅是給機器去解析,還是寫給我們自己看的。這不僅適用於 JavaScript,對其他程式語言亦是如此。

隨著對程式設計理念的逐步深入理解,它會逐漸塑造你的編碼風格,不管你用的是什麼語言什麼框架。

就像畢加索會為了獲得靈感而涉足那些他並不是很贊同很感興趣的領域,學習 this 會擴充你的知識,加深對程式碼的理解。

什麼是 this

JavaScript this 指向

在我開始講解前,如果你學過一門基於類的物件導向程式語言(比如 C#,Java,C++),那請將你對 this 這個關鍵詞應該是做什麼用的先入為主的概念扔到垃圾桶裡。JavaScript 的 this 關鍵詞是很不一樣,因為 JavaScript 本來就不是一門基於類的物件導向程式語言

雖說 ES6 裡面 JavaScript 提供了類這個特性給我們用,但它只是一個語法糖,一個基於原型繼承的語法糖。

this 就是一個指標,指向我們呼叫函式的物件。

我難以強調上一句話有多重要。請記住,在 Class 新增到 ES6 之前,JavaScript 中沒有 Class 這種東西。Class 只不過是一個將物件串在一起表現得像類繼承一樣的語法糖,以一種我們已經習慣的寫法。所有的魔法背後都是用原型鏈編織起來的。

如果上面的話不好理解,那你可以這樣想,this 的上下文跟英語句子的表達很相似。比如下面的例子

Bob.callPerson(John);

就可以用英語寫成 “Bob called a person named John”。由於 callPerson() 是 Bob 發起的,那 this 就指向 Bob。我們將在下面的章節深入更多的細節。到了這篇文章結束時,你會對 this 關鍵詞有更好的理解(和信心)。

執行上下文

執行上下文 是語言規範中的一個概念,用通俗的話講,大致等同於函式的執行“環境”。具體的有:變數作用域(和 作用域鏈條,閉包裡面來自外部作用域的變數),函式引數,以及 this 物件的值。

引自: Stackoverflow.com

記住,現在起,我們專注於查明 this 關鍵詞到底指向哪。因此,我們現在要思考的就一個問題:

  • 是什麼呼叫函式?是哪個物件呼叫了函式?

為了理解這個關鍵概念,我們來測一下下面的程式碼。

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

誰呼叫了 greet 函式?是 person 這個物件對吧?在 greet() 呼叫的左邊是一個 person 物件,那麼 this 關鍵詞就指向 personthis.name 就等於 "Jay"。現在,還是用上面的例子,我加點料:

var greet = person.greet; // 將函式引用存起來;
greet(); // 呼叫函式
複製程式碼

你覺得在這種情況下控制檯會輸出什麼?“Jay”?undefined?還是別的?

正確答案是 undefined。如果你對這個結果感到驚訝,不必慚愧。你即將學習的東西將幫助你在 JavaScript 旅程中開啟關鍵的大門。

this 的值並不是由函式定義放在哪個物件裡面決定,而是函式執行時由誰來喚起決定。

對於這個意外的結果我們暫且壓下,繼續看下去。(感覺前後銜接得不夠流暢)

帶著這個困惑,我們接著測試下 this 三種不同的定義方式。

找出 this 的指向

上一節我們已經對 this 做了測試。但是這塊知識實在重要,我們需要再好好琢磨一下。在此之前,我想用下面的程式碼給你出個題:

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() 將會輸出什麼,把你的答案寫下來。如果你還想不清楚,複習下上一節。

準備好了嗎?放鬆心情,我們來看下面的答案。

答案和解析

person.details.print()

首先,誰呼叫了 print 函式?在 JavaScript 中我們都是從左讀到右。於是 this 指向 details 而不是 person。這是一個很重要的區別,如果你對這個感到陌生,那趕緊把它記下。

print 作為 details 物件的一個 key,指向一個返回 this.name 的函式。既然我們已經找出 this 指向 details ,那函式的輸出就應該是 'Jay Details'

person.print()

再來一次,找出 this 的指向。print() 是被 person 物件呼叫的,沒錯吧?

在這種情況,person 裡的 print 函式返回 this.namethis 現在指向 person 了,那 'Jay Person' 就是返回值。

console.log(name1)

這一題就有點狡猾了。在上一行有這樣一句程式碼:

var name1 = person.print;
複製程式碼

如果你是通過這句來思考的,我不會怪你。很遺憾,這樣去想是錯的。要記住,this 關鍵詞是在函式呼叫時才做繫結的。name1() 前面是什麼?什麼都沒有。因此 this 關鍵詞就將指向全域性的 window 物件去。

因此,答案是 'Jay Global'

name2.print()

看一下 name2 指向哪個物件,是 details 物件沒錯吧?

所以下面這句會列印出什麼呢?如果到目前為止的所有小點你都理解了,那這裡稍微思考下你就自然有答案了。

console.log(name2.print()) // ??
複製程式碼

答案是 'Jay Details',因為 printname2 調起的,而 name2 指向 details

詞法作用域

你可能會問:“什麼是詞法作用域?

逗我呢,我們不是在探討 this 關鍵詞嗎,這個又是哪裡冒出來的?好吧,當我們用起 ES6 的箭頭函式,這個就要考慮了。如果你已經寫了不止一年的 JavaScript,那你很可能已經碰到箭頭函式。隨著 ES6 逐漸成為現實標準,箭頭函式也變得越來越常用。

JavaScript 的詞法作用域 並不好懂。如果你 理解閉包,那要理解這個概念就容易多了。來看下下面的小段程式碼。

// outerFn 的詞法作用域
var outerFn = function() {
    var n = 5;
    console.log(innerItem);
    // innerFn 的詞法作用域
    var innerFn = function() {  
        var innerItem = "inner";    // 錯了。只能坐著電梯向上,不能向下。
        console.log(n);
    };
    return innerFn;
};
outerFn()();
複製程式碼

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

JavaScript 的詞法作用域就像樓裡的一架只能向上走的詭異電梯

建築的頂層就是全域性 windows 物件。如果你現在在一樓,你就可以看到並訪問那些放在樓上的東西,比如放在二樓的 outerFn 和放在三樓的 window 物件。

這就是為什麼我們執行程式碼 outerFn()(),它在控制檯打出了 5 而不是 undefined

然而,當我們試著在 outerFn 詞法作用域下打出日誌 innerItem,我們遇到了下面的報錯。請記住,JavaScript 的詞法作用域就好像建築裡面那個只能向上走的詭異電梯。由於 outerFn 的詞法作用域在 innerFn 上面,所以它不能向下走到 innerFn 的詞法作用域裡面並拿到裡面的值。這就是觸發下面報錯的原因:

test.html:304 Uncaught ReferenceError: innerItem is not defined
at outerFn (test.html:304)
at test.html:313
複製程式碼

this 和箭頭函式

ES6 裡面,不管你喜歡與否,箭頭函式被引入了進來。對於那些還沒用慣箭頭函式或者新學 JavaScript 的人來說,當箭頭函式和 this 關鍵詞混合使用時會發生什麼,這個點可能會給你帶來小小的困惑和淡淡的憂傷。那這個小節就是為你們準備的!

當涉及到 this 關鍵詞,箭頭函式普通函式 主要的不同是什麼?

答案:

箭頭函式按詞法作用域來繫結它的上下文,所以 this 實際上會引用到原來的上下文。

引自:hackernoon.com

我實在沒法給出比這個更好的總結。

箭頭函式保持它當前執行上下文的詞法作用域不變,而普通函式則不會。換句話說,箭頭函式從包含它的詞法作用域中繼承到了 this 的值。

我們不妨來測試一些程式碼片段,確保你真的理解了。想清楚這塊知識點未來會讓你少點頭痛,因為你會發現 this 關鍵詞和箭頭函式太經常一起用了。

示例

仔細閱讀下面的程式碼片段。

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();
複製程式碼

如果我們看執行上下文,那這兩個函式都是被 object 呼叫的。所以,就此斷定這兩個函式裡面的 this 都指向 object 不為過吧?是的,但我建議你拷貝這段程式碼然後自己測一下。

這裡有個大問題:

arrow()doubleArrow() 裡面的 map 函式裡面的 this 又指向哪裡呢?

this 和箭頭函式

上一張圖已經給了一個大大的提示。如果你還不確定,那請花5分鐘將我們上一節討論的內容再好好想想。然後,根據你的理解,在實際執行程式碼前把你認為的 this 應該指向哪裡寫下來。在下一節我們將會回答這個問題。

回顧執行上下文

這個標題已經把答案洩露出來了。在你看不到的地方,map 函式對呼叫它的陣列進行遍歷,將陣列的每一項傳到回撥函式裡面並把執行結果返回。如果你對 JavaScript 的 map 函式不太瞭解或有所好奇,可以讀讀這個瞭解更多。

總之,由於 map() 是被 this.data 調起的,於是 this 將指向那個儲存在 data 這個 key 裡面的陣列,即 [1,2,3]。同樣的邏輯,this.dataDouble 應該指向另一個陣列,值為 [1,2,3]

現在,如果函式是 object 呼叫的,我們已經確定 this 指向 object 對吧?好,那來看看下面的程式碼片段。

double: function() {
    return this.data.map(function(item) {
        console.log(this);      // 這裡的 this 是什麼??
        return item * 2;
    });
}
複製程式碼

這裡有個很有迷惑性的問題:傳給 map() 的那個匿名函式是誰呼叫的?答案是:這裡沒有一個物件是。為了看得更明白,這裡給出一個 map 函式的基本實現。

// 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])); 前面有什麼物件嗎?沒。因此,this 關鍵詞指向全域性的 windows 物件。那,為什麼 this.dataDouble.map 使用了箭頭函式會使得 this 指向 object 呢?

我想再說一遍這句話,因為它實在很重要:

箭頭函式按詞法作用域將它的上下文繫結到 原來的上下文

現在,你可能會問:原來的上下文是什麼?問得好!

誰是 doubleArrow() 的初始呼叫者?就是 object 對吧?那它就是原來的上下文 ?

this 和 use strict

為了讓 JavaScript 更加健壯及儘量減少人為出錯,ES5 引進了嚴格模式。一個典型的例子就是 this 在嚴格模式下的表現。你如果想按照嚴格模式來寫程式碼,你只需要在你正在寫的程式碼的作用域最頂端加上這麼一行 "use strict;"

記住,傳統的 JavaScript 只有函式作用域,沒有塊作用域。舉個例子:

function strict() {
    // 函式級嚴格模式寫法
    '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 關鍵詞提供了塊作用域的特性。

現在,來看一段簡單程式碼,看下 this 在嚴格模式和非嚴格模式下會怎麼表現。在繼續之前,請將下面的程式碼執行一下。

(function() {
    "use strict";
    console.log(this);
})();
(function() {
    // 不使用嚴格模式
    console.log(this);
})();
複製程式碼

正如你看到的,this 在嚴格模式下指向 undefined。相對的,非嚴格模式下 this 指向全域性變數 window。大部分情況下,開發者使用 this ,並不希望它指向全域性 window 物件。嚴格模式幫我們在使用 this 關鍵詞時,儘量少做搬起石頭砸自己腳的蠢事。

舉個例子,如果全域性的 window 物件剛好有一個 key 的名字和你希望訪問到的物件的 key 相同,會怎樣?上程式碼吧:

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

這段程式碼有兩個問題。

  1. this 將不會指向 item
  2. 如果程式在非嚴格模式下執行,將不會有錯誤丟擲,因為全域性的 window 物件也有一個名為 document 的屬性。

在這個簡單示例中,因為程式碼較短也就不會形成大問題。

如果你是在生產環境像上面那樣寫,當用到 getDoc 返回的資料時,你將收穫一堆難以定位的報錯。如果你程式碼庫比較大,物件間互動比較多,那問題就更嚴重了。

值得慶幸的是,如果我們是在嚴格模式下跑這段程式碼,由於 this 是 undefined,於是立刻就有一個報錯拋給我們:

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

明確設定執行上下文

先前假定大家都對執行上下文不熟,於是我們聊了很多關於執行上下文和 this 的知識。

讓人歡喜讓人憂的是,在 JavaScript 中通過使用內建的特性開發者就可以直接操作執行上下文了。這些特性包括:

  • bind():不需要執行函式就可以將 this 的值準確設定到你選擇的一個物件上。還可以通過逗號隔開傳遞多個引數,如 func.bind(this, param1, param2, ...)
  • apply():將 this 的值準確設定到你選擇的一個物件上。第二個引數是一個陣列,陣列的每一項是你希望傳遞給函式的引數。最後,執行函式
  • call():將 this 的值準確設定到你選擇的一個物件上,然後想 bind 一樣通過逗號分隔傳遞多個引數給函式。如:print.call(this, param1, param2, ...)。最後,執行函式

上面提到的所有內建函式都有一個共同點,就是它們都是用來將 this 關鍵詞指向到其他地方。這些特性可以讓我們玩一些騷操作。只是呢,這個話題太廣了都夠寫好幾篇文章了,所以簡潔起見,這篇文章我不打算展開它的實際應用。

重點:上面那三個函式,只有 bind() 在設定好 this 關鍵詞後不立刻執行函式。

什麼時候用 bind、call 和 apply

你可能在想:現在已經很亂了,學習所有這些的目的是什麼?

首先,你會看到 bind、call 和 apply 這幾個函式到處都會用到,特別是在一些大型的庫和框架。如果你沒理解它做了些什麼,那可憐的你就只用上了 JavaScript 提供的強大能力的一小部分而已。

如果你不想了解一些可能的用法而想立刻讀下去,當然了,你可以直接跳過這節,沒關係。

下面列出來的應用場景都是一些具有深度和廣度的話題(一篇文章基本上是講不完的),所以我放了一些連結供你深度閱讀用。未來我可能會在這篇終極指南里面繼續新增新的小節,這樣大家就可以一次看過癮。

  1. 方法借用
  2. 柯里化
  3. 偏函式應用
  4. 依賴注入

如果我漏掉了其他實踐案例,請留言告知。我會經常來優化這篇指南,這樣你作為讀者就可以讀到最豐富的內容。

閱讀高質量的開原始碼可以升級你的知識和技能。

講真,你會在一些開原始碼上看到 this 關鍵詞、call、apply 和 bind 的實際應用。我會將這塊結合著其他能幫你成為更好的程式設計師的方法一起講。

在我看來,開始閱讀最好的開原始碼是 underscore。它並不像其他開源專案,如 d3,那樣鐵板一塊,而是內部程式碼相互比較獨立,因而它是教學用的最佳選擇。另外,它程式碼簡潔,文件詳細,編碼風格也是相當容易學習。

JavaScript 的 this 和 bind

前面提到了,bind 允許你明確設定 this 的指向而不用實際去執行函式。這裡是一個簡單示例:

var bobObj = {
    name: "Bob"
};
function print() {
    return this.name;
}
// 將 this 明確指向 "bobObj"
var printNameBob = print.bind(bobObj);
console.log(printNameBob());    // this 會指向 bob,於是輸出結果是 "Bob"
複製程式碼

在上面的示例中,如果你把 bind 那行去掉,那 this 將會指向全域性 window 物件。

這好像很蠢,但在你想將 this 繫結到具體物件前你就必須用 bind 來繫結。在某些場景下,我們可能想從另一個物件中借用一些方法。舉個例子,

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
複製程式碼

在這個程式碼片段裡,obj2 有一個名為 printSecondData 的方法,而我們想將這個方法借給 obj1。在下一行

var getSecondData = obj2.printSecondData.bind(obj1);
複製程式碼

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

練習

在下面的程式碼中

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

怎麼讓 this 關鍵詞指向 object。提示:你並不需要重寫 this.data.forEach

答案

在上一節中,我們瞭解了執行上下文。如果你對匿名函式呼叫那部分看得夠細心,你就知道它並不會作為某個物件的方法被呼叫。因此,this 關鍵詞指向了全域性 window 物件。

於是我們需要將 object 作為上下文繫結到匿名函式上,使得裡面的 this 指向 object。現在,double 函式跑起來時,是 object 呼叫了它,那麼 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() 的呼叫上下文是什麼?是全域性上下文。於是,我們就會看到下面的報錯。

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

所以,當我們用到 this 關鍵詞時,就要小心在意我們呼叫函式的方式。我們可以在提供 API 給使用者時固定 this 關鍵詞,以此減少這種型別的錯誤。但請記住,這麼做的代價是犧牲了靈活性,所以做決定前要考慮清楚。

var double = object.double.bind(object);
double();  // 不再報錯
複製程式碼

JavaScript this 和 call

call 方法和 bind 很相似,但就如它名字所暗示的,call 會立刻呼起(執行)函式,這是兩個函式的最大區別。

var item = {
    name: "I am"
};
function print() {
    return this.name;
}
// 立刻執行
var printNameBob = console.log(print.call(item));
複製程式碼

callapplybind 大部分使用場景是重疊的。作為一個程式設計師最重要的還是先了解清楚這三個方法之間的差異,從而能根據它們的設計和目的的不同來選用。只要你瞭解清楚了,你就可以用一種更有創意的方式來使用它們,寫出更獨到精彩的程式碼。

在引數數量固定的場景,callbind 是不錯的選擇。比如說,一個叫 doLogin 的函式經常是接受兩個引數:usernamepassword。在這個場景下,如果你需要將 this 繫結到一個特定的物件上,callbind 會挺好用的。

如何使用 call

以前一個最常用的場景是把一個類陣列物件,比如 arguments 物件,轉化成陣列。舉個例子:

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 物件轉化成一個陣列。在下一個例子中,我們將會呼叫一個 Array 物件的方法,並將 argument 物件設定為方法的 this,以此來將傳進來引數加在一起。

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

我們在一個類陣列物件上呼叫了 reduce 函式。要知道 arguments 不是一個陣列,但我們給了它呼叫 reduce 方法的能力。如果你對 reduce 感興趣,可以在這裡瞭解更多

練習

現在是時候鞏固下你新學到的知識。

  1. document.querySelectorAll() 返回一個類陣列物件 NodeList。請寫一個函式,它接收一個 CSS 選擇器,然後返回一個選擇到的 DOM 節點陣列。
  2. 請寫一個函式,它接收一個由鍵值對組成的陣列,然後將這些鍵值對設定到 this 關鍵詞指向的物件上,最後將該物件返回。如果 this 是 nullundefined,那就新建一個 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 就是接受陣列版本的 call。於是當使用 apply 時,多聯想下陣列。

將一個方法應用(apply)到一個陣列上。

我用這句話來記住它,而且還挺管用。apply 為你的現有堆積的軍火庫又新增了一樣利器,增加了很多新的可能,你很快就能體會到這一點。

當你要處理引數數量動態變化的場景,用 apply 吧。將一系列資料轉化為陣列並用上 apply 能讓你寫出更好用和更具彈性的程式碼,會讓你的工作更輕鬆。

如何使用 apply

Math.minmax 都是可以接受多個引數並返回最小值和最大值的函式。除了直接傳 n 個引數,你也可以將這 n 個引數放到一個陣列裡然後藉助 apply 將它傳到 min 函式裡。

Math.min(1,2,3,4); // 返回 1
Math.min([1,2,3,4]); // 返回 NaN。只接受數字
Math.min.apply(null, [1,2,3,4]); // 返回 1
複製程式碼

看暈了嗎?如果真暈了,那我來解釋下。使用 apply 時我們要傳一個陣列因為它需要陣列作為第二個引數。而下面

Math.min.apply(null, [1,2,3,4]); // 返回 1
複製程式碼

做的事情基本等同於

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

這就是我想指出來的 apply 的神奇之處。它和 call 工作原理,不過我們只要傳給它一個陣列而不是 n 個引數。很好玩對吧?橋豆麻袋,這是否意味著 Math.min.call(null, 1,2,3,4); 執行起來和 Math.min.apply(null, [1,2,3,4]); 一樣?

啊,你說對了!看來你已經開始掌握它了 ?

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

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

沒錯,你甚至可以傳一個類陣列物件作為 apply 的第二個引數。很酷對吧?

練習

  1. 寫一個函式,它接受一個由鍵值對組成的陣列,然後將這些鍵值對設定到 this 關鍵詞指向的物件上,最後將該物件返回。如果 this 是 nullundefined,那就新建一個 object。示例:set.apply( {name: "jay"}, [{age: 10}]); // 返回 {name: "jay", age: 10}
  2. 寫一個類似 Math.maxmin 的函式,不過接收的不是數字而是運算。前兩個引數必須是數字,而後面的引數你要將其轉化為一個函式陣列。下面提供一個方便你上手理解的示例:
function operate() {
    if (arguments.length < 3) {
        throw new Error("至少要三個引數");
    }
    if (typeof arguments[0] !== 'number' || typeof arguments[1] !== 'number') {
        throw new Error("前兩個引數必須是數字");
    }
    // 寫程式碼
    // 這是一個由函式組成的陣列。你可以用 call、apply 或者 bind。但不要直接遍歷引數然後直接塞到一個陣列裡
    var args;
    var result = 0;
    // 好了,開始吧,祝好運
}
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 裡面是怎麼運作的。

我還強烈推薦你去學習 JavaScript 原型鏈,不單是因為裡面用到大量的 this 關鍵詞,而且它還是 JavaScript 實現繼承的標準方式。

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

  • 編寫高質量 JavaScript程式碼的68個有效方法:雖然是本古董,但此書確實寫得挺好而且還提供了簡單易懂的示例,教你怎麼用好 this、apply、call 和 bind 來寫出好程式碼。書的作者是 TC39 的一個成員 Dave Hermann,所以你大可放心,他對 JavaScript 肯定理解深刻。
  • 你不知道的 JS —— this 和物件原型:Kyle Simpson 以一種清晰明瞭、對初學者很友好的方式,解釋了物件和原型是怎麼相互影響運作起來的,寫得很棒!

總結

考慮到 this 關鍵詞已經用到了難以計量的程式碼中,它是 JavaScript 中我們不得不聊的話題。

一個優秀的藝術家肯定精於工具的使用。作為一個 JavaScript 開發者,怎麼用好它的特性是最最重要的。

如果你想看到一些從特定角度對 this 關鍵詞深入剖析的文章或者更多的程式碼,請別忘了告訴我。這些可能的角度可以是(但不限於)下面這些:

  • thisnew 關鍵詞。
  • JavaScript 的原型鏈。
  • this 和 JavaScript 的類。

另外,關於這篇文章你如果有什麼具體的問題或補充,請給我發郵件或資訊。我剛在我的 Github 個人主頁更新了我的郵箱地址。我希望將這個教程完善起來,這樣不管哪個級別的開發者看到它都能從中受益。讓我們一起把它做好!

多謝捧場了老鐵,然後,這篇文章還能再補充點什麼對讀者有用的東西,我真的很期待聽到你的觀點和建議。

保重,下次見!

關於作者 Jay

我是一個現居韓國首爾的程式設計師。我創立這個部落格的目的,就是想用文字形式將所學所想沉澱下來,也希望為社群做些貢獻。我熱衷於資料結構和演算法,而後臺和資料庫則是我心中最愛。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章