JavaScript中的函數表達式

在JavaScript中,函數是個很是重要的對象,函數一般有三種表現形式:函數聲明,函數表達式和函數構造器建立的函數。html

本文中主要看看函數表達式及其相關的知識點。前端

函數表達式

首先,看看函數表達式的表現形式,函數表達式(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; } "這個函數表達式沒有函數名稱,是個匿名函數表達式

 

例子二:爲了解釋函數表達式另外兩個特色,繼續看看下面的例子。this

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"執行正常,可是對函數表達式的執行失敗了。spa

對於這個例子,能夠參考"JavaScript的執行上下文"一文中的內容,當代碼開始執行的時候,能夠獲得下圖所示的Global VO。code

在Global VO中,對"add"函數表現爲JavaScript的"Hoisting"效果,因此即便在"add"定義以前依然可使用;server

可是對於"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解釋器額外的作了一些事情:

  1. 當解釋器在代碼執行階段遇到命名函數表達式時,在函數表達式建立以前,解釋器建立一個特定的輔助對象,並添加到當前做用域鏈的最前端
  2. 而後當解釋器建立了函數表達式,在建立階段,函數獲取了[[Scope]] 屬性(當前函數上下文的做用域鏈)
  3. 此後,函數表達式的函數名添加到特定對象上做爲惟一的屬性;這個屬性的值是引用到函數表達式上
  4. 最後一步是從父做用域鏈中移除那個特定的對象

下面是表示這一過程的僞代碼:

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的污染
相關文章
相關標籤/搜索