关于界面的价值观与方法论
6789101112131415

作用域链 词法作用域 与闭包(三)

15February2009

在经历了漫长的作用域链、调用对象和词法作用域之后,终于可以讨论闭包了。

对于闭包这个概念,有各种各样的解释。我比较喜欢的是闭包是《Java 理论与实践: 闭包之争》中的定义:可以包含自由(未绑定)变量的代码块;这些变量不是在这个代码块或者任何全局上下文中定义的,而是在定义代码块的环境中定义。

听起来很抽象,但它指出了闭包定义的两个要素:允许自由变量,由定义环境而不是执行环境决定。

先说说自由变量。凡是不是在该代码块中定义的变量就叫做自由变量。如

function g() {return x;}

其中的x并不是在这个函数中定义的,它的值是存储在函数外部的。所以它就是一个自由变量。

那么一个代码块中的自由变量该到哪里去取呢?这个问题即是决定了这个代码块是否属于闭包。闭包定义要求,这个代码块中的自由变量,应该到定义它的环境中取。这也就是说,这个代码块的作用域应该是词法(静态)作用域。

由于同一个代码块,无论被调用多少次,或是在何处调用,它的定义只会在同一个环境中(否则就不能被认为是“同一个代码块”,因为定义环境不同)。闭包要求其自由变量的读取是在定义环境中决定,也就得到它的一个特性:闭包的自由变量永远指向同一处,无论它在何处被调用。

或许这样说,还是有点抽象。我做一个现实中的比喻。我之前为这个blog做模板的时候,需要在本机中装一个PHP+MySQL的环境。这就遇到一个问题:我白天在公司,晚上在家。如果白天做的时候对服务器配置做了一些调整,那么即使把所有的wordpress程序都copy回家,也无法在家接着做。如果把Wordpress程序文件看作是一个函数,它明显是受它的执行环境所影响的。换句话说,Wordpress有一部分“变量”,不是它自身定义的,而是从外部(PHP服务器或MySQL数据库)中得到的。

后来就找到一个很不错的软件,叫Uniform Server。它是一个文件包,里面包含了Apache和MySQL的环境。把它Copy到U盘上之后,点击Start.bat,就会自动在机器上建立一个服务器环境。更关键的是,下班回家时,关掉电脑,拔出移动硬盘带回家,在家里的电脑重新启动它,它就又可以在家里的电脑上重新建立一个服务器环境,而这个环境跟办公室电脑里一模一样,之前在公司作的改动,回到家里后还存在。

所以呢,可以认为这个Uniform Server就是一个闭包。它的变量(服务器配置)是在它被定义(也就是当我解压到U盘上)的时候就确定了的,而跟它在什么地方被调用执行(开启这个软件)无关。

其实从这个比喻中也可以看出,闭包的好处:作用域始终保持一样,不用担心执行环境不同导致的作用域不同。

最后举一个很简单的例子,来帮助理解闭包(我这里用jquery的写法,以简化代码):

var t = $('input#text').val()     //取得文本框的值t
$('input#btn').click(function() {
     doSth(t);                 //把t传给某个函数
   //... 其他操作
});

这个例子稍微有点怪怪的,因为它的文本框的值是在绑定点击事件之前去取,而不是在点击的时候。我们姑且就把它当作是因为某些特殊的要求不得不这样做吧。所以呢,绑定到点击事件上的那个匿名函数中,t其实使用的是它上一层作用域里的变量t。

这个时候呢,需求有点改变:要求在点击按钮之后,先弹出来一个对话框,和用户进行某些交互(比如要求登录),之后再按原来的步骤继续。为了通用性,我们需要建一个函数:

function openDialog(f) {
     // ... 对话框要作的一些操作
   f()                 //再执行后续的操作
}

这里的openDialog函数是打开一个通用的对话框,完成对话框的操作之后呢,再执行后续操作。这里的后续操作可以是一个函数,通过f变量传进openDialog里,让它来决定什么时候调用。

而之前的第一段代码就可以改为:

var t = $('input#text').val()     //取得文本框的值t
$('input#btn').click(function() {
     function f() {
         doSth(t);
       //... 其他操作
   }
    openDialog(f);
});

和第一段代码比较一下,我们只是简单的把原先的代码用一个函数装起来,然后传给openDialog,就搞定啦。

可能有人会觉得,这个本来就是自然而然的事情啊。其实这里面的关键在于,doSth(t);以及其他语句的执行,已经由原来的点击事件的地方,换到openDialog函数里去了,而后者的代码却是写在不知道什么地方的。之所以可以“自然而然”的随便把代码到处“扔”,就是因为闭包的特性(再想想我那个U盘)。

事实上,如果有兴趣,再仔细琢磨一下jquery的那个click函数,会发现,其实它也是因为有闭包,所以才可以把那个匿名函数扔进去(click函数本身的定义和执行,也是在遥远的未知的地方),从而实现绑定点击事件。

关于JS变量声明的小细节

10January2009

对于从未声明过的对象,如果尝试给它赋值,会隐式的将它声明为全局对象。比如:

(function() {
  s = 'abc';
})();
alert(s); //会弹出 abc

如果尝试读一个未声明的对象,JS会报错。比如:

alert(a); //不会弹出对话框,会报错

这个结论是犀牛书中所写。但是有意思的是,在IE里,如果尝试读一个未声明的对象,并不会报错,当然也不会继续执行JS。这点其实挺要命,不执行也不告诉你为什么。

但这个结论是铁律,所以如下的写法是经常容易犯的错误:

if(!a) {
    //do sth. ...
}

这样写有时候的初衷是:如果变量a不存在,就执行里面的语句。但如果a未定义,在Firefox中会报错;在IE里不会报错,但也不会执行以后的所有语句。

所以该如何探测一个变量是否被定义呢?一个巧妙的办法是:

if(!window.a) {
    //do sth. ...
}

这个方法其实很奇妙,因为读一个未声明的变量,会报错;但读一个未声明的属性,就不会报错。天晓得JS为啥规定这样奇怪的语法。

但是这样的方法只适用与全局变量。若是某个函数里的局部变量,还是用typeof去判断吧:

if(typeof a == 'undefined') {
    //do sth. ...
}

所以,结论就是,养成习惯,不要在变量未定义之前使用它

接下来再看一个例子:

var a = "I'm out";
(function() {
    alert(a);
    var a = "I'm in";
})();

好,提问,运行这段代码,会弹出什么文字?

回答一:弹出“I’m out”。错。JS没有块级作用域,无论var语句在何处,在整个函数体内它都是有定义的。

回答二:弹出“I’m in”。错。请再重新看上面那句话,它只是在整个函数体内有定义,但只有在被赋值之后才会有值。

正确答案是:“undefined”。是不是很晕?自己试试看吧。

总之,再重复一遍之前的结论:不要在变量未定义之前使用它

最后介绍一个我也搞不明白的问题:

在HTML文档里写上这段代码:

<script type="text/javascript">
    window['a'] = 'Hi';
</script>
<script type="text/javascript" src="out.js"></script>
<script type="text/javascript">
    alert(a);
</script>

然后在out.js里写上这句:

 if(false) {
     var a = 'Hello';
 }

然后用FF和IE6分别运行,看看你得到什么。

在FF里会弹出“Hi”,但是在IE6中,会得到“undefined”。

很神奇吧?按语法,无论如何,a都不可能是undefined。但是IE6里就会。

如果把两个语句都写在同一个文件里,就不会有这个情况。
如果把out.js里改成window.a,或者把前一个改成var a,也不会有这个情况。
如果把out.js里的var a移到if语句之外,或是把if的条件改为true,也不会有这个情况。

这个bug其实危害性很大。所以呢,只能给自己定一个好习惯,那就是不要在条件语句中声明变量,虽然这么做,语法上并不禁止。

全局字体最佳实践

28December2008
body {font-family: Arial, sans-serif; }

这是我迄今发现的最好的全局字体解决方案。当然,所谓“最好”,也只是基于我个人的评判标准。所以我还是得分析一下其他的写法有什么缺点,最后再总结这个写法的特点。

body {font-family: "宋体", sans-serif; }

这个写法可能的缺点在于:
1. 宋体在Safari和Vista的IE 7下,看起来很难看。(我稍后把截图补上)
2. 宋体的英文字很难看。
3. 如果在CSS里写中文,你得小心你HTML和CSS的编码是否一致。

body {font-family: SimSun,sans-serif; }

这样写,可以避免上面的第三个问题。但是宋体本身确实很难看。我们希望在不同平台下,都用各自默认的字体。XP是宋体,Vista是微软雅黑,Mac是黑体。这样的话,只能将字体的第一个设置为英文字体,这样遇到中文的时候,浏览器会自动调用默认字体(Vista IE 7的一些版本里貌似默认还是宋体,这个我就无能为力了,交给用户设置的自主权吧)

body {font-family: Tahoma, sans-serif; }

这是一个不错的解决办法。Tahoma其实是一个挺漂亮的字体(我同事喜欢称它“大河马”,哈哈)。但是它其实会带来一些问题:
1. 由Tahoma显示的中文,在IE 6里,下划线会紧紧的贴住中文字,很难看。
2. IE 6下,Tahoma无法正确的设定为13px。它会跟14px一样大。但是其他浏览器没有这个问题。
3. 如果一行里同时出现中文和英文,且这一行里有元素被定义了vertical-align属性,在IE 6、7里会导致文字高低不齐,甚至下划线错位。

body {font-family: Arial, ans-serif; }

上述两个问题,Arial都没有。但是Arial也有缺点:
1. 比Tahoma难看。
2. Tahoma里的第三个问题也同样存在。
不过,这个bug是有个解决办法的,就是将这一行定义zoom:1

所以,如果不嫌难看,定义为Arial是最合适的。如果实在不喜欢,可以将全局定义为Tahoma,然后再将有下划线的(如链接)文字定义为Arial,至少可以缓解一下。

最后,对于全局字体,补充一点:IE里,所有的表单元素都不继承body的字体属性,需要单独设置:

input, label, select, option, textarea, button, fieldset, legend {
    font-family:Tahoma,sans-serif;
}