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的污染
相关文章
相关标签/搜索