JavaScript
读书笔记
备注:为了防止标签错乱,现在规定,标题用
##
,一级标题是###
, 二级是####
, 三级是#####
。
面向对象的程序设计
内容(3点):
函数表达式的特征
使用函数实现递归
使用闭包定义私有变量
函数表达式是Javascript
中的一个既强大又容易令人困惑的特性。前面说过,定义函数的方式有两种:一种是函数声明;另一种就是函数表达式。函数声明的语法是这样的。
function functionName(arg0 , arg1 , arg2){
// 函数体
}
首先是function
关键字,然后是函数的名字,这就是指定函数名的方式。Firefox
,Safari
,Chrome
和Opera
都给函数定义了一个非标准的name
属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于跟在function
关键字后面的标识符。
// 只在上述浏览器生效
console.log(functionName.name); // 'functionName'
关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting
),意思是在执行代码之前会先读取函数声明。这久意味着可以把函数声明放在调用它的语句后面。
sayHi();
function sayHi(){
console.log('hi');
}
这个例子不会抛出错误,因为在代码执行之前就会先读取函数声明。
第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种形式。
var functionName = function(arg0 , arg1 , arg2){
// 函数体
};
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量functionName
。这种情况下创建的函数叫做匿名函数(anonymous function),因为function
关键字后面没有标识符。匿名函数的name
属性是空字符串。
函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误。
sayHi(); // 错误:函数还不存在
var sayHi = function(){
console.log('hi');
}
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。例如,执行以下代码的结果可能会让人意想不到。
// 不要这样做
if (condition){
funcion sayHi(){
console.log('hi');
}
}else {
funcion sayHi(){
console.log('Yo!');
}
}
由于函数声明提升,这个代码在不同的浏览器会出现不同的效果。
不过,如果使用函数表达式,就没有问题。
var sayHi;
if (condition){
sayHi = function(){
console.log('hi');
}
} else {
sayHi = function() {
console.log('Yo!');
}
}
这个例子不会有什么意外,不同的函数会根据condition
被赋值给sayHi
。
能够创建函数再赋值给变量,也能够把函数作为其他函数的返回值。
function returnFunc(){
return function(){
console.log('被欲望玷污的亚瑟');
}
}
// 调用该函数
var innerF = returnFunc()
innerF(); // 调用内部函数~
1 递归
递归函数是在一个函数通过名字调用自身的情况下构成的。如下:
function factorial(num){
if (num <= 1){
return 1;
}else {
return num* factorial(num-1);
}
}
会出问题:如果函数被重新赋值
var af = factorial;
factorial = null;
console.log(af(5)); // 出错
解决方案:内部使用
arguments.callee
.
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
因为 arguments,callee
是一个指向正在执行的函数的指针。
不过严格模式下,不能通过脚本访问arguments.callee
,访问这个属性会导致错误。
改进:
var factorial = function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
};
console.log(factorial(6));
var af = factorial;
console.log(af(7)); // ok
2 闭包
有不少开发人员总是搞不清匿名函数和闭包这两个概念,因此经常混用。闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。如下:
function createComparisonFunction(prpertyName){
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;
}
}
}
注意其中的
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
这两行代码位于内部函数(一个匿名函数)中。这两行代码访问了外部函数中的变量propertyName
。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName
。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction()
的作用域。要彻底搞清楚其中的细节,必须从理解函数第一次被调用的时候都会发生什么入手。
有关如何创建作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。当某个函数第一次被调用时,会创建一个执行环境(execution context
)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[Scope]
)。然后,使用this
,arguments
和其他命名参数的值来初始化函数的活动对象(activation object
)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在函数执行的过程中,为读取和写入变量的值,就需要在作用域链中查找变量。如下:
function compare(v1,v2){
if(v1<v2) {
return -1;
}else if ( v1> v2){
return 1;
}else {
return 0;
}
}
var result = compare(5,10);
以上代码先定义了compare()
函数,然后又再全局作用域中调用了它。当第一次调用compare()
时,会创建一个包含this
,arguments
,v1
,v2
的活动对象。全局执行环境的变量对象(包含this
,result
,compare
)在compare()
执行环境的作用域链中则处于第二位。
后台的每个执行环境都有一个表示变量的对象————变量对象。全局环境的变量对象始终存在,而像compare()
函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建compare()
函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存到内部的[[Scope]]
属性中。当调用compare()
函数时,会为函数创建一个执行环境,通过复制函数的[[Scope]]
属性中对象构建起执行环境的作用域链。此后,又一个活动对象(在此作为变量对象使用)被创建并推入执行环境作用域链的前端。对于这个例子中的compare()
函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数会讲包含函数(即外部函数)的活动对象添加到它的作用域中。因此在createComparisonFunction()
函数内部定义的匿名函数的作用域链中,实际将包含外部函数createComparisonFunction()
的活动对象。
var compare = createComparisonFunction('name');
var result = compare({name:'Ann'} , {name:'Rose'});
在匿名函数从 createComparisonFunction()
中被返回后,它的作用域链被初始化为包含createComparisonFunction()
函数的活动对象和全局变量对象。这样,匿名函数就可以访问createComparisonFunction()
中定义的所有变量。更为重要的是,createComparisonFunction()
函数执行完毕后,其活动对象也不会销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()
函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()
的活动对象才会被销毁。如下:
// 创建函数
var compare = createComparisonFunction('name');
// 调用函数
var result = compare({name:'Ann'} , {name:'Rose'});
// 解除对匿名函数的引用(以便释放内存)
compare = null;
首先,创建的比较函数被保存在变量compare
中。而通过将compare
设置为null
解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也可以安全地销毁了。
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议只在绝对必要时才考虑使用闭包。虽然像
V8
等优化后的Javascript
引擎会尝试回收被闭包占用的内存,但是还是要慎重使用闭包。
2.1 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中的任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面的代码可以清晰地说明这个问题。
// 问题闭包
function createFuncArr() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i + 100;
};
}
return result;
}
var funcArr = createFuncArr();
for (var index = 0; index < funcArr.length; index++) {
console.log(funcArr[index]());
}
输出10次 110。
ps:为什么要使用索引遍历数组:javascript 四种数组遍历方法
这个函数会返回一个数组。表面上看,似乎每个函数都应该返回自己的 索引值+100
,即位置0
的函数返回0+100
,位置1
的函数返回1+100
,以此类推。但实际上,每个函数都返回10+100
。因为每个函数的作用域中都保存着createFuncArr()
函数的活动对象,所有它们引用的都是同一个变量i
。当createFuncArr()
函数返回后,变量i
的值是10
,此时每个函数都引用着保存变量i
的同一个变量对象,所以在每个函数的内部i
的值都是10。一下是一些解决方案:
// 非闭包的解决方案
function createFuncArr() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function (index) {
return index + 100;
};
}
return result;
}
var funcArr = createFuncArr();
for (var index = 0; index < funcArr.length; index++) {
console.log(funcArr[index](index));
}
这里不在内部函数中去引用外部函数的变量,而是让调用者去传递进来,这样当然就避免了这个问题。但是,如果调用者不是使用索引的方式去遍历数组,这就比较麻烦了。
// 闭包的解决方式:
function createFuncArr() {
var result = [];
for (var i = 0; i < 10; i++) {
result[i] = function (index) {
return function () {
return index + 100;
}
}(i);
}
return result;
}
var funcArr = createFuncArr();
for (var index = 0; index < funcArr.length; index++) {
console.log(funcArr[index]());
}
采用闭包的解决方式,避免了上述的问题,不需要调用者传递索引值进来了。但是要注意一下为什么这种写法可行,不会出现之前的问题。
因为第一个匿名函数是立即执行的,立即获得i
的值,在每次遍历的时候。也就每次获得了对应的索引值。然后里面的闭包,会引用这个index
,因为每个index
是不同的(本质上,是每个最里面的闭包的[Scope] chain
是不同的),所以结果也就每个都不同了。
这里没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数
index
,也就是最终的函数需要的返回值的组成部分。在调用每个匿名函数时,我们传入了变量i
。由于函数参数是按值传递的,所以就会将变量i
的当前值复制给参数num
。而这个匿名函数内部,又创建并返回了一个访问index
的闭包。这样一来,result
数组中的每个函数都有自己的index
变量的一个副本,因此就可以返回各自不同的值了。
ps:这个问题,如果在python
中,一个简单的解决方式就是设置默认参数;在ECMAScript6
中似乎也开始支持默认参数了。
2.2 关于this
对象
在闭包中使用this
对象也可能会导致一些问题。我们知道,this
对象是运行时基于函数的执行环境绑定的:在全局函数中,this
等于window
,而当函数被作为某个对象的方法调用是,this
等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this
对象通常指向window
。但是由于编写闭包的方式不同,这一点可能不会那么明显。如下:
name = 'Ann';
var obj = {
name: 'Rose',
getName: function () {
return function () {
return this.name;
};
}
};
console.log(obj.getName()()); // Ann
无闭包的情况下:
name = 'Ann';
var obj = {
name: 'Rose',
getName: function () {
return this.name;
}
};
console.log(obj.getName()); // Rose
关于javascript闭包中的this对象 这篇文章很清楚地解释了这个现象。
来看一下在闭包的情况下执行
console.log(obj.getName()()); // Ann
这一句代码其实相当于下面两句:
var func = obj.getName();
console.log(func());
在第一句代码(var func = obj.getName();
)中的this
是obj
,而第二句代码,没有被任何对象调用,所以第二句代码中的this
就是全局的this
,也就是window
;
解决方案:
name = 'Ann';
var obj = {
name: 'Rose',
getName: function () {
other = this; // todo
return function () {
return other.name; // todo
};
}
};
console.log(obj.getName()()); // Rose
修改如上,通过对obj.getName()()
的拆分,可以明确看到第二个函数的执行环境是全局执行环境。而obj.getName()
的执行环境是局部的,this
是obj
,所以,在这里,先取得this
的值(other=this;
),然后就可以去使用这个this
的值(return other.name;
)了。
ps:这种方式并没有改变闭包的
this
(也无法改变),而是提前取得了内部的this
,然后在外部去使用。
arguments
和this
一样存在这个问题。如果想访问作用域中的arguments
对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
2.3 内存泄露 (大雾)
由于IE9
之前的版本对Javascript
对象和COM
对象使用不同的垃圾收集例程,因此闭包在IE
的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML
元素,那么就意味着该元素无法被销毁。如下:
function assignHandler(){
var element = document.getElementById('someElement');
element.onclick = funciton(){
alert(element.id);
}
}
以上代码创建了一个作为element
元素事件处理程序的闭包,而这个闭包又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()
的活动对象的引用,因此就会导致无法减少element
的引用数。只要匿名函数存在,element
的引用数至少是1
,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过下面的代码解决:
function assignHandler(){
var element = document.getElementById('someElement');
var id = element.id;
element.onclick = funciton(){
alert(id);
}
element = null;
}
在上面的代码中,通过把element.id
的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但是仅仅做到这一步,还是不能解决内存泄露的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element
。即使闭包不直接引用element
,包含函数的活动对象也仍然会保存一个引用。因此,有必要把element
变量设置为null
。这样就能够消除对DOM
对象的引用,顺利地减少其引用输出,确保正常回收其占用的内存。
3 模仿块级作用域
如前所述,Javascript
中没有块级作用域的概念。这意味着,在语句块中定义的变量,实际上是在包含函数中而非语句中创建的。如下:
function outputNumbers(count){
for (var i=0; i<count; i++){
console.log(i);
}
console.log(i); // 依然生效
}
即使是这样,也不会报错:
function outputNumbers(count){
for (var i=0; i<count; i++){
console.log(i);
}
var i;
console.log(i); // 依然生效 --> 依然是 5
}
outputNumbers(5);
Javascript
从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。
用块级作用域(通常称为私有作用域)的匿名函数的语法如下所示:
(function(){
})();
ps: 这里为什么要给匿名函数外部加一个圆括号?
第一:不加的话,会出现语法错误。不加表示定义了一个匿名函数,而定义的函数后面加一个
()
并不是调用,而是语法错误。第二:加了只会,将函数定义变成了一个匿名函数表达式,然后是调用函数表达式
()
,这没有任何问题。
// 利用匿名函数模仿块级作用域
function outputNumbers(count){
(function () {
for (var i=0; i<count; i++){
console.log(i);
}
})();
console.log(i); // 报错:ReferenceError: i is not defined
}
outputNumbers(5);
ps: 为什么此匿名函数可以访问外部函数中的
count
?因为此匿名函数实际上是一个闭包。
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。如下示例:
(function(){
var now = new Date();
if (now.getMonth() == 0 && now.getDate() ==1){
console.log('Happy new year!');
}
})();
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数指向完毕,就可以立即销毁其作用域链了。
4 私有变量
严格来讲,Javascript
中没有私有成员的概念;所有对象的属性都是公有的。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。如下:
function add(num1 , num2){
var sum = num1 + num2;
return sum;
}
在这个函数内部,有3个私有变量:num1
,num2
和sum
。在函数内部可以访问这几个变量,但是在函数外部不能访问它们。如果在这个函数内部创建了一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。(怎么理解?)
我们吧有权访问私有变量和私有函数的公有方法称为特权方法(privileged method
)。有两种在对象上创建特权方法的方式。
- 第一种是在构造函数中定义特权方法,基本模式如下:
function MyObject(){
// 私有变量和函数
var privateVar = 10;
function privateFunc(){
return privateVar%2===0;
}
// 特权方法
this.publicMethod = function(){
privateVar++;
return privateFunc();
}
}
var obj = new MyObject();
console.log(obj.publicMethod());
console.log(obj.publicMethod());
console.log(obj.publicMethod());
这个模式在构造函数内部定义了所以私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。[废话,不是闭包,只要是内部函数不都可以访问外部函数中的局部变量吗!]对这个例子而言,变量privateVar
和函数privateFunc()
只能通过publicMethod()
来访问。在创建MyObject
实例之后,除了使用publicMethod()
这一个途径外,没有任何方法可以直接访问privateVar
和privateFunc()
。
利用私有和特权成员,可以隐藏那些不应该被直接修改数据。比如:
function Person(name){
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
不过,在构造函数中定义特权方法也有缺点,就像之前说的,构造函数中定义的方法,不能被所有当前类型的实例共享,造成内存的浪费。
4.1 静态私有变量
通过在私有作用域中定义私有变量或函数,通用可以创建特权方法,其基本模式如下:
(function(){
// 私有变量和函数
var privateVar = 10;
function privateFunc(){
return privateVar%2===0;
}
// 构造函数
MyObject = function(){
}
// 特权方法
MyObject.prototype.publicMethod = function(){
privateVar++;
return privateFunc();
}
})();
这部分内容差点让我坚持不住了,我被这奇怪的逻辑给搞得晕头转向的。
这个模式创建了一个私有作用域(外部的匿名函数),并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数(只能在私有作用域中被访问),但这不是我们想要的。出于同样的原因,我们也没有在声明MyObject
时使用var
关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject
就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下,给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。怎么理解共享,为什么会共享?由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域(也就是外部函数的作用域)的引用。如下:
(function(){
var name = "";
Person = function(value){
name = value;
};
Person.prototype.getName = function(){
return name;
};
Person.prototype.setName = function(value){
name = value;
};
})();
var p1 = new Person('tom');
var p2 = new Person('ann');
console.log(p1.getName()+' ### '+p2.getName());
p1.setName('张飞');
console.log(p1.getName()+' ### '+p2.getName());
输出:
ann ### ann
张飞 ### 张飞
这个例子中的Person
构造函数与getName()
和setName()
方法一样,都有权访问私有变量name
。在这种模式下,变量name
变成了一个静态的、由所有实例共享的属性。(怎么理解由所有实例共享,为什么会被所有实例共享?)也就是说,在一个实例上调用setName()
会影响所有实例。而调用setName()
或新建一个Person
实例都会赋予name
属性一个新值。结果就是所有的实例都会返回相同的值。
上面已经说明了为什么
name
会被所有实例共享了。原因就是:[由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域(也就是外部函数的作用域)的引用]. ===>[name
就是在包含作用域中定义的变量。]
这个私有变量被所有实例共享,是两个原因组合在一起导致的。第一个是原型方法会被所以实例共享;第二个是闭包总是保存这对包含作用域的引用。(而原型方法,也就是这里的闭包。)
4.2 模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法,而道格拉斯所说的模块模式(module pattern
)则是为单例创建私有变量和特权方法。所有单例(singleton
),指的是只有一个实例的对象。按照惯例,Javascript
是以对象字面量的方式来创建单例对象的。
var singleton = {
name : value,
method: function(){
// 这里是方法的代码
}
};
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function(){
// 私有变量和私有函数
var privateVar = 10;
function privateFunc(){
return (privateVar %2 === 0);
}
// 特权 / 公有方法和属性
return {
publicProperty : true,
publicMethod : function() {
privateVar++;
return privateFunc();
}
};
}();
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:
var application = function(){
// 私有变量和函数
var componets = [];
// 初始化
components.push(new BaseComponent());
// 公共
return {
getComponentCount: function(){
return components.length;
}
registerComponent: function(component){
if (typeof component == 'object'){
components.push(component);
}
}
};
}();
在Web
应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application
对象。在创建这个对象的过程中,首先声明了一个私有的components
数组,并向数组中添加了一个BaseComponent
的新实例。(这里只是一个实例,没有具体的代码)而返回对象的getComponents()
和registerComponent()
方法,都是有权访问数组components
的特权方法。前者只是返回已注册的组件数目,后者用于注册新组件。
简而言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么久可以使用模块模式。以这种模式创建的每个单例都是Object
的实例,因为最终要通过一个对象字面量来表示它。事实上,这也没什么;毕竟,单例通常是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没有什么必要使用instanceof
操作符来检查其对象类型了。
4.3 增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其进行增强的情况。如下:
var singleton = function(){
var privateVar = 10;
function privateFunc(){
return privateVar %2 === 0;
}
var obj = new CustomType();
obj.publicProperty = true;
obj.publicMethod = function(){
privateVar++;
return privateFunc();
}
return obj;
}();
如果前面演示模块模式的例子中的application
对象必须是BaseComponent
的实例,那么就可以使用下面的代码。
var application = function(){
// 私有变量和函数
var components = [];
// 初始化
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;
}();
在这个重写后的应用程序(application
)单例中,首先也是像前面的例子中一样定义了私有变量。主要的不同之处在于命名变量app
的创建过程,因为它必须是BaseComponent
的实例。这个实际上是application
对象的局部变量版。此后,我们又为app
对象添加了能够访问私有变量的公有方法。最后一步是返回app
对象,结果仍然是将它赋值给全局变量application
。
5 小结
在Javascript
编程中,函数表达式是一种非常有用的技术,使用函数表达式可以无需对函数命令,从而实现动态编程。匿名函数,是一种使用Javascript
函数的强大方式。以下总结了函数表达式的特点。
- 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫做匿名函数。
- 在无法确定如何引用函数的情况下,递归函数就会变得比较复杂;
- 递归函数应该使用使用
arguments.callee
来递归调用自身,不要使用函数名——函数名可能会发生变化。(严格模式下,不能使用arguments.callee
)
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下。
- 在后台执行环境中,闭包的作用域包含它自己的作用域,包含函数的作用域和全局的作用域。
- 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。
- 但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保持到闭包不存在为止。
使用闭包可以在Javascript
中模仿块级作用域(Javascript
中没有块级作用域的概念)。要点如下.
- 即使
Javascript
中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量。 - 有权访问私有变量的公有方法叫做特权方法。
- 可以使用构造函数模式(导致内存浪费),原型模式(导致私有变量被全部实例共享)来实现自定义类型的特权方法,也可以使用模块模式,增强模式来实现单例的特权方法。
Javascript
中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过因为创建闭包必须维护额外的作用域(系统自己维护),所有过度使用它们可能会占用大量内存。