學習并運用JavaScript的原生函數
來自: http://www.w3cplus.com/JavaScript/learning-javascript-native-functions-and-how-to-use-them.html
簡介
盡管 JavaScript 總是讓人產生誤解,但是它已經成為了最流行的編程語言之一。理解 JavaScript 的內在原理很困難。同樣的,迫使 JavaScript 成為常規規范,如面向對象或函數編程,同樣具有挑戰性。這里我強調闡明 JavaScript 核心部分的原生函數。
在這篇文章中,我將討論以下幾種行為:
- Call/Apply
- Bind
- Map
- Filter
首先我會定義這個函數(利用Mozilla的聲明方式),然后提供一個例子,最后實現此函數。
為了解釋這些行為,我需要先解釋一下復雜的 this 關鍵字以及類似數組的 arguments 對象。
this 和 arguments 對象
JavaScript 的作用域是基于函數而言的,術語一般稱為 作用域 ,變量和方法的作用域都是當前函數。 此外,函數執行的作用域是他們被定義的作用域而不是執行的作用域 。如果你想了解更多有關于作用域的知識,可以參考 你應該知道的4種 JavaScript 設計模式 這篇文章。 this 對象引用當前函數的上下文并且可以以多種方式被調用。例如,它可以被綁定到 window 對象(全局作用域)。
this.globalVar = { myGlobalVarsMethod: function (){ // Implementation } }; console.log(this.globalVar); // { myGlobalVarsMethod: [Function] }
并且變量可以綁定到已存在的函數中,如下:
this.globalVariable = 'globalVariable'; function globalFunction (){ this.innerVariable = 'innerVariable'; console.log(this.globalVariable === undefined); // true 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 )。在生產環境下嚴格模式者會被優先考慮;然而,我故意選擇不使用此模式以避免拋出異常。下面是嚴格模式下的一個簡單例子:
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 (調用函數的參考)。
在一個函數中聲明變量參數會替換/覆蓋原先的參數對象。
如下列出的一些參數對象:
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 創建一個數組:
var args = Array.prototype.slice.call(arguments);
Call/Apply
無論 call 還是 apply 都是調用對象的一個方法。關于使用點操作符, call 和 apply 都接受其作為第一個參數。如上所述,每一個函數都保持在其所定義的特定作用域內。因此,當你調用對象時必須考慮到函數的作用域。
Mozilla 瀏覽器的 apply 和 call 調用聲明如下所示:
fun.apply(thisArg, [argsArray]) fun.call(thisArg[, arg1[, arg2[, ...]]])
通過傳遞 thisArg 參數,在特定的上下文中,被調用的函數可以訪問或修改對象。下面的例子闡明了 call 的使用。
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 需要傳入由參數組成的數組。我是這樣記憶的:“ A pply uses an A rray。”當你的程序無關乎參數數目時, apply 方法可能會更加適用。
Currying(柯里化) (部分函數應用)是應用 call 和 apply 的一個函數式編程。Currying 允許我們創建返回已知條件的函數。這里是一個 currying 函數:
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 的 聲明 :
fun.bind(thisArg[, arg1[, arg2[, ...]]])
通俗地說,我們是通過 bind 向函數 fun 傳遞 thisArg 參數。實質上就是每次 fun 函數都必須通過傳遞 thisArg 參數調用 bind 方法。讓我們在一個簡單的例子中仔細看看。
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 的實現原理:
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 元素創建了一個新數組并以回調的方式返回。關于我提到的修改或轉換元素,實踐表明,如果元素是對象(而不是原語),這只是克隆對象并不是從物理上改變了原生的。
以下是該方法的聲明:
arr.map(callback[, thisArg])
回調方法有三個參數,即 currentValue , index ,和 array 。
這里是一個有關于 map 的簡單例子:
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 是用來做什么的,讓我們看一下它具體是如何實現的:
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 有許多應用,包括選擇偶數,用一個特定的屬性選擇對象,或選擇有效的電話號碼。
這里是其中一種聲明方法:
arr.filter(callback[, thisArg])
同樣的, thisArg 是可選的參數并且回調函數接受三個參數, currentValue , index 和 array 。
這里是一個有關于 filter 的例子:
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 的實現:
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 方法 。
本文根據 @DEVAN PATEL 的《 Learning JavaScript Native Functions and How to Use Them 》所譯,整個譯文帶有我們自己的理解與思想,如果譯得不好或有不對之處還請同行朋友指點。如需轉載此譯文,需注明英文出處: https://scotch.io/tutorials/learning-javascript-native-functions-and-how-to-use-them 。
靜子
在校學生,本科計算機專業。一個積極進取、愛笑的女生,熱愛前端,喜歡與人交流分享。想要通過自己的努力做到心中的那個自己。微博:@靜-如秋葉