4.9
作用域
在编程语言中,作用域控制着变量与参数的可见性及生命周期。对程序员来说这是一个重要的帮助,因为它减少了名称冲突,并且提供了自动内存管理。
大多数使用C语言语法的语言都拥有块级作用域。在一个代码块中(括在一对花括号中的语句集)定义的所有变量在代码块的外部是不可见的。定义在代码块中的变量在代码块执行结束后会被释放掉。这是件好事。
糟糕的是,尽管代码块的语法似乎表现出它支持块级作用域,但实际上JavaScript并不支持。这个混淆之处可能成为错误之源。
JavaScript确实有函数作用域。定义在函数中的参数和变量在函数外部是不可见的。但在一个函数中的任何位置定义的变量在该函数中的任何地方都可见
(默然说话:我的天呀,真是一个灾难。。。。)。
很多现代语言都推荐尽可能迟地声明变量。而用在JavaScript上却会成为糟糕的建议,因为它缺少块级作用域。所以,最好的做法是在函数体的顶部声明函数中可能用到的所有变量。
4.10
闭包
只有函数作用域的好处是内部函数可以访问定义它们的外部函数的参数和变量(除了this和arguments)。这是一件非常好的事情。
我们的getElementsByAttribute函数可以工作是因为它声明了一个results变量,且传递给walk_the_DOM的内部函数也可以访问results变量。
一个更有趣的情形是内部函数拥有比它的外部函数更长的生命周期。
之前,我们构造了一个myObject对象,它拥有一个value属性和一个increment方法。假定我们希望保护该值不会被非法更改。
与前面直接定义一个对象不同,我们通过调用一个函数的形式去初始化myObject,该函数将返回一个对象。此函数定义了一个value变量。该变量对increment和getValue方法总是可见的,但函数的作用域使得它对其他的程序来说是不可见的。
var myObject=function(){
var value=0;
return {
increment:function(inc){
value+=typeof inc==='number'?inc:1;
},
getValue:function(){
return value;
}
}
}();
我们并没有把一个函数赋值给myObject,我们是把调用该函数后返回的结果赋值给它(
默然说话:注意最后一行的())。该函数返回一个包含两个方法的对象,并且这些方法继续享有访问value变量的特权。
本章之前的Quo构造器产生出带有status属性和get_status方法的一个对象。但那看起来并不是十分有趣。为什么要用一个getter方法去访问本可以直接访问到的属性呢?如果status是私有属性时,它才是更有意义的。所以,让我们定义另一种形式的quo函数来做此事:
//创建一个名为quo的构造函数。
//它构造出带有get_status方法和status私有属性的一个对象。
var quo=function(status){
return {
get_status:function(){
return status;
},
set_status:function(st){
status=st;
}
};
};
//构造一个quo实例
var myQuo=quo(“amazed”);
document.writeln(myQuo.get_status());
这个quo函数被设计成无须在前面加上new来使用,所以名字也没有首字母大写(
默然说话:当然,你也可以加
new,效果是一样的)。当我们调用quo时,它返回包含get_status方法的一个新对象。该对象的一个引用保存在myQuo中。即使quo函数已经运行结束,但get_status方法仍然享有访问status的特权。get_status方法并不是访问该参数的一个拷贝,它访问的就是该参数本身。因为该函数可以访问它被创建时所处的上下文环境。这就被称为闭包。
//定义一个函数,它设置一个DOM节点为黄色,然后把它渐变为白色
var fade=function(node){
var level=1;
var step=function(){
var hex=level.toString(16);
node.style.backgroundColor='#FFFF' +hex+hex;
if(level<15){
level+=1;
setTimeout(step,100);
}
};
step();
};
<body onload=”fade(document.body)”></body>
我们调用fade,把document.body作为参数传递给它(HTML<body>标签所创建的节点).fade函数设置level为1。它定义了一个step函数;接着调用step函数,fade函数结束。
step函数把fade函数的level变量转化为16进制字符。接着,它修改fade函数得到的节点的背景色。然后查看fade函数的level变量。如果背景还没变成白色,那就增大level变量再使用setTimeout让自己再次运行。
step很快被再次调用,这时fade函数早已运行结束,但只要fade的内部函数需要,它的变量就会保留(
默然说话:耶!伟大的闭包!!!)。
理解内部函数能访问外部函数的实际变量本身而不是一个副本非常重要,看下面的例子。
//糟糕的例子
//构造一个函数,用错误的方式给一个数组中的节点设置事件处理程序。
//当点击一个节点时,按照预想应该弹出一个对话框显示节点的序号
//但其实所有的事件总是会显示节点的数目。
var add_the_handlers=function(nodes){
var i;
for(i=0;i<nodes.length;i++){
nodes[i].onclick=function(e){
alert(i);//因为这里是直接引用了变量i,而不是副本,所以当点击节点时,总是显示循环之后i的值
}
}
}
<body onload="add_the_handlers(document.getElementsByTagName('div'))">
<div style="width:300px;height:300px;border:1px solid black;"></div>
<div style="width:300px;height:300px;border:1px solid black;"></div>
<div style="width:300px;height:300px;border:1px solid black;"></div>
<div style="width:300px;height:300px;border:1px solid black;"></div>
</body>
add_the_handlers函数目的是给每个事件处理函数一个唯一值(
默然说话:即每一次循环时i的值,它需要很多个i的副本,每个i值都不一样),但它直接引用了i,所以每个事件处理函数都得到了循环后i最终的值。
//好例子
//构造一个函数,用正确的方式给一个数组中的节点设置事件处理程序。
//你点击一个节点,将会弹出不同的序号
var add_the_handlers=function(nodes){
var i;
for(i=0;i<nodes.length;i++){
nodes[i].onclick=function(e){
return function(){
alert(e);
};
}(i);
}
};
<body onload=" add_the_handlers(document.getElementsByTagName('div'))">
<div style="width:300px;height:300px;border:1px solid black;"></div>
<div style="width:300px;height:300px;border:1px solid black;"></div>
<div style="width:300px;height:300px;border:1px solid black;"></div>
<div style="width:300px;height:300px;border:1px solid black;"></div>
</body>
</html>
现在,我们定义了一个函数并立即传递i进去执行,而不是把一个函数赋值给onclick。那个函数将返回一个事件处理函数。这个事件处理函数打印的是e而不是i,这样就可以避免以上情况(
默然说话:中文版的源代码有误,中文翻译也有误,害我花了半个小时的时间才理解这段文字的本意,为了让读者容易理解,我对函数进行了修改)
4.11
回调
函数可以让不连续事件的处理变得更容易。例如:假定有这么一个序列,由用户交互开始,向服务器发送请求,最终显示服务器的响应。最纯朴的定法可能会是这样的:
request=prepare_the_request();
response=send_request_synchronously(request);
display(response);
这种方式的问题在于网络上的同步请将会导致客户端进入假死状态。如果网络传输或服务器很慢,响应性的降低将是不可接受的。
更好的方式是发起异步的请求,提供一个当服务器的响应到达时将被调用的回调函数。这样客户端不会被阻塞。
request=prepare_the_request();
send_request_asynchronously(request,function(response){
display(response);
})
(
默然说话:不要试图运行这两段代码,因为这两段代码仅仅是用来说明的,属于伪代码)
4.12
模块
模块是一个提供接口却隐藏状态与实现的函数或对象,我们可以使用函数和闭包来构造模块。通过使用函数去产生模块,我们几乎可以完全摒弃全局变量的使用,从而缓解这个JavaScript的最为糟糕的特性之一所带来的影响。
举例来说,假定我们想要给String增加一个deentityify方法。它的任务是寻找字符串中的HTML字符实体并替换为它们对应的字符。在一个对象中保存字符实体的名字和它们对应的字符是有意义的。但我们该在哪里保存该对象呢?我们可以把它放到一个全局变量中,但全局变量是魔鬼。我们可以把它定义在该函数中,但是那有运行时的损耗,因为该函数在每次被执行的时候该定义都会被初始化一次。理想的方式是将其放入一个闭包,
String.method('deentityify',function(){
//字符映射表,它映射字符的名字到对应的字符
var entity={
quot:'"',
lt:'<',
gt:'>'
};
//返回deentityify方法
return function(){
//这才是deentityify方法。它调用字符串的replace方法,
//查找'&'开头和';'结束的子字符串。如果这些字符可以在字符映射表中找到,
//那么就将该字符替换为映射表中的值,它用到了一个正则表达式(参见第七章)
return this.replace(/&([^&;]+);/g,
function(a,b){
var r=entity[b];
return typeof r==='string'?r:a;
}
);
};
}());
请注意最后一行,我们用()运算法立刻调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法。
document.writeln("< "> ".deentityify()); //输出<”>
模块模式利用了函数作用域和闭包来创建绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符映射表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数,利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把它们保存到一个可访问到的地方。
使用模块模式就可以摒弃全局变量的使用。它促进了信息隐藏和其他优秀的设计实践。对于应用程序的封装,或者构造其他单例对象(
译注:
JavaScript的单例就是用定义对象的方法创建对象。它通常作为工具为程序其他部分提供功能支持。),模块模式非常有效。
模块模式也可以用来产生安全的对象。假定我们想要构造一个用来产生序列号的对象:
var serialMaker=function(){
//返回一个用来产生唯一字符串的对象。
//唯一字符串由两部分组成:前缀+序列号
//该对象包含一个设置前缀的方法,一个设置序列号的方法
//和一个产生唯一字符串的gensym方法
var prefix='';
var seq=0;
return {
setPrefix:function(p){
prefix=String(p);
},
setSeq:function(s){
seq=s;
},
gensym:function(){
var result=prefix+seq;
seq+=1;
return result;
}
};
};
var seqer=serialMaker();
seqer.setPrefix('Q');
seqer.setSeq(1000);
var unique=seqer.gensym();//unique的值是"Q1000"
alert(unique);
seqer包含的方法都没有用this或that。因此没有办法损害seqer。除非调用对应的方法,否则没法改变prefix或seq的值。seqer对象是可变的,所以它的方法可能会被替换,但替换后的方法依然不能访问私有成员。seqer就是一组函数的集合,而且那些函数被授予特权,拥有使用或修改私有状态的能力。
4.13
级联
有一些方法没有返回值。如果我们让这些方法返回this而不是undefined,就可以启动级联。在一个级联中,我们可以在单独一条的语句中依次调用同一个对象的很多方法。一个启用级联的Ajax类库可能允许我们以这样的形式去编码:
//默然说话:这段代码仅为了说明级联的概念,无法运行,其实级联就是Java中的连续打点调用方法的形式
getElement('myBoxDiv').
move(350,150).
width(100).
height(100).
color('red').
border('10px outset').
padding('4px').
appendText('Please stand by').
on('mousedown',function(m){
this.startDrag(m,this.getNinth(m));
}).
on('mousemove','drag').
on('mouseup','stopDrag').
later(2000,function(){
this.color('yellow').
setHTML("What hath God wraught?").
slide(400,40,200,200);
}).
tip('This box is resizeable');
在这个例子中,getElement函数产生一个对应于id=”myBoxDiv”的DOM元素并提供了其他功能的对象。该方法允许我们移动元素,修改它的尺寸和样式,并添加行为。这些方法每一个都返回该对象,所以调用返回的结果可以被下一次调用所用。
级联可以产生出具备很强表现力的接口。它也能帮助控制那种构造试图一次做很多事情的接口的趋势(
默然说话:说实话,我非常不喜欢这样的编码,因为这样编码易读性太差。级联基本上适用于那些一次编码之后再也不修改的代码,或者适用于那些你不想让包括你自己在内的任何人都看不懂的代码)。
4.14
套用
函数也是值,从而我们可以用有趣的方式去操作函数值。套用允许我们将函数与传递给它的参数相结合去产生出一个新的函数。
var add1=add.curry(1);
document.writeln(add1(6)); //书上写结果是7,可我实际调试的结果是undefined
add1是把1传递给add函数的curry方法后创建的一个函数。add1函数把1添加到它的参数中。JavaScript并没有curry方法,但我们可能通过给Function.prototype添加功能来实现:
Function.method('curry', function ( ) {
var slice = Array.prototype.slice,
args = slice.apply(arguments),
that = this;
return function ( ) {
return that.apply(null, args.concat(slice.apply(arguments)));
};
});
curry方法通过创建一个闭包,它包括了原始函数和被套用的参数。curry方法返回另一个函数,该函数被调用时,会返回一个结果,这个结果包括了curry方法传入的参数和自己的参数。它使用了Array的concet方法把它们连接在了一起。
由于arguments数组并非一个真正的数组,所以它并没有concat方法。要避开这个问题,我们必须在两个arguments数组上都应用数组的slice方法。这样产生出拥有concat方法的常规数组。
4.15
默记法(memoization)
函数可以用对象去记住先前操作的结果,从而能避免无谓的运算。这种优化被称为默记法(
memoization
:用于加快程序运算速度的一种优化技术,原书中文版翻译为记忆,我在这里翻译为默记法)。JavaScript的对象和数组要实现这种优化是非常方便的。
比如说,我们想要一个递归函数来计算Fibonacci。一个Fibonacci数字是之前两个Fibonacci数字之和。最前面的两个数字是0和1.
var fibonacci=function(n){
return n<2?n:fibonacci(n-1)+fibonacci(n-2);
}
for(var i=0;i<=10;i++){
document.writeln('//'+i+':'+fibonacci(i)+'<br />');
}
运行结果:
//0:0
//1:1
//2:1
//3:2
//4:3
//5:5
//6:8
//7:13
//8:21
//9:34
//10:55
程序可以工作,但fibonacci函数被调用了453次。我们调用了11次,而它自身调用了442次。如果我们让该函数应用默记法,就可以显著地减少它的运算量。
我们在一个名为memo的数组里保存我们的存储结果,存储结果可以隐藏在闭包中。当我们的函数被调用时,这个函数首先看是否已经知道存储结果,如果已经知道,就立即返回这个存储结果。
var fibonacci=function(){
var memo=[0,1];
var fib=function(n){
var result=memo[n];
if(typeof result!=='number'){
result=fib(n-1)+fib(n-2);
memo[n]=result;
}
return result;
};
return fib;
}();
这个函数返回同样的结果,但它只被调用了29次。我们调用了它11次。它自身调用了18次。
我们可以把这种形式一般化,编写一个函数来帮助我们构造带默记法功能的函数。memoizer函数将取得一个初始的memo数组和fundamental函数。它返回一个管理memo存储和在需要时调用fundamental函数的shell函数。我们传递这个shell函数和该函数的参数给fundamental函数:
var memoizer=function(memo,fundamental){
var shell=function(n){
var result=memo[n];
if(typeof result!=='number'){
result=fundamental(shell,n);
memo[n]=result;
}
return result;
};
return shell;
};
现在,我们可以使用memoizer来定义fibonacci函数,提供其初始的memo数组和fundamental函数:
var fibonacci=memoizer([0,1],function(shell,n){
return shell(n-1)+shell(n-2);
});
通过设计能产生出其他函数的函数,可以极大减少我们必须要做的工作。例如:要产生一个默记法的阶乘函数,我们只须提供基本的阶乘公式即可:
var factorial=memoizer([1,1],function(shell,n){
return n*shell(n-1);
});