本节目录
一、创建函数的两种方式
这篇文章介绍过,定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。
1.1 函数声明
函数声明的语法是这样的:
function functionName(arg0, arg1, arg2) {
//函数体
}
函数声明的一个重要特征就是函数声明提升(function declaration hoisting)
,意思是在执行代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面:
sayHi();
function sayHi(){
alert("Hi!");
}
1.2 函数表达式
第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种形式:
var functionName = function(arg0, arg1, arg2){
//函数体
};
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName
。
这种情况下创建的函数叫做匿名函数(anonymous function)
,因为 function
关键字后面没有标识符。(匿名函数有时候也叫拉姆达函数。)
函数表达式没有变量提升:
sayHi(); //错误:函数还不存在
var sayHi = function(){
alert("Hi!");
};
1.3 函数声明提升的问题
//不要这样做!
if(condition){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
alert("Yo!");
}
}
//可以这样做
var sayHi;
if(condition){
sayHi = function(){
alert("Hi!");
};
} else {
sayHi = function(){
alert("Yo!");
};
}
第一个例子,由于函数声明提升,大多数浏览器会返回第二个声明,忽略 condition
;Firefox 会在 condition
为 true
时返回第一个声明。而第二个例子就没有这样的问题。
1.3 函数作为函数的返回值
function funcA(){
return function() {
console.log('返回函数执行')
}
}
// 使用上面的函数
funcA()() // 返回函数执行
// 或者
var funcB = funcA();
funcB(); // 返回函数执行
二、递归
2.1 递归函数
递归函数是在一个函数通过名字调用自身的情况下构成的:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
这是一个经典的递归阶乘函数。
虽然这个函数表面看来没什么问题,但把该函数作为值赋值给其他变量的时候可能会导致它出错。
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错!
以上代码先把 factorial()
函数保存在变量 anotherFactorial
中,然后将 factorial
变量设置为 null
,结果指向原始函数的引用只剩下一个。但在接下来调用 anotherFactorial()
时,由于必须执行 factorial()
,而 factorial
已经不再是函数,所以就会导致错误。
在这种情况下,使用 arguments.callee
可以解决这个问题。
2.2 arguments.callee ---- 回调自身
arguments.callee
是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用,例如:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * arguments.callee(num-1);
}
}
但在严格模式下,不能通过脚本访问 arguments.callee
,访问这个属性会导致错误。
不过,可以使用命名函数表达式来达成相同的结果。
2.3 命名函数表达式 ---- 最好
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
以上代码创建了一个名为 f()
的命名函数表达式,然后将它赋值给变量 factorial
。即便把函数赋值给了另一个变量,函数的名字 f
仍然有效,所以递归调用照样能正确完成。
这种方式在严格模式和非严格模式下都行得通。
三、闭包
3.1 闭包概念
闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数。
// 计算两个对象相同属性值的差值
function funcA(name) {
return function(object1, object2){
var value1 = object1[name];
var value2 = object2[name];
return value1 - value2;
};
}
// 使用方式
var objA = {value : '1'};
var objB = {value : '5'};
var funcB = funcA('value');
funcB(objA,objB); // -4
即使这个内部函数被返回了,而且是通过funcB
在其他地方被调用了,它仍然可以访问变量 value
。之所以还能够访问这个变量,是因为内部函数的作用域链中包含 funcA()
的作用域。
3.2 作用域链
这篇文章介绍了作用域链的概念。
有关如何创建作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后,使用 arguments
和其他命名参数的值来初始化函数的活动对象。
在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在创建函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的 [[Scope]]
属性中。
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。
但是,闭包的情况又有所不同。在一个函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中。
更为重要的是,外部函数在执行完毕后,其活动对象也不会被销毁,因为内部函数的作用域链仍然在引用这个活动对象。换句话说,当外部函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,外部函数的活动对象才会被销毁,例如:
var objA = {value : '1'};
var objB = {value : '5'};
//创建函数
var funcA = funcA(name){
return function(object1, object2){
var value1 = object1[name];
var value2 = object2[name];
return value1 - value2;
};
}
//调用函数
var funcB = funcA('value');
funcB(objA,objB); // -4
//解除对匿名函数的引用(以便释放内存)
funcB = null;
3.3 闭包的缺点(内存占用和变量问题)
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。(但是实际开发中闭包真香)
作用域链有一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值
。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result; // [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
}
createFunctions()[5]() // 10
这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数返回 0,位置 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; // [ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
}
createFunctions()[5]() // 5
我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。
这里的匿名函数有一个参数 num
,也就是最终的函数要返回的值。
在调用每个匿名函数时,我们传入了变量 i
。由于函数参数是按值传递的,所以就会将变量 i
的当前值复制给参数 num
。
而在这个匿名函数内部,又创建并返回了一个访问 num
的闭包。这样一来,result
数组中的每个函数都有自己 num
变量的一个副本,因此就可以返回各自不同的数值了。
3.4 关于 this 对象
在闭包中使用 this
对象也可能会导致一些问题。this
对象是在运行时基于函数的执行环境绑定的:在全局函数中,this
等于window
,而当函数被作为某个对象的方法调用时,this
等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this 对象通常指向window①。但有时候
由于编写闭包的方式不同,这一点可能不会那么明显
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
// 等同于
var funcA = object.getNameFunc();
funcA();
以上代码先创建了一个全局变量 name
,又创建了一个包含 name
属性的对象。这个对象还包含一个方法——getNameFunc()
,它返回一个匿名函数,而匿名函数又返回 this.name
。
由于 getNameFunc()
返回一个函数,因此调用 object.getNameFunc()()
就会立即调用它返回的函数(就相当于在调用的环境中创建该函数),结果就是"The Window"
,即全局 name
变量的值。
所以在这里匿名函数没有取得其包含作用域(或外部作用域)的 this 对象。
3.4.1 获取父级作用域的 this 对象
每个函数在被调用时都会自动取得两个特殊变量:this
和 arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
不过,把外部作用域中的 this
对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下所示。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
在定义匿名函数之前,我们把 this
对象赋值给了一个名叫 that
的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声名的一个变量。
即使在函数返回之后,that
也仍然引用着 object
,所以调用 object.getNameFunc()()
就返回了 "My Object"
。
同理: 如果想访问作用域中的 arguments
对象,也必须将对该对象的引用保存到另一个闭包能够访问的变量中。
3.4.2 this 情况举例
var name = "The Window";
var object = {
name : "My Object",
getName: function(){
return this.name;
}
};
object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window",在非严格模式下
(object.getName = function(){return this.name;})(); //"The Window",在非严格模式下
第一行代码跟平常一样调用了 object.getName()
,返回的是 "My Object"
,因为 this.name
就是 object.name
。
第二行代码在调用这个方法前先给它加上了括号。虽然加上括号之后,就好像只是在引用一个函数,但 this
的值得到了维持,因为 object.getName
和(object.getName)
的定义是相同的。
第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身(等同于第四行),所以 this
的值不能得到维持,结果就返回了"The Window"
。
3.5 内存泄漏
3.5.1 循环引用
这篇文章介绍过垃圾收集。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
以上代码创建了一个作为 element
元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。
由于匿名函数保存了一个对 assignHandler()
的活动对象的引用,因此就会导致无法减少 element
的引用数。只要匿名函数存在,element
的引用数至少也是 1,因此它所占用的内存就永远不会被回收。
3.5.2 解决方法
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
在上面的代码中,通过把 element.id
的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。
必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着 element
。即使闭包不直接引用 element
,包含函数的活动对象中也仍然会保存一个引用。
因此,有必要把 element
变量设置为 null
。这样就能够解除对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
四、模仿块级作用域
4.1 函数的私有作用域(块级作用域)
函数的私有作用域(块级作用域):
var someFunction = function(){
//这里是块级作用域
};
someFunction();
匿名函数的私有作用域(块级作用域):
(function(){
//这里是块级作用域
})();
4.2 函数声明转换为函数表达式 ---- (function{})()
function(){
//这里是块级作用域
}(); //出错!
上面这段代码会导致语法错误,是因为 JavaScript 将 function
关键字当作一个函数声明的开始,而函数声明后面不能跟圆括号。
然而,函数表达式的后面可以跟圆括号。要将函数声明转换成函数表达式,只要像下面这样给它加上一对圆括号即可。
(function(){
//这里是块级作用域
})();
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //导致一个错误!
}
我们在for
循环外部插入了一个私有作用域。在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i
只能在循环中使用,使用后即被销毁。
而在私有作用域中能够访问变量count
,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的
所有变量。
4.3 模仿块级作用域的优点
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //导致一个错误!
}
变量 i
只能在循环中使用,使用后即被销毁。
而在私有作用域中能够访问变量 count
,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量。
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。
在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。
而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
五、私有变量
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。
私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)
。
5.1 构造函数中定义特权方法
这篇文章介绍了构造函数的概念。
例一:
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。对这个例子而言,变量 privateVariable
和函数 privateFunction()
只能通过特权方法 publicMethod()
来访问。在创建 MyObject
的实例后,除了使用 publicMethod()
这一个途径外,没有任何办法可以直接访问 privateVariable
和 privateFunction()
。
例二:
function Person(name){
this.getName = function(){
return name;
};
this.setName = function (value) {
name = value;
};
}
var person = new Person("Nicholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"
定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。
5.2 静态私有变量
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){
};
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();
需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明MyObject
时使用var
关键字。
初始化未经声明的变量,总是会创建一个全局变量
。
因此,MyObject
就成了一个全局变量,能够在私有作用域之外被访问到。
但也要知道,在严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。这也就意味着,外部函数作用域内的私有变量是由所有实例共享的。
(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
构造函数与getName()
和setName()
方法一样,都有权访问私有变量name
。
在这种模式下,变量 name
就变成了一个静态的、由所有实例共享的属性。
也就是说,在一个实例上调用 setName()
会影响所有实例。而调用 setName()
或新建一个 Person
实例都会赋予 name
属性一个新值。结果就是所有实例都会返回相同的值。因为作为特权方法的闭包,总是保存着对包含作用域的引用。而引用类型值是共享的。
5.3 模块模式(有点迷糊呀)
模块模式是为单例创建私有变量和特权方法。所谓单例(singleton)
,指的就是只有一个实例的对象。
这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时非常有用。
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是 Object
的实例,因为最终要通过一个对象字面量来表示它。
5.4 增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
来看下面的例子:
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;
}();