JAVASCRIPT FUNCTIONS 詳解

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文是@堂主 對《Pro JavaScript with Mootools》一書的第二章函數部分知識點講解的翻譯。該書的作者 Mark Joseph Obcena 是 Mootools 庫的作者和目前開發團隊的 Leader。雖然本篇文章實際譯於 2012 年初,但個人感覺這部分對 Javascript 函數的基本知識、內部機制及 JavaScript 解析器的運行機制講的非常明白,脈絡也清楚,對初學者掌握 JavaScript 函數基礎知識很有好處。尤其難得的是不同於其他 JavaScript書籍講述的都是分散的知識點,這本書的知識講解是有清晰脈絡的,循序漸進。換句話說,這本書中的 JavaScript 知識是串起來的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然這本《Pro JavaScript with Mootools》國內並未正式引進,但我依然建議有需求的可以從 Amazon 上自行買來看一下,或者網上搜一下 PDF 的版本(確實有 PDF 全版下載的)。我個人是當初花了近 300 大洋從 Amazon 上買了一本英文原版的,還是更喜歡紙質版的閱讀體驗。這本書其實可以理解爲 “基於 MooTools 實踐項目的 JavaScript 指南”,總的脈絡是 “JavaScript 基礎知識 - 高級技巧 - MooTools 對原生 JavaScript 的改進”,非常值得一讀。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本篇譯文字數較多,近 4 萬字,我不知道能有幾位看官有耐心看完。如果真有,且發現@堂主 一些地方翻譯的不對或有優化建議,歡迎留言指教,共同成長。另外,非本土產技術類書籍,優先建議還是直接讀英文原版。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面是譯文正式內容:"}]},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JavaScript 最好的特色之一是其對函數的實現。不同於其他編程語言爲不同情景提供不同的函數類型,JavaScript 只爲我們提供了一種涵蓋所有情景(如內嵌函數、匿名函數或是對象方法)的函數類型。請不要被一個從外表看似簡單的 JavaScript 函數迷惑——這個基本的函數聲明如同一座城堡,隱藏着非常複雜的內部操作。我們整章都是圍繞着諸如函數結構、作用域、執行上下文以及函數執行等這些在實際操作中需要重點去考慮的問題來講述的。搞明白這些平時會被忽略的細節不但有助你更加了解這門語言,同時也會爲你在解決異常複雜的問題時提供巨大幫助。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"關於函數(The Function)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最開始,我們需要統一一些基本術語。從現在開始,我們將"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"函數(functions) "},{"type":"text","text":"的概念定義爲“執行一個明確的動作並提供一個返回值的獨立代碼塊”。函數可以接收作爲值傳遞給它的"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"參數(arguments) "},{"type":"text","text":",函數可以被用來提供"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"返回值(return value)"},{"type":"text","marks":[{"type":"strong"}],"text":" "},{"type":"text","text":",也可以通過"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"調用(invoking) "},{"type":"text","text":"被多次執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 一個帶有2個參數的基本函數:\nfunction add(one, two) {\n return one + two;\n}\n\/\/ 調用這個函數並給它2個參數:\nvar result = add(1, 42);\nconsole.log(result); \/\/ 43\n\/\/ 再次調用這個函數,給它另外2個參數\nresult = add(5, 20);\nconsole.log(result); \/\/ 25"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JavaScript 是一個將函數作爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"一等對象(first-class functions) "},{"type":"text","text":"的語言。一個一等對象的函數意味着函數可以儲存在變量中,可以被作爲參數傳遞給其他函數使用,也可以作爲其他函數的返回值。這麼做的合理性是因爲在 JavaScript 中隨處可見的函數其實都是對象。這門語言還允許你創建新的函數並在運行時改變函數的定義。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一種函數,多種形式(One Function, Multiple Forms)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然在 JavaScript 中只存在一種函數類型,但卻存在多種函數形式,這意味着可以通過不同的方式去創建一個函數。這些形式中最常見的是下面這種被稱爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"函數字面量(function literal)"},{"type":"text","marks":[{"type":"strong"}],"text":" "},{"type":"text","text":"的創建語法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function Identifier(FormalParamters, ...) {\n FunctionBody\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是一個 function 關鍵字後面跟着一個空格,之後是一個自選的"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"標識符(identifier) "},{"type":"text","text":"用以說明你的函數;之後跟着的是以逗號分割的"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"形參(formal parameters) "},{"type":"text","text":"列表,該形參列表處於一對圓括號中,這些形參會在函數內部轉變爲可用的局部變量;最後是一個自選的"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"函數體(funciton body) "},{"type":"text","text":",在這裏面你可以書寫聲明和表達式。請注意下面的說法是正確的:一個函數有多個可選部分。我們現在還沒針對這個問題進行詳細的說明,因爲對其的解答將貫穿本章。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:在本書的很多章節我們都會看到"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"字面量(literal)"},{"type":"text","text":"這個術語。在JavaScript中,字面量是指在你代碼中明確定義的值。“mark”、1 或者 true 是字符串、數字和布爾字面量的例子,而 function() 和 [1, 2] 則分別是函數和數組字面量的例子。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在標識符(或後面我們會見到的針對這個對象本身)後面使用調用"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"操作符(invocation operator)"},{"type":"text","text":"“()”的被稱爲一個函數。同時調用操作符()也可以爲函數傳遞"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"實參(actual arguments)"},{"type":"text","marks":[{"type":"strong"}],"text":" "},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:一個函數的形參是指在創建函數時圓括號中被聲明的有命名的變量,而實參則是指函數被調用時傳給它的值。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲函數同時也是對象,所以它也具有方法和屬性。我們將在本書第三章更多的討論函數的方法和屬性,這裏只需要先記住函數具有兩個基本的屬性:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 名稱(name):保存着函數標識符這個字符串的值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 長度(length):這是一個關於函數形參數量的整數(如果函數沒有形參,其 length 爲 0) 函數聲明(Function Declaration)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"採用基本語法,我們創建第一種函數形式,稱之爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"函數聲明(function declaration) "},{"type":"text","text":"。函數聲明是所有函數形式中最簡單的一種,且絕大部分的開發者都在他們的代碼中使用這種形式。下面的代碼定義了一個新的函數,它的名字是 “add”:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 一個名爲“add”的函數\nfunction add(a, b) {\n return a + b;\n}\nconsole.log(typeof add); \/\/ 'function'\nconsole.log(add.name); \/\/ 'add'\nconsole.log(add.length); \/\/ '2'\nconsole.log(add(20, 5)); \/\/ '25'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在函數聲明中需要賦予被聲明的函數一個標識符,這個標識符將在當前作用域中創建一個值爲函數的變量。在我們的例子中,我們在全局作用域中創建了一個 add 的變量,這個變量的 name 屬性值爲 add,這等價於這個函數的標識符,且這個函數的 length 爲 2,因爲我們爲其設置了 2 個形參。因爲 JavaScript 是基於詞法作用域(lexically scoped)的,所以標識符被固定在它們被定義的作用域而不是語法上或是其被調用時的作用域。記住這一點很重要,因爲 JavaScript 允許我們在函數中定義函數,這種情況下關於作用域的規則可能會變得不易理解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 外層函數,全局作用域\nfunction outer() {\n \/\/ 內層函數,局部作用域\n function inner() {\n \/\/ ...\n }\n}\n\/\/ 檢測外層函數\nconsole.log(typeof outer); \/\/ 'function'\n\/\/ 運行外層函數來創建一個新的函數\nouter();\n\/\/ 檢測內層函數\nconsole.log(typeof inner); \/\/ 'undefined'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個例子中,我們在全局作用域中創建了一個 outer 變量併爲之賦值爲 outer 函數。當我們調用它時,它創建了一個名爲 inner 的局部變量,這個局部變量被賦值爲 inner 函數,當我們使用 typeof 操作符進行檢測的時候,在全局作用域中 outer 函數是可以被有效訪問的,但 inner 函數卻只能在 outer 函數內部被訪問到 —— 這是因爲 inner 函數只存在於一個局部作用域中。因爲函數聲明同時還創建了一個同名的變量作爲他的標識符,所以你必須確定在當前作用域不存在其他同名標識符的變量。否則,後面同名變量的值會覆蓋前面的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 當前作用域中的一個變量\nvar items = 1;\n\/\/ 一個被聲明爲同名的函數\nfunction items() {\n \/\/ ...\n};\nconsole.log(typeof items); \/\/ 'function' 而非 'number'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們過一會會討論更多關於函數作用域的細節,現在我們看一下另外一種形式的函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"函數表達式(Function Expression)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面要說的函數形式具備一定的優勢,這個優勢在於函數被儲存在一個變量中,這種形式的函數被稱爲函數表達式(funciton expression)。不同於明確的聲明一個函數,這時的函數以一個變量返回值的面貌出現。下面是一個和上面一樣的add函數,但這次我們使用了函數表達式:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function(a, b) {\n return a + b;\n};\nconsole.log(typeof add); \/\/ 'function'\nconsole.log(add.name); \/\/ '' 或 'anonymous'\nconsole.log(add.length); \/\/ '2'\nconsole.log(add(20, 5)); \/\/ '25'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們創建了一個函數字面量作爲 add 這個變量的值,下面我們就可以使用這個變量來調用這個函數,如最後的那個語句展示的我們用它來求兩個數的和。你會注意到它的 length 屬性和對應的函數聲明的 length 屬性是一樣,但是 name 屬性卻不一樣。在一些 JavaScript 解析器中,這個值會是空字符串,而在另一些中則會是 “anonymous”。發生這種情況的原因是我們並未給一個函數字面量指定一個標識符。在 JavaSrcipt 中,一個未使用明確標識符的函數被稱爲一個匿名函數(anonymous)。函數表達式的作用域規則不同於函數聲明的作用域規則,這是因爲其取決於被賦值的那個變量的作用域。記住在 JavaScript 中,由關鍵字 var 聲明的變量是一個局部變量,而忽略了這個關鍵字則會創建一個全局變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 外層函數,全局作用域\nvar outer = function() {\n \/\/ 內層函數,局部作用域\n var localInner = function() {\n \/\/ ...\n };\n \/\/ 內層函數,全局作用域\n globalInner = function() {\n \/\/ ...\n };\n}\n\/\/ 檢測外層函數\nconsole.log(typeof outer); \/\/ 'function'\n\/\/ 運行外層函數來創建一個新的函數\nouter();\n\/\/ 檢測新的函數\nconsole.log(typeof localInner); \/\/ 'undefined'\nconsole.log(typeof globalInner); \/\/ 'function'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"outer 函數被定義在全局作用域中,這是因爲雖然我們使用了 var 關鍵字,但其在當前應用中處於最高層級。在這個函數內部有另外的2個函數:localInner 和 globalInner。localInner 函數被賦值給一個局部變量,在 outer 外部無法訪問它。而 globalIner 則因在定義時缺失 var 關鍵字,其結果是這個變量及其引用的函數都處於全局作用域中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"命名的函數表達式(Named Function Expression)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然函數表達式經常被書寫爲採用匿名函數的形式,但你依然可以爲這個匿名函數賦予一個明確的標識符。這個函數表達式的變種被稱爲一個"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"命名的函數表達式(named function expression) 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function add(a, b) {\n return a + b;\n};\nconsole.log(typeof add); \/\/ 'function'\nconsole.log(add.name); \/\/ 'add'\nconsole.log(add.length); \/\/ '2'\nconsole.log(add(20, 5)); \/\/'25'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個例子和採用匿名函數方式的函數表達式是一樣的,但我們爲函數字面量賦予了一個明確的標識符。和前一個例子不同,這時的 name 屬性的值是 “add”,這個值同我們爲其賦予的那個標識符是一致的。JavaScript 允許我們爲匿名函數賦予一個明確的標識符,這樣就可以在這個函數內部引用其本身。你可能會問爲什麼我們需要這個特徵,下面讓我們來看兩個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var myFn = function() {\n \/\/ 引用這個函數\n console.log(typeof myFn);\n};\nmyFn(); \/\/ 'function'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的這個例子,myFn 這個函數可以輕鬆的通過它的變量名來引用,這是因爲它的變量名在其作用域中是有效的。不過,看一下下面的這個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 全局作用域\nvar createFn = function() {\n \/\/ 返回函數\n return function() {\n console.log(typeof myFn);\n };\n};\n\/\/ 不同的作用域\n(function() {\n \/\/ 將createFn的返回值賦予一個局部變量\n var myFn = createFn();\n \/\/ 檢測引用是否可行\n myFn(); \/\/ 'undefined'\n})();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個例子可能有點複雜,我們稍後會討論它的細節。現在,我們只關心函數本身。在全局作用域中,我們創建了一個 createFn 函數,它返回一個和前面例子一樣的 log 函數。之後我們創建了一個匿名的局部作用域,在其中定義了一個變量 myFn,並把 createFn 的返回值賦予這個變量。這段代碼和前面那個看起來很像,但不同的是我們沒使用一個被明確賦值爲函數字面量的變量,而是使用了一個由其他函數產生的返回值。而且,變量 myFn 一個不同的局部作用域中,在這個作用域中訪問不到上面 createFn 函數作用域中的返回值。因此,在這個例子中,log 函數不會返回 “function” 而是會返回一個 “undefined”。通過爲匿名函數設置一個明確的標識符,即使我們通過持有它的變量訪問到它,也可以去引用這個函數自身。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 全局作用域\nvar createFn = function() {\n \/\/ 返回函數\n return function myFn() {\n console.log(typeof myFn);\n };\n};\n\/\/ 不同的作用域\n(function() {\n \/\/ 將createFn的返回值賦予一個局部變量\n var myFn = createFn();\n \/\/ 檢測引用是否可行\n myFn(); \/\/ 'function'\n})();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"添加一個明確的標識符類似於創建一個新的可訪問該函數內部的變量,使用這個變量就可以引用這個函數自身。這樣使得函數在其內部調用自身(用於遞歸操作)或在其本身上執行操作成爲可能。一個命名了的函數聲明同一個採用匿名函數形式的函數聲明具有相同的作用域規則:引用它的變量作用域決定了這個函數是局部的或是全局的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 一個有着不同標識符的函數\nvar myFn = function fnID() {\n console.log(typeof fnID);\n};\n\/\/ 對於變量\nconsole.log(typeof myFn); \/\/ 'function'\n\/\/ 對於標識符\nconsole.log(typeof fnID); \/\/ 'undefined'\nmyFn(); \/\/ 'function'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個例子顯示了,通過變量 myFn 可以成功的引用函數,但通過標識符 fnID 卻無法從外部訪問到它。但是,通過標識符卻可以在函數內部引用其自身。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"自執行函數(Single-Execution Function)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在前面介紹函數表達式時曾接觸過匿名函數,其還有着更廣泛的用處。其中最重要的一項技術就是使用匿名函數創建一個立即執行的函數——且不需要事先把它們先存在變量裏。這種函數形式我們稱之爲自執行函數(single-execution function)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 創建一個函數並立即調用其自身\n(function() {\n var msg = 'Hello World';\n console.log(msg);\n})();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們創建了一個函數字面量並把它包裹在一對圓括號中。之後我們使用函數調用操作符()來立即執行這個函數。這個函數並未儲存在一個變量裏,或是任何針對它而創建的引用。這是個“一次性運行”的函數:創造它,執行它,之後繼續其他的操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要想理解自執行函數是如何工作的,你要記住函數都是對象,而對象都是值。因爲在 JavaScript 中值可以被立即使用而無需先被儲存在變量裏,所以你可以在一對圓括號中插入一個匿名函數來立即運行它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,如果我們像下面這麼做:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 這麼寫會被認爲是一個語法錯誤\nfunction() {\n var msg = 'Hello World';\n console.log(msg);\n}();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 JavaScript 解析器遇到這行代碼會拋出一個語法錯誤,因爲解析器會把這個函數當成一個函數聲明。這看起來是一個沒有標識符的函數聲明,而因爲函數聲明的方式必須要在 function 關鍵字之後跟着一個標識符,所以解析器會拋出錯誤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們把函數放在一對圓括號中來告訴解析器這不是一個函數聲明,更準確的說,我們創建了一個函數並立即運行了它的值。因爲我們沒有一個可用於調用這個函數的標識符,所以我們需要把函數放在一對圓括號中以便可以創建一個正確的方法來調用到這個函數。這種包圍在外層的圓括號應該出現在我們沒有一個明確的方式來調用函數的時候,比如我們現在說的這種自執行函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:執行操作符()可以既可以放在圓括號外面,也可以放在圓括號裏面,如:(function() {…}())。但一般情況下大家更習慣於把執行操作符放在外面。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自執行函數的用處很多,其中最重要的一點是爲變量和標識符創造一個受保護的局部作用域,看下面的例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 頂層作用域\nvar a = 1;\n\/\/ 一個由自執行函數創建的局部作用域\n(function() {\n \/\/局部作用域\n var a = 2;\n})();\nconsole.log(a); \/\/ 1"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,外面先在頂層作用域創建了一個值爲 1 的變量 a,之後創建一個自執行函數並在裏面再次聲明一個 a 變量並賦值爲 2。因爲這是一個局部作用域,所以外面的頂層作用域中的變量 a 的值並不會被改變。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這項技術目前很流行,尤其對於 JavaScript 庫(library)的開發者,因爲局部變量進入一個不同作用域時需要避免標識符衝突。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種自執行函數的用處是通過一次性的執行來爲你提供它的返回值:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 把一個自執行函數的返回值保存在一個變量裏\nvar name = (function(name) {\n return ['Hello', name].join(' ');\n})('Mark');\nconsole.log(name); \/\/ 'Hello Mark'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"別被這段代碼迷惑到:我們這裏不是創建了一個函數表達式,而是創建了一個自執行函數並立即執行它,把它的返回值賦予變量 name。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"自執行函數另一個特色是可以爲它配置標識符,類似一個函數聲明的做法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"(function myFn() {\n console.log(typeof myFn); \/\/ 'function'\n})();\nconsole.log(myFn); \/\/ 'undefined'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然這看起來像是一個函數聲明,但這卻是一個自執行函數。雖然我們爲它設置了一個標識符,但它並不會像函數聲明那樣在當前作用域創建一個變量。這個標識符使得你可以在函數內部引用其自身,而不必另外在當前作用域再新建一個變量。這對於避免覆蓋當前作用域中已存在的變量尤其有好處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同其他的函數形式一樣,自執行函數也可以通過執行操作符來傳遞參數。通過在函數內部把函數的標誌符作爲一個變量並把該函數的返回值儲存在該變量中,我們可以創建一個遞歸的函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var number = 12;\nvar numberFactorial = (function factorial(number) {\n return (number === 0) ? 1 : number * factorial(number - 1);\n})(number);\nconsole.log(numberFactorial); \/\/479001600"}]},{"type":"heading","attrs":{"align":null,"level":2}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":"函數對象(Function Object)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後一種函數形式,就是"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"函數對象(funciton object) "},{"type":"text","text":",它不同於上面幾種採用函數字面量的方式,這種函數形式的語法如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 一個函數對象\nnew Function('FormalArgument1', 'FormalArgument2',..., 'FunctionBody');"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,我們使用 Function 的構造函數創建了一個新的函數並把字符串作爲參數傳遞給它。前面的已經命名的參數爲新建函數對象的參數,最後一個參數爲這個函數的函數體。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:雖然這裏我們把這種形式成爲函數對象,但請記住其實所有的函數都是對象。我們在這裏採用這個術語的目的是爲了和函數字面量的方式進行區分。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們採用這種形式創建一個函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = new Function('a', 'b', 'return a + b;');\nconsole.log(typeof add); \/\/ 'function'\nconsole.log(add.name); \/\/ '' 或 'anonymous'\nconsole.log(add.length); \/\/ '2'\nconsole.log(add(20, 5)); \/\/ '25'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你可能會發現這種方式比採用函數字面量方式創建一個匿名函數要更簡單。和匿名函數一樣,對其檢測 name 屬性會得到一個空的字符串或 anonymous。在第一行,我們使用 Function 的構造函數創建了一個新的函數,並賦值給變量 add。這個函數接收 2 個參數 a 和 b,會在運行時將 a 和 b 相加並把相加結果做作爲函數返回值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用這種函數形式類似於使用 eval:最後的一個字符串參數會在函數運行時作爲函數體裏的代碼被執行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:你不是必須將命名的參數作爲分開的字符串傳遞,Function 構造函數也允許一個字符串裏包含多個以逗號分隔的項這種傳參方式。比如:new Function(‘a, b’, ‘return a + b;’);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然這種函數形式有它的用處,但其相比函數字面量的方式存在一個顯著的劣勢,就是它是處在全局作用域中的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 全局變量\nvar x = 1;\n\/\/ 局部作用域\n(function() {\n \/\/ 局部變量\n var x = 5;\n var myFn = new Function('console.log(x)');\n myFn(); \/\/ 1, not 5\n})();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然我們在獨立的作用域中定義了一個局部變量,但輸出結果卻是 1 而非 5,這是因爲 Function 構造函數是運行在全局作用域中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參數(Arguments)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有函數都能從內部訪問到它們的實參。這些實參會在函數內部變爲一個個局部變量,其值是函數在調用時傳進來的那個值。另外,如果函數在調用時實際使用的參數少於它在定義時確定的形參,那麼那些多餘的未用到的參數的值就會是 undefined。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var myFn = function(frist, second) {\n console.log('frist : ' + frist);\n console.log('second : ' + second);\n};\nmyFn(1, 2);\n\/\/ first : 1\n\/\/ second : 2\nmyFn('a', 'b', 'c');\n\/\/ first : a\n\/\/ second : b\nmyFn('test');\n\/\/ first : test\n\/\/ second : undefined"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲 JavaScript 允許向函數傳遞任意個數的參數,這也同時爲我們提供了一個方式來判斷函數在調用時使用的實參和函數定義時的形參的數量是否相同。這個檢測的方式通過 arguments 這個對象來實現,這個對象類似於數組,儲存着該函數的實參:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var myFn = function(frist, second) {\n console.log('length : ' + arguments.length);\n console.log('frist : ' + arguments[0]);\n};\nmyFn(1, 2);\n\/\/ length : 2\n\/\/ frist : 1\nmyFn('a', 'b', 'c');\n\/\/ length : 3\n\/\/ frist : a\nmyFn('test');\n\/\/ length : 2\n\/\/ frist : test"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"arguments 對象的 length 屬性可以顯示我們傳遞函數的實參個數。對實參的調用可以對 arguments 對象使用類似數組的下標法:arguments[0] 表示傳遞的第一個實參,arguments[1] 表示第二個實參。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 arguments 對象取代有名字的參數,你可以創建一個可以對不同數量參數進行處理的函數。比如可以使用這種技巧來幫助我們改進前面的那個 add 函數,使得其可以對任意數量的參數進行累加,最後返回累加的值:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function(){\n var result = 0,\n len = arguments.length;\n while(len--) result += arguments[len];\n console.log(result);\n};\nadd(15); \/\/ 15\nadd(31, 32, 92); \/\/ 135\nadd(19, 53, 27, 41, 101); \/\/ 241"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"arguments 對象有一個很大的問題需要引起你的注意:它是一個可變的對象,你可以改變其內部的參數值甚至是把它整個變成另一個對象:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var rewriteArgs = function() {\n arguments[0] = 'no';\n console.log(arguments[0]);\n};\nrewriteArgs('yes'); \/\/ 'no'\nvar replaceArgs = function() {\n arguments = null;\n console.log(arguments === null);\n};\nreplaceArgs(); \/\/ 'true'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面第一個函數向我們展示瞭如果重置一個參數的值;後面的函數向我們展示瞭如何整體更改一個 arguments 對象。對於 arguments 對象來說,唯一的固定屬性就是 length 了,即使你在函數內部動態的增加了 arguments 對象裏的參數,length 依然只顯示函數調用時賦予的實參的數量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var appendArgs = function() {\n arguments[2] = 'three';\n console.log(arguments.length);\n};\nappendArgs('one', 'two'); \/\/ 2"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當你寫代碼的時候,請確保沒有更改 arguments 內的參數值或覆蓋這個對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於 arguments 對象還有另一個屬性值:callee,這是一個針對該函數自身的引用。在前面的代碼中我們使用函數的標識符來實現在函數內部引用其自身,現在我們換一種方式,使用 arguments.callee:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var number = 12;\nvar numberFactorial = (function(number) {\n return (number === 0) ? 1 : number * arguments.callee(number - 1);\n})(number);\nconsole.log(numberFactorial); \/\/479001600"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意這裏我們創建的是一個匿名函數,雖然我們沒有函數標識符,但依然可以通過 arguments.callee 來準確的引用其自身。創建這個屬性的意圖就是爲了能在沒有標識符可供使用的時候(或者就算是有一個標識符時也可以使用 callee)來提供一個有效方式在函數內部引用其自身。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然這是一個很有用的屬性,但在新的 ECMAScript 5 的規範中,arguments.callee 屬性卻被廢棄了。如果使用 ES5 的嚴格模式,該屬性會引起一個報錯。所以,除非真的是有必要,否則輕易不要使用這個屬性,而是用我們前面說過的方法使用標識符來達到同樣的目的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然 JavaScript 允許給函數傳遞很多參數,但卻並未提供一個設置參數默認值的方法,不過我們可以通過判斷參數值是否是 undefined 來模擬配置默認值的操作:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var greet = function(name, greeting) {\n \/\/ 檢測參數是否是定義了的\n \/\/ 如果不是,就提供一個默認值\n name = name || 'Mark';\n greeting = greeting || 'Hello';\n console.log([greeting, name]).join(' ');\n};\ngreet('Tim', 'Hi'); \/\/ 'Hi Tim'\ngreet('Tim'); \/\/ 'Hello Tim'\ngreet(); \/\/ 'Hello Mark'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲未在函數調用時賦值的參數其值爲 undefined,而 undefined 在布爾判斷時返回的是 false,所以我們可以使用邏輯或運算符 || 來爲參數設置一個默認值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一點需要特別注意的是,原生類型的參數(如字符串和整數)是以值的方式來傳遞的,這意味着這些值的改變不會對外層作用域引起反射。不過,作爲參數使用的函數和對象,則是以他們的引用來傳遞,在函數作用域中的對參數的任何改動都會引起外層的反射:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var obj = {name : 'Mark'};\nvar changeNative = function(name) {\n name = 'Joseph';\n console.log(name);\n};\nchangeNative(obj.name); \/\/ 'Joseph'\nconsole.log(obj.name); \/\/ 'Mark'\nvar changeObj = function(obj) {\n obj.name = 'joseph';\n console.log(obj.name);\n};\nchangeObj(obj); \/\/ 'Joseph'\nconsole.log(obj.name); \/\/ 'Joseph'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一步我們將 obj.name 作爲參數傳給函數,因爲其爲一個原生的字符串類型,其傳遞的是它值的拷貝(儲存在棧上),所以在函數內部對其進行改變不會對外層作用域中的 obj 產生影響。而接下來我們把 obj 對象本身作爲一個參數傳遞,因爲函數和對象等在作爲參數進行傳遞時其傳遞的是對自身的引用(儲存在堆上),所以局部作用域中對其屬性值的任何更改都會立即反射到外層作用域中的 obj 對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,你可能會說之前我曾提到過 arguments 對象是類數組的。這意味着雖然 arguments 對象看起來像數組(可以通過下標來用於),但它沒有數組的那些方法。如果你喜歡,你可以用數組的 Array.prototype.slice 方法把 arguments 對象轉變爲一個真正的數組:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var argsToArray = function() {\n console.log(typeof arguments.callee); \/\/ 'function'\n var args = Array.prototype.slice.call(arguments);\n console.log(typeof arguments.callee); \/\/ 'undefined'\n console.log(typeof arguments.slice); \/\/ 'function'\n};\nargsToArray();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":"返回值(Return Values)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Return 關鍵字用來爲函數提供一個明確的返回值,JavaScript 允許在函數內部書寫多個 return 關鍵字,函數會再其中一個執行後立即退出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var isOne = function(number) {\n if (number === 1) return true;\n console.log('Not one ..');\n return false;\n};\nvar one = isOne(1);\nconsole.log(one); \/\/ true\nvar two = isOne(2); \/\/ Not one ..\nconsole.log(two); \/\/ false"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個函數第一次被引用時,我們傳進去一個參數 1,因爲我們在函數內部先做了一個條件判斷,當前傳入的參數1使得該條件判斷語句返回 true,於是 return true 代碼會被執行,函數同時立即停止。在第二次引用時我們傳進去的參數 2 不符合前面的條件判斷語句要求,於是函數會一直執行到最後的 return false代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在函數內部設置多個 return 語句對於函數分層執行是很有好處的。這同時也被普遍應用於在函數運行最開始對必須的變量進行檢測,如有不符合的情況則立即退出函數執行,這既能節省時間又能爲我們提供一個錯誤提示。下面的這個例子就是一段從 DOM 元素中獲取其自定義屬性值的代碼片段:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var getData = function(id) {\n if (!id) return null;\n var element = $(id);\n if (!element) return null;\n return element.get('data-name');\n};\nconsole.log(getData()); \/\/ null\nconsole.log(getData('non existent id')); \/\/ null\nconsole.log(getData('main')); \/\/ 'Tim'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"組後關於函數返回值要提醒各位的一點是:不論你希望與否,函數總是會提供一個返回值。如果未顯示地設置 return 關鍵字或設置的 return 未有機會執行,則函數會返回一個 undefined。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"函數內部(Function Internals)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們前面討論過了函數形式、參數以及函數的返回值等與函數有關的核心話題,下面我們要討論一些代碼之下的東西。在下面的章節裏,我們會討論一些函數內部的幕後事務,讓我們一起來偷窺下當 JavaScript 解析器進入一個函數時會做些什麼。我們不會陷入針對細節的討論,而是關注那些有利於我們更好的理解函數概念的那些重要的點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有些人可能會覺得在最開始接觸 JavaScript 的時候,這門語言在某些時候會顯得不那麼嚴謹,而且它的規則也不那麼好理解。瞭解一些內部機制有助於我們更好的理解那些看起來隨意的規則,同時在後面的章節裏會看到,瞭解 JavaScript 的內部工作機制會對你書寫出可靠的、健壯的代碼有着巨大的幫助。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:JavaScript 解析器在現實中的工作方式會因其製造廠商不同而不相一致,所以我們下面要討論的一些解析器的細節可能不全是準確的。不過 ECMAScript 規範對解析器應該如何執行函數提供了基本的規則描述,所以對於函數內部發生的事,我們是有着一套官方指南的。"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"可執行代碼和執行上下文(Executable Code and Execution Contexts)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JavaScript 區分三種可執行代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• 全局代碼(Global code)是指出現在應用代碼中頂層的代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• 函數代碼(Function code)是指在函數內部的代碼或是在函數體之前被調用的代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"• Eval 代碼(Eval code)是指被傳進 eval 方法中並被其執行的代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的例子展示了這三種不同的可執行代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 這是全局代碼\nvar name = 'John';\nvar age = 20;\nfunction add(a, b) {\n \/\/ 這是函數代碼\n var result = a + b;\n return result;\n}\n(function() {\n \/\/ 這是函數代碼\n var day = 'Tuesday';\n var time = function() {\n \/\/ 這還是函數代碼\n \/\/ 不過和上面的代碼在作用域上是分開的\n return day;\n };\n})();\n\/\/ 這是eval代碼\neval('alert(\"yay!\");');"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面我們創建的 name、age 以及大部分的函數都在頂層代碼中,這意味着它們是全局代碼。不過,處於函數中的代碼是函數代碼,它被視爲同全局代碼是相分隔的。函數中內嵌的函數,其內部代碼同外部的函數代碼也被視爲是相分隔的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼爲什麼我們需要對 JavaScript 中的代碼進行分類呢?這是爲了在解析器解析代碼時能夠追蹤到其當前所處的位置,JavaScript 解析器採用了一個被稱爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"執行上下文(execution context) "},{"type":"text","text":"的內部機制。在處理一段腳本的過程中,JavaScript 會創建並進入不同的執行上下文,這個行爲本身不僅保存着它運行到這個函數當前位置所經過的軌跡,同時還儲存着函數正常運行所需要的數據。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每個 JavaScript 程序都至少有一個執行上下文,通常我們稱之爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"全局執行上下文(global execution context) "},{"type":"text","text":",當一個 JavaScript 解析器開始解析你的程序的時候,它首先“進入”全局執行上下文並在這個執行上下文環境中處理代碼。當它遇到一個函數,它會創建一個新的執行上下文並進入這個上下文利用這個環境來執行函數代碼。當函數執行完畢或者遇到一個 return 結束之後,解析器會退出當先的執行上下文並回到之前所處的那個執行上下文環境。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個看起來不是很好理解,我們下面用一個簡單的例子來把它理清:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var a = 1;\nvar add = function(a, b) {\n return a + b;\n};\nvar callAdd = function(a, b) {\n return add(a, b);\n};\nadd(a, 2);\ncall(1, 2);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段簡單的代碼不單足夠幫助我們來理解上面說的事情,同時還是一個很好的例子來展示 JavaScript 是如何創建、進入並離開一個執行上下文的。讓我們一步一步來分析:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當程序開始執行,Javascript 解析器首先進入全局執行上下文並在這裏解析代碼。它會先創建三個變量 a、add、callAdd,並分別爲它們賦值爲數字 1、一個函數和另一個函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解析器遇到了一個針對 add 函數的調用。於是解析器創建了一個新的執行上下文,進入這個上下文,計算 a + b 表達式的值,之後返回這個表達式的值。當這個值被返回後,解析器離開了這個它新創建的執行上下文,把它銷燬掉,重新回到全局執行上下文。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來解析器遇到了另一個函數調用,這次是對 callAdd 的調用。像第二步一樣,解析器會新創建一個執行上下文,並在它解析 callAdd 函數體中的代碼之前先進入這個執行上下文。當它對函數體內的代碼進行處理的時候,遇到了一個新的函數調用——這次是對 add 的調用,於是解析器會再新建一個執行上下文並進入這裏。此時,我們已有了三個執行上下文:一個全局執行上下文、一個針對 callAdd 的執行上下文,一個針對 add 函數的執行上下文。最後一個是當前被激活的執行上下文。當 add 函數調用執行完畢後,當前的執行上下文會被銷燬並回到 callAdd 的執行上下文中,callAdd 的執行上下文中的運行結果也是返回一個值,這通知解析器退出並銷燬當前的執行上下文,重新回到全局執行上下文中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行上下文的概念對於一個在代碼中不會直接面對它的前端新人來說,可能是會有一點複雜,這是可以理解的。你此時可能會問,那既然我們在編程中不會直接面對執行上下文,那我們又爲什麼要討論它呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"答案就在於執行上下文的其他那些用途。我在前面提到過 JavaScript 解析器依靠執行上下文來保存它運行到當前位置所經過的軌跡,此外一些程序內部相互關聯的對象也要依靠執行上下文來正確處理你的程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"變量和變量初始化(Variables and Variable Instantition)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些內部的對象之一就是"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"變量對象(variable object) "},{"type":"text","text":"。每一個執行上下文都擁有它自己的變量對象用來記錄在當前上下文環境中定義的變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 JavaScript 中創建變量的過程被稱爲變量初始化(variable instantition)。因爲 JavaScript 是基於詞法作用域的,這意味着一個變量所處的作用域由其在代碼中被實例化的位置所決定。唯一的例外是不採用關鍵字 var 創建的變量是全局變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nvar add = function(a, b) {\n var localResult = a + b;\n globalResult = localResult;\n return localResult;\n};\nadd(1, 2);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個代碼片段中,變量 fruit 和函數 add 處於全局作用域中,在整個腳本中都能被訪問到。而對於變量 localResult、a、b 則是局部變量,只能在函數內部被訪問到。而變量 globalResult 因爲在聲明時缺少關鍵字 var,所以它會成爲一個全局變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當 JavaScript 解析器進入一個執行上下文中,首先要做的就是變量初始化操作。解析器首先會在當前的執行上下文中創建一個 variable 對象,之後在當前上下文環境中搜索 var 聲明,創建這些變量並添加進之前創建的 variable 對象中,此時這些變量的值都被設置爲 undefined。讓我們審視一下我們的演示代碼,我們可以說變量 fruit 和 add 通過 variable 對象在當前執行上下文中被初始化,而變量 localResult、a、b 則通過 variable 對象在 add 函數的上下文空間中被初始化。而 globalResult 則是一個需要被特別注意的變量,這個我們一會再來討論它。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於變量初始化有很重要的一點需要我們去記住,就是它同執行上下文是緊密結合的。回憶一下,前面我們對 JavaScript 劃分了三種不同的執行代碼:全局代碼、函數代碼和 eval 代碼。同理,我們也可以說存在着三種不同的執行上下文:全局執行上下文、函數執行上下文、eval 執行上下文。因爲變量初始化是通過處於執行上下文中的 variable 對象實現的,進而可以說也存在着三種類型的變量:全局變量、處於函數作用域中的變量以及來自 eval 代碼中的變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這爲我們引出了很多人對這門語言感覺困惑的那些問題中一個:JavaScript 沒有塊級作用域。在其他的類 C 語言中,一對花括號中的代碼被稱爲一個"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"塊(block) "},{"type":"text","text":",塊有着自己獨立的作用域。因爲變量初始化發生在執行上下文這一層級中,所以在當前執行上下文中任意位置被初始化的變量,在這整個上下文空間中(包括其內部的其他子上下文空間)都是可見的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var x = 1;\nif (false) {\n var y =2;\n}\nconsole.log(x); \/\/ 1\nconsole.log(y); \/\/ undefined"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在擁有塊級作用域的語言中,console.log(y) 會拋出一個錯誤,因爲條件判斷語句中的代碼是不會被執行的,那麼變量 y 自然也不會被初始化。但在 JavaScript 中這並不會拋出一個錯誤,而是告訴我們 y 的值是 undefined,這個值是一個變量已經被初始化但還未被賦值時所具有的默認值。這個行爲看起來挺有意思,不是麼?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,如果我們還記得變量初始化是發生在執行上下文這一層級中,我們就會明白這種行爲其實正是我們所期望的。當 JavaScript 開始解析上面的代碼塊的時候,它首先會進入全局執行上下文,之後在整個上下文環境中尋找變量聲明並初始化它們,之後把他們加入 variable 對象中去。所以我們的代碼實際上是像下面這樣被解析的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var x;\nvar y;\nx = 1;\nif (false) {\n y = 2;\n}\nconsole.log(x); \/\/ 1\nconsole.log(y); \/\/ undefined"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的在上下文環境中的初始化也適用於函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function test() {\n console.log(value); \/\/ undefined\n var value = 1;\n console.log(value); \/\/ 1\n}\ntest();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然我們對變量的賦值操作是在第一行 log 語句之後才進行的,但第一行的 log 還是會給我們返回一個 undefined 而非一個報錯。這是因爲變量初始化是先於函數內其他任何執行代碼之前進行的。我們的變量會在第一時間被初始化並被暫時設置爲 undefined,其到了第二行代碼被執行時才被正式賦值爲 1。所以說將變量初始化的操作放在代碼或函數的最前面是一個好習慣,這樣可以保證在當前作用域的任何位置,變量都是可用的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就像你見到的,創建變量的過程(初始化)和給變量賦值的過程(聲明)是被 JavaScript 解析器分開執行的。我們回到上一個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function(a, b) {\n var localResult = a + b;\n globalResult = localResult;\n return localResult;\n};\nadd(1, 2);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個代碼片段中,變量 localResult 是函數的一個局部變量,但是 globalResult 卻是一個全局變量。對於這個現象最常見的解釋是因爲在創建變量時缺少關鍵字 var 於是變量成了全局的,但這並不是一個靠譜的解釋。現在我們已經知道了變量的初始化和聲明是分開進行的,所以我們可以從一個解析器的視角把上面的代碼重寫:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function(a, b) {\n var localResult;\n localResult = a + b;\n globalResult = localResult;\n return localResult;\n};\nadd(1, 2);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"變量 localResult 會被初始化並會在當前執行上下文的 variable 對象中創建一個針對它的引用。當解析器看到 “localResult = a + b;” 這一行時,它會在當前執行上下文環境的 variable 對象中檢查是否存在一個 localResult 對象,因爲現在存在這麼一個變量,於是這個值(a + b)被賦給了它。然而,當解析器遇到 “globalResult = localResult;” 這一行代碼時,它不論在當前環境的 variable 對象中還是在更上一級的執行上下文環境(對本例來說是全局執行上下文)的 variable 對象中都沒找到一個名爲 globalResult 的對象引用。因爲解析器始終找不到這麼一個引用,於是它認爲這是一個新的變量,並會在它所尋找的最後一層執行上下文環境——總會是全局執行上下文——中創建這麼一個新的變量。於是, globalResult 最後成了一個全局變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"作用域和作用域鏈(Scoping and Scope Chain)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在執行上下文的作用域中查找變量的過程被稱爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"標識符解析(indentifier resolution) "},{"type":"text","text":",這個過程的實現依賴於函數內部另一個同執行上下文相關聯的對象——"},{"type":"text","marks":[{"type":"strong"}],"text":" "},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"作用域鏈(scope chain) "},{"type":"text","text":"。就像它的名字所蘊含的那樣,作用域鏈是一個有序鏈表,其包含着用以告訴 JavaScript 解析器一個標識符到底關聯着哪一個變量的對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每一個執行上下文都有其自己的作用域鏈,該作用域鏈在解析器進入該執行上下文之前就已經被創建好了。一個作用域鏈可以包含數個對象,其中的一個便是當前執行上下文的 variable 對象。我們看一下下面的簡單代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nvar animal = 'cat';\nconsole.log(fruit); \/\/ 'banana'\nconsole.log(animal); \/\/ 'cat'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段代碼運行在全局執行上下文中,所以變量 fruit 和 animal 儲存在全局執行上下文的 variable 對象中。當解析器遇到 “console.log(fruit);” 這段代碼,它看到了標識符 fruit 並在當前的作用域鏈(目前只包含了一個對象,就是當前全局執行上下文的 variable 對象)中尋找這個標識符的值,於是接下來解析器發現這個變量有一個內容爲 “banana” 的值。下一行的 log 語句的執行過程同這個是一樣的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同時,全局執行上下文中的 variable 對象還有另外一個用途,就是被用做 global 對象。解析器對 global 對象有其自身的內部實現方式,但依然可以通過 JavaScript 在當前窗口中自身的window對象或當前 JavaScript 解析器的 global 對象來訪問到。所有的全局對象實際上都是 global 對象中的成員:在上面的例子中,你可以通過 window.fruit、global.fruit 或 window.animal、global.animal 來引用變量 fruit 和 animal。global 對象對所有的作用域鏈和執行上下文都可用。在我們這個只是全局代碼的例子裏,global 對象是這個作用域鏈中僅有的一個對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好吧,這使得函數變得更加不易理解了。除了 global 對象之外,一個函數的作用域鏈還包含擁有其自身執行上下文環境的變量對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nvar animal = 'cat';\nfunction sayFruit() {\n var fruit = 'apple';\n console.log(fruit); \/\/ 'apple'\n console.log(animal); \/\/ 'cat'\n}\nconsole.log(fruit); \/\/ 'banana'\nconsole.log(animal); \/\/ 'cat'\nsayFruit();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於全局執行上下文中的代碼,fruit 和 animal 標識符分別指向 “banana” 和 “cat” 值,因爲它們的引用是被存儲在執行上下文的 variable 對象中(也就是 global 對象中)的。不過,在 sayFruit 函數裏標識符 fruit 對應的卻是另一個值 —— “apple”。因爲在這個函數內部,聲明並初始化了另一個變量 fruit。因爲當前執行上下文中的 variable 對象在作用域鏈中處在更靠前的位置(相比全局執行上下文中的 variable 對象而言),所以 JavaScript 解析器會知道現在處理的應該是一個局部變量而非全局變量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲 JavaScript 是基於詞法作用域的,所以標識符解析還依賴於函數在代碼中的位置。一個嵌在函數中的函數,可以訪問到其外層函數中的變量:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nfunction outer() {\n var fruit = 'orange';\n function inner() {\n console.log(fruit); \/\/ 'orange'\n }\n inner();\n}\nouter();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"inner 函數中的變量 fruit 具有一個 “orange” 的值是因爲這個函數的作用域鏈不單單包含了它自己的 variable 對象,同時還包含了它被聲明時所處的那個函數(這裏指 outer 函數)的 variable 對象。當解析器遇到 inner 函數中的標識符 fruit,它首先會在作用域鏈最前面的 inner 函數的 variable 對象中尋找與之同名的標識符,如果沒有,則去下一個 variable 對象(outer 函數的)中去找。當解析器找到了它需要的標識符,它就會停在那並把 fruit 的值設置爲 “orange”。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過要注意的是,這種方式只適用於採用函數字面量創建的函數。而採用構造函數方式創建的函數則不會這樣:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nfunction outer() {\n var fruit = 'orange';\n var inner = new Function('console.log(fruit);');\n inner(); \/\/ 'banana'\n}\nouter();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個例子裏,我們的 inner 函數不能訪問 outer 函數裏的局部變量 fruit,所以 log 語句的輸出結果是 “banana” 而非 “orange”。發生這種情況的原因是因爲採用 new Function() 創建的函數其作用域鏈僅含有它自己的 variable 對象和 global 對象,而其外圍函數的 variable 對象都不會被加入到它的作用域鏈中。因爲在這個採用構造函數方式新建的函數自身的 variable 對象中沒有找到標識符 fruit,於是解析器去後面一層的 global 對象中查找,在這裏面找到了一個 fruit 標識符,其值爲 “banana”,於是被 log 了出來。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作用域鏈的創建發生在解析器創建執行上下文之後、變量初始化之前。在全局代碼中,解析器首先會創建一個全局執行上下文,之後創建作用域鏈,之後繼續創建全局執行上下文的 variable 對象(這個對象同時也成爲 global 對象),再之後解析器會進行變量初始化,之後把儲存了這些初始化了的變量的 variable 對象加入到前面創建的作用域鏈中。在函數代碼中,發生的情況也是一樣的,唯一不同的是 global 對象會首先被加入到函數的作用域鏈,之後把其外圍函數的的 variable 對象加入作用域鏈,最後加入作用域鏈的是該函數自己的 variable 對象。因爲作用域鏈在技術角度來講屬於邏輯上的一個棧,所以解析器的查找操作所遵循的是從棧上第一個元素開始向下順序查找。這就是爲什麼我們絕大部分的局部變量是最後才被加入到作用域鏈卻在解析時最先被找到的原因。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}}],"text":"閉包(Closures)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JavaScript 中函數是一等對象以及函數可以引用到其外圍函數的變量使得 JavaScript 相比其他語言具備了一個非常強大的功能:"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"閉包(closures) "},{"type":"text","text":"。雖然增加這個概念會使對 JavaScript 這部分的學習和理解變得更加困難,但必須承認這個特色使函數的用途變得非常強大。在前面我們已經討論過了 JavaScript 函數的內在工作機制,這正好能幫助我們瞭解閉包是如何工作的,以及我們應該如何在代碼中使用閉包。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般情況下,JavaScript 變量的生命週期被限定在聲明其的函數內。全局變量在整個程序未結束之前一直存在,局部變量則在函數未結束之前一直存在。當一個函數執行完畢,其內部的局部變量會被 JavaScript 解析器的垃圾回收機制銷燬從而不再是一個變量。當一個內嵌函數保存了其外層函數一個變量的引用,即使外層函數執行完畢,這個引用也繼續被保存着。當這種情況發生,我們說創建了一個閉包。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不好理解?讓我們看幾個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\n(function() {\n var fruit = 'apple';\n console.log(fruit); \/\/ 'apple'\n})();\nconsole.log(fruit); \/\/ 'banana'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,我們有一個創建了一個 fruit 變量的自執行函數。在這個函數內部,變量 fruit 的值是 apple。當這個函數執行完畢,值爲 apple 的變量 fruit 便被銷燬。於是只剩下了值爲 banana 的全局變量 fruit。此種情況下我們並未創建一個閉包。再看看另一種情況:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\n(function() {\n var fruit = 'apple';\n function inner() {\n console.log(fruit); \/\/ 'apple'\n }\n inner();\n})();\nconsole.log(fruit); \/\/ 'banana'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這段代碼和上一個很類似,自執行函數創建了一個 fruit 變量和一個 inner 函數。當 inner 函數被調用時,它引用了外層函數中的變量 fruit,於是我們的得到了一個 apple 而不是 banana。不幸的是,對於自執行函數來說,這個 inner 函數是一個局部對象,所以在自執行函數結束後,inner 函數也會被銷燬掉。我們還是沒創建一個閉包,再來看一個例子:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nvar inner;\n(function() {\n var fruit = 'apple';\n inner = function() {\n console.log(fruit);\n }\n})();\nconsole.log(fruit); \/\/ 'banana'\ninner(); \/\/ 'apple'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在開始變得有趣了。在全局作用域中我們聲明瞭一個名爲 inner 的變量,在自執行函數中我們把一個 log 出 fruit 變量值的函數作爲值賦給全局變量 inner。正常情況下,當自執行函數結束後,其內部的局部變量 fruit 應該被銷燬,就像我們前面 2 個例子那樣。但是因爲在 inner 函數中依然保持着對局部變量 fruit 的引用,所以最後我們在調用 inner 時會 log 出 apple。這時可以說我們創建了一個閉包。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個閉包會在這種情況下被創建:一個內層函數嵌套在一個外層函數裏,這個內層函數被儲存在其外層函數作用域之外的作用域的 variable 對象中,同時還保存着對其外層函數局部變量的引用。雖然外層函數中的這個 inner 函數不會再被運行,但其對外層函數變量的引用卻依然保留着,這是因爲在函數內部的作用域鏈中依然保存着該變量的引用,即使外層的函數此時已經不存在了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要記住一個函數的作用域鏈同它的執行上下文是綁定的,同其他那些與執行上下文關聯緊密的對象一樣,作用域鏈在函數執行上下文被創建之後創建,並隨着函數執行上下文的銷燬而銷燬。解析器只有在函數被調用時纔會創建該函數的執行上下文。在上面的例子中,inner 函數是在最後一行代碼被執行時調用的,而此時,原匿名函數的執行上下文(連同它的作用域鏈和 variable 對象)都已經被銷燬了。那麼 inner 函數是如何引用到已經被銷燬的保存在局部作用域中的局部變量的呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個問題的答案引出了函數內部對象中一個被稱爲 scope 屬性(scope property)的對象。所有的 JavaScript 函數都有其自身的內在 scope 屬性,該對象中儲存着用來創建該函數作用域鏈的那些對象。當解析器要爲一個函數創建作用域鏈,它會去查看 scope 屬性看看哪些項是需要被加進作用域鏈中的。因爲相比執行上下文,scope 屬性同函數本身的聯繫更爲緊密,所以在函數被徹底銷燬之前,它都會一直存在——這樣苦於保證不了函數被調用多少次,它都是可用的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個在全局作用域中被創建的函數擁有一個包含了 global 對象的 scope 對象,所以它的作用域鏈僅包含了 global 對象和和它自己的 variable 對象。一個創建在其他函數中的函數,它的 scope 對象包含了封裝它的那個函數的 scope 對象中的所有對象和它自己的 variable 對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function A() {\n function B() {\n function C() {\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個代碼片段中,函數 A 的 scope 屬性中僅保存了 global 對象。因爲函數嵌套在函數 A 中,所有函數 B 的 scope 屬性會繼承函數 A 的 scope 屬性的內容並附加上函數 A 的 variable 對象。最後,函數 C 的 scope 屬性會繼承函數 B 的 scope 屬性中的所有內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外,採用函數對象方式(使用 new Function() 方法)創建的函數,在它們的 scope 屬性中只有一個項,就是 global 對象。這意味着它們不能訪問其外圍函數(如果有的話)的局部變量,也就不能用來創建閉包。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"This 關鍵字(The “this” Keyword)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面我們討論了一些函數的內部機制,最後我們還有一個項目要討論:this 關鍵字。如果你對其他的面向對象的編程語言有使用經驗,你應該會對一些關鍵字感到熟悉,比如 this 或者 self,用以指代當前的實例。不過在 JavaScript 中 this 關鍵字會便得有些複雜,因爲它的值取決於執行上下文和函數的調用者。同時 this 還是動態的,這意味着它的值可以在程序運行時被更改。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"this 的值總是一個對象,並且有些一系列規則來明確在當前代碼塊中哪一個對象會成爲 this。其中最簡單的規則就是,在全局環境中,this 指向全局對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nconsole.log(fruit); \/\/ 'banana'\nconsole.log(this.fruit); \/\/ 'banana'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回憶一下,全局上下文中聲明的變量都會成爲全局 global 對象的屬性。這裏我們會看到 this.fruit 會正確的指向 fruit 變量,這向我們展示在這段代碼中 this 關鍵字是指向 global 對象的。對於全局上下文中聲明的函數,在其函數體中 this 關鍵字也是指向 global 對象的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\nfunction sayFruit() {\n console.log(this.fruit);\n}\nsayFruit(); \/\/ 'banana'\n(function() {\n console.log(this.fruit); \/\/ 'banana'\n})();\nvar tellFruit = new Function('console.log(this.fruit);');\ntellFruit(); \/\/ 'banana'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於作爲一個對象的屬性(或方法)的函數,this 關鍵字指向的是這個對象本身而非 global 對象:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = {\n name : 'banana',\n say : function() {\n console.log(this.name);\n }\n};\nfruit.say(); \/\/ 'banana'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在第三章我們會深入討論關於對象的話題,但是現在,我們要關注 this.name 屬性是如何指向 fruit 對象的 name 屬性的。在本質上,這和前面的例子是一樣的:因爲上面例子中的函數是 global 對象的屬性,所以函數體內的 this 關鍵字會指向 global 對象。所以對於作爲某個對象屬性的函數而言,其函數體內的 this 關鍵字指向的就是這個對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於嵌套的函數而言,遵循第一條規則:不論它們出現在哪裏,它們總是將 global 對象作爲其函數體中 this 關鍵字的默認值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana';\n(function() {\n (function() {\n console.log(this.fruit); \/\/ 'banana'\n })();\n})();\nvar object = {\n fruit : 'orange',\n say : function() {\n var sayFruit = function() {\n console.log(this.fruit); \/\/ 'banana'\n };\n sayFruit();\n }\n};\nobject.say();"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏,我們看到處在兩層套嵌的子執行函數中的標識符 this.fruit 指向的是 global 對象中的 fruit 變量。在 say 函數中有一個內嵌函數的例子中,即使 say 函數自身的 this 指向的是 object 對象,但內嵌的 sayFruit 函數中的 this.fruit 指向的還是 banana。這意味着外層函數並不會對內嵌函數代碼體中 this 關鍵字的值產生任何影響。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我在前面提到過 this 關鍵字的值是可變的,且在 JavaScript 中能夠對 this 的值進行改變是很有用的。有兩種方法可以應用於更改函數 this 關鍵字的值:apply 方法和 call 方法。這兩種方法實際上都是應用於無需使用調用操作符 () 來調用函數,雖然沒有了調用操作符,但你還是可以通過 apply 和 call 方法給函數傳遞參數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"apply 方法接收 2 個參數:thisValue 被用於指明函數體中 this 關鍵字所指向的對象;另一個參數是 params,它以數組的形式向函數傳遞參數。當使用一個無參數或第一個參數爲 null 的 apply 方法去調用一個函數的時候,那麼被調用的函數內部 this 指向的就會是 global 對象並且也意味着沒有參數傳遞給它:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var fruit = 'banana'\nvar object = {\n fruit : 'orange',\n say : function() {\n console.log(this.fruit);\n }\n};\nobject.say(); \/\/ 'banana'\nobject.say.apply(); \/\/ 'banana'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果要將一個函數內部的 this 關鍵字指向另一個對象,簡單的做法就是使用 apply 方法並把那個對象的引用作爲參數傳進去:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add() {\n console.log(this.a + this.b);\n}\nvar a = 12;\nvar b = 13;\nvar values = {\n a : 50,\n b : 23\n};\nadd.apply(values); \/\/ 73"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"apply 方法的第二個參數是以一個數組的形式向被調用的函數傳遞參數,數組中的項要和被調用函數的形參保持一致。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add(a, b) {\n console.log(a); \/\/ 20\n console.log(b); \/\/ 50\n console.log(a + b); \/\/ 70\n}\nadd.apply(null, [20, 50]);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面說到的另一個方法 call,和 apply 方法的工作機制是一樣的,所不同的是在 thisValue 參數之後跟着的是自選數量的參數,而不是一個數組:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add(a, b) {\n console.log(a); \/\/ 20\n console.log(b); \/\/ 50\n console.log(a + b); \/\/ 70\n}\nadd.call(null, 20, 50);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"高級的函數技巧(Advanced Function Techniques)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面的內容主要是關於我們對函數的基礎知識的一些討論。不過,要想完整的展現出 JavaScript 函數的魅力,我們還必須能夠應用前面學到的這些分散的知識。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在下面的章節中,我們會討論一些高級的函數技巧,並探索目前所掌握的技能其更廣泛的應用範圍。我想說,本書不會是 JavaScript 學習的終點,我們不可能把關於這門語言的所有信息都寫出來,而應該是開啓你探索之路的一個起點。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"限制作用域(Limiting Scope)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,我在維護一個用戶的姓名和年齡這個事情上遇到了問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ user對象保存了一些信息\nvar user = {\n name : 'Mark',\n age : 23\n};\nfunction setName(name) {\n \/\/ 首先確保name是一個字符串\n if (typeof name === 'string') user.name = name;\n}\nfunction getName() {\n return user.name;\n}\nfunction setAge(age) {\n \/\/ 首先確保age是一個數字\n if (typeof age === 'number') user.age = age;\n}\nfunction getAge() {\n return user.age;\n}\n\/\/ 設置一個新的名字\nsetName('Joseph');\nconsole.log(getName()); \/\/ 'Joseph'\n\/\/ 設置一個新的年齡\nsetAge(22);\nconsole.log(getAge()); \/\/ 22"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前爲止,一切都正常。setName 和 setAge 函數確保我們要設置的值是正確的類型。但我們要注意到,user 變量是出在全局作用域中的,可以在該作用域內的任何地方被訪問到,這回導致你可以不適應我們的設置函數也能夠設置 name 和 age 的值:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"user.name = 22;\nuser.age = 'Joseph';\nconsole.log(getName()); \/\/ 22\nconsole.log(getAge()); \/\/ Joseph"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很明顯這樣不好,因爲我們希望這些值能夠保持其數據類型的正確性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼我們該怎麼做呢?如何你回憶一下,你會記起一個創建在函數內部的變量會成爲一個局部變量,在該函數外部是不能被訪問到的,另外閉包卻可以爲一個函數能夠保存其外層函數局部變量的引用提供途徑。結合這些知識點,我們可以把 user 變成一個受限制的局部變量,再利用閉包來使得獲取、設置等函數可以對其進行操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"\/\/ 創建一個自執行函數\n\/\/ 包圍我們的代碼使得user變成局部變量\n(function() {\n \/\/ user對象保存了一些信息\n var user = {\n name : 'Mark',\n age : 23\n };\n setName = function(name) {\n \/\/ 首先確保name是一個字符串\n if (typeof name === 'string') user.name = name;\n };\n getName = function() {\n return user.name;\n };\n setAge = function(age) {\n \/\/ 首先確保age是一個數字\n if (typeof age === 'number') user.age = age;\n };\n getAge = function() {\n return user.age;\n }\n})();\n\/\/ 設置一個新的名字\nsetName('Joseph');\nconsole.log(getName()); \/\/ 'Joseph'\n\/\/ 設置一個新的年齡\nsetAge(22);\nconsole.log(getAge()); \/\/ 22"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,如果有什麼人想不通過我們的 setName 和 setAge 方法來設置 user.name 和 user.age 的值,他就會得到一個報錯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"柯里化(Currying)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"函數作爲一等對象最大的好處就是可以在程序運行時創建它們並將之儲存在變量裏。如下面的這段代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add(a, b) {\n return a + b;\n}\nadd(5, 2);\nadd(5, 5);\nadd(5, 200);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏我們每次都使用 add 函數將數字 5 和其他三個數字進行相加,如果能把數字 5 內置在函數中而不用每次調用時都作爲參數傳進去是個不錯的主意。我們可以將 add 函數的內部實現機制變爲 5 + b 的方式,但這會導致我們代碼中其他已經使用了舊版 add 函數的部分發生錯誤。那有沒有什麼方法可以實現不修改原有 add 函數的優化方式?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然我們可以,這種技術被稱爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"柯里化(partial application 或 currying) "},{"type":"text","text":",其實現涉及到一個可爲其提前“提供”一些參數的函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add= function(a, b) {\n return a + b;\n};\nfunction add5() {\n return add(5, b);\n}\nadd5(2);\nadd5(5);\nadd5(200);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在,我們創建了一個調用 add 函數並預置了一個參數值(這裏是5)的 add5 函數,add5 函數本質上來講其實就是預置了一個參數(柯里化)的 add 函數。不過,上面的例子並沒展示出這門技術動態的一面,如果我們提供的默認值是另外一個應該怎麼做?按照上面的例子,我們必須要再次新建一個函數來提供一個新的預置參數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"函數作爲一等對象遲早都會派上用處,看,下面應用場景來了。不同於明確的創建一個新的 add5 函數,我們可以像下面這樣來做。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"function add(a, b) {\n return a + b;\n}\nfunction curryAdd(a) {\n return function(b) {\n return add(a, b);\n }\n}\nvar add5 = curryAdd(5);\nadd5(2);\nadd5(5);\nadd5(200);"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在來介紹一下這個新的函數 curryAdd,它接收一個參數,這個參數會作爲 add 函數的參數 a,同時返回一個新的匿名函數,這個匿名函數接收一個參數 b 來作爲 add 函數的另一個參數。當我們通過 curryAdd(5) 來調用這個函數時,它返回一個已經儲存了我們一個明確參數值的函數,這個參數值此時被當做是這個匿名函數的一個局部變量。因爲我們創建了一個閉包,所以即使這個匿名函數已經執行完畢,但我們還是可以通過它來最終求出我們需要的 a + b 的值。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們這裏指介紹了一種柯里化函數一個極爲簡單常見的應用場景,但這可以很好的說明柯里化函數是如何工作的。瞭解並掌握這種技巧會對你日常的編程工作帶來很多便利的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"裝飾(Decoration)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一項綜合使用函數動態賦值和閉包的技術被稱爲"},{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"裝飾(decoration) "},{"type":"text","text":"。這裏的關鍵詞是“裝飾”(decorate),函數的裝飾是指能夠動態的爲一個函數增加新的功能特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們有一個函數,它把一個對象作爲參數,它的工作是把這個參數對象內的名值對儲存到另一個對象裏去:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"(function() {\n var storage = {};\n store = function(obj) {\n for (var i in obj) storage[i] = obj[i];\n };\n retrieve = function(key) {\n return storage[key];\n };\n})();\nconsole.log(retrieve('name')); \/\/ undefined\nstore({\n name : 'Mark',\n age : '23'\n});\nconsole.log(retrieve('name')); \/\/ 'Mark'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看起來似乎不錯,但如果我們的需求變成不單可以給 store 函數傳由名值對組成的對象做參數,還可以直接傳名值對,就是類似 store(‘name’, ‘Mark’); 這種形式的,那我們目前的函數就不能起作用了,我們需要對函數進行改進。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以通過爲 store 函數套上一層裝飾者函數來實現想要的改進:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var decoratePair = function(fn) {\n return function(key, value) {\n if (typeof key === 'string') {\n var _temp = {};\n _temp[key] = value;\n key = _temp;\n }\n return fn(key);\n }\n};\n(function() {\n var storage = {};\n store = decoratePair(function(obj) {\n for (var i in obj) storage[i] = obj[i];\n });\n retrieve = function(key) {\n return storage[key];\n };\n})();\nconsole.log(retrieve('name')); \/\/ undefined\nstore('name', 'Mark');\nconsole.log(retrieve('name')); \/\/ 'Mark'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這應該是目前爲止我們看過的比較複雜的例子了,讓我們一步一步的來分析下這段代碼。首先,我們聲明瞭一個名爲 decoratePair 的函數,這個函數只接收一個參數 fn,這個函數會被我們進行裝飾。之後 decoratePair 會返回一個新的被裝飾過的函數,這個函數接收兩個參數,key 和 value。我們原先的 store 函數只接收一個對象類型的參數,現在通過裝飾者函數可以判斷第一個參數是對象還是字符串。如果第一個參數不是字符串,則fn函數會立即被執行;如果第一個參數是字符串,則 decoratePair 的返回值函數會先把傳進去的參數 key 和 value 以名值對的方式存進一個私有變量 _temp 裏,之後把 _temp 賦值給一個變量 key,這時變量 key 引用的是一個符合 fn 函數參數要求的對象,之後再來調用 fn 函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們上面的裝飾者函數可以確保在調用被包裝的 fn 函數時傳輸的是類型正確的參數,但是修飾着函數也可以用在函數被調用後爲其增加特性。下面有一個簡單的裝飾者函數,它調用 add 函數的 2 個參數,並返回這 2 個參數的和與第二個參數的積。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function(a, b) {\n return a + b;\n};\nvar decorateMultiply = function(fn) {\n return function(a, b) {\n var result = fn(a, b);\n return result * b;\n }\n};\nvar addThenMultiply = decorateMultiply(add);\nconsole.log(add(2, 3)); \/\/ 5\nconsole.log(addThenMultiply(2, 3)); \/\/ 15"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"裝飾者函數的用途很廣,它可以幫助你在無需直接修改函數的情況下爲其增加功能。它尤其適用於那些你不能直接修改的內建函數和第三方代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"組合(Combination)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#000000","name":"user"}},{"type":"strong"}],"text":"組合(combination)"},{"type":"text","text":"是一項和裝飾者函數相似的技術,它的用途是使用兩個(或數個)函數來創造一個新的函數。這和聲明一個新的函數不同,組合者函數只是將一個函數的返回值作爲參數傳給下一個函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"**var add = function(a, b) {\n return a + b;\n};\nvar square = function(a) {\n return a * a;\n};\nvar result = square(add(3, 5));\nconsole.log(result); \/\/ 64**"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"square(add(3, 5)) 這段代碼顯示了組合者函數是如何工作的,但這還不能算一個正確的組合者函數。這裏,add(3, 5) 的返回值 8,作爲參數傳給了 square 函數,之後 square 函數返回了 64。要把它變成一個組合者函數,我們要將加工過程自動化,免得每次都要去敲 square(add(3, 5))。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"var add = function(a, b) {\n return a + b;\n};\nvar square = function(a) {\n return a * a;\n};\nvar combine = function(fnA, fnB) {\n return function() {\n var args = Array.prototype.slice.call(arguments);\n var result = fnA.apply(null, args);\n return fnB.call(null, result);\n }\n};\nvar addThenSquare = combine(add, square);\nvar result = addThenSquare(3, 5);\nconsole.log(result); \/\/ 64"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個代碼片段中我們先創建了兩個具備單一功能的函數 add 和 square。之後創建了一個組合者函數 combine,combine 函數接收 add 和 square 爲參數,在返回的匿名函數裏,先將傳給匿名調用函數的參數 a 和 b 轉爲一個數組 args,之後用 apply 方法調用 add 函數,將 a 與 b 的和賦值給變量 result,最後用 call 方法調用 square 方法,計算出最終的結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意在使用組合者函數時,函數的順序和參數的數量是需要被重點注意的。在我們的例子中,因爲 square 函數只需要一個參數,而 add 函數需要的則是兩個,所以我們不能得到一個 squareTheAdd(先乘後加,先傳一個參數後傳 2 個參數)函數。因爲 JavaScript 只允許函數返回一個值,所以組合者函數的使用場景往往是被限制在那些只採用單個參數的函數中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule"},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頭圖:Unsplash"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:堂主"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/rqPvdKeDN06I6U_nE4dD0Q","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/mp.weixin.qq.com\/s\/rqPvdKeDN06I6U_nE4dD0Q"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文:JAVASCRIPT FUNCTIONS 詳解"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來源:政採雲前端團隊 - 微信公衆號 [ID:Zoo-Team]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"轉載:著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章