在JavaScript中,函數是個非常重要的對象,函數通常有三種表現形式:函數聲明,函數表達式和函數構造器創建的函數。
本文中主要看看函數表達式及其相關的知識點。
函數表達式
首先,看看函數表達式的表現形式,函數表達式(Function Expression, FE)有下面四個特點:
- 在代碼中須出現在表達式的位置
- 有可選的函數名稱
- 不會影響變量對象(VO)
- 在代碼執行階段創建
下面就通過一些例子來看看函數表達式的這四個特點。
FE特點分析
例子一:在下面代碼中,"add"是一個函數對象,"sub"是一個普通JavaScript變量,但是被賦值了一個函數表達式" function (a, b){ return a - b; } ":
function add(a, b){ return a + b; } var sub = function (a, b){ return a - b; } console.log(add(1, 3)); // 4 console.log(sub(5, 1)); // 4
通過這個例子,可以直觀的看到函數表達式的前兩個特點:
-
在代碼中須出現在表達式的位置
- " function (a, b){ return a - b; } "出現在了JavaScript語句中的表達式位置
-
有可選的函數名稱
- " function (a, b){ return a - b; } "這個函數表達式沒有函數名稱,是個匿名函數表達式
例子二:為了解釋函數表達式另外兩個特點,繼續看看下面的例子。
console.log(add(1, 3)); // 4 console.log(sub); // undefined console.log(sub(5, 1)); // Uncaught TypeError: sub is not a function(…) function add(a, b){ return a + b; } var sub = function (a, b){ return a - b; }
在這個例子中,調整了代碼的執行順序,這次函數"add"執行正常,但是對函數表達式的執行失敗了。
對於這個例子,可以參考"JavaScript的執行上下文"一文中的內容,當代碼開始執行的時候,可以得到下圖所示的Global VO。
在Global VO中,對"add"函數表現為JavaScript的"Hoisting"效果,所以即使在"add"定義之前依然可以使用;
但是對於"sub"這個變量,根據"Execution Context"的初始化過程,"sub"會被初始化為"undefined",只有執行到" var sub = function (a, b){ return a - b; } "語句的時候,VO中的"sub"才會被賦值。
通過上面這個例子,可以看到了函數表達式的第四個特點
- 在代碼執行階段創建
例子三:對上面的例子進一步改動,這次給函數表達式加上了一個名字"_sub",也就是說,這里使用的是一個命名函數表達式。
var sub = function _sub(a, b){ console.log(typeof _sub); return a - b; } console.log(sub(5, 1)); // function // 4 console.log(typeof _sub) // undefined console.log(_sub(5, 1)); // Uncaught ReferenceError: _sub is not defined(…)
根據這段代碼的運行結果,可以看到"_sub"這個函數名,只能在"_sub"這個函數內部使用;當在函數外部訪問"_sub"的時候,就是得到"Uncaught ReferenceError: _sub is not defined(…)"錯誤。
所以通過這個可以看到函數表達式的第三個特點:
- 不會影響變量對象(VO)
FE的函數名
到了這里,肯定會有一個問題,"_sub"不在VO中,那在哪里?
其實對於命名函數表達式,JavaScript解釋器額外的做了一些事情:
- 當解釋器在代碼執行階段遇到命名函數表達式時,在函數表達式創建之前,解釋器創建一個特定的輔助對象,並添加到當前作用域鏈的最前端
- 然后當解釋器創建了函數表達式,在創建階段,函數獲取了[[Scope]] 屬性(當前函數上下文的作用域鏈)
- 此后,函數表達式的函數名添加到特定對象上作為唯一的屬性;這個屬性的值是引用到函數表達式上
- 最后一步是從父作用域鏈中移除那個特定的對象
下面是表示這一過程的偽代碼:
specialObject = {}; Scope = specialObject + Scope; _sub = new FunctionExpression; _sub.[[Scope]] = Scope; specialObject. _sub = _sub; // {DontDelete}, {ReadOnly} delete Scope[0]; // 從作用域鏈中刪除特殊對象specialObject
函數遞歸
這一小節可能有些鑽牛角尖,但是這里想演示遞歸調用可能出現的問題,以及通過命名函數表達式以更安全的方式執行遞歸。
下面看一個求階乘的例子,由於函數對象也是可以被改變的,所以可能會出現下面的情況引起錯誤。
function factorial(num){ if (num <= 1){ return 1; } else { return num * factorial(num-1); } } console.log(factorial(5)) // 120 newFunc = factorial factorial = null console.log(newFunc(5)); // Uncaught TypeError: factorial is not a function(…)
這時,可以利用函數的arguments對象的callee屬性來解決上面的問題,也就是說在函數中,總是使用"arguments.callee"來遞歸調用函數。
function factorial(num){ if (num <= 1){ return 1; } else { return num * arguments.callee(num-1); } }
但是上面的用法也有些問題,當在嚴格模式的時候"arguments.callee"就不能正常的工作了。
比較好的解決辦法就是使用命名函數表達式,這樣無論"factorial"怎么改變,都不會影響函數表達式" function f(num){…} "
var factorial = (function f(num){ if (num <= 1){ return 1; } else { return num * f(num-1); } });
代碼模塊化
在JavaScript中,沒有塊作用域,只有函數作用域,函數內部可以訪問外部的變量和函數,但是函數內部的變量和函數在函數外是不能訪問的。
所以,通過函數(通常直接使用函數表達式),可以模塊化JavaScript代碼。
創建模塊
為了能夠到達下面的目的,我們可以通過函數表達式來建立模塊。
- 創建一個可以重用的代碼模塊
- 模塊中封裝了使用者不必關心的內容,只暴露提供給使用者的接口
- 盡量與全局namespace進行隔離,減少對全局namespace的污染
下面看一個簡單的例子:
var Calc = (function(){ var _a, _b; return{ add: function(){ return _a + _b; }, sub: function(){ return _a - _b; }, set: function(a, b){ _a = a; _b = b; } } }()); Calc.set(10, 4); console.log(Calc.add()); // 14 console.log(Calc.sub()); // 6
代碼中通過匿名函數表達式創建了一個"Calc"模塊,這是一種常用的創建模塊的方式:
- 創建一個匿名函數表達式,這個函數表達式中包含了模塊自身的私有變量和函數;
- 通過執行這個函數表達式可以得到一個對象,對象中包含了模塊想要暴露給用戶的公共接口。
除了返回一個對象的方式,有的模塊也會使用另外一種方式,將包含模塊公共接口的對象作為全局變量的一個屬性。
這樣在代碼的其他地方,就可以直接通過全局變量的這個屬性來使用模塊了。
例如下面的例子:
(function(){ var _a, _b; var root = this; var _ = { add: function(){ return _a + _b; }, sub: function(){ return _a - _b; }, set: function(a, b){ _a = a; _b = b; } } root._ = _; }.call(this)); _.set(10, 4); console.log(_.add()); // 14 console.log(_.sub()); // 6
立即調用的函數表達式
在上面兩個例子中,都使用了匿名的函數表達式,並且都是立即執行的。如果去看看JavaScript一些開源庫的代碼,例如JQuery、underscore等等,都會發現類似的立即執行的匿名函數代碼。
立即調用的函數表達式通常表現為下面的形式:
(function () { /* code */ })(); (function () { /* code */ } ()); 在underscore這個JavaScript庫中,使用的是下面的方式: (function () { // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; /* code */ } .call(this));
在這里,underscore模塊直接對全局變量this進行了緩存,方便模塊內部使用。
總結
本文簡單介紹了JavaScript中的函數表達式,並通過三個例子解釋了函數表達式的四個特點。
- 在代碼中須出現在表達式的位置
- 有可選的函數名稱
- 不會影響變量對象(VO)
- 在代碼執行階段創建
通過函數表達式可以方便的建立JavaScript模塊,通過模塊可以實現下面的效果:
- 創建一個可以重用的代碼模塊
- 模塊中封裝了使用者不必關心的內容,只暴露提供給使用者的接口
- 盡量與全局namespace進行隔離,減少對全局namespace的污染