(文本源自“JavaScript高级程序设计”第七章的读后梳理)
第七章——函数表达式
这一章将会涉及到我们经常说到的JavaScript中的“闭包”概念。
递归、闭包、模仿块级作用域、私有变量
函数表达式,在引用类型那一板块也讲到了这部分的一些知识。
这里回忆一下,
定义函数的三种方式:函数声明,函数表达式,Function构造函数(不推荐)。
- 函数声明:说到函数声明就不得不提到函数声明提升(function declaraction hoisting),这个概念已经在前面“JS基础——作用域、原型、闭包初探(中——引用类型)”中有描述了,这里就不啰嗦了。
- 这里要记住一种情况:
//这样是不行的 //因为函数声明提升会导致两个函数被提升到代码树的顶部,并且JavaScript中没有函数签名 //这样,后面定义的sayHi()就会覆盖掉前面定义的sayHi() //那么,不管是什么结果,都会调用后一个sayHi() if(..){ function sayHi(){ alert("hi!"); } }else{ function sayHi(){ alert("goodbye!"); } }
可以使用函数表达式对这种情况进行改进:
//函数表达式不会发生将函数提升到代码树顶端的情况 //所以这个就会运行到什么情况,根据if-else的判断条件,运行哪个函数 var sayHi; if(..){ sayHi = function(){ alert("hi!"); }; }else{ sayHi = function(){ alert("goodbye!"); }; }
一、递归
也是在“引用类型——Function类型”中讲到过这种情况的发生。
递归就是在函数内部调用本函数的引用情况,一般就是这个意思:
//阶乘
function factorial(num){
if(num>0){
var sum = num*factorial(num-1);
}
return sum;
}
factorial2 = factorial;
factorial =null;
factorial2(3); //发生错误
我们知道factorial(num)里面的运行的核心代码和函数名factorial耦合在了一起。
我们知道,变量factorial只是这个递归函数的引用,如果我们把这个递归函数的引用交给其他变量,如factorial2,并且将变量factorial指向其他或者null,那么对factorial2(num)进行操作就会发生错误。
因为函数内部引用的是factorial(),此时factorial指向别处或者null。
大致可以这么理解。
怎么解决呢?
前面有说过,通过调用arguments.callee,使函数自身调用自身,而不必绑定函数名。
function factorial(num){
if(num>1){
var sum = num*arguments.callee(num-1);
}
return sum;
}
这样就解开了和函数名的耦合,大致可以这样理解:
但是严格模式不允许arguments.callee的使用。
那么可以通过函数表达式的方式解决这个问题。
var factorial = (function f(num){
if(num>1){
var sum = num*f(num-1);
}
return sum;
});
核心代码没有和factorial函数名耦合,而且这种形式在严格模式下也行得通。
二、闭包
(引用红包书的原话)闭包就是有权访问另一个函数作用域中的变量的函数。
这里可以拆分一下理解:闭包是函数,这个函数访问了另一个函数作用域中的变量。
执行环境和作用域链是理解闭包的基础。
如:
function createCompare(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1<value){
return -1;
}else if(value1>value2){
return 1;
}else{
return 0;
}
};
}
var compare = createCompare("age");
var result = compare({name:"Nicholas",age:27},{name:"dingding",age:25});
在这里createCompare()函数return的function就是闭包,因为它访问了其他作用域中的变量——createCompare()中的propertyName。
闭包的一般创建形式就是在一个函数中创建另一个函数。
我们再通过执行环境(execution context)和作用域链(scope chain)分析一下这个过程:
- 先分析一个简单的:
function compare(val1,val2){ if(val1 < val2){ return -1; }else if(val1 > val2){ return 1; }else{ return 0; } } var result = compare(5,10);
这里的全局执行环境中,有一个compare()函数,以及result变量。
这里可以分为两步理解:创建compare()函数,以及调用compare()函数。
在创建compare()函数的过程中,会创建一个关于全局执行环境的作用域链,存放在[[scope]]特性中。
在调用compare()函数的过程中,会将存储在[[scope]]中的作用域链复制出来,以创建新的作用域链:
看着复杂,其实很简单,遵循的都是执行环境、执行环境对应的变量对象、以及关于变量对象的作用域链。
上面这种情况下,当函数执行完毕以后,局部活动对象就被销毁,内存中只保存全局作用域(全局执行环境的变量对象)。 - 再将我们的闭包加进来:
闭包会在函数中创建一个新的执行环境,就会有相应的变量对象以及作用域链的延长。function createCompare(propertyName){ return function(obj1,obj2){ var val1 = obj1[propertyName]; var val2 = obj2[propertyName]; if(val1 < val2){ return -1; }else if(val1 > val2){ return 1; }else{ return 0; } }; } var compare = createCompare("age"); var result = compare({name:"Nicholas",age:27},{"name":"dingding",age:25});
这里其实就是把闭包的活动对象加到作用链的前端。
这里当createCompare()函数调用完成以后,其执行环境的作用域链被销毁,但是内存中依然有对它活动对象的引用——因为闭包的作用域链还在引用它。
直到闭包被销毁后,createCompare()的活动对象才会被销毁。
这个时候最好手动解除对闭包的引用。
compare = null;
注意:过多的会用闭包,可能会对内存造成比较大的负担。(因为闭包携带包含它的函数的作用域)
1、闭包与变量
闭包只能访问到另一个作用域中变量的最终值。
function createFunction(){
var result = [];
for(var i=0;i<10;i++){
result[i] = function(){
return i;
};
}
return result;
}
这里,你可能会觉得result[]的值是:“1,2,3,4,5,6,7,8,9,10”,其实并不是,因为这里的result数组每一个值其实是通过匿名函数返回回来的,这里的里面匿名函数读取到的其实是外包环境中i的最终值,也就是10。
所以这里result[]的值是:“10,10,10,10,10,10,10,10,10,10”。
可以通过另一种方式,改变这种情况:
function createFunction(){
var result = [];
for(var i=0;i<10;i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
这是对函数表达式的立即调用,闭包读取函数表达式的num,而函数表达式的通过立即调用将每一循环的 i 值传给了num,所以对于匿名函数中的num的最终值都是每次循环传递给它的 i 值。
2、闭包与this对象
闭包的this对象指向window:
var name = "global";
var object = {
name:"local",
getName:function(){
return function(){
alert(this.name);
};
}
};
object.getName()(); //"global"
因为闭包的执行环境具有全局性,因此其this对象通常指向window(除非通过call()、apply()来改变this对象值)。所以这里调用getName()函数中的闭包时,得到的是定义在全局对象中的name值——global。
可以这么改:闭包的外包函数的this对象放到闭包可以读取的地方——that变量。
var name = "global";
var object = {
name:"local",
getName:function(){
that = this;
return function(){
alert(that.name);
};
}
};
object.getName()(); //"local"
3、内存泄漏
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert(btn.id);
}
内存泄漏主要是指,在闭包中访问DOM元素,而没有及时的将闭包的应用解开,致使内存的泄漏。
三、模仿块级作用域
JavaScript不像C、C++、Java等语言,对于for循环等有块级作用域,这个在第四章的“执行环境与作用域链” 中提到过,那么我们怎么给这种情况一个块级作用域呢?
方法就是给for循环外套上一个立即执行的匿名函数function(),
(function(){
for(var i=0;i<10;i++){
//....
}
})();
这里的这个结构:就是在模仿块级作用域。
(function(){
})();
在这个块级作用域的外部就无法访问在for循环中设置的 i 值。
注意:这种做法可以减少闭包占用内存的问题,因为没有指向闭包的引用。只要函数执行完毕,就可以立即销毁其作用域链。
四、私有变量
私有变量就是在函数内部定义的变量。
在函数外部对它们是不可见的。
私有变量包括:函数的参数、局部变量、在函数内部定义的其他函数。
这里重要的是特权方法(privileged method):有权访问私有变量和私有函数的共有方法。
通过特权方法,可以从外界访问到函数内部的变量和属性,为什么呢?
因为特权方法是通过在函数内部创建一个闭包,而闭包通过自己的作用域链也可以访问这些变量。
创建特权方法的两种方式:
- 在构造函数中定义特权方法。
- 在原型中定义特权方法——静态私有变量。
在构造函数中定义特权方法:
function Person(){
//函数中的私有变量和私有方法
var privateVarible;
function privateFunction(){}
//通过闭包,定义特权方法
this.publicMethod = function(){
privateVarible++;
return privateFunction();
};
}
在创建Person实例以后,只能通过publicMethod()访问到privateVariable、privateFunction()。没有其他途径。
1、静态私有变量
就是将特权方法放入到原型中:
通过块级作用域中定义私有变量和私有函数,然后将闭包定义到原型中:
(function(){
//私有变量和私有函数
var privateVariable;
function privateFunction(){}
//构造函数。没有对函数表达式中的Person使用var,所以Person是一个全局变量
//这样Person才能在这个模仿的块级作用域之外内访问到。
//当然严格模式下,这样做会导致错误
Person = function(){};
Person.prototype.publicMethod = function(){
privateVariable++;
return priavteFunction();
};
})();
这中方式定义的特权方法,改善了直接在构造函数上定义特权方法的弊端,也就是提高了函数的复用性,这在“面向对象的程序设计”中讨论过,这里就不细说了。
在原型上定义的特权方法,将被所有的Person实例所共用。
2、模块模式(module pattern)
Douglas Crockford提出来的模块模式——指为单例创建私有变量和特权方法。
前面提到的创建私有变量和特权方法,都是针对自定义类型。
这里的模块模式就是针对 单例 创建私有变量和特权方法。
单例(singleton):只有一个实例的对象。
模块模式 通过为单例 添加私有变量和特权方法 能够使其得到增强:
var singleton = function(){
//私有变量和私有函数
var privateVariable;
function privateFunction(){}
//特权/公有方法和属性
return{
publicProperty:true,
publicMethod:function(){
privateVariable++;
return privateFunction();
}
};
}();
由于这个返回的对象字面量(return {...})是在匿名函数(var singleton = function(){})内部定义的,所以它有权访问匿名函数中的私有变量和函数。
从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式是在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。如:
var application = function(){
//私有变量和函数
var components = [];
//初始化
components.push(new BaseComponents());
//公共
return {
getComponentsCount:function(){
return components.length;
},
registerComponent:function(component){
if(typeof component == "object"){
components.push(component);
}
}
};
}();
这里我们创建了一个application对象,在里面以某些数据对其进行初始化,同时通过 return的对象字面量——公开一些能够访问application中私有数据的方法。
简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。
3、增强的模块模式
怎么增强的呢?
其实,前面描述的模块模式中,对于单例类我们返回的就是一个对象字面量,这个对象字面量中有可以访问这个单例类中私有变量和私有方法的特权方法、。
那么,如果,这些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况下,
我们就要返回特定的类型实例,而不是简单的返回一个对象字面量:
var singleton = function(){
//私有变量和私有函数
var privateVariable ;
function privateFunction(){}
//创建对象
var object = new CustomType();
//添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回这个对象
return object;
}();
这里我们就是不仅仅只是返回一个对象字面量,而是返回了一个CustomType()的实例——object对象。
附:更进一步的探索
- 理解词法作用域和动态作用域
- 理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
- this的原理以及几种不同使用场景的取值
- 闭包的实现原理和作用,可以列举几个在开发中闭包的实际应用
- 理解堆栈溢出和内容泄漏的原理,如何防止
- 如何处理循环的异步操作
- 理解模块化解决的实际问题,可列举几个模块化方案并理解其中原理