《JavaScript高级程序设计》Chapter 7 函数表达式
了解:
函数的定义
递归(命名表达式,callee) go
闭包(重点,闭包中的作用域,this值块级作用域,私有变量)go
- 定义函数的两种方式:函数声明(函数声明提升)、函数表达式(“表达式”,有多种不同的语法形式)
- var functionName = function(arg0, arg1, arg2){dosomething();};
//匿名函数or拉姆达函数
//其name属性为空字符串
- 函数表达式可以:
- 创建函数再赋值给变量。赋引用值。可以在if-else语句中赋不同的函数,而函数声明则不可以。不同浏览器对函数声明的if-else语句的处理不同,是“危险”的
- 将函数作为其他函数的值返回 --->递归(返回的是匿名函数)
递归
- 匿名函数:name属性值为空字符串,上面提到可以作为一个值返回(这是其中一个作用)
- 递归:函数通过名字调用自身
- --->通过arguments.callee调用更安全
- 严格模式下无法通过脚本访问arguments.callee,则可以用命名表达式来达成相同结果,如下
-
var factorial =(function f(num){ if(num <=1){ return 1; } else { return num * f(num -1); } });
-
- 命名函数表达式:可以像函数声明一样为函数表达式指定一个名字,但这并不会使函数表达式成为函数声明。命名函数表达式的名字不会进入名字空间(只在内部有效),也不会被提升。
-
var baz = function() { console.log("bar2"); }; //匿名函数定义的函数表达式 var baz = function e() { console.log("bar2"); }; //命名函数表达式
闭包
- 闭包:有权访问另一个(外部)函数作用域中的变量的函数。因为这个函数的作用域链中包括另一个函数的作用域。
- 执行函数使用“()”,不执行不用。
- 如何创建作用域链和作用域链的作用:
- [[Scope]]:函数第一次被调用的时候,会创建一个执行环境以及相应的作用域链,并把这个作用域链赋给内部属性[[Scope]],即[[Scope]]指向存放这个作用域链的位置。然后,this、arguments等其他命名参数的值初始化函数的活动对象。并且,正如之前所说,外部函数的活动对象始终处于第二位、外外部第三位。。。直到作用域链的终点的全局执行环境。
- 查找变量:在函数执行的过程中,为读取和写入变量的值,就要在作用域链中查找变量。
- 以下面这个函数为例。全局环境的变量对象(Global variable object)始终存在,而compare()函数这样的局部环境的变量对象(compare() activation object)只在函数执行的时候创建。且第一次调用compare()函数的时候,同时会创建一个作用域链(Scope chain),这个作用域链显然由内部的[[Scope]]属性(scope chain)引用。并且,显然这个作用域链本质上是指向变量对象的指针列表,且按照ES规则安排了变量等的访问顺序(局部->外部->外外部->全局,表现在0->1---->)。执行完毕后,局部及活动对象会被销毁(compare execution context/Scope Chain(这一条)/compare() activation object),而闭包的情况有所不同。
-
function compare (value1, value2){ if(value1< value2){ return -1; } else if (value1 > value2) { return 1; } else return 0; }
- 闭包的作用域情况:1、闭包包含外部函数的活动对象 2、闭包被返回后,其外部函数的作用域链会被销毁,但是外部的活动对象不会,因为按照前一条所说,闭包的作用域链仍然在引用这个活动对象。
- 注意下面的代码。如图:当闭包(匿名函数)被返回且赋给一个变量的时候,红框内的部分随着外部函数createComparisonFunction执行完毕被销毁,但是其活动对象(createComparisonFunction()activation object)因为仍被匿名对象的作用域链引用而保留。而最终当compareNames = null,即匿名对象执行完毕后,蓝色部分被销毁。最终只保留了全局活动对象(全局作用域)。
-
createComparisonFunction(propertyName){ return function(object1, object2){ var value1 = object1[propertyName]; var value2 = object2[propertyName]; if(value1 < value 2) return -1; else if (value1 >value2) return 1; else return 0; }; } var compareNames = createComparisonFucntion("name"); //返回闭包 var result = compareNames({name : "Nicholas"}, { name : "Greg"}); //仍然可以访问外部对象propertyName compareNames = null; //手动清理,释放内存 这个时候,销毁匿名对象的作用域链,清理其他作用域(除了全局作用域)
- 闭包占用内存略多,慎重使用。
- 闭包与变量,注意分析下面两端代码和图例:
- 代码一(注意到:由于10个闭包包含的createFunctions活动对象的i实际上都指向同一个值,所以最后返回的都是“最后一个值”,即10)
-
function createFunctions(){ var result = new Array(); for(var i =0; i<10; i++){ result[i] = function(){ return i }; } return result; //实际上,最后返回的数组,每一个返回的i的值都是10 }
- 代码二(每一次循环都会创建一个立即执行的中间匿名函数,并把当前i的副本传给num,并以此num来创建想要返回的目标匿名函数。如图是i=5的实例。实际上,在循环了10次后,为了实现我们的目的,创建了20个匿名函数(10个中间函数,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;//存储的对应函数返回的分别为0到9; }
-
- (闭包中的)this值:
- 需要清楚的一点就是闭包中的this值一般指向window对象。这是因为,内部函数在搜索this和arguments这两个特殊对象的时候只会搜索到其活动对象为止(不会再向上搜索),所以会赋this为windows。
- 当然,如果想让闭包访问到其外部函数的环境对象,需要将这个this保存到创建的活动对象中让其访问到。
- 注意下面代码中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" //在当前环境中将引用赋给新的变量后执行这个变量,this指向了window //在当前环境下,将这个函数赋给了新变量,因此this不能维持
- 内存泄漏:JScript对象和COM对象的垃圾收集机制在IE9前不同,后者会引起循环引用。在闭包中同样需要考虑到避免循环引用的情况,具体做法就是手动解除应用,必要的时候,将对象值置为null
- 模仿块级作用域(私有作用域-->临时需要一些变量的时候)
- 演变:functionName(count); --->函数表达式(count)--->(function(){//...})(count);
- 匿名函数执行完毕之后相关活动对象及作用域会销毁。
- 常用于不想添加全局变量的情况。实际上,应该避免向添加过多全局变量,易引起命名冲突。
- 这种模仿方式不会占用内存,因为没有指向匿名函数的引用。函数执行完毕即可销毁。(立即执行)。然而多查找一个作用域链就会影响到查找速度,这是不足。
- 私有变量
- JS没有私有成员的概念。因为所有对象属性都是共有的,但是又私有变量:函数中定义的变量,包括:函数参数、局部变量和内部定义的其他函数--这些都不能在函数外部访问。
- 创建用于访问私有变量的公有方法(特权方法):闭包可以访问外部的变量和方法。
- 创建特权方法的方法:利用构造函数、静态私有变量,模块模式,增强的模块模式
-
方法 代码 说明 利用构造函数 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("Tim");alert(person.getName); // "Tim"- 之前见惯了的写法,在知道闭包之后不,赋予了新的概念。- 对于私有变量name,不能直接访问,只能通过这两个特权方法进行访问和处理。- 缺点:结合创建对象的那部分知识,在构造函数中保存的方法和属性属于实例方法和属性,每个实例都不同的。因此通过构造函数创建的特权方法每次调用构造函数的时候都会创建新的,浪费空间。静态私有变量(在私有作用域中定义私有变量或者函数) (function(){var name = "":Person = function(value){name = value;};Person.prototype.getName = function(){return name;};Person.prototype.setName = function(value){name = value;};})();思想:1、用原型来存储共有的特权方法,以解决构造函数的缺点。2、考虑到时“特权方法”,需要访问私有变量或者方法,所以对原型方法的定义需要放在私有作用域。3、注意到构造函数Person()用函数表达式以定义成全局函数,这是因为需要在全局作用域中调用他以创建对象。缺点:显然,实例的特权方法继承自原型对象,是共有方法,指向同一个name变量,所以在一个实例中改变name会影响到所有的实例的name变量。模块模式(为单例创建私有变量和特权方法):单例,只有一个实例的对象,常用字面量的方式创建。 var application = function(){//私有变量和方法var components = new Array();components.push(new BaseComponent());//初始化。//公共部分,返回的对象return{getComponentCount: function(){return components.length;},regisiterComponent: function(component){if(typeof component == "object"){components.push(component);}}};}();//立即执行,并返回一个对象前面两个方法面向自定义类型,这个方法针对单例。用于对单例进行某些初始化,同时又需要维护其私有变量。缺点:都是Object的实例,不能检测是否属于其他的对象类型,或者是其他对象的实例。(然而实际应用的时候,并不需要时其他什么类型的实例)。改进这点需要增强的模块模式。增强的模块模式 var application = function(){//私有变量和方法var components = new Array();components.push(new BaseComponent());//初始化。//公共部分var app = new BaseComponent();//是BaseComponent的实app.getComponentCount=function(){return components.length;};app.regisiterComponent=function(component){if(typeof component == "object"){components.push(component);}};return app; //返回的对象}();//立即执行,并返回一个对象在前者的基础上,返回对象前加入增强代码,确保是某种类型的实例。