摘要:在中,一個未使用明確標識符的函數(shù)被稱為一個匿名函數(shù)。記住在中,由關鍵字聲明的變量是一個局部變量,而忽略了這個關鍵字則會創(chuàng)建一個全局變量。函數(shù)被賦值給一個局部變量,在外部無法訪問它。這個函數(shù)表達式的變種被稱為一個命名的函數(shù)表達式。
本文是@堂主 對《Pro JavaScript with Mootools》一書的第二章函數(shù)部分知識點講解的翻譯。該書的作者 Mark Joseph Obcena 是 Mootools 庫的作者和目前開發(fā)團隊的 Leader。雖然本篇文章實際譯于 2012 年初,但個人感覺這部分對 Javascript 函數(shù)的基本知識、內(nèi)部機制及 JavaScript 解析器的運行機制講的非常明白,脈絡也清楚,對初學者掌握 JavaScript 函數(shù)基礎知識很有好處。尤其難得的是不同于其他 JavaScript書籍講述的都是分散的知識點,這本書的知識講解是有清晰脈絡的,循序漸進。換句話說,這本書中的 JavaScript 知識是串起來的。
雖然這本《Pro JavaScript with Mootools》國內(nèi)并未正式引進,但我依然建議有需求的可以從 Amazon 上自行買來看一下,或者網(wǎng)上搜一下 PDF 的版本(確實有 PDF 全版下載的)。我個人是當初花了近 300 大洋從 Amazon 上買了一本英文原版的,還是更喜歡紙質(zhì)版的閱讀體驗。這本書其實可以理解為 “基于 MooTools 實踐項目的 JavaScript 指南”,總的脈絡是 “JavaScript 基礎知識 - 高級技巧 - MooTools 對原生 JavaScript 的改進”,非常值得一讀。
本篇譯文字數(shù)較多,近 4 萬字,我不知道能有幾位看官有耐心看完。如果真有,且發(fā)現(xiàn)@堂主 一些地方翻譯的不對或有優(yōu)化建議,歡迎留言指教,共同成長。另外,非本土產(chǎn)技術類書籍,優(yōu)先建議還是直接讀英文原版。
下面是譯文正式內(nèi)容:
JavaScript 最好的特色之一是其對函數(shù)的實現(xiàn)。不同于其他編程語言為不同情景提供不同的函數(shù)類型,JavaScript 只為我們提供了一種涵蓋所有情景(如內(nèi)嵌函數(shù)、匿名函數(shù)或是對象方法)的函數(shù)類型。 請不要被一個從外表看似簡單的 JavaScript 函數(shù)迷惑——這個基本的函數(shù)聲明如同一座城堡,隱藏著非常復雜的內(nèi)部操作。我們整章都是圍繞著諸如函數(shù)結構、作用域、執(zhí)行上下文以及函數(shù)執(zhí)行等這些在實際操作中需要重點去考慮的問題來講述的。搞明白這些平時會被忽略的細節(jié)不但有助你更加了解這門語言,同時也會為你在解決異常復雜的問題時提供巨大幫助。
關于函數(shù)(The Function)最開始,我們需要統(tǒng)一一些基本術語。從現(xiàn)在開始,我們將函數(shù)(functions)的概念定義為“執(zhí)行一個明確的動作并提供一個返回值的獨立代碼塊”。函數(shù)可以接收作為值傳遞給它的參數(shù)(arguments),函數(shù)可以被用來提供返回值(return value),也可以通過調(diào)用(invoking)被多次執(zhí)行。
// 一個帶有2個參數(shù)的基本函數(shù): function add(one, two) { return one + two; } // 調(diào)用這個函數(shù)并給它2個參數(shù): var result = add(1, 42); console.log(result); // 43 // 再次調(diào)用這個函數(shù),給它另外2個參數(shù) result = add(5, 20); console.log(result); // 25
JavaScript 是一個將函數(shù)作為一等對象(first-class functions)的語言。一個一等對象的函數(shù)意味著函數(shù)可以儲存在變量中,可以被作為參數(shù)傳遞給其他函數(shù)使用,也可以作為其他函數(shù)的返回值。這么做的合理性是因為在 JavaScript 中隨處可見的函數(shù)其實都是對象。這門語言還允許你創(chuàng)建新的函數(shù)并在運行時改變函數(shù)的定義。
一種函數(shù),多種形式(One Function, Multiple Forms)雖然在 JavaScript 中只存在一種函數(shù)類型,但卻存在多種函數(shù)形式,這意味著可以通過不同的方式去創(chuàng)建一個函數(shù)。這些形式中最常見的是下面這種被稱為函數(shù)字面量(function literal)的創(chuàng)建語法:
function Identifier(FormalParamters, ...) { FunctionBody }
首先是一個 function 關鍵字后面跟著一個空格,之后是一個自選的標識符(identifier)用以說明你的函數(shù);之后跟著的是以逗號分割的形參(formal parameters)列表,該形參列表處于一對圓括號中,這些形參會在函數(shù)內(nèi)部轉變?yōu)榭捎玫木植孔兞浚蛔詈笫且粋€自選的函數(shù)體(funciton body),在這里面你可以書寫聲明和表達式。請注意下面的說法是正確的:一個函數(shù)有多個可選部分。我們現(xiàn)在還沒針對這個問題進行詳細的說明,因為對其的解答將貫穿本章。
注意:在本書的很多章節(jié)我們都會看到字面量(literal)這個術語。在JavaScript中,字面量是指在你代碼中明確定義的值。“mark”、1 或者 true 是字符串、數(shù)字和布爾字面量的例子,而 function() 和 [1, 2] 則分別是函數(shù)和數(shù)組字面量的例子。
在標識符(或后面我們會見到的針對這個對象本身)后面使用調(diào)用操作符(invocation operator) “()”的被稱為一個函數(shù)。同時調(diào)用操作符()也可以為函數(shù)傳遞實參(actual arguments)。
注意:一個函數(shù)的形參是指在創(chuàng)建函數(shù)時圓括號中被聲明的有命名的變量,而實參則是指函數(shù)被調(diào)用時傳給它的值。
因為函數(shù)同時也是對象,所以它也具有方法和屬性。我們將在本書第三章更多的討論函數(shù)的方法和屬性,這里只需要先記住函數(shù)具有兩個基本的屬性:
名稱(name):保存著函數(shù)標識符這個字符串的值
長度(length):這是一個關于函數(shù)形參數(shù)量的整數(shù)(如果函數(shù)沒有形參,其 length 為 0)
函數(shù)聲明(Function Declaration)
采用基本語法,我們創(chuàng)建第一種函數(shù)形式,稱之為函數(shù)聲明(function declaration)。函數(shù)聲明是所有函數(shù)形式中最簡單的一種,且絕大部分的開發(fā)者都在他們的代碼中使用這種形式。下面的代碼定義了一個新的函數(shù),它的名字是 “add”:
// 一個名為“add”的函數(shù) function add(a, b) { return a + b; } console.log(typeof add); // "function" console.log(add.name); // "add" console.log(add.length); // "2" console.log(add(20, 5)); // "25"
在函數(shù)聲明中需要賦予被聲明的函數(shù)一個標識符,這個標識符將在當前作用域中創(chuàng)建一個值為函數(shù)的變量。在我們的例子中,我們在全局作用域中創(chuàng)建了一個 add 的變量,這個變量的 name 屬性值為 add,這等價于這個函數(shù)的標識符,且這個函數(shù)的 length 為 2,因為我們?yōu)槠湓O置了 2 個形參。 因為 JavaScript 是基于詞法作用域(lexically scoped)的,所以標識符被固定在它們被定義的作用域而不是語法上或是其被調(diào)用時的作用域。記住這一點很重要,因為 JavaScript 允許我們在函數(shù)中定義函數(shù),這種情況下關于作用域的規(guī)則可能會變得不易理解。
// 外層函數(shù),全局作用域 function outer() { // 內(nèi)層函數(shù),局部作用域 function inner() { // ... } } // 檢測外層函數(shù) console.log(typeof outer); // "function" // 運行外層函數(shù)來創(chuàng)建一個新的函數(shù) outer(); // 檢測內(nèi)層函數(shù) console.log(typeof inner); // "undefined"
在這個例子中,我們在全局作用域中創(chuàng)建了一個 outer 變量并為之賦值為 outer 函數(shù)。當我們調(diào)用它時,它創(chuàng)建了一個名為 inner 的局部變量,這個局部變量被賦值為 inner 函數(shù),當我們使用 typeof 操作符進行檢測的時候,在全局作用域中 outer 函數(shù)是可以被有效訪問的,但 inner 函數(shù)卻只能在 outer 函數(shù)內(nèi)部被訪問到 —— 這是因為 inner 函數(shù)只存在于一個局部作用域中。 因為函數(shù)聲明同時還創(chuàng)建了一個同名的變量作為他的標識符,所以你必須確定在當前作用域不存在其他同名標識符的變量。否則,后面同名變量的值會覆蓋前面的:
// 當前作用域中的一個變量 var items = 1; // 一個被聲明為同名的函數(shù) function items() { // ... }; console.log(typeof items); // "function" 而非 "number"
我們過一會會討論更多關于函數(shù)作用域的細節(jié),現(xiàn)在我們看一下另外一種形式的函數(shù)。
函數(shù)表達式(Function Expression)下面要說的函數(shù)形式具備一定的優(yōu)勢,這個優(yōu)勢在于函數(shù)被儲存在一個變量中,這種形式的函數(shù)被稱為函數(shù)表達式(funciton expression)。不同于明確的聲明一個函數(shù),這時的函數(shù)以一個變量返回值的面貌出現(xiàn)。下面是一個和上面一樣的add函數(shù),但這次我們使用了函數(shù)表達式:
var add = function(a, b) { return a + b; }; console.log(typeof add); // "function" console.log(add.name); // "" 或 "anonymous" console.log(add.length); // "2" console.log(add(20, 5)); // "25"
這里我們創(chuàng)建了一個函數(shù)字面量作為 add 這個變量的值,下面我們就可以使用這個變量來調(diào)用這個函數(shù),如最后的那個語句展示的我們用它來求兩個數(shù)的和。你會注意到它的 length 屬性和對應的函數(shù)聲明的 length 屬性是一樣,但是 name 屬性卻不一樣。在一些 JavaScript 解析器中,這個值會是空字符串,而在另一些中則會是 “anonymous”。發(fā)生這種情況的原因是我們并未給一個函數(shù)字面量指定一個標識符。在 JavaSrcipt 中,一個未使用明確標識符的函數(shù)被稱為一個匿名函數(shù)(anonymous)。 函數(shù)表達式的作用域規(guī)則不同于函數(shù)聲明的作用域規(guī)則,這是因為其取決于被賦值的那個變量的作用域。記住在 JavaScript 中,由關鍵字 var 聲明的變量是一個局部變量,而忽略了這個關鍵字則會創(chuàng)建一個全局變量。
// 外層函數(shù),全局作用域 var outer = function() { // 內(nèi)層函數(shù),局部作用域 var localInner = function() { // ... }; // 內(nèi)層函數(shù),全局作用域 globalInner = function() { // ... }; } // 檢測外層函數(shù) console.log(typeof outer); // "function" // 運行外層函數(shù)來創(chuàng)建一個新的函數(shù) outer(); // 檢測新的函數(shù) console.log(typeof localInner); // "undefined" console.log(typeof globalInner); // "function"
outer 函數(shù)被定義在全局作用域中,這是因為雖然我們使用了 var 關鍵字,但其在當前應用中處于最高層級。在這個函數(shù)內(nèi)部有另外的2個函數(shù):localInner 和 globalInner。localInner 函數(shù)被賦值給一個局部變量,在 outer 外部無法訪問它。而 globalIner 則因在定義時缺失 var 關鍵字,其結果是這個變量及其引用的函數(shù)都處于全局作用域中。
命名的函數(shù)表達式(Named Function Expression)雖然函數(shù)表達式經(jīng)常被書寫為采用匿名函數(shù)的形式,但你依然可以為這個匿名函數(shù)賦予一個明確的標識符。這個函數(shù)表達式的變種被稱為一個命名的函數(shù)表達式(named function expression)。
var add = function add(a, b) { return a + b; }; console.log(typeof add); // "function" console.log(add.name); // "add" console.log(add.length); // "2" console.log(add(20, 5)); //"25"
這個例子和采用匿名函數(shù)方式的函數(shù)表達式是一樣的,但我們?yōu)楹瘮?shù)字面量賦予了一個明確的標識符。和前一個例子不同,這時的 name 屬性的值是 “add”,這個值同我們?yōu)槠滟x予的那個標識符是一致的。JavaScript 允許我們?yōu)槟涿瘮?shù)賦予一個明確的標識符,這樣就可以在這個函數(shù)內(nèi)部引用其本身。你可能會問為什么我們需要這個特征,下面讓我們來看兩個例子:
var myFn = function() { // 引用這個函數(shù) console.log(typeof myFn); }; myFn(); // "function"
上面的這個例子,myFn 這個函數(shù)可以輕松的通過它的變量名來引用,這是因為它的變量名在其作用域中是有效的。不過,看一下下面的這個例子:
// 全局作用域 var createFn = function() { // 返回函數(shù) return function() { console.log(typeof myFn); }; }; // 不同的作用域 (function() { // 將createFn的返回值賦予一個局部變量 var myFn = createFn(); // 檢測引用是否可行 myFn(); // "undefined" })();
這個例子可能有點復雜,我們稍后會討論它的細節(jié)。現(xiàn)在,我們只關心函數(shù)本身。在全局作用域中,我們創(chuàng)建了一個 createFn 函數(shù),它返回一個和前面例子一樣的 log 函數(shù)。之后我們創(chuàng)建了一個匿名的局部作用域,在其中定義了一個變量 myFn,并把 createFn 的返回值賦予這個變量。 這段代碼和前面那個看起來很像,但不同的是我們沒使用一個被明確賦值為函數(shù)字面量的變量,而是使用了一個由其他函數(shù)產(chǎn)生的返回值。而且,變量 myFn 一個不同的局部作用域中,在這個作用域中訪問不到上面 createFn 函數(shù)作用域中的返回值。因此,在這個例子中,log 函數(shù)不會返回 “function” 而是會返回一個 “undefined”。 通過為匿名函數(shù)設置一個明確的標識符,即使我們通過持有它的變量訪問到它,也可以去引用這個函數(shù)自身。
// 全局作用域 var createFn = function() { // 返回函數(shù) return function myFn() { console.log(typeof myFn); }; }; // 不同的作用域 (function() { // 將createFn的返回值賦予一個局部變量 var myFn = createFn(); // 檢測引用是否可行 myFn(); // "function" })();
添加一個明確的標識符類似于創(chuàng)建一個新的可訪問該函數(shù)內(nèi)部的變量,使用這個變量就可以引用這個函數(shù)自身。這樣使得函數(shù)在其內(nèi)部調(diào)用自身(用于遞歸操作)或在其本身上執(zhí)行操作成為可能。 一個命名了的函數(shù)聲明同一個采用匿名函數(shù)形式的函數(shù)聲明具有相同的作用域規(guī)則:引用它的變量作用域決定了這個函數(shù)是局部的或是全局的。
// 一個有著不同標識符的函數(shù) var myFn = function fnID() { console.log(typeof fnID); }; // 對于變量 console.log(typeof myFn); // "function" // 對于標識符 console.log(typeof fnID); // "undefined" myFn(); // "function"
這個例子顯示了,通過變量 myFn 可以成功的引用函數(shù),但通過標識符 fnID 卻無法從外部訪問到它。但是,通過標識符卻可以在函數(shù)內(nèi)部引用其自身。
自執(zhí)行函數(shù)(Single-Execution Function)我們在前面介紹函數(shù)表達式時曾接觸過匿名函數(shù),其還有著更廣泛的用處。其中最重要的一項技術就是使用匿名函數(shù)創(chuàng)建一個立即執(zhí)行的函數(shù)——且不需要事先把它們先存在變量里。這種函數(shù)形式我們稱之為自執(zhí)行函數(shù)(single-execution function)。
// 創(chuàng)建一個函數(shù)并立即調(diào)用其自身 (function() { var msg = "Hello World"; console.log(msg); })();
這里我們創(chuàng)建了一個函數(shù)字面量并把它包裹在一對圓括號中。之后我們使用函數(shù)調(diào)用操作符()來立即執(zhí)行這個函數(shù)。這個函數(shù)并未儲存在一個變量里,或是任何針對它而創(chuàng)建的引用。這是個“一次性運行”的函數(shù):創(chuàng)造它,執(zhí)行它,之后繼續(xù)其他的操作。
要想理解自執(zhí)行函數(shù)是如何工作的,你要記住函數(shù)都是對象,而對象都是值。因為在 JavaScript 中值可以被立即使用而無需先被儲存在變量里,所以你可以在一對圓括號中插入一個匿名函數(shù)來立即運行它。
但是,如果我們像下面這么做:
// 這么寫會被認為是一個語法錯誤 function() { var msg = "Hello World"; console.log(msg); }();
當 JavaScript 解析器遇到這行代碼會拋出一個語法錯誤,因為解析器會把這個函數(shù)當成一個函數(shù)聲明。這看起來是一個沒有標識符的函數(shù)聲明,而因為函數(shù)聲明的方式必須要在 function 關鍵字之后跟著一個標識符,所以解析器會拋出錯誤。
我們把函數(shù)放在一對圓括號中來告訴解析器這不是一個函數(shù)聲明,更準確的說,我們創(chuàng)建了一個函數(shù)并立即運行了它的值。因為我們沒有一個可用于調(diào)用這個函數(shù)的標識符,所以我們需要把函數(shù)放在一對圓括號中以便可以創(chuàng)建一個正確的方法來調(diào)用到這個函數(shù)。這種包圍在外層的圓括號應該出現(xiàn)在我們沒有一個明確的方式來調(diào)用函數(shù)的時候,比如我們現(xiàn)在說的這種自執(zhí)行函數(shù)。
注意:執(zhí)行操作符()可以既可以放在圓括號外面,也可以放在圓括號里面,如:(function() {…}())。但一般情況下大家更習慣于把執(zhí)行操作符放在外面。
自執(zhí)行函數(shù)的用處很多,其中最重要的一點是為變量和標識符創(chuàng)造一個受保護的局部作用域,看下面的例子:
// 頂層作用域 var a = 1; // 一個由自執(zhí)行函數(shù)創(chuàng)建的局部作用域 (function() { //局部作用域 var a = 2; })(); console.log(a); // 1
這里,外面先在頂層作用域創(chuàng)建了一個值為 1 的變量 a,之后創(chuàng)建一個自執(zhí)行函數(shù)并在里面再次聲明一個 a 變量并賦值為 2。因為這是一個局部作用域,所以外面的頂層作用域中的變量 a 的值并不會被改變。
這項技術目前很流行,尤其對于 JavaScript 庫(library)的開發(fā)者,因為局部變量進入一個不同作用域時需要避免標識符沖突。
另一種自執(zhí)行函數(shù)的用處是通過一次性的執(zhí)行來為你提供它的返回值:
// 把一個自執(zhí)行函數(shù)的返回值保存在一個變量里 var name = (function(name) { return ["Hello", name].join(" "); })("Mark"); console.log(name); // "Hello Mark"
別被這段代碼迷惑到:我們這里不是創(chuàng)建了一個函數(shù)表達式,而是創(chuàng)建了一個自執(zhí)行函數(shù)并立即執(zhí)行它,把它的返回值賦予變量 name。
自執(zhí)行函數(shù)另一個特色是可以為它配置標識符,類似一個函數(shù)聲明的做法:
(function myFn() { console.log(typeof myFn); // "function" })(); console.log(myFn); // "undefined"
雖然這看起來像是一個函數(shù)聲明,但這卻是一個自執(zhí)行函數(shù)。雖然我們?yōu)樗O置了一個標識符,但它并不會像函數(shù)聲明那樣在當前作用域創(chuàng)建一個變量。這個標識符使得你可以在函數(shù)內(nèi)部引用其自身,而不必另外在當前作用域再新建一個變量。這對于避免覆蓋當前作用域中已存在的變量尤其有好處。
同其他的函數(shù)形式一樣,自執(zhí)行函數(shù)也可以通過執(zhí)行操作符來傳遞參數(shù)。通過在函數(shù)內(nèi)部把函數(shù)的標志符作為一個變量并把該函數(shù)的返回值儲存在該變量中,我們可以創(chuàng)建一個遞歸的函數(shù)。
var number = 12; var numberFactorial = (function factorial(number) { return (number === 0) ? 1 : number * factorial(number - 1); })(number); console.log(numberFactorial); //479001600函數(shù)對象(Function Object)
最后一種函數(shù)形式,就是函數(shù)對象(funciton object),它不同于上面幾種采用函數(shù)字面量的方式,這種函數(shù)形式的語法如下:
// 一個函數(shù)對象 new Function("FormalArgument1", "FormalArgument2",..., "FunctionBody");
這里,我們使用 Function 的構造函數(shù)創(chuàng)建了一個新的函數(shù)并把字符串作為參數(shù)傳遞給它。前面的已經(jīng)命名的參數(shù)為新建函數(shù)對象的參數(shù),最后一個參數(shù)為這個函數(shù)的函數(shù)體。
注意:雖然這里我們把這種形式成為函數(shù)對象,但請記住其實所有的函數(shù)都是對象。我們在這里采用這個術語的目的是為了和函數(shù)字面量的方式進行區(qū)分。
下面我們采用這種形式創(chuàng)建一個函數(shù):
var add = new Function("a", "b", "return a + b;"); console.log(typeof add); // "function" console.log(add.name); // "" 或 "anonymous" console.log(add.length); // "2" console.log(add(20, 5)); // "25"
你可能會發(fā)現(xiàn)這種方式比采用函數(shù)字面量方式創(chuàng)建一個匿名函數(shù)要更簡單。和匿名函數(shù)一樣,對其檢測 name 屬性會得到一個空的字符串或 anonymous。在第一行,我們使用 Function 的構造函數(shù)創(chuàng)建了一個新的函數(shù),并賦值給變量 add。這個函數(shù)接收 2 個參數(shù) a 和 b,會在運行時將 a 和 b 相加并把相加結果做作為函數(shù)返回值。
使用這種函數(shù)形式類似于使用 eval:最后的一個字符串參數(shù)會在函數(shù)運行時作為函數(shù)體里的代碼被執(zhí)行。
注意:你不是必須將命名的參數(shù)作為分開的字符串傳遞,F(xiàn)unction 構造函數(shù)也允許一個字符串里包含多個以逗號分隔的項這種傳參方式。比如:new Function(‘a(chǎn), b’, ‘return a + b;’);
雖然這種函數(shù)形式有它的用處,但其相比函數(shù)字面量的方式存在一個顯著的劣勢,就是它是處在全局作用域中的:
// 全局變量 var x = 1; // 局部作用域 (function() { // 局部變量 var x = 5; var myFn = new Function("console.log(x)"); myFn(); // 1, not 5 })();
雖然我們在獨立的作用域中定義了一個局部變量,但輸出結果卻是 1 而非 5,這是因為 Function 構造函數(shù)是運行在全局作用域中。
參數(shù)(Arguments)所有函數(shù)都能從內(nèi)部訪問到它們的實參。這些實參會在函數(shù)內(nèi)部變?yōu)橐粋€個局部變量,其值是函數(shù)在調(diào)用時傳進來的那個值。另外,如果函數(shù)在調(diào)用時實際使用的參數(shù)少于它在定義時確定的形參,那么那些多余的未用到的參數(shù)的值就會是 undefined。
var myFn = function(frist, second) { console.log("frist : " + frist); console.log("second : " + second); }; myFn(1, 2); // first : 1 // second : 2 myFn("a", "b", "c"); // first : a // second : b myFn("test"); // first : test // second : undefined
因為 JavaScript 允許向函數(shù)傳遞任意個數(shù)的參數(shù),這也同時為我們提供了一個方式來判斷函數(shù)在調(diào)用時使用的實參和函數(shù)定義時的形參的數(shù)量是否相同。這個檢測的方式通過 arguments 這個對象來實現(xiàn),這個對象類似于數(shù)組,儲存著該函數(shù)的實參:
var myFn = function(frist, second) { console.log("length : " + arguments.length); console.log("frist : " + arguments[0]); }; myFn(1, 2); // length : 2 // frist : 1 myFn("a", "b", "c"); // length : 3 // frist : a myFn("test"); // length : 2 // frist : test
arguments 對象的 length 屬性可以顯示我們傳遞函數(shù)的實參個數(shù)。對實參的調(diào)用可以對 arguments 對象使用類似數(shù)組的下標法:arguments[0] 表示傳遞的第一個實參,arguments[1] 表示第二個實參。
使用 arguments 對象取代有名字的參數(shù),你可以創(chuàng)建一個可以對不同數(shù)量參數(shù)進行處理的函數(shù)。比如可以使用這種技巧來幫助我們改進前面的那個 add 函數(shù),使得其可以對任意數(shù)量的參數(shù)進行累加,最后返回累加的值:
var add = function(){ var result = 0, len = arguments.length; while(len--) result += arguments[len]; console.log(result); }; add(15); // 15 add(31, 32, 92); // 135 add(19, 53, 27, 41, 101); // 241
arguments 對象有一個很大的問題需要引起你的注意:它是一個可變的對象,你可以改變其內(nèi)部的參數(shù)值甚至是把它整個變成另一個對象:
var rewriteArgs = function() { arguments[0] = "no"; console.log(arguments[0]); }; rewriteArgs("yes"); // "no" var replaceArgs = function() { arguments = null; console.log(arguments === null); }; replaceArgs(); // "true"
上面第一個函數(shù)向我們展示了如果重置一個參數(shù)的值;后面的函數(shù)向我們展示了如何整體更改一個 arguments 對象。對于 arguments 對象來說,唯一的固定屬性就是 length 了,即使你在函數(shù)內(nèi)部動態(tài)的增加了 arguments 對象里的參數(shù),length 依然只顯示函數(shù)調(diào)用時賦予的實參的數(shù)量。
var appendArgs = function() { arguments[2] = "three"; console.log(arguments.length); }; appendArgs("one", "two"); // 2
當你寫代碼的時候,請確保沒有更改 arguments 內(nèi)的參數(shù)值或覆蓋這個對象。
對于 arguments 對象還有另一個屬性值:callee,這是一個針對該函數(shù)自身的引用。在前面的代碼中我們使用函數(shù)的標識符來實現(xiàn)在函數(shù)內(nèi)部引用其自身,現(xiàn)在我們換一種方式,使用 arguments.callee:
var number = 12; var numberFactorial = (function(number) { return (number === 0) ? 1 : number * arguments.callee(number - 1); })(number); console.log(numberFactorial); //479001600
注意這里我們創(chuàng)建的是一個匿名函數(shù),雖然我們沒有函數(shù)標識符,但依然可以通過 arguments.callee 來準確的引用其自身。創(chuàng)建這個屬性的意圖就是為了能在沒有標識符可供使用的時候(或者就算是有一個標識符時也可以使用 callee)來提供一個有效方式在函數(shù)內(nèi)部引用其自身。
雖然這是一個很有用的屬性,但在新的 ECMAScript 5 的規(guī)范中,arguments.callee 屬性卻被廢棄了。如果使用 ES5 的嚴格模式,該屬性會引起一個報錯。所以,除非真的是有必要,否則輕易不要使用這個屬性,而是用我們前面說過的方法使用標識符來達到同樣的目的。
雖然 JavaScript 允許給函數(shù)傳遞很多參數(shù),但卻并未提供一個設置參數(shù)默認值的方法,不過我們可以通過判斷參數(shù)值是否是 undefined 來模擬配置默認值的操作:
var greet = function(name, greeting) { // 檢測參數(shù)是否是定義了的 // 如果不是,就提供一個默認值 name = name || "Mark"; greeting = greeting || "Hello"; console.log([greeting, name]).join(" "); }; greet("Tim", "Hi"); // "Hi Tim" greet("Tim"); // "Hello Tim" greet(); // "Hello Mark"
因為未在函數(shù)調(diào)用時賦值的參數(shù)其值為 undefined,而 undefined 在布爾判斷時返回的是 false,所以我們可以使用邏輯或運算符 || 來為參數(shù)設置一個默認值。
另外一點需要特別注意的是,原生類型的參數(shù)(如字符串和整數(shù))是以值的方式來傳遞的,這意味著這些值的改變不會對外層作用域引起反射。不過,作為參數(shù)使用的函數(shù)和對象,則是以他們的引用來傳遞,在函數(shù)作用域中的對參數(shù)的任何改動都會引起外層的反射:
var obj = {name : "Mark"}; var changeNative = function(name) { name = "Joseph"; console.log(name); }; changeNative(obj.name); // "Joseph" console.log(obj.name); // "Mark" var changeObj = function(obj) { obj.name = "joseph"; console.log(obj.name); }; changeObj(obj); // "Joseph" console.log(obj.name); // "Joseph"
第一步我們將 obj.name 作為參數(shù)傳給函數(shù),因為其為一個原生的字符串類型,其傳遞的是它值的拷貝(儲存在棧上),所以在函數(shù)內(nèi)部對其進行改變不會對外層作用域中的 obj 產(chǎn)生影響。而接下來我們把 obj 對象本身作為一個參數(shù)傳遞,因為函數(shù)和對象等在作為參數(shù)進行傳遞時其傳遞的是對自身的引用(儲存在堆上),所以局部作用域中對其屬性值的任何更改都會立即反射到外層作用域中的 obj 對象。
最后,你可能會說之前我曾提到過 arguments 對象是類數(shù)組的。這意味著雖然 arguments 對象看起來像數(shù)組(可以通過下標來用于),但它沒有數(shù)組的那些方法。如果你喜歡,你可以用數(shù)組的 Array.prototype.slice 方法把 arguments 對象轉變?yōu)橐粋€真正的數(shù)組:
var argsToArray = function() { console.log(typeof arguments.callee); // "function" var args = Array.prototype.slice.call(arguments); console.log(typeof arguments.callee); // "undefined" console.log(typeof arguments.slice); // "function" }; argsToArray();返回值(Return Values)
Return 關鍵字用來為函數(shù)提供一個明確的返回值,JavaScript 允許在函數(shù)內(nèi)部書寫多個 return 關鍵字,函數(shù)會再其中一個執(zhí)行后立即退出。
var isOne = function(number) { if (number === 1) return true; console.log("Not one .."); return false; }; var one = isOne(1); console.log(one); // true var two = isOne(2); // Not one .. console.log(two); // false
在這個函數(shù)第一次被引用時,我們傳進去一個參數(shù) 1,因為我們在函數(shù)內(nèi)部先做了一個條件判斷,當前傳入的參數(shù)1使得該條件判斷語句返回 true,于是 return true 代碼會被執(zhí)行,函數(shù)同時立即停止。在第二次引用時我們傳進去的參數(shù) 2 不符合前面的條件判斷語句要求,于是函數(shù)會一直執(zhí)行到最后的 return false代碼。
在函數(shù)內(nèi)部設置多個 return 語句對于函數(shù)分層執(zhí)行是很有好處的。這同時也被普遍應用于在函數(shù)運行最開始對必須的變量進行檢測,如有不符合的情況則立即退出函數(shù)執(zhí)行,這既能節(jié)省時間又能為我們提供一個錯誤提示。下面的這個例子就是一段從 DOM 元素中獲取其自定義屬性值的代碼片段:
var getData = function(id) { if (!id) return null; var element = $(id); if (!element) return null; return element.get("data-name"); }; console.log(getData()); // null console.log(getData("non existent id")); // null console.log(getData("main")); // "Tim"
組后關于函數(shù)返回值要提醒各位的一點是:不論你希望與否,函數(shù)總是會提供一個返回值。如果未顯示地設置 return 關鍵字或設置的 return 未有機會執(zhí)行,則函數(shù)會返回一個 undefined。
函數(shù)內(nèi)部(Function Internals)我們前面討論過了函數(shù)形式、參數(shù)以及函數(shù)的返回值等與函數(shù)有關的核心話題,下面我們要討論一些代碼之下的東西。在下面的章節(jié)里,我們會討論一些函數(shù)內(nèi)部的幕后事務,讓我們一起來偷窺下當 JavaScript 解析器進入一個函數(shù)時會做些什么。我們不會陷入針對細節(jié)的討論,而是關注那些有利于我們更好的理解函數(shù)概念的那些重要的點。
有些人可能會覺得在最開始接觸 JavaScript 的時候,這門語言在某些時候會顯得不那么嚴謹,而且它的規(guī)則也不那么好理解。了解一些內(nèi)部機制有助于我們更好的理解那些看起來隨意的規(guī)則,同時在后面的章節(jié)里會看到,了解 JavaScript 的內(nèi)部工作機制會對你書寫出可靠的、健壯的代碼有著巨大的幫助。
注意:JavaScript 解析器在現(xiàn)實中的工作方式會因其制造廠商不同而不相一致,所以我們下面要討論的一些解析器的細節(jié)可能不全是準確的。不過 ECMAScript 規(guī)范對解析器應該如何執(zhí)行函數(shù)提供了基本的規(guī)則描述,所以對于函數(shù)內(nèi)部發(fā)生的事,我們是有著一套官方指南的。可執(zhí)行代碼和執(zhí)行上下文(Executable Code and Execution Contexts)
JavaScript 區(qū)分三種可執(zhí)行代碼:
全局代碼(Global code)是指出現(xiàn)在應用代碼中頂層的代碼。
函數(shù)代碼(Function code)是指在函數(shù)內(nèi)部的代碼或是在函數(shù)體之前被調(diào)用的代碼。
Eval 代碼(Eval code)是指被傳進 eval 方法中并被其執(zhí)行的代碼。
下面的例子展示了這三種不同的可執(zhí)行代碼:
// 這是全局代碼 var name = "John"; var age = 20; function add(a, b) { // 這是函數(shù)代碼 var result = a + b; return result; } (function() { // 這是函數(shù)代碼 var day = "Tuesday"; var time = function() { // 這還是函數(shù)代碼 // 不過和上面的代碼在作用域上是分開的 return day; }; })(); // 這是eval代碼 eval("alert("yay!");");
上面我們創(chuàng)建的 name、age 以及大部分的函數(shù)都在頂層代碼中,這意味著它們是全局代碼。不過,處于函數(shù)中的代碼是函數(shù)代碼,它被視為同全局代碼是相分隔的。函數(shù)中內(nèi)嵌的函數(shù),其內(nèi)部代碼同外部的函數(shù)代碼也被視為是相分隔的。
那么為什么我們需要對 JavaScript 中的代碼進行分類呢?這是為了在解析器解析代碼時能夠追蹤到其當前所處的位置,JavaScript 解析器采用了一個被稱為執(zhí)行上下文(execution context)的內(nèi)部機制。在處理一段腳本的過程中,JavaScript 會創(chuàng)建并進入不同的執(zhí)行上下文,這個行為本身不僅保存著它運行到這個函數(shù)當前位置所經(jīng)過的軌跡,同時還儲存著函數(shù)正常運行所需要的數(shù)據(jù)。
每個 JavaScript 程序都至少有一個執(zhí)行上下文,通常我們稱之為全局執(zhí)行上下文(global execution context),當一個 JavaScript 解析器開始解析你的程序的時候,它首先“進入”全局執(zhí)行上下文并在這個執(zhí)行上下文環(huán)境中處理代碼。當它遇到一個函數(shù),它會創(chuàng)建一個新的執(zhí)行上下文并進入這個上下文利用這個環(huán)境來執(zhí)行函數(shù)代碼。當函數(shù)執(zhí)行完畢或者遇到一個 return 結束之后,解析器會退出當先的執(zhí)行上下文并回到之前所處的那個執(zhí)行上下文環(huán)境。
這個看起來不是很好理解,我們下面用一個簡單的例子來把它理清:
var a = 1; var add = function(a, b) { return a + b; }; var callAdd = function(a, b) { return add(a, b); }; add(a, 2); call(1, 2);
這段簡單的代碼不單足夠幫助我們來理解上面說的事情,同時還是一個很好的例子來展示 JavaScript 是如何創(chuàng)建、進入并離開一個執(zhí)行上下文的。讓我們一步一步來分析:
當程序開始執(zhí)行,Javascript 解析器首先進入全局執(zhí)行上下文并在這里解析代碼。它會先創(chuàng)建三個變量 a、add、callAdd,并分別為它們賦值為數(shù)字 1、一個函數(shù)和另一個函數(shù)。
解析器遇到了一個針對 add 函數(shù)的調(diào)用。于是解析器創(chuàng)建了一個新的執(zhí)行上下文,進入這個上下文,計算 a + b 表達式的值,之后返回這個表達式的值。當這個值被返回后,解析器離開了這個它新創(chuàng)建的執(zhí)行上下文,把它銷毀掉,重新回到全局執(zhí)行上下文。
接下來解析器遇到了另一個函數(shù)調(diào)用,這次是對 callAdd 的調(diào)用。像第二步一樣,解析器會新創(chuàng)建一個執(zhí)行上下文,并在它解析 callAdd 函數(shù)體中的代碼之前先進入這個執(zhí)行上下文。當它對函數(shù)體內(nèi)的代碼進行處理的時候,遇到了一個新的函數(shù)調(diào)用——這次是對 add 的調(diào)用,于是解析器會再新建一個執(zhí)行上下文并進入這里。此時,我們已有了三個執(zhí)行上下文:一個全局執(zhí)行上下文、一個針對 callAdd 的執(zhí)行上下文,一個針對 add 函數(shù)的執(zhí)行上下文。最后一個是當前被激活的執(zhí)行上下文。當 add 函數(shù)調(diào)用執(zhí)行完畢后,當前的執(zhí)行上下文會被銷毀并回到 callAdd 的執(zhí)行上下文中,callAdd 的執(zhí)行上下文中的運行結果也是返回一個值,這通知解析器退出并銷毀當前的執(zhí)行上下文,重新回到全局執(zhí)行上下文中。
執(zhí)行上下文的概念對于一個在代碼中不會直接面對它的前端新人來說,可能是會有一點復雜,這是可以理解的。你此時可能會問,那既然我們在編程中不會直接面對執(zhí)行上下文,那我們又為什么要討論它呢?
答案就在于執(zhí)行上下文的其他那些用途。我在前面提到過 JavaScript 解析器依靠執(zhí)行上下文來保存它運行到當前位置所經(jīng)過的軌跡,此外一些程序內(nèi)部相互關聯(lián)的對象也要依靠執(zhí)行上下文來正確處理你的程序。
變量和變量初始化(Variables and Variable Instantition)這些內(nèi)部的對象之一就是變量對象(variable object)。每一個執(zhí)行上下文都擁有它自己的變量對象用來記錄在當前上下文環(huán)境中定義的變量。
在 JavaScript 中創(chuàng)建變量的過程被稱為變量初始化(variable instantition)。因為 JavaScript 是基于詞法作用域的,這意味著一個變量所處的作用域由其在代碼中被實例化的位置所決定。唯一的例外是不采用關鍵字 var 創(chuàng)建的變量是全局變量。
var fruit = "banana"; var add = function(a, b) { var localResult = a + b; globalResult = localResult; return localResult; }; add(1, 2);
在這個代碼片段中,變量 fruit 和函數(shù) add 處于全局作用域中,在整個腳本中都能被訪問到。而對于變量 localResult、a、b 則是局部變量,只能在函數(shù)內(nèi)部被訪問到。而變量 globalResult 因為在聲明時缺少關鍵字 var,所以它會成為一個全局變量。
當 JavaScript 解析器進入一個執(zhí)行上下文中,首先要做的就是變量初始化操作。解析器首先會在當前的執(zhí)行上下文中創(chuàng)建一個 variable 對象,之后在當前上下文環(huán)境中搜索 var 聲明,創(chuàng)建這些變量并添加進之前創(chuàng)建的 variable 對象中,此時這些變量的值都被設置為 undefined。讓我們審視一下我們的演示代碼,我們可以說變量 fruit 和 add 通過 variable 對象在當前執(zhí)行上下文中被初始化,而變量 localResult、a、b 則通過 variable 對象在 add 函數(shù)的上下文空間中被初始化。而 globalResult 則是一個需要被特別注意的變量,這個我們一會再來討論它。
關于變量初始化有很重要的一點需要我們?nèi)ビ涀。褪撬瑘?zhí)行上下文是緊密結合的。回憶一下,前面我們對 JavaScript 劃分了三種不同的執(zhí)行代碼:全局代碼、函數(shù)代碼和 eval 代碼。同理,我們也可以說存在著三種不同的執(zhí)行上下文:全局執(zhí)行上下文、函數(shù)執(zhí)行上下文、eval 執(zhí)行上下文。因為變量初始化是通過處于執(zhí)行上下文中的 variable 對象實現(xiàn)的,進而可以說也存在著三種類型的變量:全局變量、處于函數(shù)作用域中的變量以及來自 eval 代碼中的變量。
這為我們引出了很多人對這門語言感覺困惑的那些問題中一個:JavaScript 沒有塊級作用域。在其他的類 C 語言中,一對花括號中的代碼被稱為一個塊(block),塊有著自己獨立的作用域。因為變量初始化發(fā)生在執(zhí)行上下文這一層級中,所以在當前執(zhí)行上下文中任意位置被初始化的變量,在這整個上下文空間中(包括其內(nèi)部的其他子上下文空間)都是可見的:
var x = 1; if (false) { var y =2; } console.log(x); // 1 console.log(y); // undefined
在擁有塊級作用域的語言中,console.log(y) 會拋出一個錯誤,因為條件判斷語句中的代碼是不會被執(zhí)行的,那么變量 y 自然也不會被初始化。但在 JavaScript 中這并不會拋出一個錯誤,而是告訴我們 y 的值是 undefined,這個值是一個變量已經(jīng)被初始化但還未被賦值時所具有的默認值。這個行為看起來挺有意思,不是么?
不過,如果我們還記得變量初始化是發(fā)生在執(zhí)行上下文這一層級中,我們就會明白這種行為其實正是我們所期望的。當 JavaScript 開始解析上面的代碼塊的時候,它首先會進入全局執(zhí)行上下文,之后在整個上下文環(huán)境中尋找變量聲明并初始化它們,之后把他們加入 variable 對象中去。所以我們的代碼實際上是像下面這樣被解析的:
var x; var y; x = 1; if (false) { y = 2; } console.log(x); // 1 console.log(y); // undefined
同樣的在上下文環(huán)境中的初始化也適用于函數(shù):
function test() { console.log(value); // undefined var value = 1; console.log(value); // 1 } test();
雖然我們對變量的賦值操作是在第一行 log 語句之后才進行的,但第一行的 log 還是會給我們返回一個 undefined 而非一個報錯。這是因為變量初始化是先于函數(shù)內(nèi)其他任何執(zhí)行代碼之前進行的。我們的變量會在第一時間被初始化并被暫時設置為 undefined,其到了第二行代碼被執(zhí)行時才被正式賦值為 1。所以說將變量初始化的操作放在代碼或函數(shù)的最前面是一個好習慣,這樣可以保證在當前作用域的任何位置,變量都是可用的。
就像你見到的,創(chuàng)建變量的過程(初始化)和給變量賦值的過程(聲明)是被 JavaScript 解析器分開執(zhí)行的。我們回到上一個例子:
var add = function(a, b) { var localResult = a + b; globalResult = localResult; return localResult; }; add(1, 2);
在這個代碼片段中,變量 localResult 是函數(shù)的一個局部變量,但是 globalResult 卻是一個全局變量。對于這個現(xiàn)象最常見的解釋是因為在創(chuàng)建變量時缺少關鍵字 var 于是變量成了全局的,但這并不是一個靠譜的解釋。現(xiàn)在我們已經(jīng)知道了變量的初始化和聲明是分開進行的,所以我們可以從一個解析器的視角把上面的代碼重寫:
var add = function(a, b) { var localResult; localResult = a + b; globalResult = localResult; return localResult; }; add(1, 2);
變量 localResult 會被初始化并會在當前執(zhí)行上下文的 variable 對象中創(chuàng)建一個針對它的引用。當解析器看到 “l(fā)ocalResult = a + b;” 這一行時,它會在當前執(zhí)行上下文環(huán)境的 variable 對象中檢查是否存在一個 localResult 對象,因為現(xiàn)在存在這么一個變量,于是這個值(a + b)被賦給了它。然而,當解析器遇到 “globalResult = localResult;” 這一行代碼時,它不論在當前環(huán)境的 variable 對象中還是在更上一級的執(zhí)行上下文環(huán)境(對本例來說是全局執(zhí)行上下文)的 variable 對象中都沒找到一個名為 globalResult 的對象引用。因為解析器始終找不到這么一個引用,于是它認為這是一個新的變量,并會在它所尋找的最后一層執(zhí)行上下文環(huán)境——總會是全局執(zhí)行上下文——中創(chuàng)建這么一個新的變量。于是, globalResult 最后成了一個全局變量。
作用域和作用域鏈(Scoping and Scope Chain)在執(zhí)行上下文的作用域中查找變量的過程被稱為標識符解析(indentifier resolution),這個過程的實現(xiàn)依賴于函數(shù)內(nèi)部另一個同執(zhí)行上下文相關聯(lián)的對象——作用域鏈(scope chain)。就像它的名字所蘊含的那樣,作用域鏈是一個有序鏈表,其包含著用以告訴 JavaScript 解析器一個標識符到底關聯(lián)著哪一個變量的對象。
每一個執(zhí)行上下文都有其自己的作用域鏈,該作用域鏈在解析器進入該執(zhí)行上下文之前就已經(jīng)被創(chuàng)建好了。一個作用域鏈可以包含數(shù)個對象,其中的一個便是當前執(zhí)行上下文的 variable 對象。我們看一下下面的簡單代碼:
var fruit = "banana"; var animal = "cat"; console.log(fruit); // "banana" console.log(animal); // "cat"
這段代碼運行在全局執(zhí)行上下文中,所以變量 fruit 和 animal 儲存在全局執(zhí)行上下文的 variable 對象中。當解析器遇到 “console.log(fruit);” 這段代碼,它看到了標識符 fruit 并在當前的作用域鏈(目前只包含了一個對象,就是當前全局執(zhí)行上下文的 variable 對象)中尋找這個標識符的值,于是接下來解析器發(fā)現(xiàn)這個變量有一個內(nèi)容為 “banana” 的值。下一行的 log 語句的執(zhí)行過程同這個是一樣的。
同時,全局執(zhí)行上下文中的 variable 對象還有另外一個用途,就是被用做 global 對象。解析器對 global 對象有其自身的內(nèi)部實現(xiàn)方式,但依然可以通過 JavaScript 在當前窗口中自身的window對象或當前 JavaScript 解析器的 global 對象來訪問到。所有的全局對象實際上都是 global 對象中的成員:在上面的例子中,你可以通過 window.fruit、global.fruit 或 window.animal、global.animal 來引用變量 fruit 和 animal。global 對象對所有的作用域鏈和執(zhí)行上下文都可用。在我們這個只是全局代碼的例子里,global 對象是這個作用域鏈中僅有的一個對象。
好吧,這使得函數(shù)變得更加不易理解了。除了 global 對象之外,一個函數(shù)的作用域鏈還包含擁有其自身執(zhí)行上下文環(huán)境的變量對象。
var fruit = "banana"; var animal = "cat"; function sayFruit() { var fruit = "apple"; console.log(fruit); // "apple" console.log(animal); // "cat" } console.log(fruit); // "banana" console.log(animal); // "cat" sayFruit();
對于全局執(zhí)行上下文中的代碼,fruit 和 animal 標識符分別指向 “banana” 和 “cat” 值,因為它們的引用是被存儲在執(zhí)行上下文的 variable 對象中(也就是 global 對象中)的。不過,在 sayFruit 函數(shù)里標識符 fruit 對應的卻是另一個值 —— “apple”。因為在這個函數(shù)內(nèi)部,聲明并初始化了另一個變量 fruit。因為當前執(zhí)行上下文中的 variable 對象在作用域鏈中處在更靠前的位置(相比全局執(zhí)行上下文中的 variable 對象而言),所以 JavaScript 解析器會知道現(xiàn)在處理的應該是一個局部變量而非全局變量。
因為 JavaScript 是基于詞法作用域的,所以標識符解析還依賴于函數(shù)在代碼中的位置。一個嵌在函數(shù)中的函數(shù),可以訪問到其外層函數(shù)中的變量:
var fruit = "banana"; function outer() { var fruit = "orange"; function inner() { console.log(fruit); // "orange" } inner(); } outer();
inner 函數(shù)中的變量 fruit 具有一個 “orange” 的值是因為這個函數(shù)的作用域鏈不單單包含了它自己的 variable 對象,同時還包含了它被聲明時所處的那個函數(shù)(這里指 outer 函數(shù))的 variable 對象。當解析器遇到 inner 函數(shù)中的標識符 fruit,它首先會在作用域鏈最前面的 inner 函數(shù)的 variable 對象中尋找與之同名的標識符,如果沒有,則去下一個 variable 對象(outer 函數(shù)的)中去找。當解析器找到了它需要的標識符,它就會停在那并把 fruit 的值設置為 “orange”。
不過要注意的是,這種方式只適用于采用函數(shù)字面量創(chuàng)建的函數(shù)。而采用構造函數(shù)方式創(chuàng)建的函數(shù)則不會這樣:
var fruit = "banana"; function outer() { var fruit = "orange"; var inner = new Function("console.log(fruit);"); inner(); // "banana" } outer();
在這個例子里,我們的 inner 函數(shù)不能訪問 outer 函數(shù)里的局部變量 fruit,所以 log 語句的輸出結果是 “banana” 而非 “orange”。發(fā)生這種情況的原因是因為采用 new Function() 創(chuàng)建的函數(shù)其作用域鏈僅含有它自己的 variable 對象和 global 對象,而其外圍函數(shù)的 variable 對象都不會被加入到它的作用域鏈中。因為在這個采用構造函數(shù)方式新建的函數(shù)自身的 variable 對象中沒有找到標識符 fruit,于是解析器去后面一層的 global 對象中查找,在這里面找到了一個 fruit 標識符,其值為 “banana”,于是被 log 了出來。
作用域鏈的創(chuàng)建發(fā)生在解析器創(chuàng)建執(zhí)行上下文之后、變量初始化之前。在全局代碼中,解析器首先會創(chuàng)建一個全局執(zhí)行上下文,之后創(chuàng)建作用域鏈,之后繼續(xù)創(chuàng)建全局執(zhí)行上下文的 variable 對象(這個對象同時也成為 global 對象),再之后解析器會進行變量初始化,之后把儲存了這些初始化了的變量的 variable 對象加入到前面創(chuàng)建的作用域鏈中。在函數(shù)代碼中,發(fā)生的情況也是一樣的,唯一不同的是 global 對象會首先被加入到函數(shù)的作用域鏈,之后把其外圍函數(shù)的的 variable 對象加入作用域鏈,最后加入作用域鏈的是該函數(shù)自己的 variable 對象。因為作用域鏈在技術角度來講屬于邏輯上的一個棧,所以解析器的查找操作所遵循的是從棧上第一個元素開始向下順序查找。這就是為什么我們絕大部分的局部變量是最后才被加入到作用域鏈卻在解析時最先被找到的原因。
閉包(Closures)JavaScript 中函數(shù)是一等對象以及函數(shù)可以引用到其外圍函數(shù)的變量使得 JavaScript 相比其他語言具備了一個非常強大的功能:閉包(closures)。雖然增加這個概念會使對 JavaScript 這部分的學習和理解變得更加困難,但必須承認這個特色使函數(shù)的用途變得非常強大。在前面我們已經(jīng)討論過了 JavaScript 函數(shù)的內(nèi)在工作機制,這正好能幫助我們了解閉包是如何工作的,以及我們應該如何在代碼中使用閉包。
一般情況下,JavaScript 變量的生命周期被限定在聲明其的函數(shù)內(nèi)。全局變量在整個程序未結束之前一直存在,局部變量則在函數(shù)未結束之前一直存在。當一個函數(shù)執(zhí)行完畢,其內(nèi)部的局部變量會被 JavaScript 解析器的垃圾回收機制銷毀從而不再是一個變量。當一個內(nèi)嵌函數(shù)保存了其外層函數(shù)一個變量的引用,即使外層函數(shù)執(zhí)行完畢,這個引用也繼續(xù)被保存著。當這種情況發(fā)生,我們說創(chuàng)建了一個閉包。
不好理解?讓我們看幾個例子:
var fruit = "banana"; (function() { var fruit = "apple"; console.log(fruit); // "apple" })(); console.log(fruit); // "banana"
這里,我們有一個創(chuàng)建了一個 fruit 變量的自執(zhí)行函數(shù)。在這個函數(shù)內(nèi)部,變量 fruit 的值是 apple。當這個函數(shù)執(zhí)行完畢,值為 apple 的變量 fruit 便被銷毀。于是只剩下了值為 banana 的全局變量 fruit。此種情況下我們并未創(chuàng)建一個閉包。再看看另一種情況:
var fruit = "banana"; (function() { var fruit = "apple"; function inner() { console.log(fruit); // "apple" } inner(); })(); console.log(fruit); // "banana"
這段代碼和上一個很類似,自執(zhí)行函數(shù)創(chuàng)建了一個 fruit 變量和一個 inner 函數(shù)。當 inner 函數(shù)被調(diào)用時,它引用了外層函數(shù)中的變量 fruit,于是我們的得到了一個 apple 而不是 banana。不幸的是,對于自執(zhí)行函數(shù)來說,這個 inner 函數(shù)是一個局部對象,所以在自執(zhí)行函數(shù)結束后,inner 函數(shù)也會被銷毀掉。我們還是沒創(chuàng)建一個閉包,再來看一個例子:
var fruit = "banana"; var inner; (function() { var fruit = "apple"; inner = function() { console.log(fruit); } })(); console.log(fruit); // "banana" inner(); // "apple"
現(xiàn)在開始變得有趣了。在全局作用域中我們聲明了一個名為 inner 的變量,在自執(zhí)行函數(shù)中我們把一個 log 出 fruit 變量值的函數(shù)作為值賦給全局變量 inner。正常情況下,當自執(zhí)行函數(shù)結束后,其內(nèi)部的局部變量 fruit 應該被銷毀,就像我們前面 2 個例子那樣。但是因為在 inner 函數(shù)中依然保持著對局部變量 fruit 的引用,所以最后我們在調(diào)用 inner 時會 log 出 apple。這時可以說我們創(chuàng)建了一個閉包。
一個閉包會在這種情況下被創(chuàng)建:一個內(nèi)層函數(shù)嵌套在一個外層函數(shù)里,這個內(nèi)層函數(shù)被儲存在其外層函數(shù)作用域之外的作用域的 variable 對象中,同時還保存著對其外層函數(shù)局部變量的引用。雖然外層函數(shù)中的這個 inner 函數(shù)不會再被運行,但其對外層函數(shù)變量的引用卻依然保留著,這是因為在函數(shù)內(nèi)部的作用域鏈中依然保存著該變量的引用,即使外層的函數(shù)此時已經(jīng)不存在了。
要記住一個函數(shù)的作用域鏈同它的執(zhí)行上下文是綁定的,同其他那些與執(zhí)行上下文關聯(lián)緊密的對象一樣,作用域鏈在函數(shù)執(zhí)行上下文被創(chuàng)建之后創(chuàng)建,并隨著函數(shù)執(zhí)行上下文的銷毀而銷毀。解析器只有在函數(shù)被調(diào)用時才會創(chuàng)建該函數(shù)的執(zhí)行上下文。在上面的例子中,inner 函數(shù)是在最后一行代碼被執(zhí)行時調(diào)用的,而此時,原匿名函數(shù)的執(zhí)行上下文(連同它的作用域鏈和 variable 對象)都已經(jīng)被銷毀了。那么 inner 函數(shù)是如何引用到已經(jīng)被銷毀的保存在局部作用域中的局部變量的呢?
這個問題的答案引出了函數(shù)內(nèi)部對象中一個被稱為 scope 屬性(scope property)的對象。所有的 JavaScript 函數(shù)都有其自身的內(nèi)在 scope 屬性,該對象中儲存著用來創(chuàng)建該函數(shù)作用域鏈的那些對象。當解析器要為一個函數(shù)創(chuàng)建作用域鏈,它會去查看 scope 屬性看看哪些項是需要被加進作用域鏈中的。因為相比執(zhí)行上下文,scope 屬性同函數(shù)本身的聯(lián)系更為緊密,所以在函數(shù)被徹底銷毀之前,它都會一直存在——這樣苦于保證不了函數(shù)被調(diào)用多少次,它都是可用的。
一個在全局作用域中被創(chuàng)建的函數(shù)擁有一個包含了 global 對象的 scope 對象,所以它的作用域鏈僅包含了 global 對象和和它自己的 variable 對象。一個創(chuàng)建在其他函數(shù)中的函數(shù),它的 scope 對象包含了封裝它的那個函數(shù)的 scope 對象中的所有對象和它自己的 variable 對象。
function A() { function B() { function C() { } } }
在這個代碼片段中,函數(shù) A 的 scope 屬性中僅保存了 global 對象。因為函數(shù)嵌套在函數(shù) A 中,所有函數(shù) B 的 scope 屬性會繼承函數(shù) A 的 scope 屬性的內(nèi)容并附加上函數(shù) A 的 variable 對象。最后,函數(shù) C 的 scope 屬性會繼承函數(shù) B 的 scope 屬性中的所有內(nèi)容。
另外,采用函數(shù)對象方式(使用 new Function() 方法)創(chuàng)建的函數(shù),在它們的 scope 屬性中只有一個項,就是 global 對象。這意味著它們不能訪問其外圍函數(shù)(如果有的話)的局部變量,也就不能用來創(chuàng)建閉包。
This 關鍵字(The “this” Keyword)上面我們討論了一些函數(shù)的內(nèi)部機制,最后我們還有一個項目要討論:this 關鍵字。如果你對其他的面向?qū)ο蟮木幊陶Z言有使用經(jīng)驗,你應該會對一些關鍵字感到熟悉,比如 this 或者 self,用以指代當前的實例。不過在 JavaScript 中 this 關鍵字會便得有些復雜,因為它的值取決于執(zhí)行上下文和函數(shù)的調(diào)用者。同時 this 還是動態(tài)的,這意味著它的值可以在程序運行時被更改。
this 的值總是一個對象,并且有些一系列規(guī)則來明確在當前代碼塊中哪一個對象會成為 this。其中最簡單的規(guī)則就是,在全局環(huán)境中,this 指向全局對象。
var fruit = "banana"; console.log(fruit); // "banana" console.log(this.fruit); // "banana"
回憶一下,全局上下文中聲明的變量都會成為全局 global 對象的屬性。這里我們會看到 this.fruit 會正確的指向 fruit 變量,這向我們展示在這段代碼中 this 關鍵字是指向 global 對象的。對于全局上下文中聲明的函數(shù),在其函數(shù)體中 this 關鍵字也是指向 global 對象的。
var fruit = "banana"; function sayFruit() { console.log(this.fruit); } sayFruit(); // "banana" (function() { console.log(this.fruit); // "banana" })(); var tellFruit = new Function("console.log(this.fruit);"); tellFruit(); // "banana"
對于作為一個對象的屬性(或方法)的函數(shù),this 關鍵字指向的是這個對象本身而非 global 對象:
var fruit = { name : "banana", say : function() { console.log(this.name); } }; fruit.say(); // "banana"
在第三章我們會深入討論關于對象的話題,但是現(xiàn)在,我們要關注 this.name 屬性是如何指向 fruit 對象的 name 屬性的。在本質(zhì)上,這和前面的例子是一樣的:因為上面例子中的函數(shù)是 global 對象的屬性,所以函數(shù)體內(nèi)的 this 關鍵字會指向 global 對象。所以對于作為某個對象屬性的函數(shù)而言,其函數(shù)體內(nèi)的 this 關鍵字指向的就是這個對象。
對于嵌套的函數(shù)而言,遵循第一條規(guī)則:不論它們出現(xiàn)在哪里,它們總是將 global 對象作為其函數(shù)體中 this 關鍵字的默認值。
var fruit = "banana"; (function() { (function() { console.log(this.fruit); // "banana" })(); })(); var object = { fruit : "orange", say : function() { var sayFruit = function() { console.log(this.fruit); // "banana" }; sayFruit(); } }; object.say();
這里,我們看到處在兩層套嵌的子執(zhí)行函數(shù)中的標識符 this.fruit 指向的是 global 對象中的 fruit 變量。在 say 函數(shù)中有一個內(nèi)嵌函數(shù)的例子中,即使 say 函數(shù)自身的 this 指向的是 object 對象,但內(nèi)嵌的 sayFruit 函數(shù)中的 this.fruit 指向的還是 banana。這意味著外層函數(shù)并不會對內(nèi)嵌函數(shù)代碼體中 this 關鍵字的值產(chǎn)生任何影響。
我在前面提到過 this 關鍵字的值是可變的,且在 JavaScript 中能夠?qū)?this 的值進行改變是很有用的。有兩種方法可以應用于更改函數(shù) this 關鍵字的值:apply 方法和 call 方法。這兩種方法實際上都是應用于無需使用調(diào)用操作符 () 來調(diào)用函數(shù),雖然沒有了調(diào)用操作符,但你還是可以通過 apply 和 call 方法給函數(shù)傳遞參數(shù)。
apply 方法接收 2 個參數(shù):thisValue 被用于指明函數(shù)體中 this 關鍵字所指向的對象;另一個參數(shù)是 params,它以數(shù)組的形式向函數(shù)傳遞參數(shù)。當使用一個無參數(shù)或第一個參數(shù)為 null 的 apply 方法去調(diào)用一個函數(shù)的時候,那么被調(diào)用的函數(shù)內(nèi)部 this 指向的就會是 global 對象并且也意味著沒有參數(shù)傳遞給它:
var fruit = "banana" var object = { fruit : "orange", say : function() { console.log(this.fruit); } }; object.say(); // "banana" object.say.apply(); // "banana"
如果要將一個函數(shù)內(nèi)部的 this 關鍵字指向另一個對象,簡單的做法就是使用 apply 方法并把那個對象的引用作為參數(shù)傳進去:
function add() { console.log(this.a + this.b); } var a = 12; var b = 13; var values = { a : 50, b : 23 }; add.apply(values); // 73
apply 方法的第二個參數(shù)是以一個數(shù)組的形式向被調(diào)用的函數(shù)傳遞參數(shù),數(shù)組中的項要和被調(diào)用函數(shù)的形參保持一致。
function add(a, b) { console.log(a); // 20 console.log(b); // 50 console.log(a + b); // 70 } add.apply(null, [20, 50]);
上面說到的另一個方法 call,和 apply 方法的工作機制是一樣的,所不同的是在 thisValue 參數(shù)之后跟著的是自選數(shù)量的參數(shù),而不是一個數(shù)組:
function add(a, b) { console.log(a); // 20 console.log(b); // 50 console.log(a + b); // 70 } add.call(null, 20, 50);高級的函數(shù)技巧(Advanced Function Techniques)
前面的內(nèi)容主要是關于我們對函數(shù)的基礎知識的一些討論。不過,要想完整的展現(xiàn)出 JavaScript 函數(shù)的魅力,我們還必須能夠應用前面學到的這些分散的知識。
在下面的章節(jié)中,我們會討論一些高級的函數(shù)技巧,并探索目前所掌握的技能其更廣泛的應用范圍。我想說,本書不會是 JavaScript 學習的終點,我們不可能把關于這門語言的所有信息都寫出來,而應該是開啟你探索之路的一個起點。
限制作用域(Limiting Scope)現(xiàn)在,我在維護一個用戶的姓名和年齡這個事情上遇到了問題。
// user對象保存了一些信息 var user = { name : "Mark", age : 23 }; function setName(name) { // 首先確保name是一個字符串 if (typeof name === "string") user.name = name; } function getName() { return user.name; } function setAge(age) { // 首先確保age是一個數(shù)字 if (typeof age === "number") user.age = age; } function getAge() { return user.age; } // 設置一個新的名字 setName("Joseph"); console.log(getName()); // "Joseph" // 設置一個新的年齡 setAge(22); console.log(getAge()); // 22
目前為止,一切都正常。setName 和 setAge 函數(shù)確保我們要設置的值是正確的類型。但我們要注意到,user 變量是出在全局作用域中的,可以在該作用域內(nèi)的任何地方被訪問到,這回導致你可以不適應我們的設置函數(shù)也能夠設置 name 和 age 的值:
user.name = 22; user.age = "Joseph"; console.log(getName()); // 22 console.log(getAge()); // Joseph
很明顯這樣不好,因為我們希望這些值能夠保持其數(shù)據(jù)類型的正確性。
那么我們該怎么做呢?如何你回憶一下,你會記起一個創(chuàng)建在函數(shù)內(nèi)部的變量會成為一個局部變量,在該函數(shù)外部是不能被訪問到的,另外閉包卻可以為一個函數(shù)能夠保存其外層函數(shù)局部變量的引用提供途徑。結合這些知識點,我們可以把 user 變成一個受限制的局部變量,再利用閉包來使得獲取、設置等函數(shù)可以對其進行操作。
// 創(chuàng)建一個自執(zhí)行函數(shù) // 包圍我們的代碼使得user變成局部變量 (function() { // user對象保存了一些信息 var user = { name : "Mark", age : 23 }; setName = function(name) { // 首先確保name是一個字符串 if (typeof name === "string") user.name = name; }; getName = function() { return user.name; }; setAge = function(age) { // 首先確保age是一個數(shù)字 if (typeof age === "number") user.age = age; }; getAge = function() { return user.age; } })(); // 設置一個新的名字 setName("Joseph"); console.log(getName()); // "Joseph" // 設置一個新的年齡 setAge(22); console.log(getAge()); // 22
現(xiàn)在,如果有什么人想不通過我們的 setName 和 setAge 方法來設置 user.name 和 user.age 的值,他就會得到一個報錯。
柯里化(Currying)函數(shù)作為一等對象最大的好處就是可以在程序運行時創(chuàng)建它們并將之儲存在變量里。如下面的這段代碼:
function add(a, b) { return a + b; } add(5, 2); add(5, 5); add(5, 200);
這里我們每次都使用 add 函數(shù)將數(shù)字 5 和其他三個數(shù)字進行相加,如果能把數(shù)字 5 內(nèi)置在函數(shù)中而不用每次調(diào)用時都作為參數(shù)傳進去是個不錯的主意。我們可以將 add 函數(shù)的內(nèi)部實現(xiàn)機制變?yōu)?5 + b 的方式,但這會導致我們代碼中其他已經(jīng)使用了舊版 add 函數(shù)的部分發(fā)生錯誤。那有沒有什么方法可以實現(xiàn)不修改原有 add 函數(shù)的優(yōu)化方式?
當然我們可以,這種技術被稱為柯里化(partial application 或 currying),其實現(xiàn)涉及到一個可為其提前“提供”一些參數(shù)的函數(shù):
var add= function(a, b) { return a + b; }; function add5() { return add(5, b); } add5(2); add5(5); add5(200);
現(xiàn)在,我們創(chuàng)建了一個調(diào)用 add 函數(shù)并預置了一個參數(shù)值(這里是5)的 add5 函數(shù),add5 函數(shù)本質(zhì)上來講其實就是預置了一個參數(shù)(柯里化)的 add 函數(shù)。不過,上面的例子并沒展示出這門技術動態(tài)的一面,如果我們提供的默認值是另
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106872.html
Javascript anonymous functions Anonymous functions are functions that are dynamically declared at runtime. They’re called anonymous functions because they aren’t given a name in the same way as no...
摘要:函數(shù)是一等公民,是什么意思呢我來與大家探討一下,拋磚引玉。對于來說,函數(shù)可以賦值給變量,也可以作為函數(shù)參數(shù),還可以作為函數(shù)返回值,因此中函數(shù)是一等公民。也就是說,函數(shù)為第一公民是函數(shù)式編程的必要條件。 摘要: 聽起來很炫酷的一等公民是啥? 《JavaScript深入淺出》系列: JavaScript深入淺出第1課:箭頭函數(shù)中的this究竟是什么鬼? JavaScript深入淺出第2課...
摘要:概述微軟為增加了使用編寫自定義函數(shù)的支持。在上這個,按照文件可以體驗此功能,或者直接在中編寫自定義函數(shù)。已知問題不支持移動版目前需要依賴隱藏的瀏覽器進程來支持異步自定義函數(shù)當中不相關數(shù)據(jù)發(fā)生變化時,某些函數(shù)需要自動重新計算。 0. 概述 微軟為 Excel 增加了使用 JavaScript 編寫自定義函數(shù)的支持。 1. 示例 比如一個功能:兩數(shù)之和加 42: showImg(https...
摘要:標簽前端作者更多文章個人網(wǎng)站 Learning Notes - Understanding the Weird Parts of JavaScript 標簽 : 前端 JavaScript [TOC] The learning notes of the MOOC JavaScript: Understanding the Weird Parts on Udemy,including...
摘要:對象方法當用作對象屬性時,函數(shù)稱為方法箭頭函數(shù)中的當箭頭函數(shù)與常規(guī)函數(shù)用作對象方法時,有一個重要的行為。這是因為的處理在兩個函數(shù)聲明樣式中是不同的。會將函數(shù)移動到其范圍的頂部。變量聲明被提升,但不是值,因此不是函數(shù)。 簡介 JavaScript中的所有內(nèi)容都發(fā)生在函數(shù)中。 函數(shù)是一個代碼塊,可以定義一次并隨時運行。 函數(shù)可以選擇接受參數(shù),并返回一個值。 JavaScript中的函數(shù)是對...
閱讀 2290·2023-04-26 00:01
閱讀 796·2021-10-27 14:13
閱讀 1810·2021-09-02 15:11
閱讀 3381·2019-08-29 12:52
閱讀 528·2019-08-26 12:00
閱讀 2569·2019-08-26 10:57
閱讀 3405·2019-08-26 10:32
閱讀 2848·2019-08-23 18:29