本章内容
1.函数表达式的特征
2.使用函数实现递归
3.使用闭包定义私有变量
定义函数的方法:两种——函数声明,函数表达式。
函数声明的语法:关键字+函数名
非标准的name属性
函数声明提升——在执行代码之前会先读取函数声明。意味着可以把函数声明放在调用它的语句后面。
函数表达式的最常见的语法形式:
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量functionName。这种情况下创建的函数叫做匿名函数(拉姆达函数),因为function关键字后面没有标识符。匿名函数的name属性是空字符串。
函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误:
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。例如,执行以下代码的结果会让人意想不到。
具体不能执行的原因见书P176(表面上看,在condition为true时,使用一个sayHi();否则,就使用另一个定义。实际上,这在ECMAScript(即通常称为JavaScript)中属于无效语法,JavaScript引擎会尝试修正错误,将其转换为合理的状态。但问题是浏览器尝试修正错误的做法并不一致。),但是如果使用函数表达式,就没问题了。
这个例子没有什么意外,不同的函数会根据condition被赋值给sayHi。
注:函数声明与函数表达式之间的区别
博客连接:http://www.cnblogs.com/Saints/p/5991186.html
能够创建函数再赋值给变量,也就能够把函数作为其他函数的值返回。例如第五章createComparisonFunction()函数:
createComparisonFunction()就返回了一个匿名函数。返回的函数可能会被赋值给一个变量,或者以其他方式来调用;不过,在createComparisonFunction()函数内部,它是匿名的。再把函数当值来使用的情况下,都可以使用匿名函数。不过,这不是匿名函数的唯一用途。
注:匿名函数的用途。
7.1 递归
递归函数是在一个函数通过名字调用自身的情况下构成的,如下:
这是一个经典的递归阶乘函数。虽然这个函数看起来没什么问题,但下面的代码却可能导致它出错:
以上代码把factorial()函数保存在变量anotherFactorial中,然后将factorial变量设置为null,结果指向原始函数的引用只剩下一个。但接下来调用anotherFactorial()函数时,由于必须执行factorial(),而factorial已经不再是函数,所以就会导致错误。在这种情况下,使用arguments.callee可以解决这个问题。arguments.callee是一个指向正在执行函数的指针,可以用它实现定义函数的递归调用。
严格模式下,不能通过脚本访问arguments.callee,访问这个属性会导致错误。可以用命名函数表达式来达成相同的结果,如下:
以上创建了一个名为f()的命名函数表达式,然后将它赋值给变量factorial。即便把函数的名字赋值给另一个变量,函数的名字f 仍然有效,所以递归调用照样能正常完成。严格模式和非严格模式下都行得通。
7.2 闭包
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数的内部创建另一个函数。书P178--181.
匿名函数与闭包的区别,http://www.cnblogs.com/moveofgod/archive/2012/09/21/2697440.html
。按书讲。
7.2.1 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中 任何变量的最后一个值.。闭包所保存的是整个变量对象,而不是某个特殊的变量。如下:
表面上看,似乎每个函数都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1.但实际上,每个函数都返回10.因为每个函数的作用域链中都保存着creatFunctions()函数的活动对象,所以它们引用的是同一个变量i。当creatFunctions()函数返回后,变量i的值都是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10.但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下:
(解释见书P182)
7.2.2 关于this对象
在闭包中使用this对象也可能会导致一些问题。this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被当做某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window(当然,在通过call()或apply()改变函数执行环境的情况下,this就会指向其他对象)。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。如例子:
(返回字符串“The Window”,为什么不是“My Object”)(但我用的sublime是严格模式吧不可以)
因为每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问到该对象了。如下:
不同之处:在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量。而在定义闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了“My Object”
在几种特殊情况下,this的值可能会意外的改变。例如:
这里的getName()方法只简单的返回this.name的值。以下是几种调用object.getName()的方式以及各自的结果:
object.getName(); //"My Object"
(object.getName)();//"My Object"
(object.getName=object.getName)();//"The Window",在严格模式下
第一行代码跟平常一样调用了object.getName和object.getName(),返回的是“My Object”,因为this.name就是object.name。第二行代码在调用这个方法之前先给它加上了括号。虽然加上括号后,就好像是在引用一个函数,但this的值得到了维持,因为object.getName和(object.getName)的定义是相同的。第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式是函数本身,所以this的值不能得到维持,结果就返回了“The Window”。
7.2.3内存泄漏(按照书上的意思是内存被占用,得不到回收)
具体来说,如果闭包的作用域链中保存着一个HTML元素,那么意味着该元素将会无法被销毁。如下:
以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。稍微改写代码解决,如下:
通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但这样还不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。。。这样就能够解除对DOM对象的引用,顺利的减少其引用数,确保正常回收其占用的内存。
7.3模仿块级作用域
具体书P184--186
用作块级作用域(私有作用域)的匿名函数语法如下:
(function(){
//这里是块级作用域
})();
以上代码定义立即调用了一个匿名函数。将函数声明 包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。
区别下面这段代码:
function(){
//这里是块级作用域
}();//出错!!!
因为JavaScript将function关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。然后,函数表达式后面可以跟圆括号。如上上代码。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。例如:
在这个重写后的outputNumbers()函数中,我们在for循环外部插入了一个私有作用域。在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i 只能在循环中使用,使用后即被销毁。而在私有作用域中能够访问变量count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量。
这种技术常用在全局变量中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,应尽量减少向全局作用域中添加变量和函数,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,即可以使用自己的变量,又不必担心搞乱全局作用域。例如:
把这段代码放在全局作用域中,可以用来确定哪一天是1月1日;如果到了这一天,就会向用户显示一条祝贺新年的消息。其中的变量now现在是匿名函数中的局部变量,而不必在全局变量中创建它。这种做法可以减少闭包占用内存的问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
7.4 私有变量
(严格来讲,JavaScript中,没有私有成员的概念;所有对象属性都是公有的。不过,有一个私有变量的概念)任何在函数中的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。如例:
在这个函数内部,有3个私有变量:num1、num2和sum。在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。
第一种是在构造函数中定义特权方法,基本模式如下:
这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权的方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。对这个例子而言,变量privateVariable和函数privateFunction()只能通过特权方法publicMethod()来访问。在创建MyObject的实例后,除了使用publicMethod()这一个途径外,没有任何办法可以直接访问privateVariable和privateFunction()。
利用私有和特权成员,可以隐藏那些不应该直接被修改的数据。例如:
以上代码的构造函数中定义了两个特权方法:getName()和setName()。这两个方法都可以在构造函数外部使用,而且都有权访问私有变量name。但在Person构造函数外部,没有任何办法访问name。由于这两个方法是在构造函数内部定义的,他们作为闭包能够通过作用域链访问name。私有变量name在Person的每一个实例中都不相同,因为每次调用构造函数都会重新创建这两个方法。不过,在构造函数中定义特权方法也有一个缺点,那就是必须使用构造函数模式来达到这个目的。而构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免。
...7.4.1静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下:
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及其相应方法。在私有作用域中,首先定义了私有函数和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的。。要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数。(包括我们也没有在声明MyObject时使用关键字var。)
记住:初始化未经声明的变量,总是会创建一个全局变量。因此MyObject就成了一个全局变量,能够在私有作用域之外被访问到
。但是,在严格模式下给未经声明的变量赋值会导致错误。
与在构造函数中定义特权方法的区别:就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。如下:
这个例子中的Person构造函数与getName()和setName()方法一样,都有权访问私有变量name。在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用setName()hi影响所有实例。而调用setName()或新建一个Person实例都会赋予name属性一个新值。结果就是所有实例都会返回相同的值。
以这种方式创建的静态私有变量会因为使用原型而增加代码复用,但每个实例都没有自己的私有变量。
7.4.2 模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。而这里说的模块模式则是为单例创建私有变量和特权方法。所谓单例指的就是只有一个实例的对象。JavaScript是按照以对象字面量(
表示如何表达这个值,一般除去表达式,给变量赋值时,等号右边都可以认为是字面量。)的方式来创建单例对象的。
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
这个模块模式使用了一个返回对象的匿名函数。在这个函数内部,首先定义了四哟偶变量和函数。然后将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又要维护其私有变量时是非常有用的,例如:
这个例子简单的创建了一个用于管理组件的application对象。首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例。而返回对象的getComponentCount()和registerComponent()方法都是有权访问数组components'的特权方法。前者只是返回已注册的组件数目,后者用于注册新组件。
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以用模块模式。
7.4.3 增强的模块模式
增强的模块模式,即在返回对象之前加入对其增强的代码。适合单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。如例:
如果前面演示模块模式的例子中的application对象必须是BaseComponent的实例,可以:
首先也是像前面例子一样定义了私有变量。主要的不同之处在于命名变量app的创建过程,因为它必须是BaseComponent的实例。这个实例实际上是application对象的局部变量版。此后,我们又为app对象添加了能够访问私有变量的公有方法。最后一步是返回app对象,结果仍然是将它赋值给全局变量application。
7.5小结(书P192)