1.递归
函数通过名字调用自身。function factorial(num){ if(num <= 1){ return 1; }else{ return num*factorial(num-1); } } var anotherFactorial = factorial; var factorial = null; //factorial赋值null后会导致递归调用出错(factorial is not a function) console.log(anotherFactorial(4));使用callee属性解决(指向拥有arguments属性的函数,该函数),严格模式下无法使用function factorial(num){ if(num <= 1){ return 1; }else{ return num*arguments.callee(num-1); } }使用命名函数表达式来实现
function factorial(function f()){ if(num <= 1){ return 1; }else{ return num*f(num-1); } }创建一个命名为f的函数表达式,然后赋值给变量factorial
2.闭包
指有权访问另一个函数作用域中的变量的函数,创建闭包常见方式:在一个函数内部创建另一个函数function createComparisonFunction(propertyName){ //返回一个匿名函数 return function(obj1,obj2){ var value1 = obj1[propertyName]; var value2= obj2[propertyName]; if(value1<value2){ return -1; }else if(value1>value2){ return 1; }else{ return 0; } }; }在匿名函数中访问了外部函数的propertyName参数,即使这个匿名函数被返回了,并且是在其他地方被调用,仍然可以访问到该参数。因为内部函数的作用域链包含了createComarisonFunction()的作用域。
当某个函数第一次被调用时,会创建一个执行环境(execution context),及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]]),然后使用this,arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,直到作用域链的终点--全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量
function compare(value1,value2){ if(value1<value2){ return -1; return 1; }else { return 0; } var result = compare(1,2);//在全局作用域中调用compare()函数第一次调用compare()时,会创建一个包含this,arguments,value1和value2的活动对象。全局执行环境的变量对象包含this,arguments和compare。在compare()执行环境的作用域链中处于第二位。
如上图所示: 作用域链本质上是一个指向变量对象的指针列表,只引用但不实际包含变量对象。无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名称的变量。一般当函数执行完后,局部活动对象就被销毁了,内存中仅保存全局作用域(全局执行环境的变量对象)。闭包除外!
在函数内部定义的函数会将外部函数的活动对象添加到它的作用域链中。
因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。
//创建函数 var compare = createComparisonFunction("name"); //调用函数 var result = compare({name:"Ethan"},{name:"Zhangsan"}); //解除对匿名函数的引用(以便垃圾回收,释放内存) compare = null;
闭包与变量
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议只在绝对必要时再考虑使用闭包。问题:闭包只能取得包含函数中任何变量的最后一个值,闭包所保存的是整个变量对象,而不是某个特殊的变量。
function createFunction(){ var result = new Array(); for(var i=0;i<10;i++){ result[i] = function(){//声明10个函数 return i; } } return result; } var functionArr = createFunction(); functionArr.forEach(function(item,index){ console.log(item()); // 10 输出十次,每次都是同一个结果 });createFunction()函数返回一个函数数组,每个函数作用域链中都保存着createFunction()函数的活动对象,所以它们引用的是同一个变量i。当createFunction()函数返回后,变量i的值是10,所以每个函数内部i的值都是10。通过创建另一个匿名函数强制让闭包行为符合预期。
function createFunction(){ var result = new Array(); for(var i=0;i<10;i++){ result[i] = function(num){ //创建并立刻执行当前匿名函数 return function(){//返回一个函数 return num; } }(i); } return result; } var functionArr = createFunction(); functionArr.forEach(function(item,index){ console.log(item()); // 10 输出十次,每次都是同一个结果 });定义一个匿名函数,并将立即执行该匿名函数的结果赋给数组。匿名函数有一个参数num。在调用每个匿名函数时,都传入参数变量i。由于函数参数(基本类型)是按值传递的,所以会将变量i的当前值复制给参数num。在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样,result数组中的每个函数都有自己num变量的一个副本,因此返回的就是各自不同的数值了。this对象
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。匿名的执行环境具有全局性,this对象通常指向window。
var name = "Window"; var object = { name : "Ethan", getNameFun:function(){ return function(){//返回一个匿名函数结果 return this.name;//this指向window } } }; console.log(object.getNameFun()()); // Window
每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止(内部函数自己的this对象),因此永远不可能直接访问外部函数中的这两个变量,但是把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = "Window"; var object = { name : "Ethan", getNameFun:function(){ var outThis = this;//将外部函数的this对象赋值给匿名函数能访问到的变量 return function(){//返回一个匿名函数结果 return outThis.name;//访问的外部函数的name属性 } } }; console.log(object.getNameFun()()); // Ethan在定义匿名函数前将this赋值给outThis变量,定义闭包后,闭包也可以访问这个变量。即使函数返回后,outThis也引用着object。
因此: 如果想访问外部函数作用域中的this和arguments对象,必须将该对象的引用保存到另一个闭包能访问的变量中。
特殊情况:var name = "window"; var object = { name:"Ethan", getName:function(){ return this.name; } }; console.log(object.getName()); // Ethan (object.getName)();//Ethan (object.getName = object.getName)(); // window第三种调用方法中:先执行赋值语句,然后再调用赋值后的结果。赋值表达式的值是函数本身,this的值不能维持,所以返回window。
内存泄漏
如果闭包中保存着html元素(com对象使用的引用计数机制回收垃圾),那就以为着该元素将无法被销毁。
function assignHandler(){ var element = document.getElementById("hehe"); element.onclick = function(){ alert(element.id); } }在匿名函数中保存了对保存了对外部函数的活动对象的引用,只要匿名函数存在,element的引用数至少是1。因此它所占用的内存将无法被回收。解决方法: 手动断开对html元素的引用。
function assignHandler(){ var element = document.getElementById("hehe"); var id = element.id; element.onclick = function(){ alert(element.id); } //函数执行完后,闭包的作用域链依然会引用外部函数的整个活动对象(活动对象中有element引用)。所以需手动将html元素的引用置空。 element = null; }
3.模仿块级作用域
JS中没有块级作用域的概念。所以在块语句中定义的变量,实际上是包含在函数中而非语句中的。
for(var i=0;i<10;i++){ console.log(i); } alert(i); //10,仍然可以访问,这里i是在全局作用域中的 var i;//多次声明同一变量,JS会忽视后面的声明 alert(i);//10
使用闭包解决:
var name = "Ethan"; (function(){//匿名函数,私有作用域,执行完后变量i被销毁 for(var i=0;i<10;i++){ console.log(i); } console.log(name); //匿名函数是一个闭包,可以访问包含作用域中的变量 })(); alert(i); //报错,i未定义
()会将function(){} 由函数声明变成函数表达式,在后面加上()就会立刻调用这个匿名函数。
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
4.私有变量
一:在构造函数中定义特权方法JS中没有私有成员的概念,所有对象属性都是公有的。但是在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。有权访问私有变量和私有函数的公有方法叫做 特权方法(privileged method),有两种在对象上创建特权方法的方式。
function Person(name){ this.getName = function(){//闭包,通过作用域链可访问包含函数的name参数 return name; }; this.setName = function(value){ name = value; } } var p = new Person("Ethan"); console.log(p.getName());// Ethan p.setName("Zhangsan"); console.log(p.getName()); // Zhangsan缺点:构造函数模式,每个实例都会创建同样的一组新方法。
改进:静态私有变量,通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法
(function(){ var privateVariable = 10; //声明私有变量 function privateFunction(){//声明私有函数 return false; } // function MyObject(){ 如果使用函数声明的方式定义构造函数,就成私有函数了,无法被外部访问 // } //构造函数 MyObject = function(){//省略var,使用表达式方式给构造函数赋值,可以在全局被调用。 }; //特权方法,在构造函数原型上定义,可被多个实例对象共享 MyObject.prototype.publicMethod = function(){ privateVariable ++; return privateFunction(); } })();与在构造函数中定义的区别:私有变量和函数是所有实例共享的,特权方法在原型上定义,所有实例调用的都是同一个函数。而这个特权函数作为一个闭包,总是保存着对包含作用域的引用(外部匿名函数--私有作用域)
(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("Ethan"); console.log(person1.getName()); //Ethan person1.setName("Zhangsan"); console.log(person1.getName()); //Zhangsan var person2 = new Person("Collen"); console.log(person1.getName()); //Collen console.log(person2.getName()); //Collen
name成了一个静态,由所有实例共享的属性。在一个实例上调用setName()会影响所有的实例。
多查找作用域链中的一个层次,就会在一定程度上影响查找速度。正式使用闭包和私有变量的一个明显不足之处。
二:模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。而模块模式则是为单例创建私有变量和特权方法。
var singleton = function(){ //私有变量和私有函数 var privateVariable = 10; function privateFunction(){ return false; } //特权方法和属性 return {//返回一个对象 publicVariable : 20, publicFunction:function(){ privateVariable++; //操作私有变量 return privateFunction();//返回调用私有方法的结果,false } }; }(); console.log(singleton.publicFunction()); //false console.log(singleton.publicVariable); //20如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,就可以使用模块模式。这种模式创建的每个单例都是Object的实例增强的模块模式
这种模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和方法对其加以增强的情况。
var application = function(){ //私有变量和函数 var components = new Array(); //初始化 components.push(new BaseComponent()); //创建application的一个局部副本 var app = new BaseComponent(); //公共接口 app.getComponentCount = function(){ return components.length; }; app.registerComponent = function(component){ if(typeof component == "object"){ components.push(component); } } return app; }();
在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量。
注意:创建闭包必须维护额外的作用域,所以过度使用它可能会占用大量内存