js函数表达式重点知识

参考书籍:《JavaScript高级程序设计(第3版)》

 

函数表达式

函数表达式是JavaScript中的一个即强大又容易令人困惑的特性。

function functionName(arg0, arg1, arg2) {
    // 函数体
}

Firefox,Safari,Chrome和Opera都给函数定义了一个非标准的name属性 

// 只在Firefox,Safari,Chrome,Opera有效
console.log(functionName.name);
// functionName

 

关于函数声明,它的一个重要特征就是函数声明提升

 

函数表达式

var functionName = function (arg0, arg1, arg2) {
    // 函数体
}

function关键字后面没有标识符,这叫匿名函数。

 

理解函数提升的关键,就是lijie 函数声明与函数表达式之间的区别。

 

请看一个复杂的函数createComparisonFunction

function createComparisonFunction (propertyName) {
    return function (object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if (value1 < value2) {
            return -1;
        } else if (value > value2) {
            return 1;
        } else {
            return 0;
        }
    }
}

createComparisonFunction()返回了一个匿名函数。返回的函数可能会被赋值给一个变量,或者以其他方式被调用。

在把函数当成值来使用的情况下,都可以使用匿名函数。

 

 

递归

递归函数是在一个函数通过名字调用自身的情况下构成的。

function factorial (num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}

这是一个经典的递归阶乘函数。

优化这个函数

var factorial = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1)
    }
})

 

 

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

创建闭包的常见方式,就是在一个函数内部创建另一个函数。以前面的createComparisonFunction()函数为例。

function createComparisonFunction (propertyName) {
    return function (object1, object2) {
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if (value1 < value2) {
            return -1;
        } else if (value > value2) {
            return 1;
        } else {
            return 0;
        }
    }
}

在这个例子中,突出的那两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction()的作用域。

要彻底搞清楚其中的细节,必须从理解函数被调用的时候都会发生什么入手。

 

当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。

在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

 

来看下面的例子

function compare (value1, value2) {
    if (value1 < value2) {
        return -1;
    } else if (value1 > value2) {
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当调用compare()时,会创建一个包含arguments,value1,value2的活动对象。全局执行环境的变量对象(包含result和compare)在compare()执行环境的作用域链中则处于第二位。

 

 上图展示了包含上述关系的compare()函数执行时的作用域链。

 

后台的每个执行环境都有一个表示变量的对象——变量对象。

全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含对象。

一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是,闭包的情况又有所不同。

在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。在createComparisonFunction()执行结束的时候,这个活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。

匿名函数被销毁后,这个外部函数的活动对象才会被销毁。

// 创建函数
var compareNames = createComparisonFunction("name");

// 调用函数
var result = compareNames({ name: "Nicholas"}, { name: "Greg" });

// 解除对匿名函数的引用(以便释放内存)
compareNames = null;

 

注意:由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

 

 

闭包与变量

请看一个例子

function createFunctions () {
    var result = new Array();

    for (var i = 0; i < 10; i++) {
        result[i] = function () {
            return i;
        }
    }

    return result;
}

var fns = createFunctions();

console.log(fns[1]());

这个函数会生成一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,即fns[0]() 返回0,fns[1]() 返回1,但实际上,每个函数都返回10.

因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以他们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部的i的值都是10.

如何优化这个函数?

通过创建另一个匿名函数强制让闭包的行为符合预期

function createFunctions () {
    var result = new Array();

    for (var i = 0; i < 10; i++) {
        result[i] = function(num) {
            return function() {
                return num;
            }
        }(i)
    }

    return result;
}

var fns = createFunctions();

console.log(fns[1]());

 

 

关于this对象

在闭包中使用this对象可能会导致一些问题。

var name = "The Window";

var object = {
    name: "My Object",

    getNameFunc: function () {
        return function () {
            return this.name
        }
    }
}

var getName = object.getNameFunc();
console.log(getName()); // The Window

把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。

var name = "The Window";

var object = {
    name: "My Object",

    getNameFunc: function () {
        var that = this;
        return function () {
            return that.name
        }
    }
}

var getName = object.getNameFunc();
console.log(getName()); // My Object

 

 

内存泄漏

由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。

具体来说,如果闭包的作用域链中保存着一个HTML元素,那么久意味着该元素将无法被销毁。来看下面的例子。

function assignHandler () {
    var element = document.getElementById("someElement");
    element.onclick = function () {
        alert(element.id);
    }
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就用于不会被回收。优化代码如下。

function assignHandler () {
    var element = document.getElementById("someElement");
    var id = element.id;

    element.onclick = function () {
        alert(id);
    }

    element = null;
}

 

 

模仿块级作用域

通常称为私有作用域

(function(){
    // 这里是块级作用域
})()

请看下面一段代码:

function outputNumbers(count) {
    (function(){
        for (var i = 0; i < count; i++) {
            console.log(i)
        }
    })();
    console.log(i); // 导致一个错误
}

outputNumbers(3);

一般来说,我们都应该尽量少向全局作用域中添加变量和函数。

另一个例子:

(function () {
    var now = new Date();
    if (now.getMonth() == 0 && now.getDate() == 1) {
        console.log('Happy New Year!')
    } else {
        console.log("It's a normal day")
    }
})()

 

私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。

私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。来看下面的例子。

function add (num1, num2) {
    var sum = num1 + num2;
    return sum;
}

在这个函数内部,有3个私有变量:num1, num2,sum.在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。

我们把有权访问私有变量和私有函数的公有方法称为特权方法

有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下:

function myObject () {
    var privateVariable = 10;

    function privateFunction () {
        return false;
    }

    this.publicMethod = function () {
        privateVariable++;
        return privateFunction();
    }
}

var obj = new myObject();
obj.publicMethod();

 

利用私有和特权成员,可以隐藏那么不应该被直接修改的数据,例如:

 1 function Person (name) {
 2     this.getName = function () {
 3         return name;
 4     }
 5     this.setName = function (value) {
 6         name = value
 7     }
 8 }
 9 
10 var a = new Person('cathy');
11 console.log(a.getName());
12 a.setName('nick');
13 console.log(a.getName());

 

构造函数模式的缺点是针对每个实例都会创建同样一组新方法。

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。

 

 

模块模式

为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。

var singleton = {
    name: value,
    method: function () {
        // 这里是方法的代码
    }
}

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:

 1 var singleton = function () {
 2     // 私有变量和私有函数
 3     var privateVariable = 10;
 4 
 5     function privateFunction () {
 6         return false;
 7     }
 8 
 9     return {
10         publicProperty: true,
11         publicMethod: function () {
12             privateVariable++;
13             return privateFunction();
14         }
15     }
16 }

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。

从本质上来讲,这个对象字面量定义的是单例的公共接口。

这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。例如: 

var application = function () {
    // 私有变量和函数
    var components = new Array();

    // 初始化
    components.push(new BaseComponent());

    // 公共
    return {
        getComponentCount: function () {
            return components.length;
        },
        registerComponent: function(component) {
            if (typeof component == 'object') {
                components.push(component);
            }
        }
    }
}

在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例。而返回对象的getComponentsCount和registerComponent方法都是有权访问数组components的特权方法。

 

增强的模块模式

有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。

var singleton = function () {

    // 私有变量和私有函数
    var privateVariable = 10;

    function privateFunction () {
        return false;
    }

    // 创建对象
    var object = new CustomType();

    // 添加特权/公有属性和方法
    object.publicProperty = true;

    object.publicMethod = function () {
        privateVariable++;
        return privateFunction();
    }

    // 返回这个对象
    return object;
} ();

 

如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,那么就可以使用以下代码。

var application = function () {

    // 私有变量和函数
    var components = new Array();

    // 初始化
    components.push(new BaseComponent());

    // 创建application的一个局部副本
    var app = new BaseComponent();

    // 公共接口
    app.getComponentCount = function () {
        return components.length;
    }

    app.registerComponent = function () {
        if (typeof component == 'object') {
            components.push(component)
        }
    }

    // 返回这个副本
    return app;
}();

在这个重写后的应用程序单例中,首先也像前面例子中一样定义了私有变量。主要的不同之处在于命名变量app的创建过程,因为它必须是BaseComponent的实例。这个实例实际上是application对象的局部变量版。此后,我们又为app对象添加了能够访问私有变量的公有方法。最后一步是返回app对象,结果仍然是将它赋值给全局变量application.

 

小结

在JavaScript编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用JavaScript函数的强大方式。以下总结了函数表达式的特点。

在无法确定如何引用函数的情况下,递归函数就会变得比较复杂。

 

当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量。原理如下:

1. 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。

2. 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。

3. 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

 

使用闭包可以在JavaScript中模仿块级作用域,要点如下:

1. 创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。

2. 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

 

闭包还可以用于在对象中创建私有变量,相关概念和要点如下:

1. 即使JavaScript中没有正式的私有变量对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。

2. 有权访问私有变量的公有方法叫做特权方法。

3. 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。

 

JavaScript中的函数表达式和闭包都是及其有用的特性,利用它们可以实现很多功能。不过因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。

 

posted on 2020-02-09 16:30  白小鸽  阅读(243)  评论(0编辑  收藏  举报