JavaScript高级程序设计 函数表达式
函数表达式
函数定义的两种方式:
- 函数声明(函数声明提升,非标准name属性可访问给函数指定的名字)
函数声明提升:执行代码前先读取函数声明
function functionName(arg0, arg1, arg2){ //函数体 }
- 函数表达式(name属性为空字符串,匿名函数)
var functionName = function(arg0, arg1, arg2){ //函数体 }; //注意这个分号
与if···else···语句结合使用只能用函数表达式(理解函数声明提升的关键--函数声明和函数表达式的区别)
7.1 递归
递归函数:一个函数通过名字调用自身
递归函数调用容易出问题:这里用一个最简单的阶乘函数来表示
function factorial(num){ if (num <= 1){ return 1; }else{ return num * factorial(num-1); } }
使用下面的代码会让他出错:
var anotherFactorial = factorial; factorial = null; alert(anotherFactorial(4)); //出错!
这里的anotherFactorial是factorial的一个副本,但factorial函数内仍在调用factorial函数,但此时的factorial函数已经被置空。所以这里会出现因为命名问题而产生的错误。
解决方法:
- arguments.callee 指向正在执行函数的指针(避免直接写明函数名带来的问题) 但严格模式下不能通过脚本访问
- 命名函数表达式
var factorial = (function f(num){ if(num <= 1){ return 1; }else{ return num * f(num-1); } });
这种方法在严格模式下也能使用
7.2 闭包
闭包:有权访问另一个函数作用域中变量的函数 (常见情况就是在一个函数内部创建另一个函数)
之所以内部函数可以访问到外部函数的变量,是因为内部函数的作用域链中包含了外部函数的作用域 => 函数第一次被调用的时候发生了什么?
当某个函数第一次被调用时,会创建一个执行环境和相应的作用域链,并把作用域链赋值给一个特殊的内部属性([[Scope]])。然后再用this,arguments和其他命名参数来初始化函数的活动对象。在作用域链中,外部函数的活动对象始终处于第二位。
执行环境有一个与之关联的变量对象,环境中有权访问的所有变量和函数都在这个变量对象中。为了保证对这些变量对象的有序访问,出现了作用域链。作用域链从自身的环境的活动对象开始,一直链接到全局环境的活动对象。
当一个函数创建时,会创建一个预先包含全局变量对象的作用域链保存在[[Scope]]属性中。在调用函数时,会为函数创建一个执行环境,通过复制[[Scope]]中的对象构建起执行环境的作用域链。之后每有一个活动对象被创建就被推入作用域链的前端。(是引用而不是变量对象本身)
函数执行完毕,局部变量就会被销毁,内存中仅保留全局作用域。
闭包的特殊性:对于闭包,内部函数被单独调用时,作用域链会包括外部函数的执行环境。所以外部函数执行结束后,其作用域链会被销毁,但其活动对象仍留在内存中。直到其内部函数被销毁后才会被销毁。
//闭包 function createComparisonFunction(propertyName){ return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[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: "Grey"}); //删除对函数的引用,以释放内存 compareNames = null;
过度使用闭包会造成内存占用过多,慎重使用。
7.2.1 闭包与变量
作用域链配置机制的副作用:闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){ var result = new Array(); for (var i = 0; i < 10; i++){ result[i] = function(){ return i; }; } return result; }
function createFunctions(){ var result = new Array(); for (var i = 0; i < 10; i++){ result[i] = function(num){ return function(){ return num; } }(i); } return result; }
第一种情况下,数组每一次都返回10。因为每个函数的作用域链都保存着外部函数的活动对象,所以引用的都是同一个变量i,外部函数返回后,变量i的值是10,所以每一个函数内部的i值都是10。
第二种情况可以符合预期。这里没有直接把闭包赋值给数组,而是定义了一个匿名函数,把实时的i值赋给num。
7.2.2 关于this对象
匿名函数的执行环境具有全局性,其this对象通常指向window。
var name = "The Window"; var object = { name: "My Object", getNameFunc: function(){ return function(){ return this.name; }; } }; alert(object.getNameFunc()()); //"The Window"
内部函数搜索this,arguments(调用时会自动获取)时会到其活动对象为止,不可能访问外部函数中的这两个变量。
解决方法:
var name = "The Window"; var object = { name: "My Object", getNameFunc: function(){ var that = this; return function(){ return that.name; }; } }; alert(object.getNameFunc()()): //"My Object"
7.2.3 内存泄漏
如果闭包的作用域链中保存了一个HTML元素,那么该元素无法被销毁。
function assignHandler(){ var element = document.getElementById("someElement"); element.onclick = function(){ alert(element.id); }; }
由于在匿名函数中调用了包含函数的活动变量element,所以只要匿名函数还存在,element的引用数至少是1(参考垃圾清理机制),内存永远不会被回收。
解决方法:
function assignHandler(){ var element = document.getElementById("someElement"); var id = element.id; element.onclick = function(){ alert(id); }; element = null; }
在闭包中删除了对HTML元素的循环引用(id)。但还要element变量置为null,因为闭包会引用整个包含函数的活动变量,包括element(即使不直接引用)。
7.3 模仿块级作用域
JavaScript没有块级作用域的概念(比如for循环中的i,在for循环之外也可以被访问到)。对已声明的变量重复声明会被忽略,但初始化会改变值。
解决方法:通过匿名函数来模拟块级作用域
(function(){ //块级作用域 })():
实际上是一个函数表达式(不能去掉function外层的圆括号,因为JavaScript将function关键字当作一个函数声明的开始,然而函数声明后是不可以跟圆括号的,加这层圆括号可以将函数声明转换为函数表达式)
作用:临时需要一些变量,限制向全局作用域添加过多的变量和函数。
这样可以减少闭包占用的内存问题,因为没有指向匿名函数引用。只要函数执行完毕,作用域链就可以被销毁了。
7.4 私有变量
私有变量:在函数中定义的变量(不能在函数外部访问)
创建访问私有变量的公有方法:通过闭包(可以通过自己的作用域链访问这些私有变量)
特权方法:有权访问私有变量和私有函数的公有方法
在对象上创建特权方法的方式:构造函数中定义特权方法
function MyObject(){ //私有变量和私有函数 var privateVariable = 10; function privateFunction(){ return false; } //特权方法 this.publicMethod = function(){ privateVariable++; return privateFunction(); }; }
除了使用publicMethod()之外无法直接访问私有变量。
还可以利用这个特性来隐藏那些不应该被直接修改的数据。
7.4.1 静态私有变量
在私有作用域中定义变量或函数,从而创建特权方法。
(function(){ //私有变量和私有函数 var privateVariable = 10; function privateFunction(){ return false; } //构造函数,全局变量(没有用var) MyObject = function(){ }; //公有/特权方法 MyObject.prototype.publicMethod = function(){ privateVariable++; return privateFunction(); }; })();
公有方法是在原型上定义的(原型模式),使用的函数表达式而不是函数声明(函数声明只能创建局部函数)。
与构造函数法的区别是,私有变量和函数是由实例共享的(在原型上定义的)。
这种情况下,私有变量就成为了静态的、所有实例共享的。不足之处在于,多查找作用域链的一个层次就会在一定程度上影响查找速度。
7.4.2 模块模式
模块模式:为单例创建私有变量和特权方法(单例:只有一个实例的对象) => 用对象字面量的方法来创建
使用返回对象的匿名函数的方法。可以应用在需要对单例进行某些初始化,但是又要维护其私有变量的时候。
7.4.3 增强的模块模式
单例必须是某种类型的实例,必须添加某些属性和方法对其增强的情况。