在使用Dreamweaver保存网页时,发生如下错误提示信息:
在onload运行_afterSave.htm时,发生了一个Javascript错误。(连接出现几次)
以下翻译器没有被装载,由于错误:Server Model SSI.htm:有不正确的设置信息。
引起错误的原因有很多,最有可能的便是Configuration内的JS脚本损坏。
重装了Dreamweaver,但是问题依然存在,最终解决方案是:
1, 退出正在使用的Dreamweaver
2, 在C盘,C:\Documents and Settings\Administrator\Application Data(隐藏文件)\Adobe\Dreamweaver。里面有个文件夹“Configuration”,把它全部删除即可。
月度存档: 十一月 2009
以下翻译器没有被装载,由于错误:Server Model SSI.htm
css实现html元素透明的解决方案
CSS3草案中定义了{opacity: | inherit;}来声明元素的透明度,这已经得到了大多数现代浏览器的支持,而IE则很早通过特定的私有属性filter来实现的,所以HTML元素的透明效果已经无处不在了。首先看看A级浏览器所支持的用CSS实现元素透明的方案:
| 浏览器 | 最低 版本 |
方案 |
|---|---|---|
| Internet Explorer | 4.0 | filter: alpha(opacity=xx); |
| 5.5 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=xx); |
|
| 8.0 | filter: "alpha(opacity=xx)"; |
|
| Firefox (Gecko) | 0.9 (1.7) | opacity |
| Opera | 9.0 | opacity |
| Safari (WebKit) | 1.2 (125) | opacity |
实际上在IE8中,-ms-filter是filter的别名,两者区别是-ms-filter的属相值必须被单引号或双引号包围,而filter中则不是必须,而在IE8之前的版本中,filter的属性值必须不被单引号或双引号包围。
IE中的HTML元素要实现透明,则其必须具备layout,这样的元素有仅可读的属性hasLayout,且其值为true。具体情况如下:
body、img、table、tr、th、td等元素的hasLayout一直为true。type为text、button、file或select的input的hasLayout一直为true。- 设置
{position:absolute}的元素的hasLayout为true - 设置
{float:left|right}的元素的hasLayout为true - 设置
{display:inline-block}的元素的hasLayout为true - 设置
{height:xx}或{width:xx}的元素必须具体以下两个条件之一,其hasLayout才能为true:- IE8兼容模式和IE8以前的浏览器中,在标准(strict)模式下其
display的值是block,如demo3就不行。 - 元素在怪癖模式(compat mode)下。
- IE8兼容模式和IE8以前的浏览器中,在标准(strict)模式下其
- 设置了
{zoom:xx}的元素在IE8的兼容模式或IE8之前的浏览器中其hasLayout为true,但在IE8的标准模式下则不会触发hasLayout。 - 设置了
{writing-mode:tb-rl}的元素的hasLayout为true。 - 元素的
contentEditable的属性值为true。 - 在IE8标准模式下设置了
{display:block}的元素的hasLayout一直为true,如demo8。
关于hasLayout的更多详情可以看Exploring Internet Explorer “HasLayout” Overview和On having layout
从上面就可以看出IE实现HTML元素的透明如此混乱,为了向下兼容和自己的私有属性让IE上实现元素透明有多种方式,比如CSS Opacity实例中的demo1到demo8,虽然IE团队推荐实现透明的方式是:
{
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=50);
opacity: .5;
}
而目前简单最好用的实现方式是如CSS Opacity中demo4这样:
{
filter:alpha(opacity=30);
opacity:.3;
}
实际上目前最流行的JavaScript框架的设置样式方法都是应用这种方式,并且针对IE设置了{zoom:1}来让元素的hasLayout为true,但在IE8的标准模式下zoom并不能触发hasLayout,所以利用它们设置hasLayout为false的元素的透明度时在IE8的标准模式下是失败的,这个bug在YUI(我已经给YUI团队提交了这个bug,他们会在下个版本修复,最新的2.8.0中依旧存在,期待2.9.0吧)、Prototype、jQuery和Mootools的最新版本中都存在,具体请在IE8标准模式下看demo9到demo11。同样由于在IE8中设置透明度的方式多种多样,所以利用JavaScript获取HTML元素的透明度值需要考虑多种情况,YUI完美解决了这个问题,Prototype比jQuery稍微周全一点,而Mootools直接是bug,具体可以在IE下看demo1到demo8的演示。从这个角度给4个框架来个排名的话,YUI第一、Prototype第二、jQuery第三、Mootools垫底。
我简单的实现了设置和获取Opacity的函数,可以避开上面框架存在的bug,请在IE8标准模式下看demo12:
//设置CSS opacity 属性的函数,解决IE8的问题
var setOpacity = function(el,i){
if(window.getComputedStyle){// for non-IE
el.style.opacity = i;
}else if(document.documentElement.currentStyle){ // for IE
if(!el.currentStyle.hasLayout){
el.style.zoom = 1;
}
if(!el.currentStyle.hasLayout){ //在IE8中zoom不生效,所以再次设置inline-block
el.style.display = 'inline-block';
}
try{
//测试是否已有filter
//http://msdn.microsoft.com/en-us/library/ms532847%28VS.85%29.aspx
if(el.filters){
if(el.filters('alpha')){
el.filters('alpha').opacity = i * 100;
}else{
el.style.filter += 'alpha(opacity='+ i * 100 +')';
}
}
}catch(e){
el.style.filter = 'alpha(opacity='+ i * 100 +')';
}
}
}
//获取CSS opacity 属性值的函数
//借鉴自http://developer.yahoel.com/yui/docs/YAHOO.util.Dom.html#method_getStyle
var getOpacity = function(el){
var value;
if(window.getComputedStyle){
value = el.style.opacity;
if(!value){
value = el.ownerDocument.defaultView.getComputedStyle(el,null)['opacity'];
}
return value;
}else if(document.documentElement.currentStyle){
value = 100;
try { // will error if no DXImageTransform
value = el.filters['DXImageTransform.Microsoft.Alpha'].opacity;
} catch(e) {
try { // make sure its in the document
value = el.filters('alpha').opacity;
} catch(err) {
}
}
return value / 100;
}
}
不得不说,这些事都是IE整出来的……
10条有用的可用性结论和指南
每个人都会赞同可用性是网站的一个很重要的方面的说法。无论你是在做一个作品集网站、网上商店还是网络应用,让你的页面对用户来说可以容易和快乐的使用是一个关键。这些年,很多人在网站和界面设计的很多方面完成了很多的研究,这些结论在帮助我们提高我们的工作方面很有价值。这里是能够帮助你提高你的网站的用户体验的10条有用的可用性结论和指南。
Form标签最好放到字段的上面
UX Matters的一份研究发现表单中的标签(label)的理想位置是在字段(field)的上面。在很多的表单中,标签被放到字段的左边,以形成一个两栏的布局;尽管这样看起来不错,但它不是最容易使用的布局。为什么这样说呢?因为表单通常是垂直导向的;也就是用户从上向下填写表单,用户是向下浏览表单的。这样在标签下面放置字段比在标签右边放置字段要更容易(浏览)一些。

Tumblr 遵循UX Matter的建议,提供了一个简单而简洁的注册表单。
在左侧放置标签还会引发另一个问题:你让标签居左还是居右?居左可让表单可扫描但是会将标签和字段分离,从而使得区分哪个标签归哪个字段就比较困难。居右则相反:它可以实现好的界面但是可扫描性弱些。在各种情况下,标签在上面是最好的。该研究同时发现标签最好不要加粗,尽管此条建议并不能令人完全信服……
用户关注面部
当别人进入到视野的时候,人们本能的会立马注意到他们。在网站页面,我们趋向于关注人的面部和眼睛, 这为市场人员提供了一个引起关注的很好的技术。但是我们被人们的面部和眼睛吸引只是个开始;事实证明,我们确实会将目光转向图片中的人正在看的方向。

一个正在看我们的小孩儿的图片的眼动跟踪热图,来自于UsableWorld的研究。

那么现在这个小孩正在看内容。注意人们在看向标题和文字的方向增长。
这里是一份描述这个的眼动跟踪研究。我们本能的被吸引到脸部,但是如果那张脸在看其他地方而不是我们,我们同样会看那个方向。利用这种现象来吸引你的用户的注意力到你的页面或广告中最重要的部分吧。
设计的质量是可信度的一个指标
各种研究已转向到是什么影响人们对网站的可信度的看法方向:
- Stanford-Makovsy网站可信度研究2002: 调查当前什么让一个网站可信(pdf)
- 什么让一个网站可信?大量定量研究的报告(pdf)
- 计算机可信度元素(pdf)
- 影响网站可信度的元素:一份源自自我评定研究的早期结果(Proceedings of ACM CHI 2000 Conference on Human Factors in Computing Systems, v.2, New York: ACM Press)

我们不知道Fever app 是不是真的比较好,但是时尚的用户界面和网站给我们很好的第一印象。
这些研究的一个有趣的结论是用户真的通过封面来判断一本书… 更确切的说,一个网站依赖其设计。比如布局、一致性、排版、色彩和样式之类的元素都会影响到用户对你的网站的理解以及你的项目的形象。你的网站不应该仅仅有一个良好的形象,还应该就是你的用户所需要的。
其它影响可信度的因素有:网站的内容的质量、错误数、更新频率、易用以及可信赖的作者。
大多数用户 不 滚动
Jakob Nielsen关于多少用户滚动的研究(在 Prioritizing Web Usability)显示只有23%的访问者在第一次访问一个网站的时候滚动。这意味着77% 的用户并不滚动;他们只是看折叠线以上(也就是不用向下滚动而在屏幕上可见的页面区域)的内容。而且,回访时滚动的用户的百分比也有所降低,只有16%的用户在他们第二次访问时滚动。这些数据强调在显著的位置放置关键的内容是多么的重要。特别是在登录页面。
这不是说你应该把所有东西塞到页面的上部区域里面,只是说,你应该最好的利用那一块地方。把内容填塞进去只会让内容难懂;当用户看到太多的信息,他们会不知道该从哪里开始看。

Basecamp很好的利用了空间。折叠线以上(768 像素高),它显示一个巨大的截图、标语、有价值的主张、行为召唤、客户列表、视频和使用图片展示的简短特性列表。
这对主页是最重要的,大部分新访问者都会浏览首页。所以请在那里提供这些核心要素:
- 网站名称;
- 网站的有价值的主张(比如说,用户将从使用中得到什么好处);
- 与用户相关的主要部分导航。
但是,现在用户的习惯已经发生明显的改变了。最近研究证明用户对滚动相当接受,而且在某种情况下他们会期望滚动到页面的底部。很多用户喜欢滚动胜过分页,而且对很多用户来说页面的最重要的信息并不是必须放在“折叠线上面”(这是因为各种大量的可见的显示方案是无用的,拒用)。所以将你的布局分割成段以方便浏览是个很不错的主意,使用一些空白分开它们吧。
想了解更多信息可以查看这些文章:将折叠展开 (Clicktale), 分页与滚动 ( 威之卡大学 – SURL), 打破折叠的神话 (盒子与箭头)。(多谢Fred Leuck).
蓝色是链接的最佳颜色
尽管给你的网站一个独特的设计是很棒的,但是当遇到可用性的时候,做其他人正在做的是最好的。遵从惯例,因为当一个人访问一个新的网站的时候,他们寻找东西的第一个位置就是他们在其他大多数网站找到它们的地方;他们利用他们的经验来理解新内容的意思。这被称为使用习惯。人们期望某些东西保持一致,比如链接颜色、网站的logo的位置、tab导航的行为等。

Google在其网站上保持所有的链接都是蓝色只有一个原因:大多数用户对这个颜色熟悉,这使得很容易定位。
你的链接应该是什么颜色?第一要义就是差异(对比):链接要足够暗(或者亮)以和背景色相对照。其次,它要能从其他文本中凸显出来;所以,不要在黑色字体上使用黑色链接。最后,研究表明(Van Schaik 和Ling)如果可用性是你的重点,保持蓝色链接是最好的。浏览器的默认链接颜色是蓝色,所以用户比较期望它。选择一个其它颜色绝对不是问题,但是它将会影响到用户找到它的速度。
理想的搜索框是27个字符宽
搜索框的理想宽度是多少?有这回事儿吗?Jakob Nielsen做了一次关于在网站的搜索框中搜索问题的长度的调查(Prioritizing Web Usability)。结果是现在的大部分搜索框太短了。搜索框太短的问题是即便你可以输入较长的问题,你也只能一次看到文本的一部分,从而使得难于检查或编辑你输入的内容。
该研究发现搜索框的平均宽度是18个字符。数据显示27%的查询太长以至于不能输入。扩展搜索框到27个字符能够满足90%的查询。请注意,你可以使用em设置宽度,而不是像素或者pt。1em正好是一个“m”字符的宽度和高度(而无论你的网站使用多大的字号)。那么,就用这个单位来控制搜索的文本框为27个字符宽吧。

Google的搜索框足够宽来容纳长句子。

Apple的搜索框有些短,会切断查询内容:“Microsoft Office 2008.”
总体来说,搜索框太宽比太短要好,这样用户就可以快速检查、核实并提交查询。这条准则相当简单,但是不幸的是常常被无视。在输入区域里使用一些内边距(padding)同样可以提高设计和用户体验。
空白可以增进理解
很多设计师都知道空白的价值,也就是页面中段落、图片、按钮和其它元素之间的空白。空白通过给各元素足够的空间来“呼吸”以避免页面过于拥挤凌乱。我们也可以通过减少条目之间的空间和增加它们与页面中的其它条目之间的空间来进行分组。在页面中显示条目之间的关系(比如,在这个条目集那里显示这个按钮)和构筑元素的等级很重要。

注意Netsetter网站上的大大的内容margin、padding和段落空间。所有的这些空间让内容更容易和舒适的阅读。
空白同样让内容更加清晰易读。一份研究(Lin, 2004)发现段落之间和左右间距可以增进理解20%左右。用户会发现更容易聚焦和处理使用大空白的内容。
事实上,根据Chaperro、Shaikh和Baker的研究,一个页面的布局(包括空白、标题、缩进和插图)可能不会明显的影响表现但是肯定会影响用户的舒适度和体验。
有效的用户测试并不一定要广泛
Jakob Nielsen关于在可用性测试中的测试对象的理想数字的研究表明仅仅测试5个用户就可以发现你的网站的所有问题的85%,而15名用户就能发现差不多所有的问题。
最大的问题通常是第一个或者第二个用户发现的,后续的用户会确认这些问题并发现其它的一些小问题。只有两个测试用户的话可能能发现你的网站的一半的问题。这意味着测试并不是必须要很广泛或者很昂贵以获取较好的结果。最大的收益来自于从0个测试用户到1个,所以不必担心测试用户太少:任何测试都聊胜于无。
信息产品页面可以助你引人注目
如果你的网站有产品页面,人们在线购物的时候将一定会看到它们。但是很多产品页面缺乏足够的信息,甚至不足以让浏览器快速浏览。这是个严肃的问题,因为产品信息有助于让人们下定购买的决心。研究显示缺乏产品信息会导致大概8%的可用性问题和甚至高达10%的用户失败(也就是说用户放弃并离开这个网站) (Prioritizing Web Usability).

Apple为其产品提供独立的“技术特性”页面,这可以将复杂的详情页面独立于简单的产品销售页面,然后在他们(用户)需要的时候提供一个方便的入口。
为你的产品提供详细的信息,但是不要掉进用太多文字炮轰用户的陷阱。让这些信息易于理解。通过将文字分成小段并使用大量的子标题让页面可浏览,为你的产品添加大量的图片,并使用合适的语言:不要使用术语,你的用户可能不懂。
大部分用户无视广告
Jakob Nielsen在其AlertBox entry中报告说大部分用户根本就无视广告横幅。如果他们在一个页面中寻找一个信息片段或者专心的看内容,他们是不会被旁边的广告扰乱的。
这意味着用户不仅会避开广告,而且他们还会避开一切看起来像广告的东西,即便它们跟本不是广告。一些重风格的导航条会看起来像横幅广告,所以小心使用那些元素。

FlashDen左侧的方形横幅确实不是广告:它们只是内容链接,但是它们的确看起来和广告条很像,以至于会被一些用户无视。
也就是说,如果广告看起来像内容,人们会浏览并点击。这会带来更多的广告收入但是会以你的用户的信任为代价,因为他们点击他们认为真的是内容的东西。在你采取那种方式前,请先衡量一下这笔交易:短期收益与长期信任。
号外:来自于我们的个案研究的结论
最近几年,Smashing Magazine的编辑团队带领了一些个案研究以试图找到一些普遍的方案和实践。到目前为止,我们已经分析了网站表单、博客、排版和作品集;更多的个案研究将在下个月发布。我们发现了一些能够为你的下一个设计充当指导方针的有趣的模式。
在这里,我们将回顾一下我们在那些个案研究中发现的一些实践和设计模式,简化这些概述,以使你更方便。
根据我们的排版研究:
- 行高(像素) ÷ 主体字体大小(像素) = 1.48
1.5 通常被推荐于传统印刷的书籍中,那么我们的研究也支持这个单凭经验的方法。只有很少的网站使用小于这个的,而且使用超过1.48的网站就像也在减少。 - 行长度(像素) ÷ 行高(像素) = 27.8
平均的行的长度是538.64 像素(不包括外边距和内边距),这是相当大的,考虑到很多网站仍然在body中使用是12至13像素大小的字体。 - 段落之间的空间(像素) ÷ 行高(像素) = 0.754
结果是段落空间(就是一个段落的最后一行和下一段落的第一行之间的空间) 难得的等于行间空白了(这将是完美的垂直节奏的主要特点)。更常见的是,段落间距正好是行间空白的75%。原因可能是行间空白通常都包括下行线;而因为大部分字符都没有下行线,那么多余的空白就出现在了线的下面了。 - 每行字符数最好是55到75
根据传统排版图书,每行文字的最佳字符数是55-75,但是事实上,每行75-85个字符更流行。
根据我们的博客设计研究:
- 布局通常采用固定宽度(基于像素) (92%)并且通常是居中的(94%)。固定布局的宽度大部分在951px和1000px之间(56%)。
- 首页显示10到20篇日志的摘要(62%);
- 一个网站的整体布局的58%用来显示主内容。
根据我们的网站表单设计研究:
- 注册链接写为“sign up” (40%)并被放置在页面的右上角;
- 注册表单有简单的布局,以避免分散用户的注意力(61%);
- 输入字段的标题加粗(62%),字段垂直放置的要明显多于水平放置的(86%).
- 设计师趋向于采用一些强制性字段和可选字段;
- 未提供Email验证(82%),但是需要密码验证(72%);
- “Submit”按钮即有居左的(56%)也有居右的(26%)。
根据我们的作品集网站研究:
- 89%的布局水平居中,而且他们中的大部分使用巨大的水平导航菜单。
- 47.2%的作品集网站有一个客户页面,67.2%有一些独立的服务页面。
- 63.6%对每一个项目会有个详细的页面,包括个案研究、感言、截屏幻灯、草稿和草图等。
- 联系页面包括努力方向、手机号码、Email地址、邮寄地址、名片以及在线表单。
其它资源(英文)
- 15个你从未听说过的有价值的可用性PDF
- 15个你应该知道的重要的研究结论
- 你应该知道的10个额外的研究成果
- 良好的可用性简介
- 11个从眼动跟踪研究中获得的成果
- 25个超级有用的可用性手册和清单
- 10个增加销售和用户忠诚度的可用性结论
- 网站表单设计指南:一份眼动跟踪研究
对我们所讲的有什么想法吗?或者了解其它的有用的可用性结论?请在下面留言给我们,多谢!
关于作者
Dmitry Fadeyev Usability Post 博客的创建者,在那里你可以读到他关于良好设计和可用性的观点。Follow Dmitry on Twitter @usabilitypost
给JavaScript新手的24条实用建议
为JavaScript做一点性能小提升吧!
本文列出了24条能让你的代码编写过程更为轻松高效的建议。也许您还是JavaScript初学者,刚刚写完自己的Hello World,那这里有很多对您的工作将十分有用的小贴士;也许有些技巧您已经知道,那就试试快速浏览一下,看能不能发现一点新东西吧!
注:本文多次用到Firebug的console对象,请参考Firebug Console API 。关于firebug的更详细介绍,请猛击这里。
1. 用 === 代替 ==
JavaScript里有两种不同的相等运算符:===|!== 和==|!=。相比之下,前者更值得推荐。请尽量使用前者。
引用:
“如果两个比较对象有着同样的类型和值,===返回true,!==返回false。”
– JavaScript: The Good Parts
不过,如果使用==和!=,在操作不同数据类型时, 你可能会遇到一些意想不到的问题。在进行相等判断前,JavaScript会试图将它们转换为字符串、数字或 Boolean量。
2. 避免使用Eval函数
Eval函数把一个字串作为参数,并把字串作为JavaScript语句执行,返回结果(参考)。
此函数不仅会降低你脚本的执行效率,而且还大大增加了安全风险,因为它赋予了作为文本的参数太大的权利。千万别用!
3. 不要使用快速写法
技术上说,你可以省略掉大部分花括弧和句尾分号,绝大多数浏览器都能正确执行以下语句:
代码:
if(someVariableExists)
x = false
不过,如果是这样的呢:
代码:
if(someVariableExists)
x = false
anotherFunctionCall();
你可能会认为它和下面的语句相等:
代码:
if(someVariableExists) {
x = false;
anotherFunctionCall();
}
不幸的是,事实并非如此。现实情况是它等价于:
代码:
if(someVariableExists) {
x = false;
}
anotherFunctionCall();
如您注意到的,再漂亮的缩进也不能代替这华丽的花括弧。在所有情况下都请写清楚花括号和句尾分号。在只有一行语句的时候能偶尔省略掉,虽然下这么做也是极度不被推荐的:
代码:
if(2 + 2 === 4) return 'nicely done';
多考虑下将来吧,孩子
假设,在将来的开发过程中,你需要为这个 if 语句添加更多的命令呢?到时候你还不是得把括号给加上?
4. 好好利用JS Lint
JSLint 是由 Douglas Crockford 编写的一个调试器。你只需要贴上你的代码,它就能快速为您扫描出任何明显的错误和问题。
引用:
“JSLint扫描接收的代码。发现问题,描述问题,并给出其在源码中的大概位置。可发现的问题包括但不限于语法错误,虽然语法错误确实是最常见的。JSLint也会用
约定俗成的习惯检查代码的格式化风格,以及结构错误。通过JSLint的扫描并不能保证你的程序就完全正确。它只是为您提供了额外一双发现错误的眼睛。”– JSLint 文档
完成代码之前,把它放到JSLint里检查一下,快速消灭你的无心之过。
5. 在页面底部加载脚本
正如下图所示:

请记住—— 我们要千方百计保证客户端的页面载入速度尽可能的快。而脚本没载入完成,浏览器就没法加载页面的剩余部分。
如果你的JS文件只是添加一些额外功能——例如,为点击某链接绑定事件——那大可以等页面加载基本完成后再做。把JS文件放到页面最后,body的结束标签之前,这样做最好了。
更好的写法是
代码:
<p>超哥是世界上最帅的人。benhuoer.com是世界上最好看的博客。</p>
<script type="text/javascript" src="path/to/file.js"></script>
<script type="text/javascript" src="path/to/anotherFile.js"></script>
</body>
</html> <!--mce:0--><!--mce:1-->
6. 在 For 语句外部声明变量
当需要执行冗长的for语句时,不要让JavaScript引擎每次都重复那些没有必要的操作。例如:
这样不好
代码:
for(var i = 0; i < someArray.length; i++) {
var container = document.getElementById('container');
container.innerHtml += 'my number: ' + i;
console.log(i);
}
这段代码每次都重新定义数组长度,每次都在遍历DOM寻找container元素 —— 太傻了!
这样好多了
代码:
var container = document.getElementById('container');
for(var i = 0, len = someArray.length; i < len; i++) {
container.innerHtml += 'my number: ' + i;
console.log(i);
}
我要给留言改进这段代码的人额外惊喜!欢迎大家留言讨论!
7. 快速构建字串
要对一个数组或对象做循环操作时,不要老惦记着一表人才的for语句,拿点创意出来嘛!明明就还有很多更快的办法:
代码:
var arr = ['item 1', 'item 2', 'item 3', ...];
var list = '<ul><li>' + arr.join('</li><li>') + '</li></ul>';
引用:
“没那么多繁文缛节来烦你;你就信我一次好了(或者你也可以自己试一试)—— 这真的是迄今能找到的最快办法了!
用点土办法,也别管它背后究竟发生了什么抽象的东西,通常土办法都比那些优雅的办法要快捷得多!”
– James Padolsey, james.padolsey.com
8. 减少全局变量
引用:
“把你踩在全局的那些乱七八糟的脚印都归于一人名下,能显著降低与其他应用、小工具或JS库冲突的可能性。”
– Douglas Crockford
代码:
var name = 'Jeffrey';
var lastName = 'Way';
function doSomething() {...}
console.log(name); // Jeffrey -- or window.name
更好的写法
代码:
var DudeNameSpace = {
name : 'Jeffrey',
lastName : 'Way',
doSomething : function() {...}
}
console.log(DudeNameSpace.name); // Jeffrey
注意看,我们是如何戏剧化地把“乱七八糟的脚印”都归到“DudeNameSpace”这对象之下的。
9. 写好注释
可能一开始你会觉得并无必要,但相信我,你将来会主动想要尽可能写好代码的注释的。当你几个月后再回看某项目时,结果却发现很难想起当时写某句东西时脑子在想的什么了,是不是很让人沮丧呢?或者,如果有同事要修订你的代码呢?一定,一定要为你代码里的重要部分加上注释。
代码:
// 遍历数组,输出各自名称
for(var i = 0, len = array.length; i < len; i++) {
console.log(array);
}
10. 试试渐进增强
一定要记得为未启用JavaScript的情况提供替代方案。大家可能会认为,“大部分我的访客都启用了JavaScript的,我才不用担心”。这样的话,你可就大错特错了!
你有没有试过看看禁用JavaScript后你那漂亮的滑动器都成啥样了?(你可以下载 Web Developer ToolBar 轻松完成这项任务。)禁用之后你的网站可能就彻底失去了可用性!经验之谈:开发初期总是按照没有JavaScript来设计你的网站,之后再进行渐进地功能增强,小心翼翼地改变你地布局。
11. 不要传递字串给 “setInterval” 或 “setTimeout”
看看下面的代码:
代码:
setInterval(
"document.getElementById('container').innerHTML += 'My new number: ' + i", 3000
);
不仅执行不高效,而且和 eval 函数有着同样的高风险。千万不要把字串传递给 setInterval 和 setTimeout。恰当的做法是,传递一个函数名:
代码:
setInterval(someFunction, 3000);
12. 不要使用with语句
初识之下,“with”语句似乎还挺好用的。它用于设置代码在特定对象中的作用域。其基本用法是提供深入到对象中处理元素的快速写法。例如:
代码:
with (being.person.man.bodyparts) {
arms = true;
legs = true;
}
– 等价于 —
代码:
being.person.man.bodyparts.arms = true;
being.person.man.bodyparts.legs= true;
不幸的是,测试表明,若你要为对象插入新成员,with的表现非常糟糕,它的执行速度非常缓慢。替代方案是声明一个变量:
代码:
var o = being.person.man.bodyparts;
o.arms = true;
o.legs = true;
13. 使用 {},而不用New Object()
在JavaScript有多种方式能新建对象。最传统的方法是 new 语句,如下:
代码:
var o = new Object();
o.name = 'Benhuoer';
o.lastName = 'Yang';
o.someFunction = function() {
console.log(this.name);
}
不过,这一方法读起来却比较糟糕。我强烈建议你采用下面这种在文字样式上更为强健的写法:
更好的写法
代码:
var o = {
name: 'Jeffrey',
lastName = 'Way',
someFunction : function() {
console.log(this.name);
}
};
注意,如果你想新建一个空对象,用 {} 就能行:
代码:
var o = {};
引用:
“对象字面符(Objects literals)帮助我们写出支持很多特性,同时又关联性强、简明直接的代码。没必要直接调用新建语句,然后再费心维护声明变量和传递变量的语句之间的正确顺序,等等。” – dyn-web.com
14. 使用[],而不用New Array()
新建数组时的同类型运用。
行得通的写法
代码:
var a = new Array();
a[0] = "Joe";
a[1] = 'Plumber';
更好的写法
代码:
var a = ['Joe','Plumber'];
引用:
“在JavaScript编程中经常遇到的一个错误是,该用数组时却用了对象,该用对象时却用了数组。规则其实很简单:当属性名是小的连续整数时,你应该使用数组。其他情况,使用对象。” – Douglas Crockford
15. 一长列变量声明?别写那么多var,用逗号吧
代码:
var someItem = 'some string';
var anotherItem = 'another string';
var oneMoreItem = 'one more string';
更好的写法
代码:
var someItem = 'some string',
anotherItem = 'another string',
oneMoreItem = 'one more string';
…不言自明。我不知道这样做能否提升代码执行速度,但是确实让你的代码干净许多。
17. 千万千万记得写分号
大部分浏览器都允许你不写句尾分号:
代码:
var someItem = 'some string'
function doSomething() {
return 'something'
}
之前已经说过,这样做会造成潜在的更大、更难以发现的问题:
更好的写法
代码:
var someItem = 'some string';
function doSomething() {
return 'something';
}
18. “For in” 语句
遍历对象时,你可能会发现你还需要获取方法函数。所以遇到这种情况时,请一定记得给你的代码包一层 if 语句,用以过滤信息。
代码:
for(key in object) {
if(object.hasOwnProperty(key) {
...then do something...
}
}
引自[/i][i] Douglas Crockford 所作:[/i][i] JavaScript: The Good Parts
19. 使用Firebug的“Timer”功能优化你的代码
想要轻松地快速了解某项操作的用时吗?使用Firebug的timer功能来记录结果好了。
代码:
function TimeTracker(){
console.time("MyTimer");
for(x=5000; x > 0; x--){}
console.timeEnd("MyTimer");
}
20. 读,读,读……Read, Read, Read…
虽然我是Web开发博客(就像这个!)的超级粉丝,但吃饭和睡觉前除了看书好像也别无选择~ 在你的床头柜上摆一本Web开发的好书吧!下列书单都是我的最爱:
- Object-Oriented JavaScript(暂无中文版)
- JavaScript: The Good Parts(中文版)
- Learning jQuery 1.3(暂无中文版,但你可以看看老版本的中文版)
- Learning JavaScript(中文版)
阅读他们…… 反复阅读很多次!我现在都还在读。
21. 自决的函数
相比于调用函数,让函数在页面载入或者某一父函数被调用时自动执行,是十分简单方便的做法。你只需要把你的函数包在父辈之内,然后添上一个额外的括号,本质上这括号就触发了你定义的函数(了解更多)。
代码:
(function doSomething() {
return {
name: 'jeff',
lastName: 'way'
};
})();
22. 原生 JavaScript 总是会比使用代码库来的快
诸如jQuery和Mootools这样的JavaScript库,能为你写代码的过程省下不少时间——尤其是当需要 AJAX 操作时。不过你可得记住,只要你的代码写得恰当,原生JavaScript总是会比利用代码库的写法执行得快一些。
jQuery的“each” 方法对于循环操作十分便利,但是使用原生态的for语句总归会快很多。
23. Crockford 的 JSON.Parse
尽管 JavaScript 2会内建JSON处理器,但写这篇文章之时,我们还是需要自己实现。Douglas Crockford,JSON的创建者,已经为我们创作出能直接使用的处理器了。您可以在这里下载。
导入这段代码,你就能新建 JSON 全局对象,然后处理你的 .json 文件。
代码:
var response = JSON.parse(xhr.responseText);
var container = document.getElementById('container');
for(var i = 0, len = response.length; i < len; i++) {
container.innerHTML += '<li>' + response.name + ' : ' + response.email + '</li>';
}
关于JSON,请查看更多介绍。
24. 移去“Language”
很多年前,language还是每段script标签必备属性:
代码:
<script type="text/javascript" language="javascript">
...
</script>
不过现在,这属性已经没啥用很久了…… 所以,删掉算啦!
就这些了,朋友们~
就是这些了,这就是我给JavaScript初学者的24条小建议。各位亲爱的朋友,你们的看法呢?你们有什么快速小贴士吗?感谢你的耐心阅读。
引用:
英文原文:24-javascript-best-practices-for-beginners
译文原文:给JavaScript新手的24条建议
© 请尊重版权,若需转载,务必保留链接。
深入理解JavaScript闭包(closure)
最近在网上查阅了不少Javascript闭包(closure)相关的资料,写的大多是非常的学术和专业。对于初学者来说别说理解闭包了,就连文字叙述都很难看懂。撰写此文的目的就是用最通俗的文字揭开Javascript闭包的真实面目。
一、什么是闭包?
“官方”的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
相信很少有人能直接看懂这句话,因为他描述的太学术。其实这句话通俗的来说就是:JavaScript中所有的function都是一个闭包。不过一般来说,嵌套的function所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。看下面这段代码:
function a() { var i = 0; function b() { alert(++i); } return b; } var c = a(); c();
这段代码有两个特点:
- 函数b嵌套在函数a内部;
- 函数a返回函数b。
引用关系如图:

这样在执行完var c=a()后,变量c实际上是指向了函数b,b中用到了变量i,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个我们通常所谓的“闭包”。
让我们说的更透彻一些。所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标 对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目 标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新 的值,和上次那次调用的是各自独立的。
为了更深刻的理解闭包,下面让我们继续探索闭包的作用和效果。
二、闭包有什么作用和效果?
简而言之,闭包的作用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,因为a的内部函数b的执行需要依赖a中的变量。这是对闭包作用的非常直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包需要循序渐进的过程。
在上面的例子中,由于闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。
那么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)
三、闭包的微观世界
如果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
- 当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
- 当执行函数a的时候,a会进入相应的执行环境(excution context)。
- 在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
- 然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
- 下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
- 最后把所有函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即a的作用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、a的活动对象和window对象,如下图所示:
如图所示,当在函数b中访问一个变量的时候,搜索顺序是:
- 先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。
- 如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
- 如果整个作用域链上都无法找到,则返回undefined。
小结,本段中提到了两个重要的词语:函数的定义与执行。文中提到函数的作用域是在定义函数时候就已经确定,而不是在执行的时候确定(参看步骤1和3)。用一段代码来说明这个问题:
function f(x) { var g = function () { return x; } return g; } var h = f(1); alert(h());
这段代码中变量h指向了f中的那个匿名函数(由g返回)。
- 假设函数h的作用域是在执行alert(h())确定的,那么此时h的作用域链是:h的活动对象->alert的活动对象->window对象。
- 假设函数h的作用域实在定义时确定的,就是说h指向的那个匿名函数在定义的时候就已经确定了作用域。那么在执行的时候,h的作用域链为:h的活动对象->f的活动对象->window对象。
如果第一种假设成立,那输出值就是undefined;如果第二种假设成立,输出值则为1。
运行结果证明了第2个假设是正确的,说明函数的作用域确实是在定义这个函数的时候就已经确定了。
四、闭包的应用场景
- 保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。
- 在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。
- 通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)推荐阅读:http://javascript.crockford.com/private.html
私有属性和方法在Constructor外是无法被访问的function Constructor(...) { var that = this; var membername = value; function membername(...) {...} }
以上3点是闭包最基本的应用场景,很多经典案例都源于此。
五、Javascript的垃圾回收机制
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。
六、结语
理解JavaScript的闭包是迈向高级JS程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。如果您对本文有任何的建议和疑问,欢迎留言。转载请著名出处。
Javascript 闭包
Javascript 闭包
翻译:为之漫笔
链接:http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.html
简介
- Closure
- 所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
闭包是 ECMAScript (JavaScript)最强大的特性之一,但用好闭包的前提是必须理解闭包。闭包的创建相对容易,人们甚至会在不经意间创建闭包,但这些无意创建的闭包却存在潜在的危害,尤其是在比较常见的浏览器环境下。如果想要扬长避短地使用闭包这一特性,则必须了解它们的工作机制。而闭包工作机制的实现很大程度上有赖于标识符(或者说对象属性)解析过程中作用域的角色。
关于闭包,最简单的描述就是 ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。
遗憾的是,要适当地理解闭包就必须理解闭包背后运行的机制,以及许多相关的技术细节。虽然本文的前半部分并没有涉及 ECMA 262 规范指定的某些算法,但仍然有许多无法回避或简化的内容。对于个别熟悉对象属性名解析的人来说,可以跳过相关的内容,但是除非你对闭包也非常熟悉,否则最好是不要跳过下面几节。
对象属性名解析
ECMAScript 认可两类对象:原生(Native)对象和宿主(Host)对象,其中宿主对象包含一个被称为内置对象的原生对象的子类(ECMA 262 3rd Ed Section 4.3)。原生对象属于语言,而宿主对象由环境提供,比如说可能是文档对象、DOM 等类似的对象。
原生对象具有松散和动态的命名属性(对于某些实现的内置对象子类别而言,动态性是受限的--但这不是太大的问题)。对象的命名属性用于保存值,该值可以是指向另一个对象(Objects)的引用(在这个意义上说,函数也是对象),也可以是一些基本的数据类型,比如:String、Number、Boolean、Null 或 Undefined。其中比较特殊的是 Undefined 类型,因为可以给对象的属性指定一个 Undefined 类型的值,而不会删除对象的相应属性。而且,该属性只是保存着 undefined 值。
下面简要介绍一下如何设置和读取对象的属性值,并最大程度地体现相应的内部细节。
值的赋予
对象的命名属性可以通过为该命名属性赋值来创建,或重新赋值。即,对于:
var objectRef = new Object(); //创建一个普通的 javascript 对象。
可以通过下面语句来创建名为 “testNumber” 的属性:
objectRef.testNumber = 5;
/* - 或- */
objectRef["testNumber"] = 5;
在赋值之前,对象中没有“testNumber” 属性,但在赋值后,则创建一个属性。之后的任何赋值语句都不需要再创建这个属性,而只会重新设置它的值:
objectRef.testNumber = 8;
/* - 或- */
objectRef["testNumber"] = 8;
稍后我们会介绍,Javascript 对象都有原型(prototypes)属性,而这些原型本身也是对象,因而也可以带有命名的属性。但是,原型对象命名属性的作用并不体现在赋值阶段。同样,在将值赋给其命名属性时,如果对象没有该属性则会创建该命名属性,否则会重设该属性的值。
值的读取
当读取对象的属性值时,原型对象的作用便体现出来。如果对象的原型中包含属性访问器(property accessor)所使用的属性名,那么该属性的值就会返回:
/* 为命名属性赋值。如果在赋值前对象没有相应的属性,那么赋值后就会得到一个:*/ objectRef.testNumber = 8; /* 从属性中读取值 */ var val = objectRef.testNumber; /* 现在, - val - 中保存着刚赋给对象命名属性的值 8*/
而且,由于所有对象都有原型,而原型本身也是对象,所以原型也可能有原型,这样就构成了所谓的原型链。原型链终止于链中原型为 null 的对象。Object 构造函数的默认原型就有一个 null 原型,因此:
var objectRef = new Object(); //创建一个普通的 JavaScript 对象。
创建了一个原型为 Object.prototype 的对象,而该原型自身则拥有一个值为 null 的原型。也就是说,objectRef 的原型链中只包含一个对象-- Object.prototype。但对于下面的代码而言:
/* 创建 - MyObject1 - 类型对象的函数*/ function MyObject1(formalParameter){ /* 给创建的对象添加一个名为 - testNumber - 的属性并将传递给构造函数的第一个参数指定为该属性的值:*/ this.testNumber = formalParameter; } /* 创建 - MyObject2 - 类型对象的函数*/ function MyObject2(formalParameter){ /* 给创建的对象添加一个名为 - testString - 的属性并将传递给构造函数的第一个参数指定为该属性的值:*/ this.testString = formalParameter; } /* 接下来的操作用 MyObject1 类的实例替换了所有与 MyObject2 类的实例相关联的原型。而且,为 MyObject1 构造函数传递了参数 - 8 - ,因而其 - testNumber - 属性被赋予该值:*/ MyObject2.prototype = new MyObject1( 8 ); /* 最后,将一个字符串作为构造函数的第一个参数, 创建一个 - MyObject2 - 的实例,并将指向该对象的 引用赋给变量 - objectRef - :*/ var objectRef = new MyObject2( "String_Value" );
被变量 objectRef 所引用的 MyObject2 的实例拥有一个原型链。该链中的第一个对象是在创建后被指定给 MyObject2 构造函数的 prototype 属性的 MyObject1 的一个实例。MyObject1 的实例也有一个原型,即与 Object.prototype 所引用的对象对应的默认的 Object 对象的原型。最后, Object.prototype 有一个值为 null 的原型,因此这条原型链到此结束。
当某个属性访问器尝试读取由 objectRef 所引用的对象的属性值时,整个原型链都会被搜索。在下面这种简单的情况下:
var val = objectRef.testString;
因为 objectRef 所引用的 MyObject2 的实例有一个名为“testString”的属性,因此被设置为“String_Value”的该属性的值被赋给了变量 val。但是:
var val = objectRef.testNumber;
则不能从 MyObject2 实例自身中读取到相应的命名属性值,因为该实例没有这个属性。然而,变量 val 的值仍然被设置为 8,而不是未定义--这是因为在该实例中查找相应的命名属性失败后,解释程序会继续检查其原型对象。而该实例的原型对象是 MyObject1 的实例,这个实例有一个名为“testNumber”的属性并且值为 8,所以这个属性访问器最后会取得值 8。而且,虽然 MyObject1 和 MyObject2 都没有定义 toString 方法,但是当属性访问器通过 objectRef 读取 toString 属性的值时:
var val = objectRef.toString;
变量 val 也会被赋予一个函数的引用。这个函数就是在 Object.prototype 的 toString 属性中所保存的函数。之所以会返回这个函数,是因为发生了搜索objectRef 原型链的过程。当在作为对象的 objectRef 中发现没有“toString”属性存在时,会搜索其原型对象,而当原型对象中不存在该属性时,则会继续搜索原型的原型。而原型链中最终的原型是 Object.prototype,这个对象确实有一个 toString 方法,因此该方法的引用被返回。
最后:
var val = objectRef.madeUpProperty;
返回 undefined,因为在搜索原型链的过程中,直至 Object.prototype 的原型--null,都没有找到任何对象有名为“madeUpPeoperty”的属性,因此最终返回 undefined。
不论是在对象或对象的原型中,读取命名属性值的时候只返回首先找到的属性值。而当为对象的命名属性赋值时,如果对象自身不存在该属性则创建相应的属性。
这意味着,如果执行像 objectRef.testNumber = 3 这样一条赋值语句,那么这个 MyObject2 的实例自身也会创建一个名为“testNumber”的属性,而之后任何读取该命名属性的尝试都将获得相同的新值。这时候,属性访问器不会再进一步搜索原型链,但 MyObject1 实例值为 8 的“testNumber”属性并没有被修改。给 objectRef 对象的赋值只是遮挡了其原型链中相应的属性。
注意:ECMAScript 为 Object 类型定义了一个内部 [[prototype]] 属性。这个属性不能通过脚本直接访问,但在属性访问器解析过程中,则需要用到这个内部 [[prototype]] 属性所引用的对象链--即原型链。可以通过一个公共的 prototype 属性,来对与内部的 [[prototype]] 属性对应的原型对象进行赋值或定义。这两者之间的关系在 ECMA 262(3rd edition)中有详细描述,但超出了本文要讨论的范畴。
标识符解析、执行环境和作用域链
执行环境
执行环境是 ECMAScript 规范(ECMA 262 第 3 版)用于定义 ECMAScript 实现必要行为的一个抽象的概念。对如何实现执行环境,规范没有作规定。但由于执行环境中包含引用规范所定义结构的相关属性,因此执行环境中应该保有(甚至实现)带有属性的对象--即使属性不是公共属性。
所有 JavaScript 代码都是在一个执行环境中被执行的。全局代码(作为内置的 JS 文件执行的代码,或者 HTML 页面加载的代码)是在我将称之为“全局执行环境”的执行环境中执行的,而对函数的每次调用(有可能是作为构造函数)同样有关联的执行环境。通过 eval 函数执行的代码也有截然不同的执行环境,但因为 JavaScript 程序员在正常情况下一般不会使用 eval,所以这里不作讨论。有关执行环境的详细说明请参阅 ECMA 262(第 3 版)第 10.2 节。
当调用一个 JavaScript 函数时,该函数就会进入相应的执行环境。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的 JavaScript 代码就构成了一个执行环境栈。
在创建执行环境的过程中,会按照定义的先后顺序完成一系列操作。首先,在一个函数的执行环境中,会创建一个“活动”对象。活动对象是规范中规定的另外一种机制。之所以称之为对象,是因为它拥有可访问的命名属性,但是它又不像正常对象那样具有原型(至少没有预定义的原型),而且不能通过 JavaScript 代码直接引用活动对象。
为函数调用创建执行环境的下一步是创建一个 arguments 对象,这是一个类似数组的对象,它以整数索引的数组成员一一对应地保存着调用函数时所传递的参数。这个对象也有 length 和 callee 属性(这两个属性与我们讨论的内容无关,详见规范)。然后,会为活动对象创建一个名为“arguments”的属性,该属性引用前面创建的 arguments 对象。
接着,为执行环境分配作用域。作用域由对象列表(链)组成。每个函数对象都有一个内部的 [[scope]] 属性(该属性我们稍后会详细介绍),这个属性也由对象列表(链)组成。指定给一个函数调用执行环境的作用域,由该函数对象的 [[scope]] 属性所引用的对象列表(链)组成,同时,活动对象被添加到该对象列表的顶部(链的前端)。
之后会发生由 ECMA 262 中所谓“可变”对象完成的“变量实例化”的过程。只不过此时使用活动对象作为可变对象(这里很重要,请注意:它们是同一个对象)。此时会将函数的形式参数创建为可变对象命名属性,如果调用函数时传递的参数与形式参数一致,则将相应参数的值赋给这些命名属性(否则,会给命名属性赋 undefined 值)。对于定义的内部函数,会以其声明时所用名称为可变对象创建同名属性,而相应的内部函数则被创建为函数对象并指定给该属性。变量实例化的最后一步是将在函数内部声明的所有局部变量创建为可变对象的命名属性。
根据声明的局部变量创建的可变对象的属性在变量实例化过程会被赋予 undefined 值。在执行函数体内的代码、并计算相应的赋值表达式之前不会对局部变量执行真正的实例化。
事实上,拥有 arguments 属性的活动对象和拥有与函数局部变量对应的命名属性的可变对象是同一个对象。因此,可以将标识符 arguments 作为函数的局部变量来看待。
最后,在this可以被使用之前,还必须先对其赋值。如果赋的值是一个对象的引用,则 this.m 访问的便是该对象上的 m。如果(内部)赋的值是 null,则this就指向全局对象。 (此段由 pangba 刘未鹏 翻译)
(原文备考:Finally a value is assigned for use with the this keyword. If the value assigned refers to an object then property accessors prefixed with the this keyword reference properties of that object. If the value assigned (internally) is null then the this keyword will refer to the global object. )
创建全局执行环境的过程会稍有不同,因为它没有参数,所以不需要通过定义的活动对象来引用这些参数。但全局执行环境也需要一个作用域,而它的作用域链实际上只由一个对象--全局对象--组成。全局执行环境也会有变量实例化的过程,它的内部函数就是涉及大部分 JavaScript 代码的、常规的顶级函数声明。而且,在变量实例化过程中全局对象就是可变对象,这就是为什么全局性声明的函数是全局对象属性的原因。全局性声明的变量同样如此。
全局执行环境也会使用 this 对象来引用全局对象。
作用域链与 [[scope]]
调用函数时创建的执行环境会包含一个作用域链,这个作用域链是通过将该执行环境的活动(可变)对象添加到保存于所调用函数对象的 [[scope]] 属性中的作用域链前端而构成的。所以,理解函数对象内部的 [[scope]] 属性的定义过程至关重要。
在 ECMAScript 中,函数也是对象。函数对象在变量实例化过程中会根据函数声明来创建,或者是在计算函数表达式或调用 Function 构造函数时创建。
通过调用 Function 构造函数创建的函数对象,其内部的 [[scope]] 属性引用的作用域链中始终只包含全局对象。
通过函数声明或函数表达式创建的函数对象,其内部的 [[scope]] 属性引用的则是创建它们的执行环境的作用域链。
在最简单的情况下,比如声明如下全局函数:-
function exampleFunction(formalParameter){
... // 函数体内的代码
}
当为创建全局执行环境而进行变量实例化时,会根据上面的函数声明创建相应的函数对象。因为全局执行环境的作用域链中只包含全局对象,所以它就给自己创建的、并以名为“exampleFunction”的属性引用的这个函数对象的内部 [[scope]] 属性,赋予了只包含全局对象的作用域链。
当在全局环境中计算函数表达式时,也会发生类似的指定作用域链的过程:-
var exampleFuncRef = function(){
... // 函数体代码
}
在这种情况下,不同的是在全局执行环境的变量实例化过程中,会先为全局对象创建一个命名属性。而在计算赋值语句之前,暂时不会创建函数对象,也不会将该函数对象的引用指定给全局对象的命名属性。但是,最终还是会在全局执行环境中创建这个函数对象(当计算函数表达式时。译者注),而为这个创建的函数对象的 [[scope]] 属性指定的作用域链中仍然只包含全局对象。
内部的函数声明或表达式会导致在包含它们的外部函数的执行环境中创建相应的函数对象,因此这些函数对象的作用域链会稍微复杂一些。在下面的代码中,先定义了一个带有内部函数声明的外部函数,然后调用外部函数:
function exampleOuterFunction(formalParameter){
function exampleInnerFuncitonDec(){
... // 内部函数体代码
}
... // 其余的外部函数体代码
}
exampleOuterFunction( 5 );
与外部函数声明对应的函数对象会在全局执行环境的变量实例化过程中被创建。因此,外部函数对象的 [[scope]] 属性中会包含一个只有全局对象的“单项目”作用域链。
当在全局执行环境中调用 exampleOuterFunction 函数时,会为该函数调用创建一个新的执行环境和一个活动(可变)对象。这个新执行环境的作用域就由新的活动对象后跟外部函数对象的 [[scope]] 属性所引用的作用域链(只有全局对象)构成。在新执行环境的变量实例化过程中,会创建一个与内部函数声明对应的函数对象,而同时会给这个函数对象的 [[scope]] 属性指定创建该函数对象的执行环境(即新执行环境。译者注)的作用域值--即一个包含活动对象后跟全局对象的作用域链。
到目前为止,所有过程都是自动、或者由源代码的结构所控制的。但我们发现,执行环境的作用域链定义了执行环境所创建的函数对象的 [[scope]] 属性,而函数对象的 [[scope]] 属性则定义了它的执行环境的作用域(包括相应的活动对象)。不过,ECMAScript 也提供了用于修改作用域链 with 语句。
with 语句会计算一个表达式,如果该表达式是一个对象,那么就将这个对象添加到当前执行环境的作用域链中(在活动<可变>对象之前)。然后,执行 with 语句(它自身也可能是一个语句块)中的其他语句。之后,又恢复到调用它之前的执行环境的作用域链中。
with 语句不会影响在变量实例化过程中根据函数声明创建函数对象。但是,可以在一个 with 语句内部对函数表达式求值:-
/* 创建全局变量 - y - 它引用一个对象:- */ var y = {x:5}; // 带有一个属性 - x - 的对象直接量 function exampleFuncWith(){ var z; /* 将全局对象 - y - 引用的对象添加到作用域链的前端:- */ with(y){ /* 对函数表达式求值,以创建函数对象并将该函数对象的引用指定给局部变量 - z - :- */ z = function(){ ... // 内部函数表达式中的代码; } } ... } /* 执行 - exampleFuncWith - 函数:- */ exampleFuncWith();
在调用 exampleFuncWith 函数所创建的执行环境中包含一个由其活动对象后跟全局对象构成的作用域链。而在执行 with 语句时,又会把全局变量 y 引用的对象添加到这个作用域链的前端。在对其中的函数表达式求值的过程中,所创建函数对象的 [[scope]] 属性与创建它的执行环境的作用域保持一致--即,该属性会引用一个由对象 y 后跟调用外部函数时所创建执行环境的活动对象,后跟全局对象的作用域链。
当与 with 语句相关的语句块执行结束时,执行环境的作用域得以恢复(y 会被移除),但是已经创建的函数对象(z。译者注)的 [[scope]] 属性所引用的作用域链中位于最前面的仍然是对象 y。
标识符解析
标识符是沿作用域链逆向解析的。ECMA 262 将 this 归类为关键字而不是标识符,并非不合理。因为解析 this 值时始终要根据使用它的执行环境来判断,而与作用域链无关。
标识符解析从作用域链中的第一个对象开始。检查该对象中是否包含与标识符对应的属性名。因为作用域链是一条对象链,所以这个检查过程也会包含相应对象的原型链(如果有)。如果没有在作用域链的第一个对象中发现相应的值,解析过程会继续搜索下一个对象。这样依次类推直至找到作用域链中包含以标识符为属性名的对象为止,也有可能在作用域链的所有对象中都没有发现该标识符。
当基于对象使用属性访问器时,也会发生与上面相同的标识符解析过程。当属性访问器中有相应的属性可以替换某个对象时,这个属性就成为表示该对象的标识符,该对象在作用域链中的位置进而被确定。全局对象始终都位于作用域链的尾端。
因为与函数调用相关的执行环境将会把活动(可变)对象添加到作用域链的前端,所以在函数体内使用的标识符会首先检查自己是否与形式参数、内部函数声明的名称或局部变量一致。这些都可以由活动(可变)对象的命名属性来确定。
闭包
自动垃圾收集
ECMAScript 要求使用自动垃圾收集机制。但规范中并没有详细说明相关的细节,而是留给了实现来决定。但据了解,相当一部分实现对它们的垃圾收集操作只赋予了很低的优先级。但是,大致的思想都是相同的,即如果对象不再“可引用(由于不存在对它的引用,使执行代码无法再访问到它)”时,该对象就成为垃圾收集的目标。因而,在将来的某个时刻会将这个对象销毁并将它所占用的一切资源释放,以便操作系统重新利用。
正常情况下,当退出一个执行环境时就会满足类似的条件。此时,作用域链结构中的活动(可变)对象以及在该执行环境中创建的任何对象--包括函数对象,都不再“可引用”,因此将成为垃圾收集的目标。
构成闭包
闭包是通过在对一个函数调用的执行环境中返回一个函数对象构成的。比如,在对函数调用的过程中,将一个对内部函数对象的引用指定给另一个对象的属性。或者,直接将这样一个(内部)函数对象的引用指定给一个全局变量、或者一个全局性对象的属性,或者一个作为参数以引用方式传递给外部函数的对象。例如:-
function exampleClosureForm(arg1, arg2){
var localVar = 8;
function exampleReturned(innerArg){
return ((arg1 + arg2)/(innerArg + localVar));
}
/* 返回一个定义为 exampleReturned 的内部函数的引用 -:- */
return exampleReturned;
}
var globalVar = exampleClosureForm(2, 4);
这种情况下,在调用外部函数 exampleClosureForm 的执行环境中所创建的函数对象就不会被当作垃圾收集,因为该函数对象被一个全局变量所引用,而且仍然是可以访问的,甚至可以通过 globalVar(n) 来执行。
的确,情况比正常的时候要复杂一些。因为现在这个被变量 globalVar 引用的内部函数对象的 [[scope]] 属性所引用的作用域链中,包含着属于创建该内部函数对象的执行环境的活动对象(和全局对象)。由于在执行被 globalVar 引用的函数对象时,每次都要把该函数对象的 [[scope]] 属性所引用的整个作用域链添加到创建的(内部函数的)执行环境的作用域中(即此时的作用域中包括:内部执行环境的活动对象、外部执行环境的活动对象、全局对象。译者注), 所以这个(外部执行环境的)活动对象不会被当作垃圾收集。
闭包因此而构成。此时,内部函数对象拥有自由的变量,而位于该函数作用域链中的活动(可变)对象则成为与变量绑定的环境。
由于活动(可变)对象受限于内部函数对象(现在被 globalVar 变量引用)的 [[scope]] 属性中作用域链的引用,所以活动对象连同它的变量声明--即属性的值,都会被保留。而在对内部函数调用的执行环境中进行作用域解析时,将会把与活动(可变)对象的命名属性一致的标识符作为该对象的属性来解析。活动对象的这些属性值即使是在创建它的执行环境退出后,仍然可以被读取和设置。
在上面的例子中,当外部函数返回(退出它的执行环境)时,其活动(可变)对象的变量声明中记录了形式参数、内部函数定义以及局部变量的值。arg1 属性的值为 2,而 arg2 属性的值为 4,localVar 的值是 8,还有一个 exampleReturned 属性,它引用由外部函数返回的内部函数对象。(为方便起见,我们将在后面的讨论中,称这个活动<可变>对象为 “ActOuter1″)。
如果再次调用 exampleClosureForm 函数,如:-
var secondGlobalVar = exampleClosureForm(12, 3);
- 则会创建一个新的执行环境和一个新的活动对象。而且,会返回一个新的函数对象,该函数对象的 [[scope]] 属性引用的作用域链与前一次不同,因为这一次的作用域链中包含着第二个执行环境的活动对象,而这个活动对象的属性 arg1 值为 12 而属性 arg2 值为 3。(为方便起见,我们将在后面的讨论中,称这个活动<可变>对象为 “ActOuter2″)。
通过第二次执行 exampleClosureForm 函数,第二个、也是截然不同的闭包诞生了。
通过执行 exampleClosureForm 创建的两个函数对象分别被指定给了全局变量 globalVar 和 secondGlobalVar,并返回了表达式 ((arg1 + arg2)/(innerArg + localVar))。该表达式对其中的四个标识符应用了不同的操作符。如何确定这些标识符的值是体现闭包价值的关键所在。
我们来看一看,在执行由 globalVar 引用的函数对象--如 globalVar(2)--时的情形。此时,会创建一个新的执行环境和相应的活动对象(我们将称之为“ActInner1”),并把该活动对象添加到执行的函数对象的 [[scope]] 属性所引用的作用域链的前端。ActInner1 会带有一个属性 innerArg,根据传递的形式参数,其值被指定为 2。这个新执行环境的作用域链变成: ActInner1->ActOuter1->全局对象.
为了返回表达式 ((arg1 + arg2)/(innerArg + localVar)) 的值,要沿着作用域链进行标识符解析。表达式中标识符的值将通过依次查找作用域链中每个对象(与标识符名称一致)的属性来确定。
作用域链中的第一个对象是 ActInner1,它有一个名为 innerArg 的属性,值是 2。所有其他三个标识符在 ActOuter1 中都有对应的属性:arg1 是 2,arg2 是 4 而 localVar 是 8。最后,函数调用返回 ((2 + 2)/(2 + 8))。
现在再来看一看由 secondGlobalVar 引用的同一个函数对象的执行情况,比如 secondGlobalVar(5)。我们把这次创建的新执行环境的活动对象称为 “ActInner2”,相应的作用域链就变成了:ActInner2->ActOuter2->全局对象。ActInner2 返回 innerArg 的值 5,而 ActOuter2 分别返回 arg1、arg2 和 localVar 的值 12、3 和 8。函数调用返回的值就是 ((12 + 3)/(5 + 8))。
如果再执行一次 secondGlobalVar,则又会有一个新活动对象被添加到作用域链的前端,但 ActOuter2 仍然是链中的第二个对象,而他的命名属性会再次用于完成标识符 arg1、arg2 和 localVar 的解析。
这就是 ECMAScript 的内部函数获取、维持和访问创建他们的执行环境的形式参数、声明的内部函数以及局部变量的过程。这个过程说明了构成闭包以后,内部的函数对象在其存续过程中,如何维持对这些值的引用、如何对这些值进行读取的机制。即,创建内部函数对象的执行环境的活动(可变)对象,会保留在该函数对象的 [[scope]] 属性所引用的作用域链中。直到所有对这个内部函数的引用被释放,这个函数对象才会成为垃圾收集的目标(连同它的作用域链中任何不再需要的对象)。
内部函数自身也可能有内部函数。在通过函数执行返回内部函数构成闭包以后,相应的闭包自身也可能会返回内部函数从而构成它们自己的闭包。每次作用域链嵌套,都会增加由创建内部函数对象的执行环境引发的新活动对象。ECMAScript 规范要求作用域链是临时性的,但对作用域链的长度却没有加以限制。在具体实现中,可能会存在实际的限制,但还没有发现有具体限制数量的报告。目前来看,嵌套的内部函数所拥有的潜能,仍然超出了使用它们的人的想像能力。
通过闭包可以做什么?
对这个问题的回答可能会令你惊讶--闭包什么都可以做。据我所知,闭包使得 ECMAScript 能够模仿任何事物,因此局限性在于设计和实现要模仿事物的能力。只是从字面上看可能会觉得这么说很深奥,下面我们就来看一些更有实际意义的例子。
例 1:为函数引用设置延时
闭包的一个常见用法是在执行函数之前为要执行的函数提供参数。例如:将函数作为 setTimout 函数的第一个参数,这在 Web 浏览器的环境下是非常常见的一种应用。
setTimeout 用于有计划地执行一个函数(或者一串 JavaScript 代码,不是在本例中),要执行的函数是其第一个参数,其第二个参数是以毫秒表示的执行间隔。也就是说,当在一段代码中使用 setTimeout 时,要将一个函数的引用作为它的第一个参数,而将以毫秒表示的时间值作为第二个参数。但是,传递函数引用的同时无法为计划执行的函数提供参数。
然而,可以在代码中调用另外一个函数,由它返回一个对内部函数的引用,再把这个对内部函数对象的引用传递给 setTimeout 函数。执行这个内部函数时要使用的参数在调用返回它的外部函数时传递。这样,setTimeout 在执行这个内部函数时,不用传递参数,但该内部函数仍然能够访问在调用返回它的外部函数时传递的参数:
function callLater(paramA, paramB, paramC){
/* 返回一个由函数表达式创建的匿名内部函数的引用:- */
return (function(){
/* 这个内部函数将通过 - setTimeout - 执行,
而且当它执行时它会读取并按照传递给
外部函数的参数行事:
*/
paramA[paramB] = paramC;
});
}
...
/* 调用这个函数将返回一个在其执行环境中创建的内部函数对象的引用。
传递的参数最终将作为外部函数的参数被内部函数使用。
返回的对内部函数的引用被赋给一个全局变量:-
*/
var functRef = callLater(elStyle, "display", "none");
/* 调用 setTimeout 函数,将赋给变量 - functRef -
的内部函数的引用作为传递的第一个参数:- */
hideMenu=setTimeout(functRef, 500);
例 2: 通过对象实例方法关联函数
许多时候我们需要将一个函数对象暂时挂到一个引用上留待后面执行,因为不等到执行的时候是很难知道其具体参数的,而先前将它赋给那个引用的时候更是压根不知道的。 (此段由 pangba 刘未鹏 翻译)
(luyy朋友的翻译_2008-7-7更新)很多时候需要将一个函数引用进行赋值,以便在将来某个时候执行该函数,在执行这些函数时给函数提供参数将会是有用处的,但这些参数在执行时不容易获得,他们只有在上面赋值给时才能确定。
(原文备考:There are many other circumstances when a reference to a function object is assigned so that it would be executed at some future time where it is useful to provide parameters for the execution of that function that would not be easily available at the time of execution but cannot be known until the moment of assignment.)
一个相关的例子是,用 JavaScript 对象来封装与特定 DOM 元素的交互。这个 JavaScript 对象具有 doOnClick、doMouseOver 和 doMouseOut 方法,并且当用户在该特定的 DOM 元素中触发了相应的事件时要执行这些方法。不过,可能会创建与不同的 DOM 元素关联的任意数量的 JavaScript 对象,而且每个对象实例并不知道实例化它们的代码将会如何操纵它们(即注册事件处理函数与定义相应的事件处理函数分离。译者注)。这些对象实例并不知道如何在全局环境中引用它们自身,因为它们不知道将会指定哪个全局变量(如果有)引用它们的实例。
因而问题可以归结为执行一个与特定的 JavaScript 对象关联的事件处理函数,并且要知道调用该对象的哪个方法。
下面这个例子使用了一个基于闭包构建的一般化的函数(此句多谢未鹏指点),该函数会将对象实例与 DOM 元素事件关联起来,安排执行事件处理程序时调用对象实例的指定方法,给象的指定方法传递的参数是事件对象和与元素关联的引用,该函数返回执行相应方法后的返回值。
/* 一个关联对象实例和事件处理器的函数。 它返回的内部函数被用作事件处理器。对象实例以 - obj - 参数表示, 而在该对象实例中调用的方法名则以 - methodName - (字符串)参数表示。 */ function associateObjWithEvent(obj, methodName){ /* 下面这个返回的内部函数将作为一个 DOM 元素的事件处理器*/ return (function(e){ /* 在支持标准 DOM 规范的浏览器中,事件对象会被解析为参数 - e - , 若没有正常解析,则使用 IE 的事件对象来规范化事件对象。 */ e = e||window.event; /* 事件处理器通过保存在字符串 - methodName - 中的方法名调用了对象 - obj - 的一个方法。并传递已经规范化的事件对象和触发事件处理器的元素 的引用 - this - (之所以 this 有效是因为这个内部函数是作为该元素的方法执行的) */ return obj[methodName](e, this); }); } /* 这个构造函数用于创建将自身与 DOM 元素关联的对象, DOM 元素的 ID 作为构造函数的字符串参数。 所创建的对象会在相应的元素触发 onclick、 onmouseover 或 onmouseout 事件时, 调用相应的方法。 */ function DhtmlObject(elementId){ /* 调用一个返回 DOM 元素(如果没找到返回 null)引用的函数, 必需的参数是 ID。 将返回的值赋给局部变量 - el -。 */ var el = getElementWithId(elementId); /* - el - 值会在内部通过类型转换变为布尔值,以便 - if - 语句加以判断。 因此,如果它引用一个对象结果将返回 true,如果是 null 则返回 false。 下面的代码块只有当 - el - 变量返回一个 DOM 元素时才会被执行。 */ if(el){ /* 为给元素的事件处理器指定一个函数,该对象调用了 - associateObjWithEvent - 函数。 同时对象将自身(通过 - this - 关键字)作为调用方法的对象, 并提供了调用的方法名称。 - associateObjWithEvent - 函数会返回 一个内部函数,该内部函数被指定为 DOM 元素的事件处理器。 在响应事件时,执行这个内部函数就会调用必要的方法。 */ el.onclick = associateObjWithEvent(this, "doOnClick"); el.onmouseover = associateObjWithEvent(this, "doMouseOver"); el.onmouseout = associateObjWithEvent(this, "doMouseOut"); ... } } DhtmlObject.prototype.doOnClick = function(event, element){ ... // doOnClick 方法体。. } DhtmlObject.prototype.doMouseOver = function(event, element){ ... // doMouseOver 方法体。 } DhtmlObject.prototype.doMouseOut = function(event, element){ ... // doMouseOut 方法体。 }
这样,DhtmlObject 的任何实例都会将自身与相应的 DOM 元素关联起来,而这些 DOM 元素不必知道其他代码如何操纵它们(即当触发相应事件时,会执行什么代码。译者注),也不必理会全局命名空间的影响以及与 DhtmlObject 的其他实例间存在冲突的危险。
例 3:包装相关的功能
闭包可以用于创建额外的作用域,通过该作用域可以将相关的和具有依赖性的代码组织起来,以便将意外交互的风险降到最低。假设有一个用于构建字符串的函数,为了避免重复性的连接操作(和创建众多的中间字符串),我们的愿望是使用一个数组按顺序来存储字符串的各个部分,然后再使用 Array.prototype.join 方法(以空字符串作为其参数)输出结果。这个数组将作为输出的缓冲器,但是将数组作为函数的局部变量又会导致在每次调用函数时都重新创建一个新数组,这在每次调用函数时只重新指定数组中的可变内容的情况下并不是必要的。
一种解决方案是将这个数组声明为全局变量,这样就可以重用这个数组,而不必每次都建立新数组。但这个方案的结果是,除了引用函数的全局变量会使用这个缓冲数组外,还会多出一个全局属性引用数组自身。如此不仅使代码变得不容易管理,而且,如果要在其他地方使用这个数组时,开发者必须要再次定义函数和数组。这样一来,也使得代码不容易与其他代码整合,因为此时不仅要保证所使用的函数名在全局命名空间中是唯一的,而且还要保证函数所依赖的数组在全局命名空间中也必须是唯一的。
而通过闭包可以使作为缓冲器的数组与依赖它的函数关联起来(优雅地打包),同时也能够维持在全局命名空间外指定的缓冲数组的属性名,免除了名称冲突和意外交互的危险。
其中的关键技巧在于通过执行一个单行(in-line)函数表达式创建一个额外的执行环境,而将该函数表达式返回的内部函数作为在外部代码中使用的函数。此时,缓冲数组被定义为函数表达式的一个局部变量。这个函数表达式只需执行一次,而数组也只需创建一次,就可以供依赖它的函数重复使用。
下面的代码定义了一个函数,这个函数用于返回一个 HTML 字符串,其中大部分内容都是常量,但这些常量字符序列中需要穿插一些可变的信息,而可变的信息由调用函数时传递的参数提供。
通过执行单行函数表达式返回一个内部函数,并将返回的函数赋给一个全局变量,因此这个函数也可以称为全局函数。而缓冲数组被定义为外部函数表达式的一个局部变量。它不会暴露在全局命名空间中,而且无论什么时候调用依赖它的函数都不需要重新创建这个数组。
/* 声明一个全局变量 - getImgInPositionedDivHtml - 并将一次调用一个外部函数表达式返回的内部函数赋给它。 这个内部函数会返回一个用于表示绝对定位的 DIV 元素 包围着一个 IMG 元素 的 HTML 字符串,这样一来, 所有可变的属性值都由调用该函数时的参数提供: */ var getImgInPositionedDivHtml = (function(){ /* 外部函数表达式的局部变量 - buffAr - 保存着缓冲数组。 这个数组只会被创建一次,生成的数组实例对内部函数而言永远是可用的 因此,可供每次调用这个内部函数时使用。 其中的空字符串用作数据占位符,相应的数据 将由内部函数插入到这个数组中: */ var buffAr = [ '<div id="', '', //index 1, DIV ID 属性 '" style="position:absolute;top:', '', //index 3, DIV 顶部位置 'px;left:', '', //index 5, DIV 左端位置 'px;width:', '', //index 7, DIV 宽度 'px;height:', '', //index 9, DIV 高度 'px;overflow:hidden;\"><img src=\"', '', //index 11, IMG URL '\" width=\"', '', //index 13, IMG 宽度 '\" height=\"', '', //index 15, IMG 调蓄 '\" alt=\"', '', //index 17, IMG alt 文本内容 '\"><\/div>' ]; /* 返回作为对函数表达式求值后结果的内部函数对象。 这个内部函数就是每次调用执行的函数 - getImgInPositionedDivHtml( ... ) - */ return (function(url, id, width, height, top, left, altText){ /* 将不同的参数插入到缓冲数组相应的位置: */ buffAr[1] = id; buffAr[3] = top; buffAr[5] = left; buffAr[13] = (buffAr[7] = width); buffAr[15] = (buffAr[9] = height); buffAr[11] = url; buffAr[17] = altText; /* 返回通过使用空字符串(相当于将数组元素连接起来) 连接数组每个元素后形成的字符串: */ return buffAr.join(''); }); //:内部函数表达式结束。 })(); /*^^- :单行外部函数表达式。*/
如果一个函数依赖于另一(或多)个其他函数,而其他函数又没有必要被其他代码直接调用,那么可以运用相同的技术来包装这些函数,而通过一个公开暴露的函数来调用它们。这样,就将一个复杂的多函数处理过程封装成了一个具有移植性的代码单元。
其他例子
有关闭包的一个可能是最广为人知的应用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects。这种应用方式可以扩展到各种嵌套包含的可访问性(或可见性)的作用域结构,包括 the emulation of private static members for ECMAScript objects。
闭包可能的用途是无限的,可能理解其工作原理才是把握如何使用它的最好指南。
意外的闭包
在创建可访问的内部函数的函数体之外解析该内部函数就会构成闭包。这表明闭包很容易创建,但这样一来可能会导致一种结果,即没有认识到闭包是一种语言特性的 JavaScript 作者,会按照内部函数能完成多种任务的想法来使用内部函数。但他们对使用内部函数的结果并不明了,而且根本意识不到创建了闭包,或者那样做意味着什么。
正如下一节谈到 IE 中内存泄漏问题时所提及的,意外创建的闭包可能导致严重的负面效应,而且也会影响到代码的性能。问题不在于闭包本身,如果能够真正做到谨慎地使用它们,反而会有助于创建高效的代码。换句话说,使用内部函数会影响到效率。
使用内部函数最常见的一种情况就是将其作为 DOM 元素的事件处理器。例如,下面的代码用于向一个链接元素添加 onclick 事件处理器:
/* 定义一个全局变量,通过下面的函数将它的值 作为查询字符串的一部分添加到链接的 - href - 中: */ var quantaty = 5; /* 当给这个函数传递一个链接(作为函数中的参数 - linkRef -)时, 会将一个 onclick 事件处理器指定给该链接,该事件处理器 将全局变量 - quantaty - 的值作为字符串添加到链接的 - href - 属性中,然后返回 true 使该链接在单击后定位到由 - href - 属性包含的查询字符串指定的资源: */ function addGlobalQueryOnClick(linkRef){ /* 如果可以将参数 - linkRef - 通过类型转换为 ture (说明它引用了一个对象): */ if(linkRef){ /* 对一个函数表达式求值,并将对该函数对象的引用 指定给这个链接元素的 onclick 事件处理器: */ linkRef.onclick = function(){ /* 这个内部函数表达式将查询字符串 添加到附加事件处理器的元素的 - href - 属性中: */ this.href += ('?quantaty='+escape(quantaty)); return true; }; } }
无论什么时候调用 addGlobalQueryOnClick 函数,都会创建一个新的内部函数(通过赋值构成了闭包)。从效率的角度上看,如果只是调用一两次 addGlobalQueryOnClick 函数并没有什么大的妨碍,但如果频繁使用该函数,就会导致创建许多截然不同的函数对象(每对内部函数表达式求一次值,就会产生一个新的函数对象)。
上面例子中的代码没有关注内部函数在创建它的函数外部可以访问(或者说构成了闭包)这一事实。实际上,同样的效果可以通过另一种方式来完成。即单独地定义一个用于事件处理器的函数,然后将该函数的引用指定给元素的事件处理属性。这样,只需创建一个函数对象,而所有使用相同事件处理器的元素都可以共享对这个函数的引用:
/* 定义一个全局变量,通过下面的函数将它的值 作为查询字符串的一部分添加到链接的 - href - 中: */ var quantaty = 5; /* 当把一个链接(作为函数中的参数 - linkRef -)传递给这个函数时, 会给这个链接添加一个 onclick 事件处理器,该事件处理器会 将全局变量 - quantaty - 的值作为查询字符串的一部分添加到 链接的 - href - 中,然后返回 true,以便单击链接时定位到由 作为 - href - 属性值的查询字符串所指定的资源: */ function addGlobalQueryOnClick(linkRef){ /* 如果 - linkRef - 参数能够通过类型转换为 true (说明它引用了一个对象): */ if(linkRef){ /* 将一个对全局函数的引用指定给这个链接 的事件处理属性,使函数成为链接元素的事件处理器: */ linkRef.onclick = forAddQueryOnClick; } } /* 声明一个全局函数,作为链接元素的事件处理器, 这个函数将一个全局变量的值作为要添加事件处理器的 链接元素的 - href - 值的一部分: */ function forAddQueryOnClick(){ this.href += ('?quantaty='+escape(quantaty)); return true; }
在上面例子的第一个版本中,内部函数并没有作为闭包发挥应有的作用。在那种情况下,反而是不使用闭包更有效率,因为不用重复创建许多本质上相同的函数对象。
类似地考量同样适用于对象的构造函数。与下面代码中的构造函数框架类似的代码并不罕见:
function ExampleConst(param){
/* 通过对函数表达式求值创建对象的方法,
并将求值所得的函数对象的引用赋给要创建对象的属性:
*/
this.method1 = function(){
... // 方法体。
};
this.method2 = function(){
... // 方法体。
};
this.method3 = function(){
... // 方法体。
};
/* 把构造函数的参数赋给对象的一个属性:
*/
this.publicProp = param;
}
每当通过 new ExampleConst(n) 使用这个构造函数创建一个对象时,都会创建一组新的、作为对象方法的函数对象。因此,创建的对象实例越多,相应的函数对象也就越多。
Douglas Crockford 提出的模仿 JavaScript 对象私有成员的技术,就利用了将对内部函数的引用指定给在构造函数中构造对象的公共属性而形成的闭包。如果对象的方法没有利用在构造函数中形成的闭包,那么在实例化每个对象时创建的多个函数对象,会使实例化过程变慢,而且将有更多的资源被占用,以满足创建更多函数对象的需要。
这那种情况下,只创建一次函数对象,并把它们指定给构造函数 prototype 的相应属性显然更有效率。这样一来,它们就能被构造函数创建的所有对象共享了:
function ExampleConst(param){
/* 将构造函数的参数赋给对象的一个属性:
*/
this.publicProp = param;
}
/* 通过对函数表达式求值,并将结果函数对象的引用
指定给构造函数原型的相应属性来创建对象的方法:
*/
ExampleConst.prototype.method1 = function(){
... // 方法体。
};
ExampleConst.prototype.method2 = function(){
... // 方法体。
};
ExampleConst.prototype.method3 = function(){
... // 方法体。
};
Internet Explorer 的内存泄漏问题
Internet Explorer Web 浏览器(在 IE 4 到 IE 6 中核实)的垃圾收集系统中存在一个问题,即如果 ECMAScript 和某些宿主对象构成了 “循环引用”,那么这些对象将不会被当作垃圾收集。此时所谓的宿主对象指的是任何 DOM 节点(包括 document 对象及其后代元素)和 ActiveX 对象。如果在一个循环引用中包含了一或多个这样的对象,那么这些对象直到浏览器关闭都不会被释放,而它们所占用的内存同样在浏览器关闭之前都不会交回系统重用。
当两个或多个对象以首尾相连的方式相互引用时,就构成了循环引用。比如对象 1 的一个属性引用了对象 2 ,对象 2 的一个属性引用了对象 3,而对象 3 的一个属性又引用了对象 1。对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 1、2、3,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是,在 Internet Explorer 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。
闭包非常容易构成循环引用。如果一个构成闭包的函数对象被指定给,比如一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象作用域中的一个活动(或可变)对象,那么就存在一个循环引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。形成这样一个循环引用是轻而易举的,而且稍微浏览一下包含类似循环引用代码的网站(通常会出现在网站的每个页面中),就会消耗大量(甚至全部)系统内存。
多加注意可以避免形成循环引用,而在无法避免时,也可以使用补偿的方法,比如使用 IE 的 onunload 事件来来清空(null)事件处理函数的引用。时刻意识到这个问题并理解闭包的工作机制是在 IE 中避免此类问题的关键。
comp.lang.javascript FAQ notes T.O.C.
- 撰稿 Richard Cornford,2004 年 3 月
- 修改建议来自:
- Martin Honnen.
- Yann-Erwan Perio (Yep).
- Lasse Reichstein Nielsen. (definition of closure)
- Mike Scirocco.
- Dr John Stockton.
XHTML代码与HTML的区别
1.在XHTML中所有的标记都必须要有一个相应的结束标记 ,在 HTML 中,例如<p>和<li>而不一定写对应的</p>和</li>来关闭它们。但在 XHTML 中这是不合法的。XHTML 要求有严谨的结构,所有标签必须关闭。如果是单独不成对的标签,在标签最后加一个”/”来关闭它。例如: <img src=”images/.gif” width=”200″ height=”80″ alt=”uedspace” /> 或<br/>
2.在XHTML中所有标签的元素和属性的名字都必须使用小写 与HTML 不一样,XHTML 对大小写是敏感的,<title>和<TITLE>是不同的标签。XHTML 要求所有的标签和属性的名字都必须使用小写。例如:<BODY>必须写成<body> 。大小写夹杂也是不被认可的,通常 dreamweaver 自动生成的属性名字”onMouseOver”也必须修改成”onmouseover”。
3.在XHTML中所有的 标记都必须合理嵌套 因为 XHTML 要求有严谨的结构,因此所有的嵌套都必须按顺序,以前这样写代码:
<p><b></p>/b> 现在必须修改为: <p><b></b>/p> ,一层一层的嵌套必须严格对称。
4.在XHTML中所有的属性必须用引号“”括起来 在HTML 中,你可以不需要给属性值加引号,但是在 XHTML 中,它们必须被加引号。例如: <height=80> 必须修改为: <height=”80″> 特殊情况,你需要在属性值里使用双引号,你可以用”,单引号可以使用',例如: <alt=”say'hello'”>
5.在XHTML中把所有<和&特殊符号用编码表示
任何小于号(<),不是标签的一部分,都必须被编码为& l t ;
任何大于号(>),不是标签的一部分,都必须被编码为& g t ;
任何与号(&),不是实体的一部分的,都必须被编码为& a m p;
注:以上字符之间无空格。
6.在 XHTML中 规定所有属性都必须有一个值,没有值的就重复本身。例如: <td nowrap> <input type=”checkbox” name=”shirt” value=”medium” checked>
必须修改为:<td nowrap=”nowrap”> <input type=”checkbox” name=”shirt” value=”medium” checked=”checked”>
7.在XHTML中不要在注释内容中使“–”
“–”只能发生在 XHTML 注释的开头和结束,也就是说,在内容中它们不再有效。例如下面的代码是无效的:
<!–这里是注释———–这里是注释–>
用等号或者空格替换内部的虚线。
<!–这里是注释============这里是注释–>
Google网站性能优化工具Page Speed试用报告
Google近日推出了一款网站性能优化工具:Page Speed(http://code.google.com/speed/page-speed/)。它旨在帮助站长与网站开发者分析网站中存在的性能方面的问题,并有针对性地提出改进意见。Page Speed在功能方面极其类似于Yahoo!的网站性能优化YSlow,不过YSlow要比Page Speed推出早得的多。它们都是基于Firebug的Fireffox插件,使用方法也类似。这里我主要介绍一下Google新推出的Page Speed的使用,对Yslow感兴趣的朋友可以参照我以前的这篇文章《你的网站为什么会慢?——用YSlow为你的网站提速》,同时还有我翻译的Yahoo!的文章Yahoo!网站性能最佳体验的34条黄金守则——内容、JavaScript和CSS、服务器、图片、Coockie与移动应用,相信一定会对你提高网站性能有帮助。
一、Page Speed的安装及使用
Page Speed是一款Firefox插件,同时他依附于别款插件Firebug,也就是说你的Firefox浏览器中必须已经安装了Firebug才能安装Page Speed。安装环境为Firefox 3.0.4以上,Fireug 1.3.3以上。
Page Speed的使用也很简单,在Firefox中点击右下角的Firebug图标启动后,再点击Page Speed选项卡即可。要注意的是,你要对你网站内的某个页面进行性能分析,你必须先把该页面加载完成后才能使用Page Speed,也就是说只有在浏览器左下角出现“Done”或者”完成”之后才可以启用Page Speed进行分析。如果页面中流媒体,可能不会现在“完成”,这种情况要等到流媒体可以播放。

然后点击“Analyze Performance”(性能分析),这时Page Speed会根据web performance best practices (网页性能最佳实践)进行逐项打分。然后根据重要程序和优先级对每项进行排列。

此外,你还可以点击每条建议前面的“加号”展开查看详细的描述,或者直接点击每条规则相看该规则的具体内容,还可以点击“Show Resource”(查看来源)来查看每条建议是针对页面中哪部分内容提出的。
对于分析结果中的符号说明一下:
- 红色感叹号代表高优先级提示,表示这一项严重影响了你的页面性能,你需要优先对其进行性能优化;
- 橙色三角代表此项提示需要引起你的注意,并进行适当改进;
- 绿色的对号代表该项规则在你的网站中应用得到,你在修改了前面两部分的提示之后,它们有可能变为绿色的对号;
- 蓝色消息符号是为你提供了额外的帮助信息,请稍加留意(需要注意的是,如果你的页面中出现了大量的此类符号,可能是因为你在页面加载完成之前就进行了网站性能分析)。
二、活动记录
活动记录是一条页面活动的时间轴,它记录了包括网络事件、JavaScript运行在内的所有浏览器活动。你可以使用它并配合性能分析中的数据进一步对网站性能做出评估。
- 查看页面运行过程中所耗费的时间,以毫秒计算;
- 查看浏览器事件,包括页面加载完成后的事件;
- 区分造成页面响应缓慢的原因,其中包括网络来时、DNS查找、连接建立、JavaScript运行等;
- 获取在特定时间或者事件下才响应的JavaScript事件列表;
- 可以对其它标签或者窗口中打开的页面进行分析;
- 多页面加载时的页面加载顺序;
- 对根据Page Speed优化前后的表现进行对比。

三、理解Page Speed中的事件
页面记录选项卡下是通过时间线来记录各种资源加载到页面所有需要的时间。事件的记录时间间隔为10毫秒,如果事件需要的时间少于10毫秒那么它将用较短的色块来表示。时间线中没有任何颜色的表示,在浏览器事件的运行依赖于其它进程,如DOM和CSS渲染、Flash ActionScript、渲染、操作系统事件等。
| 网络事件 | 描述 | ||
|---|---|---|---|
|
|
DNS | 浏览器查找DNS所需要的时间 | |
|
|
t连接等待 | 浏览器与网站服务器建立连接(TCP)需要一定的时间。由于浏览器可以打开的连接数目是有限的,如果达到这个限制他必须等其它连接关闭之后才能再重新建立一个新的连接。(更多关于浏览器连接的信息可以参照Parallel downloads across hostnames)。 这个事件显示了浏览器等其它连接完成的时间。 | |
|
|
连接 | 浏览器和web服务器建立连接。这个事件只有打开新连接时出现,已有连接重新打开使用不包含在内。 | |
|
|
请求发送 | 浏览器发送的HTTP请求。只显示GET方式的请求。 | |
|
|
已连接 | 浏览器通过网络等待接收数据。事件随着浏览器TCP连接的结束而结束。 | |
| 本地事件 | 描述 | ||
|
|
缓存 | 浏览器成功将内容加入到缓存中。 | |
|
|
可用数据 | 可用于浏览器呈现的数据。由于web服务器发送大量的数据,如果文件很大那么有可能一个资源会出现多个该事件。 | |
|
|
获取JS | 浏览器获取JavaScript。该事件可能会延缓其它事件,如果此种情况出现,将会在其下一行列出。 | |
|
|
运行JS | 浏览器执行JavaScript。该事件可能会延缓其它事件,如果此种情况出现,将会在其下一行列出。如果获取JS和运行JS中间有时间间隔,这说明源文件中包括有延时功能的函数。 | |
此外,Page Speed还包括了对已完成的JavaScript函数的信息搜集功能,当页面中的JS函数一旦运行,PageSpeed就会捕捉到相关信息。不通过对Page Speed进行设置还可以对未触发函数、延时加载函数等进行收集。
下面的图片显示了7800毫秒时已经加载但还未触发的函数列表:

而下面则显示是已经触发运行了的JS函数:

此外Pge Speed还有诸如JavaScript函数控制、浏览器User Agent设置等更高级功能。具体使用大家可以与YSlow对比一下。
相信,用好这两款工具,对于站长和网站开发者来说会有极大的帮助。
征服高级CSS选择器
CSS是对网页设计师可用的最强大的工具之一。使用它我们可以在几分钟内改变一个网站的界面,而不用改变页面的标签。但是尽管事实上,我们每个人也都意识到了它是有用的,CSS 选择器远未发挥它们的潜力,有的时候我们还趋向于使用过多的和无用的class、id、div、span等把我们的HTML搞的很凌乱。
避免让这些“瘟疫”在你的标签中传播并保持其简洁和语义化的最佳方式,就是使用更复杂的CSS选择器,它们可以定位于指定的元素而不用使用额外的class或id,而且通过这种方式也可以让我们的代码和样式更加灵活。
CSS的优先级
在深入研究高级CSS选择器领域之前,理解CSS优先级是如何工作的是很重要的,这样我们就知道如何适当的使用我们的选择器并避免浪费大量的时间来调试一些只要我们注意到优先级的话就很容易被搞定的问题
当我们写CSS的时候我们必须注意有些选择器在级联(cascade)上会高于其它选择器,我们写在最后面的选择器将不一定会覆盖前面我们写在同一个元素的样式。
那么你如何计算指定选择器的优先级?如果你考虑到将优先级表示为用逗号隔开的四个数字就会相当简单,比如:1, 1, 1, 1 或0, 2, 0, 1
- 第一个数字(a)通常就是0,除非在标签上使用style属性;
- 第二个数字(b)是该选择器上的id的数量的总和;
- 第三个数字(c)是用在该选择器上的其它属性选择器和伪类的总和。这里包括class (.example) 和属性选择器(比如 li[id=red]);
- 第四个数字(d)计算元素(就像table、p、div等等)和伪元素(就像:first-line等);
- 通用选择器(*)是0优先级;
- 如果两个选择器有同样的优先级,在样式表中后面的那个起作用。
让我们看几个例子,这样或许比较容易理解些:
- #sidebar h2 — 0, 1, 0, 1
- h2.title — 0, 0, 1, 1
- h2 + p — 0, 0, 0, 2
- #sidebar p:first-line — 0, 1, 0, 2
在下面的例子中,第一个将会起作用,因为它比第二个优先级高:
- #sidebar p#first { color: red; } — 0, 2, 0, 1
- #sidebar p:first-line { color: blue; } — 0, 1, 0, 2
至少基本理解优先级是如何工作的是很重要的,但是一些工具比如Firebug,在我们审查指定元素的时候,按照选择器的优先级列出所有的css选择器对让我们知道在指定元素上哪个选择器是有效的是很有用的。

让你非常容易的看到那个选择器作用于一个元素上了。
有用的文章:
- CSS Specificity: Things You Should Know
- Link Specificity¯MeyerWeb
- CSS: Specificity Wars
- Assigning property values, Cascading, and Inheritance—W3C
1. 属性选择器
属性选择器(Attribute selector)让你可以基于属性来定位一个元素。你可以只指定该元素的某个属性,这样所有使用该属性——而不管它的值——的这个元素都将被定位,也可以更加明确并定位在这些属性上使用特定值的元素 —— 这就是属性选择器展示它们的威力的地方。
有6个不同类型的属性选择器:
- [att=value]
该属性有指定的确切的值。 - [att~=value]
该属性的值必须是一系列用空格隔开的多个值,(比如,class=”title featured home”),而且这些值中的一个必须是指定的值”value”。 - [att|=value]
属性的值就是“value”或者以“value”开始并立即跟上一个“-”字符,也就是“value-”。(比如lang=”zh-cn”) - [att^=value]
该属性的值以指定值开始。 - [att$=value]
该属性的值包含指定的值(而无论其位置)。 - [att*=value]
该属性的值以指定的值结束
比如,如果你想要改变你的博客上的日志部分元素的背景颜色,你可以使用一个指定每一个class属性值以“post-”开始的div的属性选择器:
div[class|="post"] { background-color: #333; }
该语句将匹配所有class属性包含”post”并带”-”字符的div元素。(注:此处英文原文有些上下文不对应,我已经更正——译者,神飞)
属性选择器的另一个很有用的用处是定位不同类型的input元素。比如,如果你想让你的文本输入框使用一个特定的宽度,你可以使用下面的属性选择器:
input[type="text"] { width: 200px; }
这将匹配所有type属性值等于“text”的input元素。
现在我们可能像为网站上不同的文件类型的链接添加不同的图标,这样你的网站的访客就会知道他们将获得一个图片、或者PDF文件、或者是一个word文档等。这就可以使用属性选择器来实现:
a[href*=".jpg"] { background: url(jpeg.gif) no-repeat left 50%; padding: 2px 0 2px 20px; } a[href*=".pdf"] { background: url(pdf.gif) no-repeat left 50%; padding: 2px 0 2px 20px; } a[href*=".doc"] { background: url(word.gif) no-repeat left 50%; padding: 2px 0 2px 20px; }
在此例中,我们使用了一个定位所有的链接(a)的href属性分别以.jpg,.pdf或.doc结束(*)的属性选择器。详细可查看前端观察之前的一篇文章《使用CSS选择器创建个性化链接样式》
浏览器支持
除了Internet Explorer 6,所有的主流浏览器都支持属性选择器。这意味着,如果你在你的网站上使用了属性选择器,你应该确保IE6用户将仍能获得一个可用的网站。比如我们的第三个例子,为链接添加图标可以给你的网站带来另一个级别的可用性,但是如果这些链接不显示任何图标,该网站仍然还是可用的。
2. 子选择器
子选择器用符号“>”表示。它允许你定位某个元素的第一级子元素。
比如,如果你想匹配所有的作为你的网站侧栏的div的子元素的h2元素,而不是可能是在div元素内的所有h2元素,也不是div的孙元素(或者更低级别的),你就可以使用下面的选择器:
div#sidebar > h2 { font-size: 20px; }
你当然也可以同时使用子元素和后代元素。比如,如果你想定位只有在body元素的子级div元素内的blockquote元素(比如你想匹配主要的div内的blockquotes,而不是其它部分的:
body > div > div blockquote { margin-left: 30px; }
浏览器支持
和属性选择器一样,子选择器不被IE6支持。如果你通过这种方式要实现的效果和网站的可用性或总体设计息息相关,你可以考虑使用一个class,或者使用只针对IE6的样式,但是这将有悖使用子选择器的目的。
3. 兄弟组合
有两类兄弟组合:临近兄弟组合和普通兄弟组合
临近兄弟组合
该选择器使用加号“+”来链接前后两个选择器。选择器中的元素有同一个父亲,而且第二个必须紧紧的跟着第一个。
临近兄弟组合可以灰常有用,比如,在处理文字的时候。比如我们想对段落后面的h2标题添加一个顶间距“margin-top”(当然或许你无需为h1标签后面的h2添加,或者它可能是页面的第一个元素):
p + h2 { margin-top: 10px; }
你甚至可以更加具体,你想定位某个特定的div后面的h2:
div.post p + h2 { margin-top: 10px; }
或者你可以搞得更加复杂:页面的第一个段落的第一行字母显示为小写字母。
.post h1 + p:first-line { font-variant: small-caps; }
因为可能大部分日志/文章的第一个段落都会紧紧的在H1标签的后面。你可以在你的选择器中借助于h1标签。
普通兄弟组合
普通兄弟组合和临近兄弟组合的工作原理很像,不同的是第二个选择其无需紧紧跟随第一个。
如果你需要定位所有的某个特定的div里面并且跟在h1标签后面的p标签的话(你可能想要这些p标签比拟的日志的标题前面的文字大些),你就可以使用这个选择器:
.post h1 ~ p { font-size: 13px; }
注:这两个选择器太像了,所有不是很好理解,可以试一试下面的这个例子:
CSS:
p + h2{color:red; } p ~ h2{font-weight:700; }
HTML:
<p>咳咳,内容。</p> <h2>标题1</h2> <h2>标题2</h2>
看看第二个h2的颜色就可以理解了吧?临近兄弟组合只能匹配第一个选择器后面的第一个兄弟选择其,普通兄弟组合则能匹配所有。注:该部分英文原文没有,是神飞翻译时自己添加。
浏览器支持
Internet Explorer 6依然不能理解兄弟选择器,但是,另一种情况,如果你的用户中只有一小部分是IE6用户,而且网站的结构不会被破坏或者受到严重影响,这是实现很多很酷的效果而无需在你的HTML代码中添加无用的class或id。
4. 伪类
动态伪类
之所以被称为动态伪类是因为它们并不存在于HTML中——而是只有当用户和网站交互的时候才会呈现。
有两类动态伪类:链接 和用户行为。链接就是:link 和:visited,而用户行为包括:hover、:active 和:focus。
在本文中提到的css选择器中,这几个应该是最常用到的。
:link伪类用于链接尚未被用户访问的时候,而:visited 伪类用于用户访问过的链接,也就是说它们是相反的。
:hover伪类用于用户移动他们的鼠标在元素上,而尚未触发或点击它的时候。:active伪类应用于用户点击元素的情况。最后,:focus伪类用于元素成为焦点的时候——最常用于表单元素。
你可以在你的样式表中使用多种用户行为动态伪类,这样你就可以,比如,根据用户的鼠标只是滑过或悬停的时候,为一个输入框定义不同的背景色:
input:focus { background: #D2D2D2; border: 1px solid #5E5E5E; } input:focus:hover { background: #C7C7C7; }
浏览器兼容性
动态伪类被所有的现代浏览器支持,甚至IE6,但是请注意,对于IE系列浏览器来说,IE6只允许:hover 伪类应用于链接元素(a标签)而且只有IE8接受:active状态。
:first-child
:first-child伪类允许你定位某个元素第一个子元素。比如,如果你想给你的无须列表的第一个li添加一个margin-top,你就可以这样写:
ul > li:first-child { margin-top: 10px; }
让我们来看一看另一个例子:比如你想让你的博客的侧栏的H2标签都有个顶部边距,以将标题和它们前面的内容区分开来,但是第一个h2不需要,你就可以使用下面的代码:
#sidebar > h2 { margin-top: 10px; } #sidebar > h2:first-child { margin-top: 0; }
浏览器兼容性
IE6 不支持 :first-child 伪类。根据伪类应用到的设计的不同,它或许不会成为关注的主要问题。比如,如果你使用:first-child 选择器来移除标题或段落上的头部或底部的间距,你的布局在IE6中不会坏掉,它只会看起来有些不同。但是如果你使用:first-child选择器从一个,比如浮动元素,移除左边距或右边距,将会让你的设计乱掉。
语言伪类
语言伪类:lang(),允许你匹配一个基于它的语言的元素。
你如,你想让你的网站的某个特定的链接根据页面的语言有不同的背景颜色:
:lang(en) > a#flag { background-image: url(english.gif); } :lang(fr) > a#flag { background-image: url(french.gif); }
这个选择器将会匹配相关的链接——如果页面的语言等于“en”或“fr”,或者以“en”或“fr”开头并在后面带个连字符“-”的话。
浏览器兼容性
不幸的是,只有IE浏览器中只有IE8支持该选择器,其它的主要浏览器都支持该伪类选择器。
5. CSS 3 伪类
:target
当你使用锚点(片段标识符 fragment identifier)的时候,比如,http://www.smashingmagazine.com/2009/08/02/bauhaus-ninety-years-of-inspiration/#comments,这“#comments”就是一个片段标识符,你就可以使用:target伪类来控制目标的样式。
举个例子,比如你有一个很长的使用了很多文字和h2标题的页面,然后在页面的头部有一个对这些标题的索引。如果在点击索引内的某个链接时,相应的标题以某种方式高亮,然后滚动到相应的位置,对读者就会很有用。很简单。
h2:target { background: #F2EBD6; }
浏览器兼容性
这一次,IE浏览器完全不支持:target伪类,另一个小问题就是Opera 在使用“前进”和后退按钮时不支持该选择器。但是其它的各个主流浏览器都支持该选择器。
UI元素状态伪类
有些HTML元素有enable 或disabled 状态(比如,文本输入框)和 checked 或unchecked 状态(单选按钮和复选框)。这些状态就可以使用:enabled、:disabled 或:checked 伪类来分别定位。
那么你就会想,如果任意一个禁用的(disabled)文本框应该使用浅灰色的背景和虚线边框:
input:disabled { border:1px dotted #999; background:#F2F2F2; }
你也可能会想让所有选中的复选框有个左边距(这样就可以在众多的复选框中很容易认出来):
input[type=”checkbox”]:checked { margin-left: 15px; }
浏览器兼容性
所有的主流浏览器,除了我们常常不报希望的IE系列浏览器,都支持UI元素状态伪类。如果你考虑只是添加附加级别的细节和增强网站的可用性,这个仍然是可以采用的。
6. CSS 3 结构伪类
:nth-child
:nth-child()伪类允许你定位某个父级元素的一个或多个特定的子元素.
你可以通过定义它的值为一个整数来定位某个单个子元素:
ul li:nth-child(3) { color: red; }
这将会让ul元素的第三个li元素的文字变成红色。注意如果在ul里面有个其它类型的元素(不是li),它也会算作其子元素。
你可以使用表达式来定位子元素。比如,下面的表达式将从第四个开始匹配每第三个元素。
ul li:nth-child(3n+4) { color: yellow; }
在上面的这个例子中,第一个黄色的li元素将会是第四个。如果也想从第一个开始匹配,你可以使用一个简单的表达式:
ul li:nth-child(3n) { color: yellow; }
这样的话,第一个黄色的li元素将会是第三个子元素,然后是它后面的每隔第三个。现在想象一下如果你只想匹配列表中的前四个子元素:
ul li:nth-child(-n+4) { color: green; }
:nth-child的值同样也可以被定义为“even” 或“odd”,和“2n” (第偶数个) 或“2n+1” (第奇数个)的效果是一样的。
:nth-last-child
:nth-last-child伪类基本上和:nth-child伪类的作用相同,但是它从最后一个元素开始算。
使用上面的一个例子来看看:
ul li:nth-child(-n+4) { color: green; }
不是匹配最前面的四个li元素,该选择器匹配最后面的四个元素。
你同样可以使用“even” 或“odd”只,同样与nth-child不同的是,这是从最后面的元素开始算的:
ul li:nth-last-child(odd) { color: grey; }
:nth-of-type
:nth-of-type伪类和:nth-child也很像,不同的是它只计算选择器中指定的那个元素。
这对定位元素中包含大量不同的元素的情况会很有用。比如,我们想控制一个文本块中的每隔一个段落,但是我们又想要无视其它元素比如图片和引用块:
p:nth-of-type(even) { color: blue; }
你也可以使用一些值,就像在:nth-child中使用的一样。
:nth-last-of-type
你能猜到它吧?!:nth-last-of-type 伪类可以像前面提到的:nth-last-child一样使用,但是这次,它将之匹配你在选择器中指定的元素类型:
ul li:nth-last-of-type(-n+4) { color: green; }
我们可以更加的聪明一些,在一个大的块级选择器中结合多种这样的伪类。比如我们想让文章中的所有的图片左浮动,除了第一个和最后一个(我们可以假设他们是满宽的,无须浮动):
.post img:nth-of-type(n+2):nth-last-of-type(n+2) { float: left; }
所以在这个选择器的第一部分,我们从第二个图片开始定位每一个图片。在第二部分中,我们定位所有的图片,除了最后一个。因为这两个选择器并非互斥的,我们可以同时使用它们,这样就可以一下子排除第一个和最后一个元素!
:last-child
:last-child伪类的作用类似于:first-child 伪类,但是会定位父级元素的最后一个子元素。
让我们假设你不想让你的日志的div的最后一个段落也有一个底部边距:
.post > p:last-child { margin-bottom: 0; }
该选择器将定位class为”post”的元素的最后一个直接子级段落。
:first-of-type 和:last-of-type
:first-of-type 伪类用于定位一个父级元素下的第一个同类子元素。
比如,你可以定位某个特定的div的第一个子级段落(p),并让其第一行字母大写:
.post > p:first-of-type:first-line { font-variant: small-caps; }
在这个选择器中,你可以确定你是在只定位class为”post”的元素的直接子级p元素,而且还是匹配第一个子级p元素。
:last-of-type伪类与此类似,只是匹配最后一个子元素。
nly-child
nly-child伪类表示一个元素是它的父级元素的唯一一个子元素。
比如说,你有一些盒子(“news”),里面有一些文字段落,当你有多于一个段落的时候,你想让文字比只有一个段落的时候小一些:
div.news > p { font-size: 1.2em; } div.news > pnly-child { font-size: 1.5em; }
第一个选择器中,我们定义”news”div的所有子级p元素的字体大小。在第二个中,我们覆盖之前的字体大小,如果该p元素是“news” div的唯一子元素的时候,它的字体大小会比较大一些。
nly-of-type
nly-of-type伪类表示一个元素是它的父级元素的唯一一个相同类型的子元素。
这有用什么用?假设你有一些日志,每一篇都有个class为”post”的div,他们中的一些有多于一张图片,但是有些可能就只有一张图片。你想让后者中的图片水平居中,而在其它的有多于一张图片的日志中,就将它左浮动。这个需求用这个选择器就很容易实现:
.post > img { float: left; } .post > imgnly-of-type { float: none; margin: auto; }
:empty
:empty伪类表示一个元素里面没有任何内容。
这个选择器可以用很多种用途。比如,你在你的“sidebar”中有若干个盒子,但是不想让空盒子显示出来:
#sidebar .box:empty { display: none; }
注意,就算”box”div里面只有一个空格,它也不会被css当作空标签的,这样就不能匹配该选择器了。
浏览器支持
Internet Explorer (直到8.0版本)都不支持结构伪类。Firefox、Safari 和Opera 在其最新版本的浏览器中指出这些选择器。这意味着,使用这些选择器对网站的可用性和可访问性是很有用的,或者如果网站的用户中的大部分是使用IE而且你不想在某些细节上无视他们,最好还是保持使用通用的class和简单的选择器来迎合这些选择器。否则你会被搞疯的!
7. 否定选择器
否定选择器:not(),可以让你定位不匹配该选择器的元素
比如,如果你需要定义表单元素中的input元素,但是又不想包括submit类型的input的时候会灰常有用——你想它们有不同的样式,以看起来像按钮:
input:not([type="submit"]) { width: 200px; padding: 3px; border: 1px solid #000000; }
另一个例子,你想你的日志的div中的所有段落(p)有比较大的字体,除了表示时间和日期的段落:
.post p:not(.date) { font-size: 13px; }
你可以想象这个选择器能带给你的潜力了吧,你能够从你的CSS文件中剥离(剔除)的无用的大量选择器也被它广泛支持吗?
浏览器支持
Internet Explorer在这里常常是让我们感到扫兴的东西:一点都不支持,甚至在IE8中。这大概意味着这些选择器将仍不得不等到一些开发者开始不再顾虑将它添加到他们的样式表中才会普及。
8. 伪元素
伪元素允许你操作HTML中不是真实存在的元素,比如一个文本块的第一行或者第一个字母。
伪元素在CSS 2.1中就已经存在,但是CSS 3说明书表示他们应该使用双冒号“::”,以与伪类区分开来。在CSS 2.1中,他们也是使用单个冒号“:”的。浏览器会将能够接受两种格式,除非这些伪元素只存在于CSS3中。
::first-line
::first-line伪元素将匹配block、inline-block、table-caption、table-cell等等级别元素的第一行
这对在你的文字块上添加一些微妙的排版细节相当有用,比如,将一片文章的第一行文字改成小写字母:
h1 + p::first-line { font-variant: small-caps; }
如果你专心的阅读了我们前面的内容,你将会了解到上面的语法意味着,紧紧的跟在H1标签之后(+)的段落会将其第一行文字显示为小写字母。
你也可以针对相关的div的第一行,而不用针对实际的段落标签(p):
div.post p::first-line { font-variant: small-caps; }
或者更进一步,定位某个特低的div的第一个段落的第一行:
div.post > p:first-child::first-line { font-variant: small-caps; }
这里,“>”符号表示你指定的是post div的直接子级元素,这样如果段落被包括在第二级div中,它就不会匹配这个选择器。
::first-letter
::first-letter伪元素将会匹配一个文本块的第一个字母,除非在同一行里面包含一些其它元素,比如图片。
和::first-line伪类一样,::first-letter通常用于给文本元素添加排版细节,比如下沉字母或首字母。
这里是如何使用::first-letter伪元素创建首字下沉的例子:
p { font-size: 12px; } p::first-letter { font-size: 24px; float: left; }
注意如果你在某些元素中同时使用::first-line 和::first-letter ,::first-letter 属性将覆盖从::first-line中继承下来的某些属性。
如果你不知道W3C规则的话,这个元素有时会产生意想不到的结果:它事实上是使用最长的规则的选择器!所以如果你计划使用它的话最好仔细的阅读一下 (其它选择器也一样)。
::before 和 ::after
::before和::after 伪元素用于在一个元素的前面或后面插入内容,纯CSS方法。
这些元素将继承它们将附加的元素的大部分属性。
假设你想在你的页面中的图标的描述前面添加文字“Graphic number x:”。你将无需写文字“Graphic number”,或者自己手动添加数字:
.post { counter-reset: image; } p.description::before { content: "Figure number " counter(image) ": "; counter-increment: image; }
那么这会产生什么?
首先,我们告诉HTML来创建“image”计数器。比如我们可以添加该属性到页面的body。同样我们也可以给该计数器起任何一个名字,只要你想,只要我们常常使用同样的名字引用它:自己试试吧!
那么我们想在class为”description”的每一个段落之前添加这一块内容: “Figure number ” — 注意只有我们在引号里面写的内容才会被创建到页面中,所以我们也要添加一个空格!
然后,我们就有了counter(image):这将用到我们之前在.post选择器中定义的属性。它默认会从数字1开始。
下一个属性在那里表示计数器知道对于每一个p.description,它需要将image计数器增加1 (counter-increment: image)。
它并不像看起来的那么复杂,而且还会灰常的有用。
::before和::after伪元素常常只使用content属性,来添加一些短语或排版元素,但是这里我们展示了我们如果以一种更加强大的结合counter-reset和counter-increment属性的方式来使用它们。
有趣的是: ::first-line 和::first-letter 伪元素可以匹配使用::before伪元素生成的内容,如果存在的话。
浏览器支持
如果使用单个冒号的话(比如, :first-letter, 而不是::first-letter),这些伪元素被IE8支持(但是不被IE7或6支持)。但是左右其他的主流浏览器都支持这些选择器。
结语
乏味的讲述终于结束了,现在该你来领悟本文的要义并自己尝试了: 开始通过创建实验性的页面并测试所有的这些选择器,在有疑问的时候返回这里并确保总是遵循W3C的规则,但是请不要只是坐在那里想这些选择器尚未被完全支持你就无视它们。
如果你敢于冒险,或者你不害怕放弃之前的遍地无用和非语义化的class和id,为什么不尝试一两个这些强大的CSS选择器到你的下一个项目中呢?我们保证你不会回头的!
参考
- CSS 2 Selectors — W3C
- CSS 3 Selectors Level 3 — W3C
- Comparison of layout engines (Cascading Style Sheets) — Wikipedia
- Generated content, automatic numbering, and lists — W3C
扩展资源
- Keeping Your Elements’ Kids in Line with Offspring — A List Apart
- Selectutorial – CSS selectors
- A Look at Some of the New Selectors Introduced in CSS3
- CSS 2.1 selectors, Part 1 and Part 2
- CSS 3 selectors explained
- CSS selectors and pseudo selectors browser compatibility
- 10 Useful CSS Properties Not Supported By Internet Explorer
- Styling a Poem with Advanced CSS Selectors
关于作者
Inayaili de León 是一个葡萄牙网页设计师。她对网页设计和前端编码有特别的爱好,而且喜欢漂亮和间接的网站。她居住在伦敦。你可以在Web Designer Notebook看到她的更多文章并follow her on Twitter。

