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