JS的函数表达式

1.函数表达式

定义函数的方式有两种,一种是函数声明,另一种是函数表达式。
关于函数声明,它的一个重要特征就是函数声明提升,意思在执行代码前会先读取函数声明。
而函数表达式和其它表达式一样,使用前必须先赋值。

1
2
3
4
5
6
7
8
9
sayHi();           // "Hi!"
function sayHi(){ // 函数声明
alert("Hi!");
}

sayHi(); // 报错,函数不存在
var sayHi = function(){ // 函数表达式
alert("Hi!");
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//不要这样做,会出现意想不到的结果
if(condition){
function sayHi(){
alert("Hi!");
}
}else{
function sayHi(){
alert("Yo!");
}
}

//可以这样做
if(condition){
sayHi = function(){
alert("Hi!");
};
}else{
sayHi = function(){
alert("Yo!");
};
}

2.递归

递归函数是在一个函数通过名字调用自身的情况下构成的。为了避免递归函数和函数名字的必要结合,可以使用arguments.callee来实现。

1
2
3
4
5
6
7
funciton factorial(num){
if (num <= 1){
return 1;
}else{
return num * arguments.callee(num - 1);
}
}

arguments.callee是指向正在执行的函数的指针,因此使用arguments.callee比使用函数名更保险。
但在严格模式下,不能通过脚本访问arguments.callee,这时可以使用命名函数表达式来实现。

1
2
3
4
5
6
7
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num - 1);
}
});

上面创建了一个名为f()的命名函数表达式,然后将它赋值给变量factorial。即使把函数赋值给另一个变量,函数f依然有效。

3.闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createComparisonFunction(propertyName) {
return function(object1, object2){
var value1 = object1[propertyName]; // 第一行
var value2 = object1[propertyName]; // 第二行

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

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

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

compareNames = null; // 解除对匿名函数的引用,释放内存

注释的两行代码访问了外部函数的变量propertyName,是因为内部函数的作用域链中包含了createComparisonFunction()的作用域。

  1. 闭包与变量
    作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
    result[i] = function(){
    return i;
    };
    }
    return result;
    }

这个函数返回的数组中每个值都是10, ,所以它们引用的都是同一个变量对象i。当createFunctions()函数返回以后,变量i的值为10。

1
2
3
4
5
6
7
8
9
10
11
12
function createFunctions(){
var result = new Array();

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

这个函数返回了各自不同的索引值。这个函数中我们没有把闭包返回给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。

  1. 关于this对象
    在闭包中使用this对象也可能会导致一些问题。在全局函数中this等于window,而当函数被当做某个对象的方法调用时,this等于那个对象。不过匿名函数的执行环境具有全局性,因此其this对象通常指向window。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var name = "The Window";

    var object = {
    name : "My Object";

    getNameFunction : funciton(){
    return funciton(){
    return this.name;
    };
    }
    };
    alert(object.getNameFunction()()); // "The Window"(非严格模式下)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "The Window";

var object = {
name : "My Object";

getNameFunction : funciton(){
var that = this;
return funciton(){
return that.name;
};
}
};
alert(object.getNameFunction()()); // "My Object"

  1. 内存泄漏
    由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些问题。
    1
    2
    3
    4
    5
    6
    function assignHandler(){
    var element = document.getElementById("someElement");
    element.onclick = function(){
    alert(element.id);
    };
    }

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

1
2
3
4
5
6
7
8
9
10
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;

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

element = null;
}

通过把element.id保存在一个变量里,在闭包中引用该变量消除了循环引用。但这样还不能解决内存泄漏问题。
必须记住,闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象也仍然会保存一个引用。因此有必要把element设置为null。

4.模仿块级作用域

匿名函数可以用来模仿块级作用域,语法如下:

1
2
3
(funciton(){
//这里是块级作用域
})();

在匿名函数中定义的任何变量,都会在执行结束时被销毁。

1
2
3
4
5
6
7
8
9
function outputNumbers(count){
(funciton(){
for (var i=0; i < count; i++){
alert(i);
}
})();

alert(i); // 导致错误
}

5.私有变量

严格来讲,JavaScript中没有私有成员的概念,所有对象属性都是公有的。
有一个私有变量的概念,任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
我们把有权访问私有变量的和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。
第一种是在构造函数中定义特权方法,基本模式如下。

1
2
3
4
5
6
7
8
9
10
11
12
function MyObject(){
var privateVariable = 10;

function privateFunction(){
return false;
}

this.publicMethod = function(){ // 特权方法
privateVariable++;
return privateFunction();
};
}

能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。在创建MyObject的实例后,除了使用publicMethod()这一途径,没有任何办法可以直接访问privateVariable 和 privateFunction()。
第二种是利用私有和特权成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value){
name = value;
};
}

var person = newPerson("Nicholas");
alert(person.getName()); // "Nicholas"
person.setName("Greg");
alert(person.getName()); // "Greg"

以上代码定义了两个特权方法:getName()和setName()。这两个方法都可以在构造函数外部使用,都有权访问私有变量name。
不过构造函数的缺点就是针对每个实例都会创建同样一组新方法。

  1. 静态私有变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    (function(){
    var name = "";
    Person = function(value){ // 定义构造函数
    name = value;
    };
    Person.prototype.getName = function(){
    return name;
    };
    Person.prototype.setName = function (value){
    name = value;
    };
    })();

    var person1 = new Person("Nicholas");
    alert(person1.getName()); // "Nicholas"
    person1.setName("Greg");
    alert(person1.getName()); // "Greg"

    var person2 = new Person("Michael");
    alert(person1.getName()); // "Michael"
    alert(person2.getName()); // "Michael"

这个模式在定义构造函数时使用了函数表达式,而且在声明Person时没有使用var关键字。由于初始化未经声明的变量,总会创建一个全局变量,因此Person就成了全局变量,能够在作用域之外被访问到。但是在严格模式下给未经声明的变量赋值会导致错误。
这个例子中的Person构造函数与getName()和setName()方法一样都有权访问私有变量name。
这个模式的特点就是私有变量和函数是由实例共享的。

  1. 模块模式
    模块模式是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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);
    }
    }
    };
    }();

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数,然后将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。
这个例子中,返回对象的getComponentCount()和registerComponent()方法都有权访问数组components的特权方法。

参考书籍:《JavaScript高级程序设计》,作者:【美】 Nicholas C.Zakas