簡介
儘管 JavaScript 總是讓人產生誤解,但是它已經成為了最流行的程式語言之一。理解 JavaScript 的內在原理很困難。同樣的,迫使 JavaScript 成為常規規範,如物件導向或函式程式設計,同樣具有挑戰性。這裡我強調闡明 JavaScript 核心部分的原生函式。
在這篇文章中,我將討論以下幾種行為:
- Call/Apply
- Bind
- Map
- Filter
首先我會定義這個函式(利用Mozilla的宣告方式),然後提供一個例子,最後實現此函式。
為了解釋這些行為,我需要先解釋一下複雜的 this
關鍵字以及類似陣列的 arguments
物件。
this
和 arguments
物件
JavaScript 的作用域是基於函式而言的,術語一般稱為作用域,變數和方法的作用域都是當前函式。此外,函式執行的作用域是他們被定義的作用域而不是執行的作用域。如果你想了解更多有關於作用域的知識,可以參考你應該知道的4種 JavaScript 設計模式這篇文章。this
物件引用當前函式的上下文並且可以以多種方式被呼叫。例如,它可以被繫結到 window
物件(全域性作用域)。
1 2 3 4 5 6 7 |
this.globalVar = { myGlobalVarsMethod: function (){ // Implementation } }; console.log(this.globalVar); // { myGlobalVarsMethod: [Function] } |
並且變數可以繫結到已存在的函式中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
this.globalVariable = 'globalVariable'; function globalFunction (){ this.innerVariable = 'innerVariable'; console.log(this.globalVariable === undefined); // false console.log(this.innerVariable === 'innerVariable'); // true return { innerFunction: function () { console.log(this.globalVariable === undefined); // true console.log(this.innerVariable === undefined); // true } } } globalFunction().innerFunction(); |
這裡存在被繫結到每一個呼叫函式的 this
物件。嚴格模式下,如果變數未定義就會丟擲異常/錯誤( TypeErrors )。在生產環境下嚴格模式者會被優先考慮;然而,我故意選擇不使用此模式以避免丟擲異常。下面是嚴格模式下的一個簡單例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
this.globalVar = 'globalVar'; function nonStrictFunctionTest () { return function () { console.log(this.globalVar); // undefined } } function strictFunctionTest () { 'use strict'; // Strict Mode return function () { console.log(this.globalVar); // TypeError: Cannot read property 'globalVar' of undefined } } nonStrictFunctionTest()(); strictFunctionTest()(); |
可能很多 JavaScript 開發人員不知道,建立函式時會有一個arguments
物件。這是一個類似陣列的物件(僅具有屬性的長度)。arguments
主要有三個屬性,即callee
(呼叫方法),length
,和caller
(呼叫函式的參考)。
在一個函式中宣告變數引數會替換/覆蓋原先的引數物件。
如下列出的一些引數物件:
1 2 3 4 5 6 7 8 9 10 |
function fn (){ console.log(typeof arguments); // [object Object] console.log(arguments[0]); // DeathStar console.log(arguments[1]); // Tatooine arguments.push("Naboo"); // TypeError: undefined is not a function var arguments = "Star Wars"; console.log(arguments[5]); // W } fn("DeathStar", "Tatooine"); |
按照如下所示,用 arguments
建立一個陣列:
1 |
var args = Array.prototype.slice.call(arguments); |
Call/Apply
無論 call
還是 apply
都是呼叫物件的一個方法。關於使用點操作符,call
和 apply
都接受其作為第一個引數。如上所述,每一個函式都保持在其所定義的特定作用域內。因此,當你呼叫物件時必須考慮到函式的作用域。
Mozilla 瀏覽器的apply
和call
呼叫宣告如下所示:
1 2 |
fun.apply(thisArg, [argsArray]) fun.call(thisArg[, arg1[, arg2[, ...]]]) |
通過傳遞 thisArg
引數,在特定的上下文中,被呼叫的函式可以訪問或修改物件。下面的例子闡明瞭 call
的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
this.lightSaberColor = 'none'; var darthVader = { team: 'Empire', lightSaberColor: 'Red' }; var printLightSaberColor = function(){ console.log(this.lightSaberColor); } printLightSaberColor() // none printLightSaberColor.call(darthVader); // Red printLightSaberColor.apply(darthVader); // Red |
注意:第一次呼叫預設為全域性作用域(window
),然而,第二次為 darthvader
。
call
和 apply
主要的區別在於他們的宣告方式不同。call
需要引數分開傳遞,而 apply
需要傳入由引數組成的陣列。我是這樣記憶的:“Apply uses an Array。”當你的程式無關乎引數數目時,apply
方法可能會更加適用。
Currying(柯里化)(部分函式應用)是應用 call
和 apply
的一個函數語言程式設計。Currying 允許我們建立返回已知條件的函式。這裡是一個 currying
函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
var curry = function(fun) { // nothing to curry. return function if (arguments.length < 1) { return this; } // Create an array with the functions arguments var args = Array.prototype.slice.call(arguments, 1); return function() { // *Apply* fn with fn's arguments return fun.apply(this, args.concat(Array.prototype.slice.call(arguments, 0))); }; }; // Creating function that already predefines adding 1 to a function addOneToNumber(a) { console.log(1 + a); } // addOneCurried is of function var addOneCurried = curry(addOneToNumber); console.log(addOneCurried(10)); // 11 |
雖然 arguments
不是陣列,但是 Array.prototype.slice
可以將類陣列的物件轉換成新陣列。
Bind
bind
方法用於明確指定呼叫 this
方法。在作用域方面,類似於 call
和 apply
。當你將一個物件繫結到一個函式的 this
物件時,你就會用到 bind
。
如下是bind
的宣告:
1 |
fun.bind(thisArg[, arg1[, arg2[, ...]]]) |
通俗地說,我們是通過 bind
向函式 fun
傳遞 thisArg
引數。實質上就是每次 fun
函式都必須通過傳遞 thisArg
引數呼叫 bind
方法。讓我們在一個簡單的例子中仔細看看。
1 2 3 4 5 6 7 8 9 10 11 12 |
var lukeSkywalker = { mother: 'Padme Amidala', father: 'Anakin Skywalker'. } var getFather = function(){ console.log(this.father); } getFather(); // undefined getFather.bind(lukeSkywalker)(); // Anakin Skywalker getFather(lukeSkywalker); // undefined |
第一個getfather()
返回值為 undefined
是因為在這裡 father
屬性沒有被定義。那這時 this
代表什麼呢?只要我們不明確的指定它,它就代表 window
的全域性物件。第二個getfather()
返回 “Anakin Skywalker”是因為getfather()
中的 this
指代的是 lukeskywalker
。許多Java/C++ 開發人員會設想最後一個getfather()
的呼叫將返回預想的結果–雖然再次返回全域性物件。
如下這裡是 bind
的實現原理:
1 2 3 4 5 6 7 |
Function.prototype.bind = function(scope) { var _that = this; return function() { return _that.apply(scope, arguments); } } |
這裡 JavaScript 的作用域是合乎邏輯的,返回函式的 this
物件是不同於 bind
的 this
物件的。因此,將 this
暫時快取給變數 _that
保證了其正確的作用域範圍。否則,this.apply(scope,arguments)
將會未定義。
Map
JavaScript 的 map
函式是遍歷陣列,同時轉換每個元素的函式程式設計技術。它用 modified
元素建立了一個新陣列並以回撥的方式返回。關於我提到的修改或轉換元素,實踐表明,如果元素是物件(而不是原語),這只是克隆物件並不是從物理上改變了原生的。
以下是該方法的宣告:
1 |
arr.map(callback[, thisArg]) |
回撥方法有三個引數,即 currentValue
,index
,和 array
。
這裡是一個有關於 map
的簡單例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
function Jedi(name) { this.name = name; } var kit = new Jedi('Kit'); var count = new Jedi('Count'); var mace = new Jedi('Mace'); var jedis = [kit, count, mace]; var lastNames = ['Fisto', 'Dooku', 'Windu']; var jedisWithFullNames = jedis.map(function(currentValue, index, array) { var clonedJedi = (JSON.parse(JSON.stringify(currentValue))) // Clone currentValue clonedJedi.name = currentValue.name + " " + lastNames[index]; return clonedJedi; }); jedisWithFullNames.map(function(currentValue) { console.log(currentValue.name); }); /** Output: Kit Fisto Count Dooku Mace Windu */ |
瞭解了 map
是用來做什麼的,讓我們看一下它具體是如何實現的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Array.prototype.map = function (fun, thisArg) { if(typeof fun !== 'function') { throw new Error("The first argument must be of type function"); } var arr = []; thisArg = (thisArg) ? thisArg : this; thisArg.forEach(function(element) { arr[arr.length] = fun.call(thisArgs, element); }); return arr; } |
注:這是一個簡單的實現。到 ECMAScript 5看全部的實現,並查閱其規範。
Filter
filter
方法是陣列的另外一種表現行為。類似於 map
,filter
返回一個新的陣列並接受一個函式和一個可選的 thisArg
引數。然而,返回的陣列僅包含適合在回撥函式測試的特定條件的元素。回撥函式必須返回一個 Boolean
–返回 true
的元素才會被接受並插入到返回的陣列。
關於 filter
有許多應用,包括選擇偶數,用一個特定的屬性選擇物件,或選擇有效的電話號碼。
這裡是其中一種宣告方法:
1 |
arr.filter(callback[, thisArg]) |
同樣的,thisArg
是可選的引數並且回撥函式接受三個引數,currentValue
,index
和 array
。
這裡是一個有關於 filter
的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function Person(name, side) { this.name = name; this.side = side; } var hanSolo = new Person('Han Solo','Rebels'); var bobaFett = new Person('Boba Fett','Empire'); var princessLeia = new Person('Princess Leia', 'Rebels'); var people = [hanSolo, bobaFett, princessLeia]; var enemies = people.filter(function (currentValue, index, array) { return currentValue.side === 'Empire'; }) .map(function(currentValue) { console.log(currentValue.name + " fights for the " + currentValue.side + "."); }); /** Output: Boba Fett fights for the Empire. */ |
有趣的是,array
方法可以創造有趣的,複雜的操作。
最後,讓我們看看 filter
的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Array.prototype.filter = function(fun, thisArg) { if(typeof fun !== 'function') { throw new Error("The first argument must be of type function"); } var arr = []; thisArg = (thisArg) ? thisArg : this; thisArg.forEach(function(element) { if (fun.call(thisArg, element)) { arr[arr.length] = element; } }); return arr; }; |
這裡是 ECMAScript 的實現規範。
總結
還有更多令人困惑但是很有用的原生函式。它們是值得用陣列和函式來回顧其中的每一種方法。
希望這篇文章可以有助於你理解 JavaScript 的內部原理和詞法作用域。儘管與實踐緊密相連,call
、apply
,和bind
還是很難把握的。為了避免傳統的迴圈技術你可以嘗試使用 map
和 filter
方法 。