函数表达式
简述
- 定义函数的方式有两种:函数表达式和函数声明。函数声明的语法如下:
function functionName(arg0, arg1, arg2) {
//函数体
}
- FireFox、Safari、Chrome、Opera都给函数定义了一个非标准的name属性,通过该属性可以访问到指定函数的名称。这个属性值永远等于跟在function关键字后面的标识符。
alert(functionName.name);//functionName
- 函数声明的一个重要特征是函数声明提升,意思是在执行代码之前会先读取作用域内的函数声明。这就意味着可以把函数声明放在调用它的语句的后面。但是函数表达式就没有这个特征了。下面是函数表达式的语法:
var functionName = function(arg0, arg1, arg2) {
//函数体
}
alert(functionName.name);//"functionName"
- 不带标识符的function叫匿名函数,这里就是创建一个匿名函数赋给functionName变量。书上说匿名函数的name属性是空字符,但是我用Chrome浏览器测试下来发现是”functionName”。下面这个例子说明了函数声明具有函数声明提升的特征,而函数表达式没有这个功能。所谓的函数提升,相当于把函数声明移到作用域的起始位置。
sayHi();
function sayHi(){
alert("Hi!");
}
sayHello();//error
var sayHello = function(){
alert("Hello!");
}
- 因为函数提升特征的存在,所以不能写出以下代码:
if(true){
function sayHi(){
alert("Hi!");
}
} else {
function sayHi(){
alert("Yo!");
}
}
sayHi();//Chrome: Hi! IE: Yo!
- 上面的例子看起来好像是说如果true则定义sayHi为Hi,否则就是Yo。但是由于函数声明提升的存在,这种写法是非常危险的。在Chrome中或许尝试修复了这种错误,不过在IE中永远会返回第二个声明。但是用函数表达式就可以解决这个问题:
var sayHi;
if(true){
sayHi = function(){
alert("Hi!");
}
} else {
sayHi = function(){
alert("Yo!");
}
}
sayHi();//Chrome: Hi! IE: Hi!
- 当把函数当成值使用的时候就都可以使用匿名函数,但这个不是匿名函数的唯一用途。
递归
- 递归函数之前已经提到过了,前面讲到阶乘函数。如果显式的调用函数名(指针),会因为这个指针可能被重置了而出现错误。为了解除这种耦合性,可以使用arguments.callee属性。该属性指向拥有arguments的函数。
// "use strict";
function factorial(num){
if (num <= 1){
return 1;
} else {
//return num * factorial(num-1);
return num * arguments.callee(num-1);
}
}
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //24
- 不过在严格模式下,该属性是无法使用的,但是我们可以这样修改:
"use strict";
var factorial = function f(num) {
if (num <= 1){
return 1;
} else {
//return num * factorial(num-1);
//return num * arguments.callee(num-1);
return num * f(num-1);
}
};
alert(factorial.name);//猜猜是什么?是f不是factorial哦
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //24
alert(f(4));//error
- 注意此处使用了命名函数表达式而不是函数声明。这里与使用匿名函数唯一的区别是这个函数有名字了!但是这个f指针并没有说像函数声明一样,添加进全局作用域中,从最后一句error可以看出。至于这个f指针到底存活在什么地方,我得学习后面的知识才能了解。暂且认为这个f指针存活在一个独立区域,只有在该函数内部去能调用它吧。但是它有一个别名,就是factorial 啦,所以我们可以在全局作用域下去调用该函数。
闭包
- 不要被名字诱惑了。闭包不是什么区域,也不是形容词,它的本质是一个拥有特殊性质的函数。它是一个函数!它是一个函数!它是一个函数!特殊性质是指:该函数有权访问另外一个函数作用域中的变量。创建闭包的常用方式,就是在一个函数内创建另外一个函数。这不是很明显嘛,前面学习过作用域链,内部函数自然有权访问外部函数的作用域中的变量了。所以内部函数自然是个闭包。
function alertProperty(object) {
return function (propertyName) {
alert(object[propertyName]);
}
}
var obj = {
name: "hange",
age: 23
}
alertProperty(obj)("name");//hange
alertProperty(obj)("age");//23
- 这个例子中,在alertProperty方法内部,返回了一个匿名函数,该匿名函数访问了alertProperty作用域中的object属性,结果也正确执行了。这说明该匿名函数的确是一个闭包。虽然说这样看起来没有什么问题,但是细细想想,alertProperty(obj)不过是返回了一个匿名函数,再调用这个匿名函数,为什么它还能请求到之前的object呢?不应该是调用完了alertProperty(obj),obj这个属性就应该被丢了吗?带着这个疑问,继续看下面的解析。
- 当一个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性([[scope]])。在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处在第三位……在函数执行过程中,为了读取和写入变量的值,就需要在作用域链中查找变量。下面是例子:
function compare(value1, value2) {
//省略
}
var result = compare(5, 10);
当第一次调用compare时,compare在执行时的作用域链如下:
第一层的作用域链肯定是自身拥有的arguments对象,和value1,value2参数。然后第二层就是全局作用域下定义的function compare和result。为什么会这样呢?看下面的解析:
- 后台的每个执行环境都有一个表示变量的对象——变量对象,图上最右边2个object区域就是变量对象了。全局环境的变量对象是始终存在的。而像compare()函数这样的局部环境的变量对象,则只会在函数执行的过程中存在。在创建compare函数时,会创建一个预先包含全局变量对象的作用域链,也就是说函数的Scope Chain中的已经指向了全局变量对象了。当在调用compare函数时,会先为函数创建一个执行环境,然后通过复制函数的[[scope]]属性中的对象构建起执行环境的作用域链,也就是说此时执行环境复制了函数的Scope Chain,体现在此时有了0指针指向了全局变量对象。然后,又有一个新的活动对象被创建并被推入执行环境作用域的前端,也就是说原来的0指针被改成了1指针,0指针指向了执行环境的变量对象。
- 上面重新回味了一下作用域链的原理。一般情况下,局部环境的变量对象会在函数执行结束后销毁,全局作用域自然会继续保存。但是,这种情况对于闭包来说情况会有所不同。
- 定义在外部函数中的内部函数会将外部函数的活动对象加入到它的作用域链中。这里就很好理解了。假设f1内部定义并且返回f2。这里的意思是说,在定义的时候。f1的[[scope]]包含了全局活动对象。f2的[[scope]]包含了f1的活动对象和全局对象。那么在执行过程中,f2除了往[[scope]]中加入执行环境的活动对象的指针,还要加入f1的活动对象和全局活动对象的指针。这就是一条作用域链。
- 假设我们通过var f2name = f1();获得内部函数的引用,此时f1已经执行完毕了,那么f1的活动对象是否会释放呢?答案自然是不会,f1执行完毕只会切断指向活动对象的指针。但是此时定义了f2函数,前面说了f2函数自然拥有指向f1活动对象的指针,并且这个f2函数还赋给了f2name,也就是说,在f2name被清空前(f2name = null;),f1活动对象还是有被引用的,所以垃圾回收器不会回收这块内存。那自然而然f2name就可以请求到f1的活动对象了。
- 闭包的缺点就是会占用更多的内存。因为你每调用一次f1赋给新的变量名,都会各自保存调用f1时留下的活动对象的指针。很明显这样很占内存~
闭包与变量
- 作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得外部函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
var funcs = createFunctions();
//every function outputs 10
for (var i=0; i < funcs.length; i++){
document.write(funcs[i]() + "<br />");
}
- 一眼看去,会以为这个function数组每个函数会返回相应的下标值,但是执行了后却发现全都是10。其实也很好理解,每个result[index]都是一个函数,但是他们返回的是i不是具体的值。等到createFunctions执行结束后,createFunctions的执行环境里面会有一个i变量,内容为10。i不会被销毁,因为result也引用了i。所以执行的时候,先搜索自己的作用域发现没有i,再去搜索createFunctions的作用域发现了10。
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
- 利用上面这段代码就可以解决这个问题。这里立刻执行一个匿名函数,将当前i值传进去。这样就相当于在外部函数和内部函数中间又加了一层内部函数(多加了个闭包),而这个内部函数保存了正确的num值(对应于i)。
关于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"
- 为什么会输出全局作用域中的name而不是object中的name呢。这是因为执行一个函数开始时,就会保存this和arguments到变量对象中。当执行到getNameFunc时,this的确是object。但是再执行内部的匿名函数时,此时的this就是window了。下面的代码在外部函数中保存this到that变量中,这样闭包就可以通过that去访问外部函数的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"
- 再来看下面的例子:
var name = "The Window";
var object = {
name : "My Object",
getName: function(){
return this.name;
}
};
alert(object.getName()); //"My Object"
alert((object.getName)()); //"My Object"
alert((object.getName = object.getName)()); //"The Window" in non-strict mode
- 第一个结果是显然的。当执行object.getName()时,getName作为object的一个方法去调用,this指向object。当执行(object.getName)()时,(object.getName)也就是创建一个this指向object的方法,再去执行它,也能达到同样的效果。但是第三个就不一样的。第三个是一个赋值语句,赋值语句的效果只是令一个值等于另外一个,在这个阶段是没有维持this的。
内存泄露
- IE9之前的版本对js对象和COM对象使用不同的垃圾回收机制,因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该对象无法被销毁。
function assignHandle(){
var element = document.getElementById("eid");
element.onclick = function() {
alert(element.id);
};
}
- 以上代码创建了一个作为element元素事件的闭包,而这个闭包又创建了一个循环引用(当成一直存在吧,第十三章详解)。由于匿名函数保存了一个对assignHandle()的活动对象的引用,因此就导致无法减少element的引用数。只要匿名函数在,element的引用数至少为1。因此此处就有内存泄露。利用以下代码进行更正:
function assignHandle(){
var element = document.getElementById("eid");
var id = element.id;
element.onclick = function() {
alert(id);
};
element = null;
}
- 通过这个方法就可以将element的引用数降至0,而id是js对象,会被正常回收。