diff --git a/Figure/chapter1/1-1.jpg b/Figure/chapter1/1-1.jpg new file mode 100644 index 0000000..f81324e Binary files /dev/null and b/Figure/chapter1/1-1.jpg differ diff --git a/Figure/chapter2/2-1.jpg b/Figure/chapter2/2-1.jpg new file mode 100644 index 0000000..6c95cd4 Binary files /dev/null and b/Figure/chapter2/2-1.jpg differ diff --git a/Figure/chapter5/5-1.jpg b/Figure/chapter5/5-1.jpg new file mode 100644 index 0000000..ae27fe7 Binary files /dev/null and b/Figure/chapter5/5-1.jpg differ diff --git a/Figure/chapter5/5-2.jpg b/Figure/chapter5/5-2.jpg new file mode 100644 index 0000000..3b7aac7 Binary files /dev/null and b/Figure/chapter5/5-2.jpg differ diff --git a/Figure/chapter6/6-1.jpg b/Figure/chapter6/6-1.jpg new file mode 100644 index 0000000..8059680 Binary files /dev/null and b/Figure/chapter6/6-1.jpg differ diff --git a/Figure/chapter6/6-10.jpg b/Figure/chapter6/6-10.jpg new file mode 100644 index 0000000..61720cf Binary files /dev/null and b/Figure/chapter6/6-10.jpg differ diff --git a/Figure/chapter6/6-2.jpg b/Figure/chapter6/6-2.jpg new file mode 100644 index 0000000..3e1c70d Binary files /dev/null and b/Figure/chapter6/6-2.jpg differ diff --git a/Figure/chapter6/6-3.jpg b/Figure/chapter6/6-3.jpg new file mode 100644 index 0000000..0bda99d Binary files /dev/null and b/Figure/chapter6/6-3.jpg differ diff --git a/Figure/chapter6/6-4.jpg b/Figure/chapter6/6-4.jpg new file mode 100644 index 0000000..b42531b Binary files /dev/null and b/Figure/chapter6/6-4.jpg differ diff --git a/Figure/chapter6/6-5.jpg b/Figure/chapter6/6-5.jpg new file mode 100644 index 0000000..0325547 Binary files /dev/null and b/Figure/chapter6/6-5.jpg differ diff --git a/Figure/chapter6/6-6.jpg b/Figure/chapter6/6-6.jpg new file mode 100644 index 0000000..7892619 Binary files /dev/null and b/Figure/chapter6/6-6.jpg differ diff --git a/Figure/chapter6/6-7.jpg b/Figure/chapter6/6-7.jpg new file mode 100644 index 0000000..461949d Binary files /dev/null and b/Figure/chapter6/6-7.jpg differ diff --git a/Figure/chapter6/6-8.jpg b/Figure/chapter6/6-8.jpg new file mode 100644 index 0000000..848cf57 Binary files /dev/null and b/Figure/chapter6/6-8.jpg differ diff --git a/Figure/chapter6/6-9.jpg b/Figure/chapter6/6-9.jpg new file mode 100644 index 0000000..0aa47e8 Binary files /dev/null and b/Figure/chapter6/6-9.jpg differ diff --git a/Figure/chapter7/7-1.jpg b/Figure/chapter7/7-1.jpg new file mode 100644 index 0000000..c4085b5 Binary files /dev/null and b/Figure/chapter7/7-1.jpg differ diff --git a/Figure/chapter7/7-2.jpg b/Figure/chapter7/7-2.jpg new file mode 100644 index 0000000..413736b Binary files /dev/null and b/Figure/chapter7/7-2.jpg differ diff --git a/Figure/chapter7/7-3.jpg b/Figure/chapter7/7-3.jpg new file mode 100644 index 0000000..6c3e2df Binary files /dev/null and b/Figure/chapter7/7-3.jpg differ diff --git a/Figure/chapter7/7-4.jpg b/Figure/chapter7/7-4.jpg new file mode 100644 index 0000000..81f9d9e Binary files /dev/null and b/Figure/chapter7/7-4.jpg differ diff --git a/Figure/chapter7/7-5.jpg b/Figure/chapter7/7-5.jpg new file mode 100644 index 0000000..a483209 Binary files /dev/null and b/Figure/chapter7/7-5.jpg differ diff --git a/Figure/chapter7/7-6.jpg b/Figure/chapter7/7-6.jpg new file mode 100644 index 0000000..32b5779 Binary files /dev/null and b/Figure/chapter7/7-6.jpg differ diff --git a/Figure/chapter7/7-7.jpg b/Figure/chapter7/7-7.jpg new file mode 100644 index 0000000..2b9a0d1 Binary files /dev/null and b/Figure/chapter7/7-7.jpg differ diff --git a/Figure/chapter7/7-8.jpg b/Figure/chapter7/7-8.jpg new file mode 100644 index 0000000..5643e3a Binary files /dev/null and b/Figure/chapter7/7-8.jpg differ diff --git a/Figure/chapter8/8-1.jpg b/Figure/chapter8/8-1.jpg new file mode 100644 index 0000000..7f7b2b4 Binary files /dev/null and b/Figure/chapter8/8-1.jpg differ diff --git a/Figure/chapter8/8-2.jpg b/Figure/chapter8/8-2.jpg new file mode 100644 index 0000000..a9498eb Binary files /dev/null and b/Figure/chapter8/8-2.jpg differ diff --git a/Figure/cover/back-cover.jpg b/Figure/cover/back-cover.jpg new file mode 100644 index 0000000..5553057 Binary files /dev/null and b/Figure/cover/back-cover.jpg differ diff --git a/Figure/cover/front-cover.jpg b/Figure/cover/front-cover.jpg new file mode 100644 index 0000000..cfc8c34 Binary files /dev/null and b/Figure/cover/front-cover.jpg differ diff --git a/README.markdown b/README.markdown index 1bdab2b..8e15051 100644 --- a/README.markdown +++ b/README.markdown @@ -1,203 +1,203 @@ # JavaScript Patterns -![JavaScript.patterns](http://img03.taobaocdn.com/tps/i3/T1np5gXj4nXXXXXXXX-320-420.jpg) +![JavaScript.patterns](./Figure/cover/front-cover.jpg) **“JavaScript patterns”中译本** - 《JavaScript 模式》 - 作者:[Stoyan Stefanov](http://www.phpied.com/) -- 翻译:[拔赤](http://jayli.github.com/) +- 翻译:[拔赤](http://jayli.github.com/)、[goddyzhao](http://goddyzhao.me)、[TooBug](http://www.toobug.net) -偷懒是程序员的优良品质,模式则是先人们总结的偷懒招式。Stoyan Stefanov 的这本书,从 JavaScript 的实际使用场景出发,提炼了不少可以让前端们偷懒的实用招式。模式的探索、创新,将永远是程序员自我提升的一条修炼之道。值得一读。 +本书从 JavaScript 的实际使用场景出发,提炼了不少有价值的前端实用模式。模式的探索、创新,将永远是程序员自我提升的一条修炼之道,值得一读。 # 目录 -## [第一章 概述](chapter1.markdown) - -- [模式](chapter1.markdown) -- [JavaScript:概念](chapter1.markdown#a2) - - [面向对象](chapter1.markdown#a3) - - [无类](chapter1.markdown#a4) - - [原型](chapter1.markdown#a5) - - [运行环境](chapter1.markdown#a6) -- [ECMAScript 5](chapter1.markdown#a7) -- [JSLint](chapter1.markdown#a8) -- [控制台工具](chapter1.markdown#a9) - -## [第二章 高质量JavaScript基本要点](chapter2.markdown) - -- [编写可维护的代码](chapter2.markdown#a2) -- [减少全局对象](chapter2.markdown#a3) - - [全局对象带来的困扰](chapter2.markdown#a4) - - [忘记var时的副作用](chapter2.markdown#a5) - - [访问全局对象](chapter2.markdown#a6) - - [单 var 模式](chapter2.markdown#a7) - - [声明提前:分散的 var 带来的问题](chapter2.markdown#a8) -- [for 循环](chapter2.markdown#a9) -- [for-in 循环](chapter2.markdown#a10) -- [(不)扩充内置原型](chapter2.markdown#a11) -- [switch 模式](chapter2.markdown#a12) -- [避免隐式类型转换](chapter2.markdown#a13) - - [避免使用 eval()](chapter2.markdown#a14) -- [使用parseInt()进行数字转换](chapter2.markdown#a15) -- [编码风格](chapter2.markdown#a16) - - [缩进](chapter2.markdown#a17) - - [花括号](chapter2.markdown#a18) - - [左花括号的放置](chapter2.markdown#a19) - - [空格](chapter2.markdown#a20) -- [命名规范](chapter2.markdown#a21) - - [构造器命名中的大小写](chapter2.markdown#a22) - - [单词分隔](chapter2.markdown#a23) - - [其他命名风格](chapter2.markdown#a24) -- [书写注释](chapter2.markdown#a25) -- [书写API文档](chapter2.markdown#a26) - - [一个例子:YUIDoc](chapter2.markdown#a27) -- [编写易读的代码](chapter2.markdown#a28) -- [相互评审](chapter2.markdown#a29) -- [生产环境中的代码压缩(Minify)](chapter2.markdown#a30) -- [运行JSLint](chapter2.markdown#a31) -- [小结](chapter2.markdown#a32) - -## [第三章 直接量和构造函数](chapter3.markdown) - -- [对象直接量](chapter3.markdown#a2) - - [对象直接量语法](chapter3.markdown#a3) - - [通过构造函数创建对象](chapter3.markdown#a4) - - [获得对象的构造器](chapter3.markdown#a5) -- [自定义构造函数](chapter3.markdown#a6) - - [构造函数的返回值](chapter3.markdown#a7) -- [强制使用new的模式](chapter3.markdown#a8) - - [命名约定](chapter3.markdown#a9) - - [使用that](chapter3.markdown#a10) - - [调用自身的构造函数](chapter3.markdown#a11) -- [数组直接量](chapter3.markdown#a12) - - [数组直接量语法](chapter3.markdown#a13) - - [有意思的数组构造器](chapter3.markdown#a14) - - [检查是不是数组](chapter3.markdown#a15) -- [JSON](chapter3.markdown#a16) - - [使用JSON](chapter3.markdown#a17) -- [正则表达式直接量](chapter3.markdown#a18) - - [正则表达式直接量语法](chapter3.markdown#a19) -- [原始值的包装对象](chapter3.markdown#a20) -- [Error对象](chapter3.markdown#a21) -- [小结](chapter3.markdown#a22) - -## [第四章 函数](chapter4.markdown#a) - -- [背景知识](chapter4.markdown#a) - - [术语释义](chapter4.markdown#a) - - [声明 vs 表达式:命名与提前](chapter4.markdown#a) - - [函数的name属性](chapter4.markdown#a) - - [函数提前](chapter4.markdown#a) -- [回调模式](chapter4.markdown#a) - - [一个回调的例子](chapter4.markdown#a) - - [回调和作用域](chapter4.markdown#a) - - [异步事件监听](chapter4.markdown#a) - - [超时](chapter4.markdown#a) - - [库中的回调](chapter4.markdown#a) -- [返回函数](chapter4.markdown#a) -- [自定义函数](chapter4.markdown#a) -- 立即执行的函数 - - 立即执行的函数的参数 - - 立即执行的函数的返回值 +## [第一章 绪言](./chapter1.markdown) + +- 模式 +- JavaScript:概念 + - 面向对象 + - 没有类 + - 原型 + - 运行环境 +- ECMAScript 5 +- JSLint +- 控制台工具 + +## [第二章 概要](./chapter2.markdown) + +- 编写可维护的代码 +- 减少全局对象 + - 全局变量的问题 + - 忘记var时的副作用 + - 访问全局对象 + - 单var模式 + - 声明提前:分散的var带来的问题 +- for循环 +- for-in循环 +- (不)扩充内置原型 +- switch模式 +- 避免隐式类型转换 + - 避免使用eval() +- 使用parseInt()进行数字转换 +- 代码规范 + - 缩进 + - 花括号 + - 左花括号的放置 + - 空格 +- 命名规范 + - 构造函数命名中的大小写 + - 单词分隔 + - 其他命名风格 +- 写注释 +- 写API文档 + - 示例:YUIDoc +- 编写易读的代码 +- 同事评审(Peer Reviews) +- 发布时的代码压缩(Minify) +- 运行JSLint +- 小结 + +## [第三章 字面量和构造函数](./chapter3.markdown) + +- 对象字面量 + - 对象字面量语法 + - 通过构造函数创建对象 + - Object()构造函数的参数 +- 自定义构造函数 + - 构造函数的返回值 +- 强制使用new的模式 + - 命名规范 + - 使用that + - 调用自身的构造函数 +- 数组字面量 + - 数组字面量语法 + - Array()构造函数的“陷阱” + - 检查是否数组 +- JSON + - 使用JSON +- 正则表达式字面量 + - 正则表达式字面量语法 +- 原始值的包装对象 +- 错误处理对象 +- 小结 + +## [第四章 函数](./chapter4.markdown) + +- 背景知识 + - 术语 + - 声明 vs 表达式:命名与提前 + - 函数的name属性 + - 声明提前 +- 回调模式 + - 回调的例子 + - 回调和作用域 + - 异步事件监听 + - 延时 + - 类库中的回调 +- 返回函数 +- 重定义函数 +- 即时函数 + - 即时函数的参数 + - 即时函数的返回值 - 好处和用法 -- 立即初始化的对象 -- 启动时间程序 -- 函数属性——一种备忘录模式 -- 对象的配置 +- 对象即时初始化 +- 条件初始化 +- 函数属性——记忆模式(Memoization) +- 配置对象 - 柯里化 (Curry) - 函数应用 - 部分应用 - - 柯里化 + - 柯里化(Currying) - 什么时候使用柯里化 -- 小节 +- 小结 -## 第五章 对象创建模式 +## [第五章 对象创建模式](./chapter5.markdown) - 命名空间模式 - - 通用的命名空间函数 -- 声明依赖 + - 通用命名空间函数 +- 依赖声明 - 私有属性和方法 - 私有成员 - 特权方法 - - 私有化失败 - - 对象直接量及其私有成员 - - 原型及其私有成员 + - 私有化成员失效 + - 对象字面量和私有成员 + - 原型和私有成员 - 将私有函数暴露为共有方法 - 模块模式 - 暴露模块模式 - - 创建构造器的模块 + - 创建构造函数的模块 - 在模块中引入全局上下文 - 沙箱模式 - 全局构造函数 - 添加模块 - - 实现这个构造函数 + - 实现构造函数 - 静态成员 - - 共有静态成员 + - 公有静态成员 - 私有静态成员 - 对象常量 - 链式调用模式 - 链式调用模式的利弊 -- method() 方法 -- 小节 +- method()方法 +- 小结 -## 第六章 代码重用模式 +## [第六章 代码复用模式](./chapter6.markdown) - 类式继承 vs 现代继承模式 - 类式继承的期望结果 -- 经典模式 1 ——默认模式 - - 使用原型链 - - 模式 1 的缺陷 -- 经典模式 2 ——借用构造器 - - 原型连 - - 通过借用构造函数实现多重继承 - - 借用构造器模式的利弊 -- 经典模式 3 ——借用并设置原型 -- 经典模式 4 ——共享原型 -- 经典模式 5 —— 临时构造器 - - 存储父类 - - 重置构造器引用 +- 类式继承1——默认模式 + - 跟踪原型链 + - 这种模式的缺点 +- 类式继承2——借用构造函数 + - 原型链 + - 利用借用构造函数模式实现多继承 + - 借用构造函数的利与弊 +- 类式继承3——借用并设置原型 +- 类式继承4——共享原型 +- 类式继承5——临时构造函数 + - 存储父类(Superclass) + - 重置构造函数引用 - Klass - 原型继承 - 讨论 - - 除了ECMAScript5之外 -- 通过拷贝属性继承 -- 混元 + - ECMAScript5中的原型继承 +- 通过复制属性继承 +- 混元(Mix-ins) - 借用方法 - - 例子:从数组借用 - - 借用和绑定 + - 例:从数组借用 + - 借用并绑定 - Function.prototype.bind() -- 小节 +- 小结 -## 第七章 设计模式 +## [第七章 设计模式](./chapter7.markdown) -- 单体 - - 使用 new - - 静态属性中的实例 - - 闭包中的实例 -- 工厂 +- 单例 + - 使用new + - 将实例放到静态属性中 + - 将实例放到闭包中 +- 工厂模式 - 内置对象工厂 -- 迭代器 -- 装饰者 +- 遍历模式 +- 装饰模式 - 用法 - 实现 - 使用列表实现 -- 策略 - - 数据校验的例子 -- 外观 -- 代理 +- 策略模式 + - 数据验证示例 +- 外观模式 +- 代理模式 - 一个例子 - - 作为缓存的代理 -- 中介者 - - 中介者例子 -- 观察者 - - 例子 1:杂志订阅 - - 例子 2:按键游戏 -- 小节 - -## 第八章 DOM和浏览器模式 - -- 分离关注点 -- DOM 脚本编程 + - 使用代理对象做缓存 +- 中介者模式 + - 中介者示例 +- 观察者模式 + - 例1:杂志订阅 + - 例2:按键游戏 +- 小结 + +## [第八章 DOM和浏览器模式](./chapter8.markdown) + +- 分离 +- DOM编程 - DOM访问 - DOM操作 - 事件 @@ -206,23 +206,20 @@ - 长时间运行的脚本 - setTimeout() - Web Workers -- 远程脚本 +- 远程脚本编程 - XMLHttpRequest - JSONP - - Frame和Image加载指示器 + - 框架(frame)和图片信标(image beacon) - 部署JavaScript - 合并脚本 - - 代码减肥和压缩 - - 过期头 + - 代码压缩 + - 缓存头 - 使用CDN - 加载策略 - - script标签的位置 - - HTTP 分块 - - 动态插入script标签非阻塞载入脚本 + - script元素的位置 + - HTTP分块 + - 动态script元素实现非阻塞下载 - 延迟加载 - 按需加载 - - 预加载 -- 小节 - -## 索引 - + - 预加载JavaScript +- 小结 diff --git a/chapter1.markdown b/chapter1.markdown index d9ff15a..c0a253c 100644 --- a/chapter1.markdown +++ b/chapter1.markdown @@ -1,145 +1,137 @@ -# 第一章 概述 +# 第一章 绪言 -JavaScript是一门Web开发语言。起初只是用来操作网页中为数不多的元素(比如图片和表单域),但谁也没想到这门语言的成长是如此迅速。除了适用于客户端浏览器编程,如今JavaScript程序可以运行于越来越多的平台之上。你可以用它来进行服务器端开发(使用.Net或Node.js)、桌面应用程序开发(运行于桌面操作系统)、以及应用程序扩展(Firefox插件或者Photoshop扩展)、移动终端应用和纯命令行的批处理脚本。 +JavaScript是一门Web开发语言。起初人们只是用它来操作网页中为数不多的元素(比如图片和表单域),但是谁也没想到这门语言可以成长得如此迅速,如今,JavaScript除了适用于客户端浏览器编程外,还可以在越来越多的平台上运行。你可以用它来开发服务端程序(使用.Net或Node.js)、桌面应用程序(运行于桌面操作系统)、应用程序扩展(Firefox插件或者Photoshop扩展)、移动终端应用和纯命令行的批处理脚本。 -JavaScript同样是一门不寻常的语言。它没有类,许多场景中它使用函数作为一等对象。起初,许多开发者认为这门语言存在很多缺陷,但最近几年情况发生了微妙的变化。有意思的是,有一些老牌语言比如Java和PHP也已经开始添加诸如闭包和匿名函数等新特性,而闭包和匿名函数则是JavaScript程序员最愿意津津乐道的话题。 +JavaScript是一门有些独特的语言。它没有类,在很多场景中它都将函数作为“第一型”(first-class objects,中文也有译作“第一类”,以前叫“一等公民”)。起初,许多开发者认为这门语言存在很多缺陷,但最近几年情况发生了微妙的变化。有意思的是,有一些老牌语言比如Java和PHP也已经开始添加诸如闭包和匿名函数等新特性,而闭包和匿名函数则是JavaScript程序员最津津乐道的话题。 -JavaScript十分灵活,可以用你所熟悉的其他任何编程语言的编程风格来写JavaScript程序。但最好的方式还是拥抱它所带来的变化、学习它所特有的编程模式。 +尽管JavaScript十分灵活,可以用你所熟悉的其他任何编程语言的编程风格来写JavaScript程序,但最好的方式还是拥抱它的独到之处、学习它所特有的编程模式。 ## 模式 -对 “模式”的广义解释是“反复发生的事件或对象的固定用法...可以用来作为重复使用的模板或模型”(http://en.wikipedia.org/wiki/Pattern)。 +对“模式”的广义解释是“反复发生的事件或对象的固定用法...可以用来作为重复使用的模板或模型”()。 在软件开发领域,模式是指常见问题的通用解决方案。模式不是简单的代码复制和粘贴,而是一种最佳实践,一种高级抽象,是解决某一类问题的范本。 -识别这些模式非常重要,因为: +学习这些模式非常重要,因为: -- 这些模式提供了经过论证的最佳实践,它可以帮助我们更好的编码,避免重复制造车轮。 -- 这些模式提供了高一层的抽象,某个时间段内大脑只能处理一定复杂度的逻辑,因此当你处理更繁琐棘手的问题时,它会帮你理清头绪,你才不会被低级的琐事阻碍大脑思考,因为所有的细枝末节都可以被归类和切分成不同的块(模式)。 -- 这些模式为开发者和团队提供了沟通的渠道,团队开发者之间往往是异地协作,不会有经常面对面的沟通机会。简单的代码编写技巧和技术问题处理方式的约定(代码注释)使得开发者之间的交流更加通畅。例如,“函数立即执行”用大白话表述成“你写好一个函数后,在函数的结束花括号的后面添加一对括号,这样能在定义函数结束后马上执行这个函数”(我的天)。 +- 这些模式提供了经过论证的最佳实践,它可以帮助我们更好的编码,避免重复造轮子。 +- 这些模式提供了高一层的抽象。一个时间段内大脑只能处理一定复杂度的逻辑,因此当你处理更繁琐棘手的问题时,使用模式可以帮你理清头绪,不会被低级的琐事阻碍大脑思考,因为所有的细枝末节都可以被归类和切分成不同的块(模式)。 +- 这些模式为开发者和团队提供了沟通的渠道,团队开发者之间往往是异地协作,不会有经常面对面的沟通机会。简单的代码编写技巧和技术问题处理方式的约定(代码注释)可以使开发者之间的交流更加通畅。例如,说“即时函数”(immediate function)比说“你写好一个函数后,在函数的结束花括号的后面添加一对括号,这样能在定义函数结束后马上执行这个函数”要更容易表达和理解。 本书将着重讨论下面这三种模式: -- 设计模式(Design patterns) -- 编码模式(Coding patterns) -- 反模式(Antipatterns) +- 设计模式(design patterns) +- 编码模式(coding patterns) +- 反模式(antipatterns) -设计模式最初的定义是来自于“GoF”(四人组,94年版“设计模式”的四个作者)的一本书,这本书在1994年出版,书名全称是“设计模式:可复用面向对象软件基础”。书中列举了一些重要的设计模式,比如单体、工厂、装饰者、观察者等等。但适用于JavaScript的设计模式并不多,尽管设计模式是脱离某种语言而存在的,但通常会以某种语言做范例来讲解设计模式,这些语言多是强类型语言,比如C++和Java。有时直接将其应用于弱类型的动态语言比如JavaScript又显得捉襟见肘。通常这些设计模式都是基于语言的强类型特性以及类的继承。而JavaScript则需要某种轻型的替代方案。本书在第七章将讨论基于 JavaScript实现的一些设计模式。 +“设计模式”最初的定义是来自于1994年出版的《设计模式:可复用面向对象软件基础》,作者是“GoF”(四人组,即四位作者)。书中列举了一些重要的设计模式,比如单例、工厂、装饰者、观察者等等。尽管设计模式是脱离某种特定的语言而存在的,但通常仍会以某种语言作为范例来讲解设计模式,这些语言多是强类型语言,比如C++和Java。有时直接将其应用于弱类型的动态语言比如JavaScript会显得毫无意义,因此适用于JavaScript的设计模式并不多。一般而言,设计模式都是基于语言的强类型特性以及基于类的继承发展而来,而对JavaScript来说则需要某种更简单的替代方案。在本书第七章将讨论基于 JavaScript实现的一些设计模式。 -编码模式更有趣一些。它们是JavaScript特有的模式和最佳实践,它利用了这门语言独有的一些特性,比如对函数的灵活运用,JavaScript编码模式是本书所要讨论的重点内容。 +“编码模式”会更有趣一些,它们是JavaScript特有的模式和最佳实践,利用了这门语言独有的一些特性,比如对函数的灵活运用。JavaScript编码模式是本书所要讨论的重点内容。 -本书中你会偶尔读到一点关于“反模式”的内容,顾名思义,反模式具有某些负作用甚至破坏性,书中会顺便一提。反模式并不是bug或代码错误,它只是一种处理问题的对策,只是这种对策带来的麻烦远超过他们解决的问题。在示例代码中我们会对反模式做明显的标注。 +本书中你会偶尔读到一点关于“反模式”的内容,顾名思义,反模式具有某些负作用甚至破坏性,书中会在讲到有关的话题时提出来。反模式并不是bug或代码错误,它只是一种处理问题的对策,但是这种对策带来的麻烦远超过他们解决的问题。在示例代码中我们会对反模式做明显的标注。 - ## JavaScript:概念 -在正式的讨论之前,应当先理清楚JavaScript中的一些重要的概念,这些概念在后续章节中会经常碰到,我们先来快速过一下。 +在正式的讨论之前,应当先理清楚JavaScript中的一些重要概念,这些概念在后续章节中会经常碰到,我们先来快速过一下。 - ### 面向对象 -JavaScript 是一门面向对象的编程语言,对于那些仓促学习JavaScript并很快丢掉它的开发者来说,这的确有点让人感到意外。你所能接触到的任何JavaScript代码片段都可以作为对象。只有五类原始类型不是对象,它们是数字、字符串、布尔值、null和undefined,前三种类型都有与之对应的包装对象(下一章会讲到)。数字、字符串和布尔值可以轻易的转换为对象类型,可以通过手动转换,也可以利用JavaScript解析器进行自动转换。 +JavaScript是一门面向对象的编程语言,这一点对于那些对JavaScript了解不多的开发者来说的确有点意外。你所能接触到的任何JavaScript代码片段都可以作为对象。只有五类原始类型不是对象,它们是数字、字符串、布尔值、`null`和`undefined`,数字、字符串和布尔值类型都有与之对应的包装对象(下一章会讲到),可以轻易的转换为对象类型,可以通过手动转换,也可以利用JavaScript解析器进行自动转换。 函数也是对象,也可以拥有属性和方法。 -在任何语言中,最简单的操作莫过于定义变量。那么,在JavaScript中定义变量的时候,其实也在和对象打交道。首先,变量自动变为一个被称作“活动对象”的内置对象的属性(如果是全局变量的话,就变为全局对象的属性)。第二,这个变量实际上也是“伪对象”,因为它有自己的属性(属性特性),用以表示变量是否可以被修改、删除或在for-in循环中枚举。这些特性并未在ECMAScript3中作规定,而ECMAScript5中提供了一组可以修改这些特性的方法。 +在任何语言中,最简单的操作莫过于定义变量。在JavaScript中定义变量的时候,其实也在和对象打交道。首先,变量自动成为一个内置对象的属性(这个内置对象被称作“活动对象”,如果是全局变量的话,就变为全局对象的属性)。其次,这个变量实际上也是“伪对象”,因为它有自己的属性(译注:原文使用了`attributes`,指内置的特性),用以表示变量是否可以被修改、删除或在`for-in`中枚举。这些特性并未在ECMAScript3中作规定,但ECMAScript5中提供了一组可以修改这些特性的方法。 -那么,到底什么是对象?对象能作这么多事情,那它们一定非常特别。实际上,对象是及其简单的。对象只是很多属性的集合,一个名值对的列表(在其他语言中可能被称作关联数组),这些属性也可以是函数(函数对象),这种函数我们称为“方法”。 +那么,到底什么是对象?对象能做这么多事情,那它们一定非常特别。实际上,对象是极其简单的。对象只是很多属性的集合,一个名值对的列表(在其他语言中可能被称作关联数组),这些属性也可以是函数(函数对象),这种函数我们称为“方法”。 -关于对象还需要了解,我们可以随时随地修改你创建的对象(当然,ECMAScript5中提供了可阻止这些修改的API)。得到一个对象后,你可以给他添加、删除或更新成员。如果你关心私有成员和访问控制,本书中我们也会讲到相关的编程模式。 +关于对象,我们还需要了解,我们可以随时随地修改已经创建的对象(ECMAScript5中提供了可阻止这些修改的API)。得到一个对象后,你可以给他添加、删除或更新成员。如果你关心私有成员和访问控制,我们也会在本书中讲到相关的模式。 最后一个需要注意的是,对象有两大类: -- 本地对象(Native):由ECMAScript标准规范定义的对象 +- 本地对象(Native):由ECMAScript标准定义的对象 - 宿主对象(Host):由宿主环境创建的对象(比如浏览器环境) -本地对象也可以被归类为内置对象(比如Array,Date)或自定义对象(var o = {})。 +本地对象也可以被归类为内置对象(比如`Array`、`Date`)或自定义对象(var o = {})。(译注:指本地对象包含内置对象和自定义对象。) -宿主对象包含window和所有DOM对象。如果你想知道你是否在使用宿主对象,将你的代码迁移到一个非浏览器环境中运行一下,如果正常工作,那么你的代码只用到了本地对象。 +宿主对象包含`window`和所有DOM对象。如果你想知道你是否在使用宿主对象,将你的代码迁移到一个非浏览器环境中运行一下,如果正常工作,那么你的代码就只用到了本地对象。 - -### 无类 +### 没有类 在本书中的许多场合都会反复碰到这个概念。JavaScript中没有类,对于其他语言的编程老手来说这个观念非常新颖,需要反复的琢磨和重新学习才能理解JavaScript只能处理对象的观念。 -没有类,你的代码变得更小巧,因为你不必使用类去创建对象,看一下Java风格的对象创建: +没有类,你的代码会变得更小巧,因为你不必使用类去创建对象,看一下Java风格的对象创建: - // Java object creation + // Java中创建对象 HelloOO hello_oo = new HelloOO(); -为了创建一个简单的对象,同样一件事情却重复做了三遍,这让这段代码看起来很“重”。而大多数情况下,我们只想让我们的对象保持简单。 +为了创建一个简单的对象,同样一件事情重复做了三遍,这让这段代码看起来很“重”。而大多数情况下,我们想让我们的对象保持简单。 -在JavaScript中,你需要一个对象,就随手创建一个空对象,然后开始给这个对象添加有趣的成员。你可以给它添加原始值、函数或其他对象作为这个对象属性。“空”对象并不是真正的空,对象中存在一些内置的属性,但并没有“自有属性”。在下一章里我们对此作详细讨论。 +在JavaScript中,你需要一个对象,就随手创建一个空对象,然后给这个对象添加你需要的成员。你可以给它添加原始值、函数或其他对象作为这个对象属性。“空”对象并不是真正的空,对象中存在一些内置的属性,但并没有“自有属性”。在下一章里我们对此作详细讨论。 -“GoF”的书中提到一条通用规则,“组合优于继承”,也就是说,如果你手头有创建这个对象所需的资源,更推荐直接将这些资源组装成你所需的对象,而不推荐先作分类再创建链式父子继承的方式来创建对象。在JavaScript中,这条规则非常容易遵守,因为JavaScript中没有类,而且对象组装无处不在。 +“GoF”的书中提到一条通用规则,“组合优于继承”,也就是说,如果你手头有创建这个对象所需的资源,更推荐直接将这些资源组装成你所需的对象,而不推荐通过先做分类再创建链式父子继承的方式来创建对象。在JavaScript中,这条规则非常容易遵守,因为JavaScript中没有类,而对象组装无处不在。 - ### 原型 -JavaScript中的确有继承,尽管这只是一种代码重用的方式(本书有专门的一章来讨论代码重用)。继承可以有多种方式,最常用的方式就是利用原型。原型(prototype)是一个普通的对象,你所创建的每一个函数会自动带有prototype属性,这个属性指向一个空对象,这个空对象包含一个constructor属性,它指向你新建的函数而不是内置的Object(),除此之外它和通过对象直接量或Object()构造函数创建的对象没什么两样。你可以给它添加新的成员,这些成员可以被其他的对象继承,并当作其他对象的自有属性来使用。 +尽管继承只是实现代码复用的其中一种方式,但在JavaScript中的确有继承(本书有专门的一章来讨论代码复用)。继承可以通过多种方式实现,最常用的就是利用原型。“原型”(prototype)是一个普通的对象,你所创建的每一个函数会自动带有`prototype`属性,这个属性指向一个空对象,这个空对象包含一个`constructor`属性,它指向你新建的函数而不是内置的`Object()`。除此之外,它和通过对象直接量或`Object()`构造函数创建的对象没什么两样。你可以给它添加新的成员,这些成员可以被其他对象继承,并当作其他对象的自有属性来使用。 -我们会详细讨论JavaScript中的继承,现在只要记住:原型是一个对象(不是类或者其他什么特别的东西),每个函数都有一个prototype属性。 +我们后面会详细讨论JavaScript中的继承,现在只要记住:原型是一个对象(不是类或者其他什么特别的东西),每个函数都有一个`prototype`属性。 - ### 运行环境 -JavaScript程序需要一个运行环境。一个天然的运行环境就是浏览器,但这绝不是唯一的运行环境。本书所讨论的编程模式更多的和JavaScript语言核心(ECMAScript)相关,因此这些编程模式是环境无关的。有两个例外: +JavaScript程序需要一个运行环境。最理所当然的运行环境就是浏览器,但这绝不是唯一的运行环境。本书所讨论的编程模式更多的和JavaScript语言核心(ECMAScript)相关,因此这些编程模式是环境无关的。除了有两个例外: - 第八章,这一章专门讲述浏览器相关的模式 -- 其他一些展示模式的实际应用的例子 +- 一些演示模式用法的实际程序 -运行环境会提供自己的宿主对象,这些宿主对象并未在ECMAScript标准中定义,它们的行为也是不可预知的。 +运行环境会提供自己的宿主对象,这些宿主对象并未在ECMAScript标准中定义,因此它们的行为也是不可预知的。 - ## ECMAScript 5 -JavaScript语言的核心部分(不包含DOM、BOM和外部宿主对象)是基于ECMAScript标准(简称为ES)来实现的。其中第三版是在1999年正式颁布的,目前大多数浏览器都实现了这个版本。第四版已经废弃了。第三版颁布后十年,2009年十二月,第五版才正式颁布。 +JavaScript语言的核心部分(不包含DOM、BOM和其它宿主对象)是基于ECMAScript标准(简称为ES)来实现的。其中第三版是在1999年正式颁布的,目前大多数浏览器都实现了这个版本。第四版已经废弃了。第三版颁布后十年,2009年12月,第五版才正式颁布。 -第五版增加了新的内置对象、方法和属性,但最重要的增加内容是所谓的严格模式(strict mode),这个模式移除了某些语言特性,让程序变得简单且健壮。比如,with语句的使用已经争论了很多年,如今,在ECMAScript5严格模式中使用with则会报错,而在非严格模式中则是ok的。我们通过一个指令来激活严格模式,这个指令在旧版本的语言实现中被忽略。也就是说,严格模式是向下兼容的,因为在不支持严格模式的旧浏览器中也不会报错。 +第五版增加了新的内置对象、方法和属性,但最重要的一项是所谓的“严格模式”(strict mode),这个模式移除了一些语言特性,让程序变得更简单更健壮。比如,对with语句的使用已经争论了很多年,现在在ECMAScript5严格模式中使用with则会报错,而在非严格模式中则是允许的。我们通过一个指令来激活严格模式,这个指令在旧版本的语言实现中被忽略。也就是说,严格模式是向下兼容的,因为在不支持严格模式的旧浏览器中也不会报错。 -对于每一个作用域(包括函数作用域、全局作用域或在eval()参数字符串的开始部分),你可以使用这种代码来激活严格模式: +对于每一个作用域(包括函数作用域、全局作用域或在传给`eval()`的参数字符串的开始部分),你可以使用这种代码来激活严格模式: function my() { "use strict"; - // rest of the function... + // 函数剩余的部分…… } -这样就激活了严格模式,函数的执行则会被限制在语言的严格子集的范围内。对于旧浏览器来说,这句话只是一个没有赋值给任何变量的字符串,因此不会报错。 +这样就激活了严格模式,函数的执行会被限制在语言的严格子集的范围内。对于旧浏览器来说,这句话只是一个没有赋值给任何变量的字符串,因此不会报错。 按照语言的发展计划,未来将会只保留“严格模式”。因此,现在的ES5只是一个过渡版本,它鼓励开发者使用严格模式,而非强制。 -本书不会讨论ES5新增特性相关的模式,因为在本书截稿时并没有任何浏览器实现了ES5,但本书的示例代码通过一些技巧鼓励开发者向新标准转变: +本书不会讨论ES5新增特性相关的模式,因为在本书截稿时并没有任何浏览器实现了ES5(译注:截止译稿校对时,Chrome/Firefox/IE9+已(部分)实现ES5,具体兼容情况可参考),但本书的示例代码有以下特点,以鼓励开发者向新标准转变: -- 确保所提供的示例代码在严格模式下不包错 +- 确保所提供的示例代码在严格模式下不报错 - 避免使用并明确指出弃用的构造函数相关的属性和方法,比如arguments.callee -- 针对ES5中的内置模式比如Object.create(),在ES3中实现等价的模式 +- 针对ES5中的内置模式比如Object.create(),在ES3中做同样的实现 - ## JSLint -JavaScript是一种解释型语言,它没有静态编译时的代码检查,所以很可能将带有简单类型错误的破碎的程序部署到线上,而且往往意识不到这些错误的存在。这时我们就需要JSLint的帮助。 +JavaScript是一种解释型语言,它没有静态编译时的代码检查,所以将一个仅仅因为类型错误而导致不正常的程序部署上线是完全可能的事情,而且开发者往往意识不到这些错误的存在,这时我们就需要JSLint的帮助。 -JSLint(http://jslint.com )是一个JavaScript代码质量检测工具,它的作者是 Douglas Crockford,JSLint会对代码作扫描,并针对潜在的问题报出警告。笔者强烈推荐你在执行代码前先通过JSlint作检查。作者给出了警告:这个工具可能“会让你不爽”,但仅仅是在开始使用它的时候不爽一下而已。你会很快从你的错误中吸取教训,并学习这些成为一名专业的JavaScript程序员应当必备的好习惯。让你的代码通过JSLint的检查,这会让你对自己的代码更加有自信,因为你不用再去担心代码中某个不起眼的地方丢失了逗号或者有某种难以察觉的语法错误。 +JSLint( )是一个JavaScript代码质量检测工具,它的作者是 Douglas Crockford。JSLint会对代码进行扫描,并针对可能存在的问题做出警告。笔者强烈推荐你在执行代码前先通过JSlint进行检查。作者的忠告:这个工具可能“会让你不爽”,但仅仅是在开始使用它的时候不爽一下而已,你会很快会从你的错误中吸取教训,并通过它们掌握一些专业的JavaScript程序员应有的好习惯。通过JSLint的检查会让你对自己的代码更有信心,因为你不用再担心代码中某个不起眼的地方丢失了一个逗号或者有某种难以察觉的语法错误。 -当开始下一章的学习时,你将发现JSLint会被多次提到。本书中除了讲解反模式的示例代码外(有清楚的注释说明)、所有示例代码均通过了JSLint的检查(使用JSLint的默认设置)。 +当开始下一章的学习时,你将发现JSLint被多次提到。本书中除了讲解反模式的示例代码外(有清楚的注释说明)、所有示例代码均通过了JSLint的检查(使用JSLint的默认设置)。 - ## 控制台工具 -console对象在本书中非常常见。这个对象并不是语言的一部分,而是运行环境的一部分,目前大多数浏览器也都实现了这个对象。比如在Firefox中,它是通过Firebug扩展引入进来的。Firebug控制台工具包含UI操作界面,可以让你快速输入并测试JavaScript代码片段,同样用它可以调试当前打开的页面(图1-1)。在这里强烈推荐使用它来辅助学习。在Webkit核心的浏览器(Safari和Chrome)也提供了类似的工具,可以监控页面情况,IE从版本8开始也提供了开发者工具。 +`console`对象在本书中非常常见。这个对象并不是语言的一部分,而是运行环境的一部分,目前大多数浏览器也都实现了这个对象。比如在Firefox中,它是通过Firebug扩展引入进来的。Firebug控制台工具包含UI操作界面,可以让你快速输入并测试JavaScript代码片段,也可以用它调试当前页面(图1-1)。笔者强烈推荐你使用它来辅助学习。Webkit核心的浏览器(Safari和Chrome)也提供了类似的工具,可以监控页面情况,IE8+也提供了开发者工具。 -本书中大多数代码都使用console对象来输出结果,而没有使用alert()或者刷新当前页面。因为用这种方法输出结果实在太简单了。 +本书中大多数代码都使用`console`对象来输出结果,而没有使用`alert()`或者刷新当前页面,因为用这种方法输出结果实在太方便了。 -图 1-1 使用Firebug控制台工具 +![console](./Figure/chapter1/1-1.jpg) -![console](http://img01.taobaocdn.com/tps/i1/T1AGmgXgxvXXXXXXXX-629-383.png) +图 1-1 使用Firebug控制台 -我们经常使用log()方法,它将传入的参数在控制台输出,有时会用到dir(),用以将传入的对象属性枚举出来,这里是一个例子: +我们经常使用`log()`方法,它将传入的参数在控制台输出,有时也会用到`dir()`,它可以将传入对象的属性枚举出来。例如: console.log("test", 1, {}, [1,2,3]); console.dir({one: 1, two: {three: 3}}); -当你在控制台输入内容时,则不必使用console.log()。为了避免混乱,有些代码片段仍然使用console.log()作输出,并假设所有的代码片段都使用控制台来作检测: +当你在控制台输入内容时,不必使用`console.log()`。为了避免混乱,有些代码片段仍然会使用`console.log()`进行输出,并假设所有的代码片段都使用控制台来运行: window.name === window['name']; // true @@ -147,6 +139,4 @@ console对象在本书中非常常见。这个对象并不是语言的一部分 console.log(window.name === window['name']); -这段代码在控制台中输出为true。 - - +这段代码在控制台中输出为`true`。 diff --git a/chapter2.markdown b/chapter2.markdown index 2913054..41dc594 100644 --- a/chapter2.markdown +++ b/chapter2.markdown @@ -1,157 +1,150 @@ - -# 第二章 高质量JavaScript基本要点 +# 第二章 概要 -本章将对一些实质内容展开讨论,这些内容包括最佳实践、模式和编写高质量JavaScript代码的习惯,比如避免全局变量、使用单var声明、循环中的length预缓存、遵守编码约定等等。本章还包括一些非必要的编程习惯,但更多的关注点将放在总体的代码创建过程上,包括撰写API文档、组织相互评审以及使用JSLint。这些习惯和最佳实践可以帮助你写出更好的、更易读的和可维护的代码,当几个月后或数年后再重读你的代码时,你就会深有体会了。 +本章将概要介绍一些编写高质量JavaScript的最佳实践、模式和习惯,比如避免全局变量、使用单`var`声明、预缓存循环中的`length`、遵守编码约定等等。本章还包括一些编程习惯,这些习惯跟具体的代码关系不大,而是更多关注代码创建的总体过程,包括撰写API文档、同事评审以及使用JSLint。这些习惯和最佳实践可以帮助你写出更好更易读和可维护性更好的代码,当几个月或数年后你再重读你的代码时,就会深有体会了。 - ## 编写可维护的代码 -修复软件bug成本很高,而且随着时间的推移,它们造成的损失也越来越大,特别是在已经打包发布了的软件发现了bug的时候。当然最好是发现bug立刻解决掉,但前提是你对你的代码依然很熟悉,否则当你转身投入到另外一个项目的开发中后,根本不记得当初代码的模样了。过了一段时间后你再去阅读当初的代码你需要: +修复软件bug成本很高,而且随着时间的推移,修复这些bug的成本会越来越高,尤其以出现在已经打包发布的软件中的bug为最甚。发现bug时立刻解决掉是最好的,但前提是你对你的代码依然很熟悉,否则当你转身投入到另外一个项目的开发中后,已经根本不记得当初的代码的模样了。当过了一段时间后你再去阅读当初的代码时你需要更多的时间: -- 时间来重新学习并理解问题 -- 时间去理解问题相关的代码 +- 重新学习并理解面临的问题 +- 理解用于问题的代码 -对大型项目或者公司来说还有一个不得不考虑的问题,就是解决这个bug的人和制造这个bug的人往往不是同一个人。因此减少理解代码所需的时间成本就显得非常重要,不管是隔了很长时间重读自己的代码还是阅读团队内其他人的代码。这对于公司的利益底线和工程师的幸福指数同样重要,因为每个人都宁愿去开发新的项目而不愿花很多时间和精力去维护旧代码。 +在大项目或者大公司的软件开发中还有另一个问题,就是解决这个bug的人和制造这个bug的人往往不是同一个人(而发现bug的往往又是另外一个人)。因此不管是隔了很长时间重读自己的代码还是阅读团队内其他人的代码,减少理解代码所需的时间成本都是非常重要的。这对于公司的利益底线和工程师的幸福指数同样重要,因为每个人都宁愿去开发新的项目而不愿花很多时间和精力去维护旧代码。 -另外一个软件开发中的普遍现象是,在读代码上花的时间要远远超过写代码的时间。常常当你专注于某个问题的时候,你会坐下来用一下午的时间产出大量的代码。当时的场景下代码是可以正常运行的,但当应用趋于成熟,会有很多因素促使你重读代码、改进代码或对代码做微调。比如: +软件开发中的另一个普遍现象是,在读代码上花的时间要远远超过写代码的时间。当你专注于某个问题的时候,你往往会坐下来用一下午的时间写出大量的代码。在当时的场景下,这些代码是可以正常运行的,但当应用趋于成熟,会有很多因素促使你重读代码、改进代码或对代码做微调。比如: - 发现了bug - 需要给应用添加新需求 - 需要将应用迁移到新的平台中运行(比如当市场中出现了新的浏览器时) - 代码重构 -- 由于架构更改或者更换另一种语言导致代码重写 +- 由于架构更改或者更换语言导致代码重写 这些不确定因素带来的后果是,少数人花几小时写的代码需要很多人花几个星期去阅读它。因此,创建可维护的代码对于一个成功的应用来说至关重要。 可维护的代码意味着代码是: - 可读的 -- 一致的 +- 风格一致的 - 可预测的 - 看起来像是同一个人写的 - 有文档的 本章接下来的部分会对这几点深入讲解。 - -## 减少全局对象 +## 减少全局变量 -JavaScript 使用函数来管理作用域,在一个函数内定义的变量称作“局部变量”,局部变量在函数外部是不可见的。另一方面,“全局变量”是不在任何函数体内部声明的变量,或者是直接使用而未明的变量。 +JavaScript使用函数来管理作用域,在一个函数内定义的变量称作“本地变量”,本地变量在函数外部是不能被访问的。与之相对,“全局变量”是不在任何函数体内部声明的变量,或者是直接使用而未声明的变量。 -每一个JavaScript运行环境都有一个“全局对象”,不在任何函数体内使用this就可以获得对这个全局对象的引用。你所创建的每一个全局变量都是这个全局对象的属性。为了方便起见,浏览器都会额外提供一个全局对象的属性window,(常常)用以指向全局对象本身。下面的示例代码中展示了如何在浏览器中创建或访问全局变量: +每一个JavaScript运行环境都有一个“全局对象”,不在任何函数体内使用this就可以获得对这个全局对象的引用。你所创建的每一个全局变量都是这个全局对象的属性。为了方便起见,浏览器会额外提供一个全局对象的属性`window`,(一般)指向全局对象本身。下面的示例代码展示了如何在浏览器中创建或访问全局变量: - myglobal = "hello"; // antipattern + myglobal = "hello"; // 反模式 console.log(myglobal); // "hello" console.log(window.myglobal); // "hello" console.log(window["myglobal"]); // "hello" console.log(this.myglobal); // "hello" - -### 全局对象带来的困扰 +### 全局变量的问题 -全局变量的问题是,它们在JavaScript代码执行期间或者整个web页面中始终是可见的。它们存在于同一个命名空间中,因此命名冲突的情况时有发生,毕竟在应用程序的不同模块中,经常会出于某种目的定义相同的全局变量。 +全局变量的问题是,它们在整个JavaScript应用或者是整个web页面中是始终被所有代码共享的。它们存在于同一个命名空间中,因此命名冲突的情况会时有发生,毕竟在应用程序的不同模块中,经常会出于某种目的定义相同的全局变量。 -同样,常常网页中所嵌入的代码并不是这个网页的开发者所写,比如: +同样,在网页中嵌入不是页面开发者编写的代码是很常见的,比如: - 网页中使用了第三方的JavaScript库 - 网页中使用了广告代码 - 网页中使用了用以分析流量和点击率的第三方统计代码 -- 网页中使用了很多组件,挂件和按钮等等 +- 网页中使用了很多组件、挂件和按钮等等 -假设某一段第三方提供的脚本定义了一个全局变量result。随后你在自己写的某个函数中也定义了一个全局变量result。这时,第二个变量就会覆盖第一个,这时就会导致第三方脚本停止工作。 +假设某一段第三方提供的脚本定义了一个全局变量result。随后你在自己写的某个函数中也定义了一个全局变量result。这时,第二个变量就会覆盖第一个,会导致第三方脚本工作不正常。 -因此,为了让你的脚本和这个页面中的其他脚本和谐相处,要尽可能少的使用全局变量,这一点非常重要。本书随后的章节中会讲到一些减少全局变量的技巧和策略,比如使用命名空间或者立即执行的匿名函数等,但减少全局变量最有效的方法是坚持使用var来声明变量。 +因此,为了让你的脚本和这个页面中的其他脚本和谐相处,要尽量少使用全局变量,这一点非常重要。本书随后的章节中会讲到一些减少全局变量的技巧和策略,比如使用命名空间或者即时函数等,但减少全局变量最有效的方法还是坚持使用`var`来声明变量。 -由于JavaScript的特点,我们经常有意无意的创建全局变量,毕竟在JavaScript中创建全局变量实在太简单了。首先,你可以不声明而直接使用变量,再者,JavaScirpt中具有“隐式全局对象”的概念,也就是说任何不通过var声明(译注:在JavaScript1.7及以后的版本中,可以通过let来声明块级作用域的变量)的变量都会成为全局对象的一个属性(可以把它们当作全局变量)。看一下下面这段代码: +在JavaScript中有意无意地创建全局变量是件很容易的事,因为它有两个特性:首先,你可以不声明而直接使用变量,其次,JavaScirpt中具有“隐式全局对象”的概念,也就是说任何不通过`var`声明的变量都会成为全局对象的一个属性(可以把它们当作全局变量)。(译注:在ES6中可以通过`let`来声明块级作用域变量。)看一下下面这段代码: function sum(x, y) { - // antipattern: implied global + // 反模式:隐式全局变量 result = x + y; return result; } -这段代码中,我们直接使用了result而没有事先声明它。这段代码是能够正常工作的,但在调用这个方法之后,会产生一个全局变量result,这会带来其他问题。 +这段代码中,我们直接使用了`result`而没有事先声明它。这段代码的确是可以正常工作,但被调用后会产生一个全局变量`result`,这可能会导致其他问题。 -解决办法是,总是使用var来声明变量,下面代码就是改进了的sum()函数: +解决办法是,总是使用var来声明变量,下面代码就是改进了的`sum()`函数: function sum(x, y) { var result = x + y; return result; } -这里我们要注意一种反模式,就是在var声明中通过链式赋值的方法创建全局变量。在下面这个代码片段中,a是局部变量,但b是全局变量,而作者的意图显然不是如此: +另一种创建全局变量的反模式,就是在`var`声明中使用链式赋值的方法。在下面这个代码片段中,`a`是局部变量,但`b`是全局变量,而作者的意图显然不是这样: - // antipattern, do not use + // 反模式 function foo() { var a = b = 0; // ... } -为什么会这样?因为这里的计算顺序是从右至左的。首先计算表达式b=0,这里的b是未声明的,这个表达式的值是0,然后通过var创建了局部变量a,并赋值为0。换言之,可以等价的将代码写成这样: +为什么会这样呢?因为这里的计算顺序是从右至左的:首先计算表达式`b=0`,这里的`b`是未声明的;这个表达式的结果是`0`,然后通过var创建了本地变量`a`,并赋值为`0`。换言之,可以将代码写成这样: var a = (b = 0); -如果变量b已经被声明,这种链式赋值的写法是ok的,不会意外的创建全局变量,比如: +如果变量b已经被声明,这种链式赋值的写法是可以使用的,不会意外地创建全局变量,比如: function foo() { var a, b; // ... - a = b = 0; // both local + a = b = 0; // 两个都是本地变量 } -> 避免使用全局变量的另一个原因是出于可移植性考虑的,如果你希望将你的代码运行于不同的平台环境(宿主),使用全局变量则非常危险。很有可能你无意间创建的某个全局变量在当前的平台环境中是不存在的,你认为可以安全的使用,而在其他的环境中却是存在的。 +> 避免使用全局变量的另一个原因是出于可移植性考虑,如果你希望将你的代码运行于不同的平台环境(宿主),那么使用全局变量就非常危险。因为很有可能你无意间创建的某个全局变量在当前的平台环境中是不存在的,你以为可以安全地使用,而在另一个环境中却是本来就存在的。 - ### 忘记var时的副作用 -隐式的全局变量和显式定义的全局变量之间有着细微的差别,差别在于通过delete来删除它们的时候表现不一致。 +隐式创建的全局变量和显式定义的全局变量之间有着细微的差别,就是通过`delete`来删除它们的时候表现不一致。 -- 通过var创建的全局变量(在任何函数体之外创建的变量)不能被删除。 -- 没有用var创建的隐式全局变量(不考虑函数内的情况)可以被删除。 +- 通过`var`创建的全局变量(在任何函数体之外创建的变量)不能被删除。 +- 没有用`var`创建的隐式全局变量(不考虑函数内的情况)可以被删除。 -也就是说,隐式全局变量并不算是真正的变量,但他们是全局对象的属性成员。属性是可以通过delete运算符删除的,而变量不可以被删除: +也就是说,隐式全局变量并不算是真正的变量,但它们却是全局对象的属性。属性是可以通过`delete`运算符删除的,而变量不可以被删除: - // define three globals + // 定义三个全局变量 var global_var = 1; - global_novar = 2; // antipattern + global_novar = 2; // 反模式 (function () { - global_fromfunc = 3; // antipattern + global_fromfunc = 3; // 反模式 }()); - // attempt to delete + // 尝试删除 delete global_var; // false delete global_novar; // true delete global_fromfunc; // true - // test the deletion + // 测试删除结果 typeof global_var; // "number" typeof global_novar; // "undefined" typeof global_fromfunc; // "undefined" 在ES5严格模式中,给未声明的变量赋值会报错(比如这段代码中提到的两个反模式)。 - ### 访问全局对象 -在浏览器中,我们可以随时随地通过window属性来访问全局对象(除非你定义了一个名叫window的局部变量)。但换一个运行环境这个方便的window可能就换成了别的名字(甚至根本就被禁止访问全局对象了)。如果不想通过这种写死window的方式来得到全局变量,有一个办法,你可以在任意层次嵌套的函数作用域内执行: +在浏览器中,我们可以随时随地通过`window`属性来访问全局对象(除非你定义了一个名叫`window`的局部变量)。但换一个运行环境这个`window`可能就换成了别的名字(甚至根本就被禁止访问全局对象了)。如果不想通过这种写死`window`的方式来访问全局变量,那么你可以在任意函数作用域内执行: var global = (function () { return this; }()); -这种方式总是可以得到全局对象,因为在被当作函数执行的函数体内(而不是被当作构造函数执行的函数体内),this总是指向全局对象。但这种情况在ECMAScript5的严格模式中行不通,因此在严格模式中你不得不寻求其他的替代方案。比如,如果你在开发一个库,你会将你的代码包装在一个立即执行的匿名函数中(在第四章会讲到),然后从全局作用域中给这个匿名函数传入一个指向this的参数。 +这种方式总是可以访问到全局对象,因为在被当作函数(而不是构造函数)执行的函数体内,`this`总是指向全局对象。但这种情况在ECMAScript5的严格模式中行不通,因此在严格模式中你不得不寻求其他的替代方案。比如,如果你在开发一个库,你会将你的代码包装在一个即时函数中(在第四章会讲到),然后从全局作用域给这个匿名函数传入一个指向`this`的参数。 - -### 单 var 模式 +### 单var模式 -在函数的顶部使用一个单独的var语句是非常推荐的一种模式,它有如下一些好处: +在函数的顶部使用唯一一个`var`语句是非常推荐的一种模式,它有如下一些好处: -- 在同一个位置可以查找到函数所需的所有变量 -- 避免当在变量声明之前使用这个变量时产生的逻辑错误(参照下一小节“声明提前:分散的 var 带来的问题”) +- 可以在同一个位置找到函数所需的所有变量 +- 避免在变量声明之前使用这个变量时产生的逻辑错误(参考下一小节“声明提前:分散的var带来的问题”) - 提醒你不要忘记声明变量,顺便减少潜在的全局变量 -- 代码量更少(输入更少且更易做代码优化) +- 代码量更少(输入代码更少且更易做代码优化) -单var模式看起来像这样: +单`var`模式看起来像这样: function func() { var a = 1, @@ -160,26 +153,25 @@ JavaScript 使用函数来管理作用域,在一个函数内定义的变量称 myobject = {}, i, j; - // function body... + // 函数体… } -你可以使用一个var语句来声明多个变量,变量之间用逗号分隔。也可以在这个语句中加入变量的初始化,这是一个非常好的实践。这种方式可以避免逻辑错误(所有未初始化的变量都被声明了,且值为undefined)并增加了代码的可读性。过段时间后再看这段代码,你会体会到声明不同类型变量的惯用名称,比如,你一眼就可看出某个变量是对象还是整数。 +你可以使用一个`var`语句来声明多个变量,变量之间用逗号分隔,也可以在这个语句中加入变量初始化的部分。这是一种非常好的实践方式,可以避免逻辑错误(所有未初始化的变量都被声明了,且值为undefined),并增加了代码的可读性。过段时间后再看这段代码,你可以从初始化的值中大概知道这个变量的用法,比如你一眼就可看出某个变量是对象还是整数。 -你可以在声明变量时多做一些额外的工作,比如在这个例子中就写了sum=a+b这种代码。另一个例子就是当代码中用到对DOM元素时,你可以把对DOM的引用赋值给一些变量,这一步就可以放在一个单独的声明语句中,比如下面这段代码: +你可以在声明变量时做一些额外的工作,比如在这个例子中就写了`sum=a+b`这种代码。另一个例子就是当代码中用到对DOM元素时,你可以把DOM引用赋值的操作也放在这个变量声明语句中,比如下面这段代码: function updateElement() { var el = document.getElementById("result"), style = el.style; - // do something with el and style... + // 使用el和style… } - -### 声明提前:分散的 var 带来的问题 +### 声明提前:分散的var带来的问题 -JavaScript 中是允许在函数的任意地方写任意多个var语句的,其实相当于在函数体顶部声明变量,这种现象被称为“变量提前”,当你在声明之前使用这个变量时,可能会造成逻辑错误。对于JavaScript来说,一旦在某个作用域(同一个函数内)里声明了一个变量,这个变量在整个作用域内都是存在的,包括在var声明语句之前。看一下这个例子: +JavaScript允许在函数的任意地方写任意多个`var`语句,但它们的行为会像在函数体顶部声明变量一样,这种现象被称为“声明提前”,当你在声明语句之前使用这个变量时,可能会造成逻辑错误。对于JavaScript来说,一旦在某个作用域(同一个函数内)里声明了一个变量,那么这个变量在整个作用域内都是存在的,包括在`var`声明语句之前的位置。看一下这个例子: - // antipattern - myname = "global"; // global variable + // 反模式 + myname = "global"; // 全局变量 function func() { alert(myname); // "undefined" var myname = "local"; @@ -187,102 +179,101 @@ JavaScript 中是允许在函数的任意地方写任意多个var语句的,其 } func(); -这个例子中,你可能期望第一个alert()弹出“global”,第二个alert()弹出“local”。这种结果看起来是合乎常理的,因为在第一个alert执行时,myname还没有声明,这时就应该“寻找”全局变量中的myname。但实际情况并不是这样,第一个alert弹出“undefined”,因为myname已经在函数内有声明了(尽管声明语句在后面)。所有的变量声明都提前到了函数的顶部。因此,为了避免类似带有“歧义”的程序逻辑,最好在使用之前一起声明它们。 +这个例子中,你可能会期望第一个`alert()`弹出“global”,第二个`alert()`弹出“local”。这种结果看起来是合乎常理的,因为在第一个`alert()`执行时,`myname`还没有被声明,这时就应该“寻找”全局变量`myname`。但实际情况并不是这样,第一个`alert()`弹出“undefined”,因为`myname`已经在函数内被声明了(尽管声明语句在后面)。所有的变量声明都会被提前到函数的顶部,因此,为了避免类似带有“歧义”的程序逻辑,最好在使用之前一起声明它们。 上一个代码片段等价于下面这个代码片段: - myname = "global"; // global variable + myname = "global"; // 全局变量 function func() { - var myname; // same as -> var myname = undefined; + var myname; // 等价于 -> var myname = undefined; alert(myname); // "undefined" myname = "local"; alert(myname); // "local" } func(); ->这里有必要对“变量提前”作进一步补充,实际上从JavaScript引擎的工作机制上看,这个过程稍微有点复杂。代码处理经过了两个阶段,第一阶段是创建变量、函数和参数,这一步是预编译的过程,它会扫描整段代码的上下文。第二阶段是代码的运行,这一阶段将创建函数表达式和一些非法的标识符(未声明的变量)。从实用性角度来讲,我们更愿意将这两个阶段归成一个概念“变量提前”,尽管这个概念并没有在ECMAScript标准中定义,但我们常常用它来解释预编译的行为过程。 +> 这里有必要对“变量提前”做进一步补充,实际上从JavaScript引擎的工作机制上看,这个过程稍微有点复杂。代码处理经过了两个阶段:第一阶段是创建变量、函数和形参,也就是预编译的过程,它会扫描整段代码的上下文;第二阶段是在代码的运行时(runtime),这一阶段将创建函数表达式和一些非法的标识符(未声明的变量)。(译注:这两个阶段并没有包含代码的执行,是在执行前的处理过程。)从实用性角度来讲,我们更愿意将这两个阶段归成一个概念“变量提前”,尽管这个概念并没有在ECMAScript标准中定义,但我们常常用它来解释预编译的行为过程。 - -## for 循环 +## for循环 -在for循环中,可以对数组或类似数组的对象(比如arguments和HTMLCollection对象)作遍历,最普通的for循环模式形如: +在`for`循环中,可以对数组或类似数组的对象(比如`arguments`和`HTMLCollection`对象)进行遍历,通常`for`循环模式形如: - // sub-optimal loop + // 非最优的循环方式 for (var i = 0; i < myarray.length; i++) { - // do something with myarray[i] + // 访问myarray[i]… } -这种模式的问题是,每次遍历都会访问数组的length属性。这降低了代码运行效率,特别是当myarray并不是一个数组而是一个HTMLCollection对象的时候。 +这种模式的问题是,每次遍历都会访问数组的length属性,这会降低代码运行效率,特别是当`myarray`不是一个数组而是一个`HTMLCollection`对象的时候。 -HTMLCollection是由DOM方法返回的对象,比如: +`HTMLCollection`是由DOM方法返回的对象集合,比如: - document.getElementsByName() - document.getElementsByClassName() - document.getElementsByTagName() -还有很多其他的HTMLCollection,这些对象是在DOM标准之前就已经在用了,这些HTMLCollection主要包括: +还有一些`HTMLCollection`是在DOM标准诞生之前就已经在用了并且现在仍然可用,包括: -**document.images** +- document.images -页面中所有的IMG元素 + 页面中所有的IMG元素 -**document.links** +- document.links -页面中所有的A元素 + 页面中所有的A元素 -**document.forms** +- document.forms -页面中所有的表单 + 页面中所有的表单 -**document.forms[0].elements** +- document.forms[0].elements -页面中第一个表单的所有字段 + 页面中第一个表单的所有字段 -这些对象的问题在于,它们均是指向文档(HTML页面)中的活动对象。也就是说每次通过它们访问集合的length时,总是会去查询DOM,而DOM操作则是很耗资源的。 +这些对象的问题在于,它们都会实时查询文档(HTML页面)中的对象。也就是说每次通过它们访问集合的`length`属性时,总是都会去查询DOM,而DOM操则是很耗资源的。 -更好的办法是为for循环缓存住要遍历的数组的长度,比如下面这段代码: +更好的办法是在`for`循环中缓存要遍历的数组的长度,比如下面这段代码: for (var i = 0, max = myarray.length; i < max; i++) { - // do something with myarray[i] + // 访问myarray[i]… } -通过这种方法只需要访问DOM节点一次以获得length,在整个循环过程中就都可以使用它。 +通过这种方法只需要获取`length`一次,然后在整个循环过程中使用它。 -不管在什么浏览器中,在遍历HTMLCollection时缓存length都可以让程序执行的更快,可以提速两倍(Safari3)到一百九十倍(IE7)不等。更多细节可以参照Nicholas Zakas的《高性能JavaScript》,这本书也是由O'Reilly出版。 +不管在什么浏览器中,在遍历`HTMLCollection`时缓存`length`都可以让程序执行的更快,可以提速2倍(Safari3)到190倍(IE7)不等。更多细节可以参照Nicholas Zakas的《高性能JavaScript》,这本书也是由O'Reilly出版。 -需要注意的是,当你在循环过程中需要修改这个元素集合(比如增加DOM元素)时,你更希望更新length而不是更新常量。 +需要注意的是,当你在循环过程中需要修改这个元素集合(比如增加DOM元素)时,你可能需要更新`length`。 -遵照单var模式,你可以将var提到循环的外部,比如: +按照“单`var`模式”,你可以将`var`提到循环的外部,比如: function looper() { var i = 0, max, myarray = []; - // ... + // … for (i = 0, max = myarray.length; i < max; i++) { - // do something with myarray[i] + // 访问myarray[i]… } } -这种模式带来的好处就是提高了代码的一致性,因为你越来越依赖这种单var模式。缺点就是在重构代码的时候不能直接复制粘贴一个循环体,比如,你正在将某个循环从一个函数拷贝至另外一个函数中,必须确保i和max也拷贝至新函数里,并且需要从旧函数中将这些没用的变量删除掉。 +当你越来越依赖“单`var`模式”时,带来的好处就是提高了代码的一致性。而缺点则是在重构代码的时候不能直接复制粘贴一个循环体,比如,你正在将某个循环从一个函数复制至另外一个函数中,那么必须确保`i`和`max`也复制到新函数里,并且需要从旧函数中将这些没用的变量删除掉。 最后一个需要对循环做出调整的地方是将i++替换成为下面两者之一: i = i + 1 i += 1 -JSLint提示你这样做,是因为++和--实际上降低了代码的可读性,如果你觉得无所谓,可以将JSLint的plusplus选项设为false(默认为true),本书所介绍的最后一个模式用到了: i += 1。 +JSLint提示你这样做,是因为`++`和`--`实际上降低了代码的可读性,如果你觉得无所谓,可以将JSLint的`plusplus`选项设为`false`(默认为`true`)。稍后,在本书所介绍的最后一个模式中用到了:`i += 1`。 -关于这种for模式还有两种变化的形式,做了少量改进,原因有二: +关于这种`for`模式还有两种变化的形式,做了少量改进,原因有二: -- 减少一个变量(没有max) +- 减少一个变量(没有`max`) - 减量循环至0,这种方式速度更快,因为和零比较要比和非零数字或数组长度比较要高效的多 第一种变化形式是: var i, myarray = []; for (i = myarray.length; i--;) { - // do something with myarray[i] + // 访问myarray[i]… } 第二种变化形式用到了while循环: @@ -290,118 +281,112 @@ JSLint提示你这样做,是因为++和--实际上降低了代码的可读性 var myarray = [], i = myarray.length; while (i--) { - // do something with myarray[i] + // 访问myarray[i]… } -这些小改进只体现在性能上,此外,JSLint不推荐使用i--。 +这些小改进只能体现在对性能要求比较苛刻的地方,此外,JSLint不推荐使用`i--`。 - -## for-in 循环 +## for-in循环 -for-in 循环用于对非数组对象作遍历。通过for-in进行循环也被称作“枚举”。 +`for-in`循环用于对非数组对象进行遍历。通过`for-in`进行循环也被称作“枚举”(enumeration)。 -从技术角度讲,for-in循环同样可以用于数组(JavaScript中数组即是对象),但不推荐这样做。当使用自定义函数扩充了数组对象时,这时更容易产生逻辑错误。另外,for-in循环中属性的遍历顺序是不固定的,所以最好数组使用普通的for循环,对象使用for-in循环。 +从技术上讲,`for-in`循环同样可以用于数组(JavaScript中数组也是对象),但不推荐这样做。当数组对象被扩充了自定义函数时,可能会产生逻辑错误。另外,`for-in`循环中属性的遍历顺序是不固定的,所以最好数组使用普通的`for`循环,对象使用`for-in`循环。 -可以使用对象的hasOwnProperty()方法将从原型链中继承来的属性过滤掉,这一点非常重要。看一下这段代码: +可以使用对象的`hasOwnProperty()`方法过滤来自原型链中继承来的属性,这一点非常重要。看一下这段代码: - // the object + // 对象 var man = { hands: 2, legs: 2, heads: 1 }; - // somewhere else in the code - // a method was added to all objects + // 在代码的另一个地方给所有的对象添加了一个方法 if (typeof Object.prototype.clone === "undefined") { Object.prototype.clone = function () {}; } -在这段例子中,我们定义了一个名叫man的对象直接量。在代码中的某个地方(可以是man定义之前也可以是之后),给Object的原型中增加了一个方法clone()。原型链是实时的,这意味着所有的对象都可以访问到这个新方法。要想在枚举man的时候避免枚举出clone()方法,则需要调用hasOwnProperty()来对原型属性进行过滤。如果不做过滤,clone()也会被遍历到,而这不是我们所希望的: +在这个例子中,我们使用对象字面量定义了一个名叫`man`的对象。在代码中的某个地方(可以是`man`定义之前也可以是之后),给`Object`的原型增加了一个方法`clone()`。原型链是实时的,这意味着所有的对象都可以访问到这个新方法。要想在枚举`man`的时候避免枚举出`clone()`方法,就需要调用`hasOwnProperty()`来过滤来自原型的属性。如果不做过滤,`clone()`也会被遍历到,这是我们不希望看到的: - // 1. - // for-in loop + // 1.for-in循环 for (var i in man) { if (man.hasOwnProperty(i)) { // filter console.log(i, ":", man[i]); } } /* - result in the console + 控制台中的结果 hands : 2 legs : 2 heads : 1 */ - // 2. - // antipattern: - // for-in loop without checking hasOwnProperty() + // 2.反模式: + // 不使用hasOwnProperty()过滤的for-in循环 for (var i in man) { console.log(i, ":", man[i]); } /* - result in the console + 控制台中的结果 hands : 2 legs : 2 heads : 1 clone: function() */ -另外一种的写法是通过Object.prototype直接调用hasOwnProperty()方法,像这样: +另外一种调用`hasOwnProperty()`的方法是通过`Object.prototype`来调用,像这样: for (var i in man) { - if (Object.prototype.hasOwnProperty.call(man, i)) { // filter + if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤 console.log(i, ":", man[i]); } } -这种做法的好处是,当man对象中重新定义了hasOwnProperty方法时,可以避免调用时的命名冲突(译注:明确指定调用的是Object.prototype上的方法而不是实例对象中的方法),这种做法同样可以避免冗长的属性查找过程(译注:这种查找过程多是在原型链上进行查找),一直查找到Object中的方法,你可以定义一个变量来“缓存”住它(译注:这里所指的是缓存住Object.prototype.hasOwnProperty): +这种做法的好处是,在`man`对象中重新定义了`hasOwnProperty`方法的情况下,可以避免调用时的命名冲突。为了避免查找属性时从`Object`对象一路找到原型的冗长过程,你可以定义一个变量来“缓存”住它: var i, hasOwn = Object.prototype.hasOwnProperty; for (i in man) { - if (hasOwn.call(man, i)) { // filter + if (hasOwn.call(man, i)) { // 过滤 console.log(i, ":", man[i]); } } ->严格说来,省略hasOwnProperty()并不是一个错误。根据具体的任务以及你对代码的自信程度,你可以省略掉它以提高一些程序执行效率。但当你对当前要遍历的对象不确定的时候,添加hasOwnProperty()则更加保险些。 +> 严格说来,省略`hasOwnProperty()`并不是一个错误。根据具体的任务以及你对代码的自信程度,你可以省略掉它以提高一些程序执行效率。但当你对当前要遍历的对象不确定的时候,添加hasOwnProperty()则更加保险些。 -这里提到一种格式上的变化写法(这种写法无法通过JSLint检查),这种写法在for循环所在的行加入了if判断条件,他的好处是能让循环语句读起来更完整和通顺(“如果元素包含属性X,则拿X做点什么”): +这里介绍一种格式上的变种(这种写法无法通过JSLint检查),这种写法在`for`循环所在的行加入了`if`判断条件,他的好处是能让循环语句读起来更完整和通顺(“如果元素包含属性X,则对X做点什么”): - // Warning: doesn't pass JSLint + // 警告:无法通过JSLint检查 var i, hasOwn = Object.prototype.hasOwnProperty; - for (i in man) if (hasOwn.call(man, i)) { // filter + for (i in man) if (hasOwn.call(man, i)) { // 过滤 console.log(i, ":", man[i]); } - ## (不)扩充内置原型 -我们可以扩充构造函数的prototype属性,这是一种非常强大的特性,用来为构造函数增加功能,但有时这个功能强大到超过我们的掌控。 +我们可以扩充构造函数的`prototype`属性来为构造函数增加功能,这个特性非常强大,但有时会强大到超过我们的掌控。 -给内置构造函数比如Object()、Array()、和Function()扩充原型看起来非常诱人,但这种做法严重降低了代码的可维护性,因为它让你的代码变得难以预测。对于那些基于你的代码做开发的开发者来说,他们更希望使用原生的JavaScript方法来保持工作的连续性,而不是使用你所添加的方法(译注:因为原生的方法更可靠,而你写的方法可能会有bug)。 +给内置构造函数如`Object()`、`Array()`、`Function()`扩充原型看起来非常诱人,但这种做法会严重降低代码的可维护性,因为它会让你的代码变得难以预测。对于那些基于你的代码来做开发的开发者来说,他们更希望使用原生的JavaScript方法来保持代码的一致性,而不愿意使用你所添加的方法。 -另外,如果将属性添加至原型中,很可能导致在那些不使用hasOwnProperty()做检测的循环中将原型上的属性遍历出来,这会造成混乱。 +另外,如果将属性添加至原型中,很可能导致原型上的属性在那些不使用`hasOwnProperty()`做过滤的循环中被遍历出来,从而造成混乱。 -因此,不扩充内置对象的原型是最好的,你也可以自己定义一个规则,仅当下列条件满足时做例外考虑: +因此,不扩充内置对象的原型是最好的,你也可以自己定义一个规则,仅当下列条件满足时才考虑扩充内置对象的原型: -1. 未来的ECMAScript版本的JavaScirpt会将你实现的方法添加为内置方法。比如,你可以实现ECMAScript5定义的一些方法,一直等到浏览器升级至支持ES5。这样,你只是提前定义了这些有用的方法。 -2. 如果你发现你自定义的方法已经不存在,要么已经在代码其他地方实现了,要么是浏览器的JavaScript引擎已经内置实现了。 -3. 你所做的扩充附带充分的文档说明,且和团队其他成员做了沟通。 +1. 未来的ECMAScript版本或者JavaScirpt会将你将要实现的方法添加为内置方法。比如,你可以实现ECMAScript5定义的一些方法,直到浏览器升级至支持ES5。这样,你只是提前定义了这些方法。 +2. 当某个属性或者方法是你在其它地方实现过的,或者是某个JavaScript引擎或浏览器的一部分,而你检查时又发现它不存在时。 +3. 在有充分的文档说明,并且和团队其他成员做了沟通的时候。 如果你遇到这三种情况之一,你可以给内置原型添加自定义方法,写法如下: if (typeof Object.protoype.myMethod !== "function") { Object.protoype.myMethod = function () { - // implementation... + // 实现… }; } - -## switch 模式 +## switch模式 -你可以通过下面这种模式的写法来增强switch语句的可读性和健壮性: +你可以通过下面这种模式来增强`switch`语句的可读性和健壮性: var inspect_me = 0, result = ''; @@ -418,125 +403,120 @@ for-in 循环用于对非数组对象作遍历。通过for-in进行循环也被 这个简单的例子所遵循的风格约定如下: -- 每个case和switch对齐(这里不考虑花括号相关的缩进规则) -- 每个case中的代码整齐缩进 -- 每个case都以break作为结束 -- 避免连续执行多个case语句块(当省略break时会发生),如果你坚持认为连续执行多case语句块是最好的方法,请务必补充文档说明,对于其他人来说,这种情况看起来是错误的。 -- 以default结束整个switch,以确保即便是在找不到匹配项时也会有正常的结果, +- 每个`case`和`switch`对齐(这里不考虑花括号相关的缩进规则)。 +- 每个`case`中的代码整齐缩进。 +- 每个`case`都以`break`作为结束。 +- 避免连续执行多个case语句块(省略break时),如果你坚持认为连续执行多个`case`语句块是最好的方法,请务必补充文档说明,对于其他人来说,会觉得这种情况是错误的写法。 +- 以`default`结束整个`switch`,以确保即便是在找不到匹配项时也有合理的结果。 - ## 避免隐式类型转换 -在JavaScript的比较操作中会有一些隐式的数据类型转换。比如诸如false == 0或""==0之类的比较都返回true。 +在JavaScript对变量进行比较时会有一些隐式的数据类型转换。比如诸如`false == 0`或`"" == 0`之类的比较都返回`true`。 -为了避免隐式类型转换造对程序造成干扰,推荐使用===和!===运算符,它们较除了比较值还会比较类型。 +为了避免隐式类型转换对程序造成干扰,推荐使用`===`和`!==`运算符,它们除了比较值还会比较类型: var zero = 0; if (zero === false) { - // not executing because zero is 0, not false + // 不会执行,因为zero是0,不是false } - // antipattern + // 反模式 if (zero == false) { - // this block is executed... + // 代码块会执行… } -另外一种观点认为当==够用的时候就不必多余的使用===。比如,当你知道typeof的返回值是一个字符串,就不必使用全等运算符。但JSLint却要求使用全等运算符,这当然会提高代码风格的一致性,并减少了阅读代码时的思考(“这里使用==是故意的还是无意的?”)。 +有一种观点认为当`==`够用的时候就不必使用`===`。比如,当你知道`typeof`的返回值是一个字符串,就不必使用全等运算符。但JSLint却要求使用全等运算符,这无疑会提高代码风格的一致性,并减少了阅读代码时的思考量(“这里使用`==`是故意的还是无意的?”)。 - ### 避免使用eval() -当你想使用eval()的时候,不要忘了那句话“eval()是魔鬼”。这个函数的参数是一个字符串,它可以执行任意字符串。如果事先知道要执行的代码是有问题的(在运行之前),则没有理由使用eval()。如果需要在运行时动态生成执行代码,往往都会有更佳的方式达到同样的目的,而非一定要使用eval()。例如,访问动态属性时可以使用方括号: +当你想使用`eval()`的时候,不要忘了那句话“`eval()` is evil”(`eval()`是魔鬼)。这个函数的参数是一个字符串,它会将传入的字符串作为JavaScript代码执行。如果用来解决问题的代码是事先知道的(在运行之前),则没有理由使用`eval()`。如果需要在运行时动态生成并执行代码,那一般都会有更好的方式达到同样的目的,而非一定要使用`eval()`。例如,访问动态属性时可以使用方括号: - // antipattern + // 反模式 var property = "name"; alert(eval("obj." + property)); - // preferred + // 更好的方式 var property = "name"; alert(obj[property]); -eval()同样有安全隐患,因为你需要运行一些容易被干扰的代码(比如运行一段来自于网络的代码)。在处理Ajax请求所返回的JSON数据时会常遇到这种情况,使用eval()是一种反模式。这种情况下最好使用浏览器的内置方法来解析JSON数据,以确保代码的安全性和数据的合法性。如果浏览器不支持JSON.parse(),你可以使用JSON.org所提供的库。 +`eval()`还有安全隐患,因为你有可能会运行一些被干扰过的代码(比如一段来自于网络的代码)。这是一种在处理Ajax请求所返回的JSON数据时比较常见的反模式。这种情况下最好使用浏览器的内置方法来解析JSON数据,以确保代码的安全性和数据的合法性。如果浏览器不支持`JSON.parse()`,你可以使用JSON.org所提供的库。 -记住,多数情况下,给setInterval()、setTimeout()和Function()构造函数传入字符串的情形和eval()类似,这种用法也是应当避免的,这一点非常重要,因为这些情形中JavaScript最终还是会执行传入的字符串参数: +值得一提的是,多数情况下,给`setInterval()`、`setTimeout()`和`Function()`构造函数传入字符串的情形和`eval()`类似,这种用法也是应当避免的,因为这些情形中JavaScript最终还是会执行传入的字符串参数: - // antipatterns + // 反模式 setTimeout("myFunc()", 1000); setTimeout("myFunc(1, 2, 3)", 1000); - // preferred + // 更好的方式 setTimeout(myFunc, 1000); setTimeout(function () { myFunc(1, 2, 3); }, 1000); -new Function()的用法和eval()非常类似,应当特别注意。这种构造函数的方式很强大,但往往被误用。如果你不得不使用eval(),你可以尝试用new Function()来代替。这有一个潜在的好处,在new Function()中运行的代码会在一个局部函数作用域内执行,因此源码中所有用var定义的变量不会自动变成全局变量。还有一种方法可以避免eval()中定义的变量转换为全局变量,即是将eval()包装在一个立即执行的匿名函数内(详细内容请参照第四章)。 +`new Function()`的用法和`eval()`非常类似,应当特别注意。这种构造函数的方式很强大,但经常会被误用。如果你不得不使用`eval()`,你可以尝试用`new Function()`来代替。这有一个潜在的好处,在`new Function()`中运行的代码会在一个局部函数作用域内执行,因此源码中所有用`var`定义的变量不会自动变成全局变量。还有一种方法可以避免`eval()`中定义的变量被转换为全局变量,即是将`eval()`包装在一个即时函数内(详细内容请参见第四章)。 -看一下这个例子,这里只有un成为了全局变量,污染了全局命名空间: +看一下这个例子,这里只有`un`成为全局变量污染了全局命名空间: console.log(typeof un);// "undefined" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined" var jsstring = "var un = 1; console.log(un);"; - eval(jsstring); // logs "1" + eval(jsstring); // 打印出 "1" jsstring = "var deux = 2; console.log(deux);"; - new Function(jsstring)(); // logs "2" + new Function(jsstring)(); // 打印出 "2" jsstring = "var trois = 3; console.log(trois);"; (function () { eval(jsstring); - }()); // logs "3" + }()); // 打印出 "3" console.log(typeof un); // "number" console.log(typeof deux); // "undefined" console.log(typeof trois); // "undefined" -eval()和Function构造函数还有一个区别,就是eval()可以修改作用域链,而Function更像是一个沙箱。不管在什么地方执行Function,它只能看到全局作用域。因此它不会太严重的污染局部变量。在下面的示例代码中,eval()可以访问且修改其作用域之外的变量,而Function不能(注意,使用Function和new Function是完全一样的)。 +`eval()`和`Function()`构造函数还有一个区别,就是`eval()`可以修改作用域链,而`Function`更像是一个沙箱。不管在什么地方执行`Function()`,它都只能看到全局作用域。因此它不会太严重的污染局部变量。在下面的示例代码中,`eval()`可以访问并修改其作用域之外的变量,而`Function()`则不能(注意,使用`Function()`和`new Function()`是完全一样的)。 (function () { var local = 1; - eval("local = 3; console.log(local)"); // logs 3 - console.log(local); // logs 3 + eval("local = 3; console.log(local)"); // 打印出 3 + console.log(local); // 打印出 3 }()); (function () { var local = 1; - Function("console.log(typeof local);")(); // logs undefined + Function("console.log(typeof local);")(); // 打印出 undefined }()); - ## 使用parseInt()进行数字转换 -可以使用parseInt()将字符串转换为数字。函数的第二个参数是转换基数(译注:“基数”指的是数字进制的方式),这个参数通常被省略。但当字符串以0为前缀时转换就会出错,例如,在表单中输入日期的一个字段。ECMAScript3中以0为前缀的字符串会被当作八进制数处理(基数为8)。但在ES5中不是这样。为了避免转换类型不一致而导致的意外结果,应当总是指定第二个参数: +你可以使用`parseInt()`将字符串转换为数字。函数的第二个参数是进制参数,这个参数应该被指定,但却通常被省略。当字符串以0为前缀时转换就会出问题,例如,在表单中输入日期的一个字段。ECMAScript3中以0为前缀的字符串会被当作八进制数处理,这一点在ES5中已经有了改变。为了避免转换类型不一致而导致的意外结果,应当总是指定第二个参数: var month = "06", year = "09"; month = parseInt(month, 10); year = parseInt(year, 10); -在这个例子中,如果省略掉parseInt的第二个参数,比如parseInt(year),返回值是0,因为“09”被认为是八进制数(等价于parseInt(year,8)),而且09是非法的八进制数。 +在这个例子中,如果省略掉parseInt的第二个参数,比如`parseInt(year)`,返回的值是0,因为“09”被认为是八进制数(等价于`parseInt(year,8)`),但09是非法的八进制数。 字符串转换为数字还有两种方法: - +"08" // result is 8 - Number("08") // 8 + +"08" // 结果为8 + Number("08") // 结果为8 -这两种方法要比parseInt()更快一些,因为顾名思义parseInt()是一种“解析”而不是简单的“转换”。但当你期望将“08 hello”这类字符串转换为数字,则必须使用parseInt(),其他方法都会返回NaN。 +这两种方法要比`parseInt()`更快一些,因为顾名思义`parseInt()`是一种“解析”而不是简单的“转换”。但当你期望将“08 hello”这类字符串转换为数字,则必须使用`parseInt()`,其他方法都会返回NaN。 - -## 编码风格 +## 代码规范 -确立并遵守编码规范非常重要,这会让你的代码风格一致、可预测、可读性更强。团队新成员通过学习编码规范可以很快进入开发状态、并写出团队其他成员易于理解的代码。 +确立并遵守代码规范非常重要,这会让你的代码风格一致、可预测,并且可读性更强。团队新成员通过学习代码规范可以很快进入开发状态,并写出让团队其他成员易于理解的代码。 -在开源社区和邮件组中关于编码风格的争论一直不断(比如关于代码缩进,用tab还是空格?)。因此,如果你打算在团队内推行某种编码规范时,要做好应对各种反对意见的心理准备,而且要吸取各种意见,这对确立并一贯遵守某种编码规范是非常重要,而不是斤斤计较的纠结于编码规范的细节。 +在开源社区和邮件组中关于编代风格的争论一直不断。(比如关于代码缩进,用tab还是空格?)因此,如果你打算在团队内推行某种编码规范时,要做好应对各种反对意见的心理准备,而且要吸取各种意见。确定并遵守代码规范非常重要,任何一种规范都可以,这甚至比代码规范中的具体约定是怎么样的还要重要。 - ### 缩进 -代码没有缩进几乎就不能读了,而不一致的缩进更加糟糕,因为它看上去像是遵循了规范,真正读起来却磕磕绊绊。因此规范的使用缩进非常重要。 +代码如果没有缩进就几乎不能读了,而不一致的缩进会使情况更加糟糕,因为它看上去像是遵守了规范,但真正读起来却没那么顺利。因此规范地使用缩进非常重要。 -有些开发者喜欢使用tab缩进,因为每个人都可以根据自己的喜好来调整tab缩进的空格数,有些人则喜欢使用空格缩进,通常是四个空格,这都无所谓,只要团队每个人都遵守同一个规范即可,本书中所有的示例代码都采用四个空格的缩进写法,这也是JSLint所推荐的。 +有些开发者喜欢使用tab缩进,因为每个人都可以根据自己的喜好来调整tab缩进的空格数,有些人则喜欢使用空格缩进,通常是四个空格。这都无所谓,只要团队每个人都遵守同一个规范即可,本书中所有的示例代码都采用四个空格的缩进写法,这也是JSLint所推荐的。(译注:电子版中看到的是用tab缩进,本译文也保留使用tab缩进。) -那么到底什么应该缩进呢?规则很简单,花括号里的内容应当缩进,包括函数体、循环(do、while、for和for-in)体、if条件、switch语句和对象直接量里的属性。下面的代码展示了如何正确的使用缩进: +那么到底什么时候应该缩进呢?规则很简单,花括号里的内容应当缩进,包括函数体、循环(`do`、`while`、`for`和`for-in`)体、`if`语句、`switch`语句和对象字面量里的属性。下面的代码展示了如何正确地使用缩进: function outer(a, b) { var c = 1, @@ -558,47 +538,45 @@ eval()和Function构造函数还有一个区别,就是eval()可以修改作用 return inner; } - ### 花括号 -应当总是使用花括号,即使是在可省略花括号的时候也应当如此。从技术角度讲,如果if或for中只有一个语句,花括号是可以省略的,但最好还是不要省略。这让你的代码更加工整一致而且易于更新。 +在特定的语句中应当总是使用花括号,即便是在可省略花括号的情况下也应当如此。从技术角度讲,如果`if`或`for`中只有一个语句,花括号是可以省略的,但最好还是不要省略,这会让你的代码更加工整一致而且易于修改。 -假设有这样一段代码,for循环中只有一条语句,你可以省略掉这里的花括号,而且不会有语法错误: +假设有这样一段代码,`for`循环中只有一条语句,你可以省略掉这里的花括号,而且不会有语法错误: - // bad practice + // 不好的方式 for (var i = 0; i < 10; i += 1) alert(i); -但如果过了一段时间,你给这个循环添加了另一行代码? +但如果过了一段时间,你给这个循环添加了另一行代码会怎样? - // bad practice + // 不好的方式 for (var i = 0; i < 10; i += 1) alert(i); alert(i + " is " + (i % 2 ? "odd" : "even")); -第二个alert实际处于循环体之外,但这里的缩进会迷惑你。长远考虑最好还是写上花括号,即便是在只有一个语句的语句块中也应如此: +第二个`alert`实际上在循环体之外,但这里的缩进会让你迷惑。从长远考虑最好还是写上花括号,即便是在只有一个语句的语句块中也应如此: - // better + // 更好的方式 for (var i = 0; i < 10; i += 1) { alert(i); } 同理,if条件句也应当如此: - // bad + // 不好的方式 if (true) alert(1); else alert(2); - // better + // 更好的试 if (true) { alert(1); } else { alert(2); } - ### 左花括号的位置 开发人员对于左大括号的位置有着不同的偏好,在同一行呢还是在下一行? @@ -615,9 +593,9 @@ eval()和Function构造函数还有一个区别,就是eval()可以修改作用 } -在这个例子中,看起来只是个人偏好问题。但有时候花括号位置的不同则会影响程序的执行。因为JavaScript会“自动插入分号”。JavaScript对行结束时的分号并无要求,它会自动将分号补全。因此,当函数return语句返回了一个对象直接量,而对象的左花括号和return不在同一行时,程序的执行就和预想的不同了: +在这个例子中,这个问题只是个人偏好问题。但有时候花括号位置的不同会影响程序的执行,因为JavaScript会“自动插入分号”。JavaScript对行尾是否有分号并没有要求,它会自动将分号补全。因此,当函数的`return`语句返回了一个对象字面量,而对象的左花括号和`return`又不在同一行时,程序的执行就和预期的不同了: - // warning: unexpected return value + // 警告:返回值和预期的不同 function func() { return { @@ -625,18 +603,18 @@ eval()和Function构造函数还有一个区别,就是eval()可以修改作用 }; } -可以看出程序作者的意图是返回一个包含了name属性的对象,但实际情况不是这样。因为return后会填补一个分号,函数的返回值就是undefined。这段代码等价于: +可以看出程序作者的意图是返回一个包含了`name`属性的对象,但实际情况不是这样。因为return后会填补一个分号,函数的返回值就是undefined。这段代码等价于: - // warning: unexpected return value + // 警告:返回值和预期的不同 function func() { return undefined; - // unreachable code follows... + // 下面的代码不会运行… { name: "Batman" }; } -结论,总是使用花括号,而且总是将左花括号与上一条语句放在同一行: +总结一下好的写法,在特写的语句中总是使用花括号,并且总是将左花括号与上一条语句放在同一行: function func() { return { @@ -644,28 +622,25 @@ eval()和Function构造函数还有一个区别,就是eval()可以修改作用 }; } ->关于分号应当注意:和花括号一样,应当总是使用分号,尽管在JavaScript解析代码时会补全行末省略的分号。严格遵守这条规则,可以让代码更加严谨,同时可以避免前面例子中所出现的歧义。 +> 关于分号也值得注意:和花括号一样,应当总是使用分号,尽管在JavaScript解析代码时会补全行末省略的分号,但严格遵守这条规则,可以让代码更加严谨,同时可以避免前面例子中所出现的歧义。 - ### 空格 -空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在JavaScript中,你可以按照同样的逻辑在表达式(相当于逗号)和语句结束(相对于完成了某个“想法”)后面添加间隔。 +空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔,在JavaScript中,你可以按照同样的逻辑在表达式(相当于逗号)和语句结束(相对于完成了某个“想法”的表达)后面添加间隔。 适合使用空格的地方包括: -- for循环中的分号之后,比如 `for (var i = 0; i < 10; i += 1) {...}` -- for循环中初始化多个变量,比如 `for (var i = 0, max = 10; i < max; i += 1) {...}` -- 分隔数组项的逗号之后,`var a = [1, 2, 3];` -- 对象属性后的逗号以及名值对之间的冒号之后,`var o = {a: 1, b: 2};` -- 函数参数中,`myFunc(a, b, c)` -- 函数声明的花括号之前,`function myFunc() {}` -- 匿名函数表达式function之后,`var myFunc = function () {};` +- for循环中的分号之后,比如`for (var i = 0; i < 10; i += 1) {...}` +- for循环中初始化多个变量,比如`for (var i = 0, max = 10; i < max; i += 1) {...}` +- 用于分隔数组元素的逗号之后,比如`var a = [1, 2, 3];` +- 对象属性后的逗号以及名值对之间的冒号之后,比如`var o = {a: 1, b: 2};` +- 函数参数中,比如`myFunc(a, b, c)` +- 函数声明的花括号之前,比如`function myFunc() {}` +- 匿名函数表达式`function`之后,比如`var myFunc = function () {};` -另外,我们推荐在运算符和操作数之间添加空格。也就是说在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=符号前后都添加空格。 +另外,我们推荐在运算符和操作数之间也添加空格。也就是说在`+`、`-`、`*`、`=`、`<`、`>`、`<=`、`>=`、`===`、`!==`、`&&`、`||`、`+=`符号前后都添加空格。 - // generous and consistent spacing - // makes the code easier to read - // allowing it to "breathe" + // 适当且一致的空格给代码留了“呼吸空间”,使代码更易读 var d = 0, a = b + 1; if (a && b && c) { @@ -673,9 +648,7 @@ eval()和Function构造函数还有一个区别,就是eval()可以修改作用 a += d; } - // antipattern - // missing or inconsistent spaces - // make the code confusing + // 反模式,缺少或者不正确的空格使得代码不易读 var d= 0, a =b+1; if (a&& b&&c) { @@ -685,61 +658,55 @@ eval()和Function构造函数还有一个区别,就是eval()可以修改作用 最后,还应当注意,最好在花括号旁边添加空格: -- 在函数、if-else语句、循环、对象直接量的左花括号之前补充空格({) -- 在右花括号和else和while之间补充空格 +- 在函数、`if-else`语句、循环、对象字面量的左花括号之前补充空格 +- 在右花括号和`else`或者`while`之间补充空格 ->垂直空白的使用经常被我们忽略,你可以使用空行来将代码单元分隔开,就像文学作品中使用段落作分隔一样。 +> 垂直空白的使用经常被我们忽略,你可以使用空行来将代码单元分隔开,就像文学作品中使用段落进行分隔一样。 - ## 命名规范 -另外一种可以提升你代码的可预测性和可维护性的方法是采用命名规范。也就是说变量和函数的命名都遵照同种习惯。 +另外一种可以提升你代码的可预测性和可维护性的方法是采用命名规范。也就是说变量和函数的命名都遵守同样的习惯。 -下面是一些建议的命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范本身更加重要。 +下面是一些建议的命名规范,你可以原样采用,也可以根据自己的喜好作调整。同样,遵循规范要比规范本身是什么样更加重要。 - -### 构造器命名中的大小写 +### 构造函数命名中的大小写 -JavaScript中没有类,但有构造函数,可以通过new来调用构造函数: +JavaScript中没有类,但有构造函数,可以通过`new`来调用构造函数: var adam = new Person(); -由于构造函数毕竟还是函数,不管我们将它用作构造器还是函数,当然希望只通过函数名就可分辨出它是构造器还是普通函数。 +由于构造函数毕竟还是函数,如果只通过函数名就可分辨出它是构造函数还是普通函数是非常有用的。 首字母大写可以提示你这是一个构造函数,而首字母小写的函数一般只认为它是普通的函数,不应该通过new来调用它: function MyConstructor() {...} function myFunction() {...} -下一章将介绍一些强制将函数用作构造器的编程模式,但遵守我们所提到的命名规范会更好的帮助程序员阅读源码。 +下一章将介绍一些强制将普通函数用作构造函数的编程模式,但遵守我们所提到的命名规范会更好的帮助程序员阅读源码。 - ### 单词分隔 -当你的变量名或函数名中含有多个单词时,单词之间的分隔也应当遵循统一的约定。最常见的做法是“驼峰式”命名,单词都是小写,每个单词的首字母是大写。 - -对于构造函数,可以使用“大驼峰式”命名,比如MyConstructor(),对于函数和方法,可以采用“小驼峰式”命名,比如myFunction(),calculateArea()和getFirstName()。 +当你的变量名或函数名中含有多个单词时,单词之间的分隔也应当遵循统一的规范。最常见的是“驼峰式”(camel case)命名,单词都是小写,每个单词的首字母是大写。 -那么对于那些不是函数的变量应当如何命名呢?变量名通常采用小驼峰式命名,还有一个不错的做法是,变量所有字母都是小写,单词之间用下划线分隔,比如,first_name,favorite_bands和old_company_name,这种方法可以帮助你区分函数和其他标识符——原始数据类型或对象。 +对于构造函数,可以使用“大驼峰式”(upper camel case)命名,比如`MyConstructor()`,对于函数和方法,可以采用“小驼峰式”(lower camel case)命名,比如`myFunction()`、`calculateArea()`和`getFirstName()`。 -ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名称是罕见的(正则表达式对象的lastIndex和ignoreCase属性)。 +那么对于那些不是函数的变量应当如何命名呢?变量名通常采用小驼峰式命名,还有一个不错的做法是,变量所有字母都是小写,单词之间用下划线分隔,比如,`first_name`、`favorite_bands`和`old_company_name`,这种方法可以帮助你区分函数和其他标识符如原始类型数据或对象。 -在ECMAScript中的属性和方法均使用驼峰式命名,尽管包含多单词的属性名称(正则表达式对象中的lastIndex和ignoreCase)并不常见。 +ECMAScript的属性和方法均使用驼峰式命名,尽管包含多个单词的属性名称并不多见(正则表达式对象的`lastIndex`和`ignoreCase`属性)。 - ### 其他命名风格 有时开发人员使用命名规范来弥补或代替语言特性的不足。 -比如,JavaScript中无法定义常量(尽管有一些内置常量比如Number.MAX_VALUE),所以开发者都采用了这种命名习惯,对于那些程序运行周期内不会更改的变量使用全大写字母来命名。比如: +比如,JavaScript中无法定义常量(尽管有一些内置常量比如`Number.MAX_VALUE`),所以开发者都采用了一种命名规范,对于那些程序运行周期内不会更改的变量使用全大写字母来命名。比如: - // precious constants, please don't touch + // 常量,请勿修改 var PI = 3.14, MAX_WIDTH = 800; -除了使用大写字母的命名方式之外,还有另一种命名规约:全局变量都大写。这种命名方式和“减少全局变量”的约定相辅相成,并让全局变量很容易辨认。 +除了使用大写字母的命名方式之外,还有另一种命名规范:全局变量全大写。这种命名方式和“减少全局变量”的约定相辅相成,并让全局变量很容易辨认。 -除了常量和全局变量的命名惯例,这里讨论另外一种命名惯例,即私有变量的命名。尽管在JavaScript是可以实现真正的私有变量的,但开发人员更喜欢在私有成员或方法名之前加上下划线前缀,比如下面的例子: +除了常量和全局变量的命名规范,这里讨论另外一种命名规范,即私有变量的命名。尽管在JavaScript是可以实现真正的私有变量的,但开发人员更喜欢在私有成员或方法名之前加上下划线前缀,比如下面的例子: var person = { getName: function () { @@ -753,45 +720,43 @@ ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名 } }; -在这个例子中,getName()的身份是一个公有方法,属于稳定的API,而_getFirst()和_getLast()则是私有方法。尽管这两个方法本质上和公有方法无异,但在方法名前加下划线前缀就是为了警告用户不要直接使用这两个私有方法,因为不能保证它们在下一个版本中还能正常工作。JSLint会对私有方法作检查,除非设置了JSLint的nomen选项为false。 +在这个例子中,`getName()`是一个公有方法,是确定的API的一部分,而`_getFirst()`和`_getLast()`则是私有方法。尽管这两个方法本质上和公有方法没有区别,但在方法名前加下划线前缀就是为了告知用户不要直接使用这两个私有方法,因为不能保证它们在下一个版本中还能正常工作。JSLint会对私有方法作检查,除非设置了JSLint的`nomen`选项为`false`。 -下面介绍一些_private风格写法的变种: +下面介绍一些`_private`风格写法的变种: -- 在名字尾部添加下划下以表明私有,比如`name_`和`getElements_()` -- 使用一个下划线前缀表明受保护的属性_protected,用两个下划线前缀表明私有属性__private +- 在名字尾部添加下划线以表明私有,比如`name_`和`getElements_()` +- 使用一个下划线前缀表明受保护的属性`_protected`,用两个下划线前缀表明私有属性`__private` - 在Firefox中实现了一些非标准的内置属性,这些属性在开头和结束都有两个下划线,比如`__proto__`和`__parent__` - -## 书写注释 +## 写注释 -写代码就要写注释,即便你认为你的代码不会被别人读到。当你对一个问题非常熟悉时,你会很快找到问题代码,但当过了几个星期后再来读这段代码,则需要绞尽脑汁的回想代码的逻辑。 +在写代码时,即便你认为你的代码不会被别人读到,也应该写好注释。因为当你对一个问题非常熟悉时,你会非常明白这些代码的作用,但当过了几个星期后再来读这段代码时,则需要绞尽脑汁的回想这些代码在干什么。 -你不必对显而易见的代码作过多的注释:每个变量和每一行都作注释。但你需要对所有的函数、他们的参数和返回值补充注释,对于那些有趣的或怪异的算法和技术也应当配备注释。对于阅读你的代码的其他人来说,注释就是一种提示,只要阅读注释、函数名以及参数,就算不读代码也能大概理解程序的逻辑。比如,这里有五到六行代码完成了某个功能,如果提供了一行描述这段代码功能的注释,读程序的人就不必再去关注代码的细节实现了。代码注释的写法并没有硬性规定,有些代码片段(比如正则表达式)的确需要比代码本身还多的注释。 +你不必对那些浅显易懂的代码写过多的注释,比如每个变量、每一行都写注释。但你应该对所有的函数、它们的参数和返回值进行注释,除此之外,对于那些值得注意的或是比较怪异的算法和技术也应当写好注释。对于其他阅读你代码的人来说,注释就是一种提示,只要阅读注释、函数名和参数,就算不读其它部分的代码也能大概理解程序的逻辑。比如,这里有五六行代码完成了某个功能,如果有一行描述这段代码功能的注释,读程序的人就不必再去关注代码的实现细节了。代码注释的写法并没有硬性规定,但有些代码片段(比如正则表达式)需要比代码本身更多的注释。 ->由于过时的注释会带来很多误导,这比不写注释还糟糕。因此保持注释时刻更新的习惯非常重要,尽管对很多人来说这很难做到。 +> 过时的注释会造成误导,这比不写注释还要糟糕。保持注释的状态为最新的习惯非常重要,尽管对很多人来说这很难做到。 -在下一小节我们会讲到,注释可以自动生成文档。 +在下一小节我们会讲到,利用注释可以自动生成文档。 - -## 书写API文档 +## 写API文档 -很多人都觉得写文档是一件枯燥且吃力不讨好的事情,但实际情况不是这样。我们可以通过代码注释自动生成文档,这样就不用再去专门写文档了。很多人觉得这是一个不错的点子,因为根据某些关键字和格式化的文档自动生成可阅读的参考手册本身就是“某种编程”。 +很多人都觉得写文档是一件很枯燥而且吃力不讨好的事情,但实际情况并不是这样。我们可以通过代码注释自动生成文档,这样就不用再去专门写文档了。很多人觉得这是一个不错的点子,因为根据某些关键字和特定的格式自动生成可阅读的参考手册本身就是“某种编程”。 -传统的APIdoc诞生自Java世界,这个工具名叫“javadoc”,和Java SDK(软件开发工具包)一起提供。但这个创意迅速被其他语言借鉴。JavaScript领域有两个非常优秀的开源工具,它们是JSDoc Toolkit(http://code.google.com/p/jsdoc-toolkit/ )和YUIDoc(http://yuilibrary.com/projects/yuidoc )。 +最早利用注释生成API文档的工具诞生自Java业界,这个工具名叫“javadoc”,和Java SDK(软件开发工具包)一起提供,但这个创意迅速被其他语言借鉴。JavaScript领域有两个非常优秀的开源工具,它们是JSDoc Toolkit()和YUIDoc()。 -生成API文档的过程包括: +生成API文档的过程: -- 以特定的格式来组织书写源代码 +- 以特定的格式来写代码 - 运行工具来对代码和注释进行解析 - 发布工具运行的结果,通常是HTML页面 -你需要学习这种特殊的语法,包括十几种标签,写法类似于: +这种语法包括十几种标签(tag),写法类似于: /** * @tag value */ -比如这里有一个函数reverse(),可以对字符串进行反序操作。它的参数和返回值都是字符串。给它补充注释如下: +比如这里有一个函数`reverse()`,可以对字符串进行反序操作。它的参数和返回值都是字符串。给它补充注释如下: /** * Reverse a string @@ -804,26 +769,25 @@ ECMAScript的属性和方法均使用Camel标记法,尽管多字的属性名 return output; }; -可以看到,@param是用来说明输入参数的标签,@return是用来说明返回值的标签,文档生成工具最终会为将这种带注释的源代码解析成格式化好的HTML文档。 +如你所见,`@param`是用来说明输入参数的标签,`@return`是用来说明返回值的标签,文档生成工具最终会将这种带注释的源代码解析成HTML文档。 - -### 一个例子:YUIDoc +### 示例:YUIDoc -YUIDoc最初的目的是为YUI库(Yahoo! User Interface)生成文档,但也可以应用于任何项目,为了更充分的使用YUIDoc你需要学习它的注释规范,比如模块和类的写法(当然在JavaScript中是没有类的概念的)。 +YUIDoc的初衷是为YUI(Yahoo! User Interface)库生成文档,但其实它也可以应用于任何项目。为了更充分的使用YUIDoc,你需要学习它的注释规范,比如模块和类的写法。(尽管在JavaScript中其实是没有类的概念的)。 让我们看一个用YUIDoc生成文档的完整例子。 -图2-1展示了最终生成的文档的模样,你可以根据项目需要随意定制HTML模板,让生成的文档更加友好和个性化。 +图2-1展示了最终生成的文档的样子,你可以根据项目需要定制HTML模板,让生成的文档更加友好和个性化。 -这里同样提供了在线的demo,请参照 http://jspatterns.com/book/2/。 +这里提供了在线的demo,请参照。 -这个例子中所有的应用作为一个模块(myapp)放在一个文件里(app.js),后续的章节会更详细的介绍模块,现在只需知道用可以用一个YUIDoc的标签来表示模块即可。 +这个例子中所有的应用作为一个模块(myapp)放在一个文件里(app.js),后续的章节会更详细的介绍模块,现在只需知道可以用一个YUIDoc的标签来表示模块即可。 图2-1 YUIDoc生成的文档 -![pic](http://img02.taobaocdn.com/tps/i2/T1fSCgXdBsXXXXXXXX-781-647.png) +![YUIDoc生成的文档](./Figure/chapter2/2-1.jpg) -app.js的开始部分: +`app.js`的开始部分: /** * My JavaScript application @@ -835,7 +799,7 @@ app.js的开始部分: var MYAPP = {}; -紧接着定义了一个包含两个方法的对象math_stuff,这两个方法分别是sum()和multi(): +紧接着定义了一个包含两个方法的对象`math_stuff`,这两个方法分别是`sum()`和`multi()`: /** * A math utility @@ -868,29 +832,29 @@ app.js的开始部分: } }; -这样就结束了第一个“类”的定义,注意粗体表示的标签。 +这样就完成了第一个“类”的定义,注意以下标签: -@namespace +- `@namespace` -指向你的对象的全局引用 + 包含对象的全局引用 -@class +- `@class` -代表一个对象或构造函数的不恰当的称谓(JavaScript中没有类) + 代表一个对象或构造函数(JavaScript中没有类) -@method +- `@method` -定义对象的方法,并指定方法的名称 + 定义对象的方法,并指定方法的名称 -@param +- `@param` -列出函数需要的参数,参数的类型放在一对花括号内,跟随其后的是参数名和描述 + 列出函数需要的参数,参数的类型放在一对花括号内,后面跟参数名和描述 -@return +- `@return` -和@param类似,用以描述方法的返回值,可以不带名字 + 和@param类似,用以描述方法的返回值,可以不带名字 -我们用构造函数来实现第二个“类”,给这个类的原型添加一个方法,能够体会到YUIDoc采用了不同的方式来创建对象: +我们来实现第二个“类”,使用一个构造函数,并给这个构造函数的原型添加一个方法,看看YUIDoc在面对不同的对象创建方式时是如何工作的: /** * Constructs Person objects @@ -924,85 +888,79 @@ app.js的开始部分: return this.first_name + ' ' + this.last_name; }; -在图2-1中可以看到生成的文档中Person构造函数的生成结果,粗体的部分是: +在图2-1中可以看到生成的文档中`Person`构造函数的生成结果,值得注意的部分是: -- @constructor 暗示了这个“类”其实是一个构造函数 -- @prototype 和 @type 用来描述对象的属性 +- `@constructor` 说明这个“类”其实是一个构造函数 +- `@prototype` 和 `@type` 用来描述对象的属性 -YUIDoc工具是语言无关的,只解析注释块,而不是JavaScript代码。它的缺点是必须要在注释中指定属性、参数和方法的名字,比如,@property first_name。好处是一旦你熟练掌握YUIDoc,就可以用它对任何语言源码进行注释的文档化。 +YUIDoc工具是与语言无关的,只解析注释块,而不是JavaScript代码。它的缺点是必须要在注释中指定属性、参数和方法的名字,比如,`@property first_name`。好处是一旦你熟练掌握YUIDoc,就可以用它对任何语言源码生成文档。 - ## 编写易读的代码 -这种将APIDoc格式的代码注释解析成API参考文档的做法看起来很偷懒,但还有另外一个目的,通过代码重审来提高代码质量。 +这种编写注释块来生成API文档的做法可不仅仅是为了偷懒,它还有另外一个作用,就是通过回头重看代码来提高代码质量。 -很多作者或编辑会告诉你“编辑非常重要”,甚至是写一本好书或好文章最最重要的步骤。将想法落实在纸上形成草稿只是第一步,草稿给读者提的信息往往重点不明晰、结构不合理、或不符合循序渐进的阅读习惯。 +随便一个作者或者编辑都会告诉你“编辑非常重要”,甚至是写一本好书或好文章最最重要的步骤。将想法落实在纸上形成草稿只是第一步,草稿确实可以给读者提供不少信息,但往往还不是重点最明晰、结构最合理、最符合阅读习惯的呈现形式。 -对于编程也是同样的道理,当你坐下来解决一个问题的时候,这时的解决方案只是一种“草案”,尽管能正常工作,但是不是最优的方法呢?是不是可读性好、易于理解、可维护佳或容易更新?当一段时间后再来review你的代码,一定会发现很多需要改进的地方,需要重新组织代码或删掉多余的内容等等。这实际上就是在“整理”你的代码了,可以很大程度提高你的代码质量。但事情往往不是这样,我们常常承受着高强度的工作压力,根本没有时间来整理代码,因此通过代码注释写文档其实是不错的机会。 +编程也是同样的道理,当你坐下来解决一个问题的时候,这时的解决方案只是一种“草案”,尽管能正常工作,但是不是最优的方法呢?是不是可读性好、易于理解、可维护和更新?假设当你过一段时间后再来回头看你的代码,一定会发现很多需要改进的地方,比如需要重新组织代码或删掉多余的内容等等。这实际上就是在“整理”你的代码了,可以很大程度上提高你的代码质量。但事实却不那么如愿,我们常常承受着高强度的工作,根本没有时间来整理代码,因此通过代码注释来写文档其实是个不错的机会。 -往往在写注释文档的时候,你会发现很多问题。你也会重新思考源代码中不合理之处,比如,某个方法中的第三个参数比第二个参数更常用,第二个参数多数情况下取值为true,因此就需要对这个方法接口进行适当的改造和包装。 +你往往会在写注释文档的时候发现很多问题,也会重新思考代码中的不合理之处,比如,某个方法中的第三个参数比第二个参数更常用,第二个参数多数情况下取值为`true`,因此就需要对这个方法进行适当的改造和包装。 -写出易读的代码(或API),是指别人能轻易读懂程序的思路。所以你需要采用更好的思路来解决手头的问题。 +写出易读的代码(或API),是指写代码时要有让别人能轻易读懂的意识。带着这个意识,你就需要不断思考采用更好的方法来解决手头的问题。 -尽管我们认为“草稿”不甚完美,但至少也算“抱佛脚”的权宜之计,一眼看上去是有点“草”,不过也无所谓,特别是当你处理的是一个关键项目时(会有人命悬与此)。其实你应当扔掉你所给出的第一个解决方案,虽然它是可以正常工作的,但毕竟是一个草率的方案,不是最佳方案。你给出的第二个方案会更加靠谱,因为这时你对问题的理解更加透彻。第二个方案不是简单的复制粘贴之前的代码,也不能投机取巧寻找某种捷径。 +说回“草稿”的问题,也算是“抱佛脚”的权宜之计,一眼看上去是有点“草”,不过至少是有用的,特别是当你处理的是一个关键项目时(比如人命关天时)。一个合适的思路是,你应当始终扔掉你所给出的第一个解决方案,虽然它是可以正常工作的,但毕竟是一个草稿,是一种仅用于验证解决问题可行性的方案。事实上,第二个方案往往会更好,因为这时你对问题的理解会更加透彻。在产生第二个方案的过程中,不要允许自己去复制粘贴之前的代码,这有助于阻止自己投机取巧利用之前的捷径,最后产生不完美的方案。 - -## 相互评审 +## 同事评审(Peer Reviews) -另外一种可以提高代码质量的方法是组织相互评审。同行的评审很正式也很规范,即便是求助于特定的工具,也不失是一种开发生产线上值得提倡的步骤。但你可能觉得没有时间去作代码互审,没关系,你可以让坐在你旁边的同事读一下你的代码,或者和她(译注:注意是“她”而不是“他”)一起过一遍你的代码。 +另外一种可以提高代码质量的方法是组织相互评审。同事评审可以用一些工具辅助,可以很正式很规范,也是一种开发流程中值得提倡的步骤。你可能觉得没有时间去作代码互相评审,没关系,你可以让坐在你旁边的同事读一下你的代码,或者和她(译注:注意是“她”而不是“他”)一起过一遍你的代码。 -同样,当你在写APIDoc或任何其他文档的时候,同行的评审能帮助你的产出物更加清晰,因为你写的文档是让别人读的,你必须确保别人能理解你所作的东西。 +同样,当你在写API文档或者其他文档的时候,同事评审能让你的产出物更加清晰,因为你写的文档是本来就是让别人读的,你得让别人通过文档知道你所做的东西。 -同行的评审是一种非常不错的习惯,不仅仅是因为它能让代码变得更好,更重要的,在评审的过程中,评审人和代码作者通过分享和讨论,两人都能取长补短、相互促进。 +同事评审是一种很好的实践,不仅仅是因为它能让代码变得更好,更重要的是,在评审的过程中,评审人和代码作者通过分享和讨论,两人都能取长补短、相互促进。 -如果你的团队只有你一个开发人员,找不出第二个人能给你作代码评审,这也没关系。你可以通过将你的代码片段开源,或把有意思的代码片段贴在博客中,会有人对你的代码感兴趣的。 +如果你的团队只有你一个开发人员,找不出第二个人能给你作代码评审,这也没关系。你可以通过将你的代码片段开源,或把有意思的代码片段贴在博客中,让全世界的人为你评审。 -另外一个非常好的习惯是使用版本管理工具(CVS,SVN或Git),一旦有人修改并提交了代码,都会发邮件通知组内成员。虽然大部分邮件都进入了垃圾箱,但总是会碰巧有人在工作间隙看到你所提交的代码,并对代码做出一些评价。 +另外一个很好的实践是使用版本管理工具(CVS、SVN或Git),一旦有人修改并提交了代码,就会发邮件通知组内成员。虽然大部分邮件都进入了垃圾箱,但总是会碰巧有人在工作间隙看到你所提交的代码,并对代码做出一些评价。 - -## 生产环境中的代码压缩(Minify) +## 发布时的代码压缩(Minify) -这里所说的代码压缩(Minify)是指去除JavaScript代码中的空格、注释以及其他不必要的部分,用以减少JavaScript文件的体积,降低网络带宽损耗。我们通常使用类似YUICompressor(Yahoo!)或Closure Compiler(Google)的压缩工具来为网页加载提速。对于生产环境(译注:“生产环境”指的是项目上线后的正式环境)中的脚本是需要作压缩的,压缩后的文件体积能减少至原来的一半以下。 +这里所说的代码压缩(Minify)是指去除JavaScript代码中的空格、注释以及其他不必要的部分,用以减少JavaScript文件的体积,降低网络带宽消耗。我们通常使用压缩工具来进行压缩,比如YUICompressor(Yahoo!)或Closure Compiler(Google),这可以减少页面加载时间。压缩用于发布的的脚本是很重要的,压缩后的文件体积能减少至原来的一半以下。 -下面这段代码是压缩后的样子(这段代码是YUI2库中的Event模块): +下面这段代码是压缩后的样子(这段代码是YUI2库中的事件模块): YAHOO.util.CustomEvent=function(D,C,B,A){this.type=D;this.scope=C||window;this.silent =B;this.signature=A||YAHOO.util.CustomEvent.LIST;this.subscribers=[];if(!this.silent) {}var E="_YUICEOnSubscribe";if(D!==E){this.subscribeEvent=new YAHOO.util.CustomEvent(E,this,true);}... -除了去除空格、空行和注释之外,压缩工具还能缩短命名的长度(前提是保证代码的安全),比如这段代码中的参数A、B、C、D。压缩工具只会重命名局部变量,因为更改全局变量会破坏代码的逻辑。这也是要尽量使用局部变量的原因。如果你使用的全局变量是对DOM节点的引用,而且程序中多次用到,最好将它赋值给一个局部变量,这样能提高查找速度,代码也会运行的更快,此外还能提高压缩比、加快下载速度(译注:在服务器开启Gzip的情况下,对下载速度的影响几乎可以忽略不计)。 +除了去除空格、空行和注释之外,压缩工具还能缩短命名的长度(在保证代码安全的前提下),比如这段代码中的参数`A`、`B`、`C`、`D`。压缩工具只会重命名局部变量,因为更改全局变量会破坏代码的逻辑,这也是要尽量使用局部变量的原因。如果你使用的全局变量是对DOM节点的引用,而且程序中多次用到,那么最好将它赋值给一个局部变量,这样能提高查找速度,代码也会运行的更快,此外还能提高压缩比、加快下载速度。 -补充说明一下,Goolge Closure Compiler还会对全局变量进行压缩(在“高级”模式中),这是很危险的,且对编程规范的要求非常苛刻。它的好处是压缩比非常高。 +补充说明一下,Goolge Closure Compiler还会为了更高的压缩比对全局变量进行压缩(在“高级”模式中),这是很危险的,且对编程规范的要求非常苛刻。 -对生产环境的脚本做压缩是相当重要的步骤,它能提升页面性能,你应当使用工具来完成压缩。千万不要试图手写“压缩好的”代码,你应当坚持使用语义化的变量命名,并保留足够的空格、缩进和注释。你写的代码是需要被人阅读的,所以应当将注意力放在代码可读性和可维护性上,代码压缩的工作交给工具去完成。 +对用于生产环境的脚本做压缩是非常重要的步骤,因为它能提升页面性能,但你应当将这个过程交给工具来完成。千万不要试图手写“压缩好的”代码,你应当在编写代码时坚持使用语义化的变量命名,并保留足够的空格、缩进和注释。你写的代码是需要被人阅读的,所以应当将注意力放在代码可读性和可维护性上,将代码压缩的工作交给工具去完成。 - -## 运行 JSLint +## 运行JSLint -在上一章我们已经介绍了JSLint,这里我们介绍更多的使用场景。对你的代码进行JSLint检查是非常好的编程习惯,你应该相信这一点。 +在上一章我们已经介绍了JSLint,本章中也提到了数次。到现在你应该已经相信用JSLint检查你的代码是一种好的编程模式了。 -JSLint的检查点都有哪些呢?它会对本章讨论过的一些模式(单var模式、parseInt()的第二个参数、总是使用花括号)做检查。JSLint还包括其他方面的检查: +JSLint的检查点都有哪些呢?它会对本章讨论过的一些模式(单`var`模式、`parseInt()`的第二个参数、总是使用花括号)做检查。JSLint还包括其他方面的检查: -- 不可达代码 -- 在使用变量之前需要声明 +- 不可达代码(译注:指永远不可能运行的代码) +- 变量在声明之前被使用 - 不安全的UTF字符 -- 使用void、with、和eval +- 使用`void`、`with`或者`eval` - 无法正确解析的正则表达式 -JSLint是基于JavaScript实现的(它是可以通过JSLint检查的),它提供了在线工具,也可以下载使用,可以运行于很多种平台的JavaScript解析器。你可以将源码下载后在本地运行,支持的环境包括WSH(Windows Scripting Host,Windows)、JSC(JavaScriptCore,MacOSX)或Rhino(Mozilla开发的JavaScript引擎)。 +JSLint是基于JavaScript实现的(它自己的代码是可以通过JSLint检查的),它提供了在线工具,也可以下载使用,可以运行于很多种平台的JavaScript解析器。你可以将源码下载后在本地运行,支持的环境包括WSH(Windows Scripting Host,Windows)、JSC(JavaScriptCore,MacOSX)或Rhino(Mozilla开发的JavaScript引擎)。 -可以将JSLint下载后和你的代码编辑器配置在一起,着是一个不错的注意,这样每次你保存代码的时候都会自动执行代码检查(比如配置快捷键)。 +将JSLint下载后和你的代码编辑器配置在一起是个很不错的主意,这样每次你保存代码的时候都会自动执行代码检查。(为它配置一个快捷键也很有用)。 - ## 小结 -本章我们讲解了编写可维护性代码的含义,本章的讨论非常重要,它不仅关系着软件项目的成功与否,还关系到参与项目的工程师的“精神健康”和“幸福指数”。随后我们讨论了一些最佳实践和模式,它们包括: +本章我们讨论了编写可维护性代码的意义,它不仅关系着软件项目的成功与否,还关系到参与项目的工程师的“精神健康”和“幸福指数”。随后我们讨论了一些最佳实践和模式,它们包括: - 减少全局对象,最好每个应用只有一个全局对象 -- 函数都使用单var模式来定义,这样可以将所有的变量放在同一个地方声明,同时可以避免“声明提前”给程序逻辑带来的影响。 -- for循环、for-in循环、switch语句、“禁止使用eval()”、不要扩充内置原型 -- 遵守统一的编码规范(在任何必要的时候保持空格、缩进、花括号和分号)和命名约定(构造函数、普通函数和变量)。 - -本章还讨论了其他一些和代码本身无关的实践,这些实践和编码过程紧密相关,包括书写注释、生成API文档,组织代码评审、不要试图去手动了“压缩”(minify)代码而牺牲代码可读性、坚持使用JSLint来对代码做检查。 +- 函数都使用单`var`模式来定义,这样可以将所有的变量放在同一个地方声明,同时可以避免“声明提前”给程序逻辑带来的影响 +- `for`循环、`for-in`循环、`switch`语句、“避免使用`eval()`”、不要扩充内置原型 +- 遵守统一的编码规范(在任何必要的时候保持空格、缩进、花括号和分号)和命名规范(构造函数、普通函数和变量)。 +本章还讨论了其他一些和代码本身无关的实践,这些实践和编码过程紧密相关,包括写注释、写API文档、组织同事评审、不要试图去手动“压缩”(minify)代码而牺牲代码可读性、坚持使用JSLint来对代码进行检查。 diff --git a/chapter3.markdown b/chapter3.markdown index 55c32b0..483409f 100644 --- a/chapter3.markdown +++ b/chapter3.markdown @@ -1,52 +1,49 @@ - -# 第三章 直接量和构造函数 +# 第三章 字面量和构造函数 -JavaScript中的直接量模式更加简洁、富有表现力,且在定义对象时不容易出错。本章将对直接量展开讨论,包括对象、数组和正则表达式直接量,以及为什么要使用等价的内置构造器函数来创建它们,比如Object()和Array()等。本章同样会介绍JSON格式,JSON是使用数组和对象直接量的形式定义的一种数据转换格式。本章还会讨论自定义构造函数,包括如何强制使用new以确保构造函数的正确执行。 +JavaScript中的字面量模式更加简洁、有表现力,而且在定义对象时不容易出错。本章将会讨论字面量,包括对象、数组和正则表达式字面量,以及为什么字面量要比等价的内置构造函数(如`Object()`、`Array()`等)要更好。本章还会介绍JSON格式,JSON是使用数组和对象字面量的形式定义的一种数据交换格式。本章还会讨论自定义构造函数,包括如何强制使用`new`以确保构造函数正确执行。 -本章还会补充讲述一些基础知识,比如内置包装对象Number()、String()和Boolean(),以及如何将它们和原始值(数字、字符串和布尔值)比较。最后,快速介绍一下Error()构造函数的用法。 +为了方便使用字面量而不是构造函数,本章还会补充一些知识,比如内置包装构造函数`Number()`、`String()`和`Boolean()`,以及如何将它们和原始值(数字、字符串和布尔值)比较。最后,快速介绍一下`Error()`构造函数的用法。 - -## 对象直接量 +## 对象字面量 -我们可以将JavaScript中的对象简单的理解为名值对组成的散列表(hash table),在其他编程语言中被称作“关联数组”。其中的值可以是原始值也可以是对象,不管是什么类型,它们都是“属性”(properties),属性值同样可以是函数,这时属性就被称为“方法”(methods)。 +我们可以将JavaScript中的对象简单地理解为名值对组成的散列表(hash table,也叫哈希表)。在其他编程语言中被称作“关联数组”。其中的值可以是原始值也可以是对象。不管是什么类型,它们都是“属性”(property),属性值同样可以是函数,这时属性就被称为“方法”(method)。 -JavaScript中自定义的对象(用户定义的本地对象)任何时候都是可变的。内置本地对象的属性也是可变的。你可以先创建一个空对象,然后在需要时给它添加功能。“对象直接量写法(object literal notation)”是按需创建对象的一种理想方式。 +JavaScript中自定义的对象(用户定义的本地对象)任何时候都是可变的。内置本地对象的属性也是可变的。你可以先创建一个空对象,然后在需要时给它添加功能。“对象字面量写法(object literal notation)”是按需创建对象的一种理想方式。 看一下这个例子: - // start with an empty object + // 定义空对象 var dog = {}; - // add one property + // 添加一个属性 dog.name = "Benji"; - // now add a method + // 添加一个方法 dog.getName = function () { return dog.name; }; 在这个例子中,我们首先定义了一个空对象,然后添加了一个属性和一个方法,在程序的生命周期内的任何时刻都可以: -1.更改属性和方法的值,比如: +- 更改属性和方法的值,比如: dog.getName = function () { - // redefine the method to return - // a hardcoded value + // 重新定义方法,返回一个硬编码的值 return "Fido"; }; -2.完全删除属性/方法 +- 删除属性/方法 delete dog.name; -3.添加更多的属性和方法 +- 添加更多的属性和方法 dog.say = function () { return "Woof!"; }; dog.fleas = true; -其实不必每次开始都创建空对象,对象直接量模式可以直接在创建对象时添加功能,就像下面这个例子所展示的: +每次都创建空对象并不是必须的,对象字面量模式可以直接在创建对象时添加功能,就像下面这个例子: var dog = { name: "Benji", @@ -55,81 +52,78 @@ JavaScript中自定义的对象(用户定义的本地对象)任何时候都 } }; -> 在本书中多次提到“空对象”(“blank object”和“empty object”)。这只是某种简称,要知道JavaScript中根本不存在真正的空对象,理解这一点至关重要。即使最简单的`{}`对象包含从Object.prototype继承来的属性和方法。我们提到的“空(empty)对象”只是说这个对象没有自己的属性,不考虑它是否有继承来的属性。 +> 在本书中多次提到“空对象”(“blank object”和“empty object”),这只是一种简称,在JavaScript中根本不存在真正的空对象,理解这一点至关重要。即使最简单的`{}`对象也会包含从`Object.prototype`继承来的属性和方法。我们提到的“空(empty)对象”只是说这个对象没有自有属性(own properties),不考虑它是否有继承来的属性。 - -### 对象直接量语法 +### 对象字面量语法 -如果你从来没有接触过对象直接量写法,第一次碰到可能会感觉怪怪的。但越到后来你就越喜欢它。本质上讲,对象直接量语法包括: +如果你从来没有接触过对象字面量的写法,可能会感觉怪怪的。但越到后来你就越喜欢它。本质上讲,对象字面量语法包括: -- 将对象主体包含在一对花括号内(`{` and `}`)。 +- 将对象主体包含在一对花括号内(`{` 和 `}`)。 - 对象内的属性或方法之间使用逗号分隔。最后一个名值对后也可以有逗号,但在IE下会报错,所以尽量不要在最后一个属性或方法后加逗号。 -- 属性名和值之间使用冒号分隔 -- 如果将对象赋值给一个变量,不要忘了在右括号`}`之后补上分号 +- 属性名和值之间使用冒号分隔。 +- 如果将对象赋值给一个变量,不要忘了在右括号`}`之后补上分号。 - ### 通过构造函数创建对象 -JavaScript中没有类的概念,这给JavaScript带来了极大的灵活性,因为你不必提前知晓关于对象的任何信息,也不需要类的“蓝图”。但JavaScript同样具有构造函数,它的语法和Java或其他语言中基于类的对象创建非常类似。 +JavaScript中没有类的概念,这给JavaScript带来了极大的灵活性,因为你不必提前知晓关于对象的任何信息,也不需要类的“蓝图”(译注:指类的结构)。但JavaScript同样具有构造函数,它的语法和Java或其他语言中基于类的对象创建非常类似。 -你可以使用自定义的构造函数来创建实例对象,也可以使用内置构造函数来创建,比如Object()、Date()、String()等等。 +你可以使用自定义的构造函数来创建对象实例,也可以使用内置构造函数来创建,比如`Object()`、`Date()`、`String()`等等。 下面这个例子展示了用两种等价的方法分别创建两个独立的实例对象: - // one way -- using a literal + // 一种方法,使用字面量 var car = {goes: "far"}; - // another way -- using a built-in constructor - // warning: this is an antipattern + // 另一种方法,使用内置构造函数 + // 注意:这是一种反模式 var car = new Object(); car.goes = "far"; -从这个例子中可以看到,直接量写法的一个明显优势是,它的代码更少。“创建对象的最佳模式是使用直接量”还有一个原因,它可以强调对象就是一个简单的可变的散列表,而不必一定派生自某个类。 +从这个例子中可以看到,字面量写法的一个明显优势是,它的代码更少。“创建对象的最佳模式是使用字面量”还有一个原因,它可以强调对象就是一个简单的可变的散列表,而不必一定派生自某个类。 -另外一个使用直接量而不是Object构造函数创建实例对象的原因是,对象直接量不需要“作用域解析”(scope resolution)。因为新创建的实例有可能包含了一个本地的构造函数,当你调用Object()的时候,解析器需要顺着作用域链从当前作用域开始查找,直到找到全局Object构造函数为止。 +另外一个使用字面量而不是`Object()`构造函数创建实例对象的原因是,对象字面量不需要“作用域解析”(scope resolution)。因为存在你已经创建了一个同名的构造函数`Object()`的可能,当你调用`Object()`的时候,解析器需要顺着作用域链从当前作用域开始查找,直到找到全局`Object()`构造函数为止。 - -### 获得对象的构造器 +### Object()构造函数的参数 -创建实例对象时能用对象直接量就不要使用new Object()构造函数,但有时你希望能继承别人写的代码,这时就需要了解构造函数的一个“特性”(也是不使用它的另一个原因),就是Object()构造函数可以接收参数,通过参数的设置可以把实例对象的创建委托给另一个内置构造函数,并返回另外一个实例对象,而这往往不是你所希望的。 +> 译注:这小节的标题是Object Constructor Catch,恕译者水平有限,实在不知如何翻译,故自作主张修改了本节标题。 -下面的示例代码中展示了给new Object()传入不同的参数:数字、字符串和布尔值,最终得到的对象都是由不同的构造函数生成的: +创建实例对象时能用对象字面量就不要使用`new Object()`构造函数,但有时你可能是在别人写的代码基础上工作,这时就需要了解构造函数的一个“特性”(也是不使用它的另一个原因),就是`Object()`构造函数可以接收参数,通过这个参数可以把对象实例的创建过程委托给另一个内置构造函数,并返回另外一个对象实例,而这往往不是你想要的。 - // Warning: antipatterns ahead +下面的示例代码中展示了给`new Object()`传入不同的参数(数字、字符串和布尔值),最终得到的对象是由不同的构造函数生成的: - // an empty object + // 注意:这是反模式 + + // 空对象 var o = new Object(); console.log(o.constructor === Object); // true - // a number object + // 数值对象 var o = new Object(1); console.log(o.constructor === Number); // true console.log(o.toFixed(2)); // "1.00" - // a string object + // 字符串对象 var o = new Object("I am a string"); console.log(o.constructor === String); // true - // normal objects don't have a substring() - // method but string objects do + // 普通对象没有substring()方法,但字符串对象有 console.log(typeof o.substring); // "function" - // a boolean object + // 布尔值对象 var o = new Object(true); console.log(o.constructor === Boolean); // true -Object()构造函数的这种特性会导致一些意想不到的结果,特别是当参数不确定的时候。最后再次提醒不要使用new Object(),尽可能的使用对象直接量来创建实例对象。 +`Object()`构造函数的这种特性会导致一些意想不到的结果,特别是当参数不确定的时候。最后再次提醒不要使用`new Object()`,尽可能的使用对象字面量来创建实例对象。 - ## 自定义构造函数 -除了对象直接量和内置构造函数之外,你也可以通过自定义的构造函数来创建实例对象,正如下面的代码所示: +除了对象字面量和内置构造函数之外,你也可以通过自定义的构造函数来创建对象实例,正如下面的代码所示: var adam = new Person("Adam"); adam.say(); // "I am Adam" -这里用了“类”Person创建了实例,这种写法看起来很像Java中的实例创建。两者的语法的确非常接近,但实际上JavaScript中没有类的概念,Person是一个函数。 +这种写法非常像Java中用`Person`类创建了一个实例,两者的语法非常接近,但实际上JavaScript中没有类的概念,`Person()`是一个函数。 -Person构造函数是如何定义的呢?看下面的代码: +`Person()`构造函数是如何定义的呢?看下面的代码: var Person = function (name) { this.name = name; @@ -138,20 +132,19 @@ Person构造函数是如何定义的呢?看下面的代码: }; }; -当你通过关键字new来调用这个构造函数时,函数体内将发生这些事情: +当你通过`new`来调用这个构造函数时,函数体内将发生这些事情: -- 创建一个空对象,将它的引用赋给this,继承函数的原型。 -- 通过this将属性和方法添加至这个对象 -- 最后返回this指向的新对象(如果没有手动返回其他的对象) +- 创建一个空对象,将它的引用赋给`this`,并继承函数的原型。 +- 通过`this`将属性和方法添加至这个对象。 +- 最后返回this指向的新对象(如果没有手动返回其他的对象)。 用代码表示这个过程如下: var Person = function (name) { - // create a new object - // using the object literal + // 使用对象字面量创建新对象 // var this = {}; - // add properties and methods + // 添加属性和方法 this.name = name; this.say = function () { return "I am " + this.name; @@ -160,92 +153,85 @@ Person构造函数是如何定义的呢?看下面的代码: //return this; }; -正如这段代码所示,say()方法添加至this中,结果是,不论何时调用new Person(),在内存中都会创建一个新函数(译注:所有Person的实例对象中的方法都是独占一块内存的)。显然这是效率很低的,因为所有实例的say()方法是一模一样的,因此没有必要“拷贝”多份。最好的办法是将方法添加至Person的原型中。 +上例中,为简便起见,`say()`方法被添加至`this`中,结果就是不论何时调用`new Person()`,在内存中都会创建一个新函数(`say()`),显然这是效率很低的,因为所有实例的`say()`方法是一模一样的。最好的办法是将方法添加至`Person()`的原型中。 Person.prototype.say = function () { return "I am " + this.name; }; -我们将会在下一章里详细讨论原型和继承。现在只要记住将需要重用的成员和方法放在原型里即可。 +我们将会在下一章里详细讨论原型和继承,现在只要记住将需要重用的成员放在原型里即可。 -关于构造函数的内部工作机制也会在后续章节中有更细致的讨论。这里我们只做概要的介绍。刚才提到,构造函数执行的时候,首先创建一个新对象,并将它的引用赋给this: +关于构造函数的内部工作机制也会在后续章节中有更细致的讨论。这里我们只做概要的介绍。刚才提到,构造函数执行的时候,首先创建一个新对象,并将它的引用赋给`this`: // var this = {}; -事实并不完全是这样,因为“空”对象并不是真的空,这个对象继承了Person的原型,看起来更像: +其实事实并不完全是这样,因为“空”对象并不是真的空,这个对象继承了`Person`的原型,看起来更像: // var this = Object.create(Person.prototype); -在后续章节会进一步讨论Object.create()。 +在后续章节会进一步讨论`Object.create()`。 - ### 构造函数的返回值 -用new调用的构造函数总是会返回一个对象,默认返回this所指向的对象。如果构造函数内没有给this赋任何属性,则返回一个“空”对象(除了继承构造函数的原型之外,没有“自己的”属性)。 +当使用`new`调用的时候,构造函数总是会返回一个对象,默认情况下返回`this`所指向的对象。如果构造函数内没有给`this`赋任何属性,则返回一个“空”对象(除了继承构造函数的原型之外,没有自有属性)。 -尽管我们不会在构造函数内写return语句,也会隐式返回this。但我们是可以返回任意指定的对象的,在下面的例子中就返回了新创建的that对象。 +尽管在构造函数中没有`return`语句的情况下,也会隐式返回`this`。但事实上我们是可以返回任意指定的对象的,在下面的例子中就返回了新创建的`that`对象。 var Objectmaker = function () { - // this `name` property will be ignored - // because the constructor - // decides to return another object instead + // name属性会被忽略,因为返回的是另一个对象 this.name = "This is it"; - // creating and returning a new object + // 创建并返回一个新对象 var that = {}; that.name = "And that's that"; return that; }; - // test + // 测试 var o = new Objectmaker(); console.log(o.name); // "And that's that" -我们看到,构造函数中其实是可以返回任意对象的,只要你返回的东西是对象即可。如果返回值不是对象(字符串、数字或布尔值),程序不会报错,但这个返回值被忽略,最终还是返回this所指的对象。 +可以看到,构造函数中其实是可以返回任意对象的,只要你返回的东西是对象即可。如果返回值不是对象(字符串、数字或布尔值),程序不会报错,但这个返回值被忽略,最终还是返回`this`所指的对象。 - ## 强制使用new的模式 -我们知道,构造函数和普通的函数无异,只是通过new调用而已。那么如果调用构造函数时忘记new会发生什么呢?漏掉new不会产生语法错误也不会有运行时错误,但可能会造成逻辑错误,导致执行结果不符合预期。这是因为如果不写new的话,函数内的this会指向全局对象(在浏览器端this指向window)。 +我们知道,构造函数和普通的函数本质一样,只是通过`new`调用而已。那么如果调用构造函数时忘记`new`会发生什么呢?漏掉`new`不会产生语法错误也不会有运行时错误,但可能会造成逻辑错误,导致执行结果不符合预期。这是因为如果不写`new`的话,函数内的`this`会指向全局对象(在浏览器端`this`指向`window`)。 -当构造函数内包含this.member之类的代码,并直接调用这个函数(省略new),实际会创建一个全局对象的属性member,可以通过window.member或member访问到它。这必然不是我们想要的结果,因为我们要努力确保全局命名空间的整洁干净。 +当构造函数内包含`this.member`之类的代码,并直接调用这个函数(省略`new`),实际上会创建一个全局对象的属性`member`,可以通过`window.member`或`member`访问到。这不是我们想要的结果,因为我们要努力确保全局命名空间干净。 - // constructor + // 构造函数 function Waffle() { this.tastes = "yummy"; } - // a new object + // 新对象 var good_morning = new Waffle(); console.log(typeof good_morning); // "object" console.log(good_morning.tastes); // "yummy" - // antipattern: - // forgotten `new` + // 反模式,漏掉new var good_morning = Waffle(); console.log(typeof good_morning); // "undefined" console.log(window.tastes); // "yummy" -ECMAScript5中修正了这种非正常的行为逻辑。在严格模式中,this是不能指向全局对象的。如果在不支持ES5的JavaScript环境中,仍然后很多方法可以确保构造函数的行为即便在省略new调用时也不会出问题。 +ECMAScript5中修正了这种出乎意料的行为逻辑。在严格模式中,`this`不再指向全局对象。如果在不支持ES5的JavaScript环境中,也有一些方法可以确保有没有`new`时构造函数的行为都保持一致。 - -### 命名约定 +### 命名规范 -最简单的选择是使用命名约定,前面的章节已经提到,构造函数名首字母大写(MyConstructor),普通函数和方法名首字母小写(myFunction)。 +一种简单解决上述问题的方法就是命名规范,前面的章节已经讨论过,构造函数首字母大写(`MyConstructor()`),普通函数和方法首字母小写(`myFunction`)。 - ### 使用that -遵守命名约定的确能帮上一些忙,但约定毕竟不是强制,不能完全避免出错。这里给出了一种模式可以确保构造函数一定会按照构造函数的方式执行。不要将所有成员挂在this上,将它们挂在that上,并返回that。 +遵守命名规范有一定的作用,但规范毕竟不是强制,不能完全避免出现错误。这里给出了一种模式可以确保构造函数一定会按照构造函数的方式执行,那就是不要将所有成员添加到`this`上,而是将它们添加到`that`上,并返回`that`。 function Waffle() { - var that = {}; + var that = {}; that.tastes = "yummy"; return that; } -如果要创建简单的实例对象,甚至不需要定义一个局部变量that,可以直接返回一个对象直接量,就像这样: +如果要创建更简单一点的对象,甚至不需要局部变量`that`,直接返回一个对象字面量即可,就像这样: function Waffle() { return { @@ -253,21 +239,20 @@ ECMAScript5中修正了这种非正常的行为逻辑。在严格模式中,thi }; } -不管用什么方式调用它(使用new或直接调用),它同都会返回一个实例对象: +不管用什么方式调用它(使用`new`或直接调用),它都会返回一个实例对象: var first = new Waffle(), second = Waffle(); console.log(first.tastes); // "yummy" console.log(second.tastes); // "yummy" -这种模式的问题是丢失了原型,因此在Waffle()的原型上的成员不会继承到这些实例对象中。 +这种模式的问题是会丢失原型,因此在`Waffle()`的原型上的成员不会被继承到这些对象中。 -> 需要注意的是,这里用的that只是一种命名约定,that不是语言的保留字,可以将它替换为任何你喜欢的名字,比如self或me。 +> 需要注意的是,这里用的`that`只是一种命名规范,`that`并不是语言特性的一部分,它可以被替换为任何你喜欢的名字,比如`self`或`me`。 - ### 调用自身的构造函数 -为了解决上述模式的问题,能够让实例对象继承原型属性,我们使用下面的方法。在构造函数中首先检查this是否是构造函数的实例,如果不是,再通过new调用构造函数,并将new的结果返回: +为了解决上述模式的问题,能够让对象继承原型上的属性,我们使用下面的方法:在构造函数中首先检查`this`是否是构造函数的实例,如果不是,则通过`new`再次调用自己: function Waffle() { @@ -279,7 +264,7 @@ ECMAScript5中修正了这种非正常的行为逻辑。在严格模式中,thi } Waffle.prototype.wantAnother = true; - // testing invocations + // 测试 var first = new Waffle(), second = Waffle(); @@ -289,90 +274,85 @@ ECMAScript5中修正了这种非正常的行为逻辑。在严格模式中,thi console.log(first.wantAnother); // true console.log(second.wantAnother); // true -另一种检查实例的通用方法是使用arguments.callee,而不是直接将构造函数名写死在代码中: +还有一种比较通用的用来检查实例的方法是使用`arguments.callee`,而不是直接将构造函数名写死在代码中: if (!(this instanceof arguments.callee)) { return new arguments.callee(); } -这里需要说明的是,在任何函数内部都会自行创建一个arguments对象,它包含函数调用时传入的参数。同时arguments包含一个callee属性,指向它所在的正在被调用的函数。需要注意,ES5严格模式中是禁止使用arguments.callee的,因此最好对它的使用加以限制,并删除任何你能在代码中找到的实例(译注:这里作者的表述很委婉,其实作者更倾向于全面禁止使用arguments.callee)。 +这种模式利用了一个事实,即在任何函数内部都会创建一个`arguments`对象,它包含函数调用时传入的参数。同时`arguments`包含一个`callee`属性,指向正在被调用的函数。需要注意,ES5严格模式中已经禁止了`arguments.callee`的使用,因此最好对它的使用加以限制,并尽可能删除现有代码中已经用到的地方。 - -## 数组直接量 +## 数组字面量 -和其他的大多数一样,JavaScript中的数组也是对象。可以通过内置构造函数Array()来创建数组,类似对象直接量,数组也可以通过直接量形式创建。而且更推荐使用直接量创建数组。 +和JavaScript中大多数“东西”一样,数组也是对象。可以通过内置构造函数`Array()`来创建数组,也可以通过字面量形式创建,就像对象字面量那样。而且更推荐使用字面量创建数组。 -这里的实例代码给出了创建两个具有相同元素的数组的两种方法,使用Array()和使用直接量模式: +这里的示例代码给出了创建两个具有相同元素的数组的两种方法,使用`Array()`和使用字面量模式: - // array of three elements - // warning: antipattern + // 有三个元素的数组 + // 注意:这是反模式 var a = new Array("itsy", "bitsy", "spider"); - // the exact same array + // 完全相同的数组 var a = ["itsy", "bitsy", "spider"]; - console.log(typeof a); // "object", because arrays are objects + console.log(typeof a); // "object",因为数组也是对象 console.log(a.constructor === Array); // true - -### 数组直接量语法 +### 数组字面量语法 -数组直接量写法非常简单:整个数组使用方括号括起来,数组元素之间使用逗号分隔。数组元素可以是任意类型,也包括数组和对象。 +数组字面量写法非常简单:整个数组使用方括号括起来,数组元素之间使用逗号分隔。数组元素可以是任意类型,包括数组和对象。 -数组直接量语法简单直接、高雅美观。毕竟数组只是从位置0开始索引的值的集合,完全没必要包含构造器和new运算符的内容(代码会更多),保持简单即可。 +数组字面量语法简单直观而且优雅,毕竟数组只是从0开始索引的一些值的集合,完全没必要引入构造器和`new`运算符(还要写更多的代码)。 - -### 有意思的数组构造器 +### Array()构造函数的“陷阱” -我们对new Array()敬而远之原因是为了避免构造函数带来的陷阱。 +我们对`new Array()`敬而远之还有一个原因,就是为了避免构造函数带来的陷阱。 -如果给Array()构造器传入一个数字,这个数字并不会成为数组的第一个元素,而是设置数组的长度。也就是说,new Array(3)创建了一个长度为3的数组,而不是某个元素是3。如果你访问数组的任意元素都会得到undefined,因为元素并不存在。下面示例代码展示了直接量和构造函数的区别: +如果给`Array()`构造函数传入一个数字,这个数字并不会成为数组的第一个元素,而是设置了数组的长度。也就是说,`new Array(3)`创建了一个长度为3的数组,而不是某个元素是3。如果你访问数组的任意元素都会得到`undefined`,因为元素并不存在。下面的示例代码展示了字面量和构造函数的区别: - // an array of one element + // 含有1个元素的数组 var a = [3]; console.log(a.length); // 1 console.log(a[0]); // 3 - // an array of three elements + // 含有3个元素的数组 var a = new Array(3); console.log(a.length); // 3 console.log(typeof a[0]); // "undefined" -尽管构造器的行为并不像我们想象的那样,当给new Array()传入一个浮点数时情况就更糟糕了。这时结果就会出错(译注:给new Array()传入浮点数会报“范围错误”RangError,new Array(3.00)则不会报错),因为数组长度不可能是浮点数。 +构造函数的行为可能有一点出乎意料,但当给`new Array()`传入一个浮点数时情况就更糟糕了,这时会出错(译注:给new Array()传入浮点数会报“范围错误”RangError),因为数组长度不可能是浮点数。 - // using array literal + // 使用数组字面量 var a = [3.14]; console.log(a[0]); // 3.14 var a = new Array(3.14); // RangeError: invalid array length console.log(typeof a); // "undefined" -为了避免在运行时动态创建数组时出现这种错误,强烈推荐使用数组直接量来代替new Array()。 +为了避免在运行时动态创建数组时出现这种错误,强烈推荐使用数组字面量来代替`new Array()`。 ->有些人用Array()构造器来做一些有意思的事情,比如用来生成重复字符串。下面这行代码返字符串包含255个空格(请读者思考为什么不是256个空格)。`var white = new Array(256).join(' ');` +> 有些人用`Array()`构造器来做一些有意思的事情,比如用来生成重复字符串。下面这行代码返回的字符串包含255个空格(请读者思考为什么不是256个空格)。`var white = new Array(256).join(' ');` - -### 检查是不是数组 +### 检查是否数组 -如果typeof的操作数是数组的话,将返回“object”。 +如果`typeof`的操作数是数组的话,将返回“object”。 console.log(typeof [1, 2]); // "object" -这个结果勉强说得过去,毕竟数组是一种对象,但对我们用处不大。往往你需要知道一个值是不是真正的数组。你可能见到过这种检查数组的方法:检查length属性、检查数组方法比如slice()等等。但这些方法非常脆弱,非数组的对象也可以拥有这些同名的属性。还有些人使用instanceof Array来判断数组,但这种方法在某些版本的IE里的多个iframe的场景中会出问题(译注:原因就是在不同iframe中创建的数组不会相互共享其prototype属性)。 +这个结果勉强说得过去,毕竟数组也是一种对象,但对我们来说这个结果却没什么用,实际上你往往是需要知道一个值是不是真正的数组。有时候你会见到一些检查数组的方法:检查`length`属性、检查数组方法比如`slice()`等等,但这些方法非常脆弱,非数组的对象也可以拥有这些同名的属性。还有些人使用`instanceof Array`来判断数组,但这种方法在某些版本的IE里的多个iframe的场景中会出问题(译注:原因就是在不同iframe中创建的数组不会相互共享其`prototype`属性)。 -ECMAScript 5定义了一个新的方法Array.isArray(),如果参数是数组的话就返回true。比如: +ECMAScript5定义了一个新的方法`Array.isArray()`,如果参数是数组的话就返回true。比如: Array.isArray([]); // true - // trying to fool the check - // with an array-like object + // 尝试用一个类似数组的对象去测试 Array.isArray({ length: 1, "0": 1, slice: function () {} }); // false -如果你的开发环境不支持ECMAScript5,可以通过Object.prototype.toString()方法来代替。如调用toString的call()方法并传入数组上下文,将返回字符串“[object Array]”。如果传入对象上下文,则返回字符串“[object Object]”。因此可以这样做: +如果你的开发环境不支持ECMAScript5,可以通过`Object.prototype.toString()`方法来代替。如调用`toString`的`call()`方法并传入数组上下文,将返回字符串“[object Array]”。如果传入对象上下文,则返回字符串“[object Object]”。因此可以这样做: if (typeof Array.isArray === "undefined") { Array.isArray = function (arg) { @@ -380,56 +360,53 @@ ECMAScript 5定义了一个新的方法Array.isArray(),如果参数是数组 }; } - ## JSON -上文我们刚刚讨论过对象和数组直接量,你已经对此很熟悉了,现在我们将目光转向JSON。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。很多语言中都实现了JSON,特别是在JavaScript中。 +我们刚刚讨论了对象和数组字面量,你应该很熟悉了,现在我们来看一看JSON。JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,可以很容易地用在多种语言中,尤其是在JavaScript中。 -JSON格式及其简单,它只是数组和对象直接量的混合写法,看一个JSON字符串的例子: +JSON格式及其简单,它只是数组和对象字面量的混合写法,看一个JSON字符串的例子: {"name": "value", "some": [1, 2, 3]} -JSON和对象直接量在语法上的唯一区别是,合法的JSON属性名均用引号包含。而在对象直接量中,只有属性名是非法的标识符时采用引号包含,比如,属性名中包含空格`{"first name": "Dave"}`。 +JSON和对象字面量在语法上的唯一区别是,合法的JSON属性名均需要用引号包含。而在对象字面量中,只有属性名是非法的标识符时才使用引号包含,比如,属性名中包含空格`{"first name": "Dave"}`。 -在JSON字符串中,不能使用函数和正则表达式直接量。 +在JSON字符串中,不能使用函数和正则表达式字面量。 - ### 使用JSON -在前面的章节中讲到,出于安全考虑,不推荐使用eval()来“粗糙的”解析JSON字符串。最好使用JSON.parse()方法,ES5中已经包含了这个方法,而且在现代浏览器的JavaScript引擎中已经内置支持JSON了。对于老旧的JavaScript引擎来说,你可以使用JSON.org所提供的JS文件(http://www.json.org/json2.js)来获得JSON对象和方法。 +在前面的章节中讲到,出于安全考虑,不推荐使用`eval()`来粗暴地解析JSON字符串。最好是使用`JSON.parse()`方法,ES5中已经包含了这个方法,并且现代浏览器的JavaScript引擎中也已经内置支持JSON了。对于老旧的JavaScript引擎来说,你可以使用JSON.org所提供的JS文件()来获得JSON对象和方法。 - // an input JSON string + // 输入JSON字符串 var jstr = '{"mykey": "my value"}'; - // antipattern + // 反模式 var data = eval('(' + jstr + ')'); - // preferred + // 更好的方式 var data = JSON.parse(jstr); console.log(data.mykey); // "my value" -如果你已经在使用某个JavaScript库了,很可能库中提供了解析JSON的方法,就不必再额外引入JSON.org的库了,比如,如果你已经使用了YUI3,你可以这样: +如果你已经在使用某个JavaScript库了,很可能这个库中已经提供了解析JSON的方法,就不必再额外引入JSON.org的库了,比如,如果你已经使用了YUI3,你可以这样: - // an input JSON string + // 输入JSON字符串 var jstr = '{"mykey": "my value"}'; - // parse the string and turn it into an object - // using a YUI instance + // 使用YUI来解析并将结果返回为一个对象 YUI().use('json-parse', function (Y) { var data = Y.JSON.parse(jstr); console.log(data.mykey); // "my value" }); -如果你使用的是jQuery,可以直接使用它提供的parseJSON()方法: +如果你使用的是jQuery,可以直接使用它提供的`parseJSON()`方法: - // an input JSON string + // 输入JSON字符串 var jstr = '{"mykey": "my value"}'; var data = jQuery.parseJSON(jstr); console.log(data.mykey); // "my value" -和JSON.parse()方法相对应的是JSON.stringify()。它将对象或数组(或任何原始值)转换为JSON字符串。 +和`JSON.parse()`方法相对应的是`JSON.stringify()`。它将对象或数组(或任何原始值)转换为JSON字符串。 var dog = { name: "Fido", @@ -439,50 +416,48 @@ JSON和对象直接量在语法上的唯一区别是,合法的JSON属性名均 var jsonstr = JSON.stringify(dog); - // jsonstr is now: + // jsonstr的值为 // {"name":"Fido","dob":"2010-04-11T22:36:22.436Z","legs":[1,2,3,4]} - -## 正则表达式直接量 +## 正则表达式字面量 JavaScript中的正则表达式也是对象,可以通过两种方式创建它们: -- 使用new RegExp()构造函数 -- 使用正则表达式直接量 +- 使用`new RegExp()`构造函数 +- 使用正则表达式字面量 -下面的示例代码展示了创建正则表达式的两种方法,创建的正则用来匹配一个反斜杠(\): +下面的示例代码展示了创建用来匹配一个反斜杠(\)的正则表达式的两种方法: - // regular expression literal + // 正则表达式字面量 var re = /\\/gm; - // constructor + // 构造函数 var re = new RegExp("\\\\", "gm"); -显然正则表达式直接量写法的代码更短,且不必强制按照类构造器的思路来写。因此更推荐使用直接量写法。 +显然正则表达式字面量写法的代码更短,而且不会让你觉得在用像类一样的构造函数的思想在写正则表达式,因此更推荐使用字面量写法。 -另外,如果使用RegExp()构造函数写法,还需要考虑对引号和反斜杠进行转义,正如上段代码所示的那样,用了四个反斜杠来匹配一个反斜杠。这会增加正则表达式的长度,而且让正则变得难于理解和维护。刚开始学习正则表达式不是很容易,所以不要放弃任何一个简化它们的机会,所以要尽量使用直接量而不是通过构造函数来创建正则。 +另外,如果使用`RegExp()`构造函数写法,还需要考虑对引号和反斜杠进行转义,正如上段代码所示的那样,用了四个反斜杠来匹配一个反斜杠。这会增加正则表达式的长度,而且让它变得难于理解和维护。正则表达式入门不是件容易的事,所以不要放弃任何一个简化它们的机会,尽量使用字面量而不是通过构造函数来创建正则表达式。 - -### 正则表达式直接量语法 +### 正则表达式字面量语法 -正则表达式直接量使用两个斜线包裹起来,正则的主体部分不包括两端的斜线。在第二个斜线之后可以指定模式匹配的修饰符用以高级匹配,修饰符不需要引号引起来,JavaScript中有三个修饰符: +正则表达式字面量使用两个斜杠包裹,主体部分不包括两端的斜线。在第二个斜线之后可以指定模式匹配的修饰符,修饰符不需要用引号引起来,JavaScript中有三个修饰符: -- g,全局匹配 -- m,多行匹配 -- i,忽略大小写的匹配 +- `g`,全局匹配 +- `m`,多行匹配 +- `i`,忽略大小写的匹配 -修饰符可以自由组合,而且顺序无关: +修饰符可以自由组合,而且与顺序无关: var re = /pattern/gmi; -使用正则表达式直接量可以让代码更加简洁高效,比如当调用String.prototype.prelace()方法时,可以传入正则表达式参数: +使用正则表达式字面量可以让代码更加简洁高效,比如当调用`String.prototype.replace()`方法时,可以传入正则表达式参数: var no_letters = "abc123XYZ".replace(/[a-z]/gi, ""); console.log(no_letters); // 123 -有一种不得不使用new RegExp()的情形,有时正则表达式是不确定的,直到运行时才能确定下来。 +有一种不得不使用`new RegExp()`的场景,就是正则表达式是不确定,只有等到运行时才能确定下来的情况。 -正则表达式直接量和Regexp()构造函数的另一个区别是,正则表达式直接量只在解析时创建一次正则表达式对象(译注:多次解析同一个正则表达式,会产生相同的实例对象)。如果在循环体内反复创建相同的正则表达式,则每个正则对象的所有属性(比如lastIndex)只会设置一次(译注:由于每次创建相同的实例对象,每个循环中的实例对象都是同一个,属性也自然相同),下面这个例子展示了两次都返回了相同的正则表达式的情形(译注:这里作者的表述只是针对ES3规范而言,下面这段代码在NodeJS、IE6-IE9、FireFox4、Chrome10、Safari5中运行结果和作者描述的不一致,Firefox 3.6中的运行结果和作者描述是一致的,原因可以在ECMAScript5规范第24页和第247页找到,也就是说在ECMAScript3规范中,用正则表达式创建的RegExp对象会共享同一个实例,而在ECMAScript5中则是两个独立的实例。而最新的Firefox4、Chrome和Safari5都遵循ECMAScript5标准,至于IE6-IE8都没有很好的遵循ECMAScript3标准,不过在这个问题上反而处理对了。很明显ECMAScript5的规范更符合开发者的期望)。 +正则表达式字面量和构造函数还有另一个区别,就是字面量只在解析时创建一次正则表达式对象(译注:多次解析同一个正则表达式,会产生相同的实例对象)。如果在循环体内反复使用相同的字面量创建对象,则会返回第一次创建的对象以及它的属性(比如`lastIndex`)。下面这个例子展示了两次返回相同的正则表达式的情形。 function getRE() { var re = /[a-z]/; @@ -497,67 +472,65 @@ JavaScript中的正则表达式也是对象,可以通过两种方式创建它 reg.foo = "baz"; console.log(re2.foo); // "baz" ->在ECMAScript5中这种情形有所改变,相同正则表达式直接量的每次计算都会创建新的实例对象,目前很多现代浏览器也对此做了纠正(译注:比如在Firefox4就纠正了Firefox3.6的这种“错误”)。 +> 在ECMAScript5中这种情况有所改变,相同正则表达式字面量的每次计算都会创建新的实例对象,目前很多现代浏览器也对此做了纠正。 -最后需要提一点,不带new调用RegExp()(作为普通的函数)和带new调用RegExp()是完全一样的。 +最后需要提一点,不带`new`调用`RegExp()`(作为普通的函数)和带`new`调用`RegExp()`是完全一样的。 - ## 原始值的包装对象 -JavaScript中有五种原始类型:数字、字符串、布尔值、null和undefined。除了null和undefined之外,其他三种都有对应的“包装对象”(wrapper objects)。可以通过内置构造函数来生成包装对象,Number()、String()、和Boolean()。 +JavaScript中有五种原始类型:数字、字符串、布尔值、`null`和`undefined`。除了`null`和`undefined`之外,其他三种都有对应的“包装对象”(primitive wrapper object)。可以通过内置构造函数`Number()`、`String()`和`Boolean()`来生成包装对象。 为了说明数字原始值和数字对象之间的区别,看一下下面这个例子: - // a primitive number + // 一个数字原始值 var n = 100; console.log(typeof n); // "number" - // a Number object + // 一个Number对象 var nobj = new Number(100); console.log(typeof nobj); // "object" -包装对象带有一些有用的属性和方法,比如,数字对象就带有toFixed()和toExponential()之类的方法。字符串对象带有substring()、chatAt()和toLowerCase()等方法以及length属性。这些方法非常方便,和原始值相比,这让包装对象具备了一定优势。其实原始值也可以调用这些方法,因为原始值会首先转换为一个临时对象,如果转换成功,则调用包装对象的方法。 +包装对象带有一些有用的属性和方法。比如,数字对象就带有`toFixed()`和`toExponential()`之类的方法,字符串对象带有`substring()`、`chatAt()`和`toLowerCase()`等方法以及`length`属性。这些方法非常方便,和原始值相比,这是包装对象的优势,但其实原始值也可以调用这些方法,因为原始值会首先转换为一个临时对象,如果转换成功,则调用包装对象的方法。 - // a primitive string be used as an object + // 像使用对象一样使用一个字符串原始值 var s = "hello"; console.log(s.toUpperCase()); // "HELLO" - // the value itself can act as an object + // 值本身也可以像对象一样 "monkey".slice(3, 6); // "key" - // same for numbers + // 数字也是一样 (22 / 7).toPrecision(3); // "3.14" 因为原始值可以根据需要转换成对象,这样的话,也不必为了用包装对象的方法而将原始值手动“包装”成对象。比如,不必使用new String("hi"),直接使用"hi"即可。 - // avoid these: + // 避免这些: var s = new String("my string"); var n = new Number(101); var b = new Boolean(true); - // better and simpler: + // 更好更简洁的办法: var s = "my string"; var n = 101; var b = true; -不得不使用包装对象的一个原因是,有时我们需要对值进行扩充并保持值的状态。原始值毕竟不是对象,不能直接对其进行扩充(译注:比如`1.property = 2`会报错)。 +不得不使用包装对象的一个场景是,有时我们需要对值进行扩充并保持值的状态。原始值毕竟不是对象,不能直接对其进行扩充。 - // primitive string + // 字符串原始值 var greet = "Hello there"; - // primitive is converted to an object - // in order to use the split() method + // 为使用split方法,原始值被转换为对象 greet.split(' ')[0]; // "Hello" - // attemting to augment a primitive is not an error + // 给原始值添加属性并不会报错 greet.smile = true; - // but it doesn't actually work + // 但实际上却没有作用 typeof greet.smile; // "undefined" -在这段示例代码中,greet只是临时转换成了对象,以保证访问其属性/方法时不会出错。另一方面,如果greet通过new String()定义为一个对象,那么扩充smile属性就会按照期望的那样执行。对字符串、数字或布尔值的扩充并不常见,除非你清楚自己想要什么,否则不必使用包装对象。 +在这段示例代码中,`greet`只是临时被转换成了对象,以保证访问其属性、方法时不会出错。而如果是另一种情况,`greet`通过`new String()`被定义为一个对象,那么扩充`smile`属性的过程就会像我们预期的那样。对字符串、数字或布尔值进行扩充的情况很少见,因此建议只在确实有必要的情况下使用包装对象。 -当省略new时,包装器将传给它的参数转换为原始值: +当省略`new`时,包装对象的构造函数将传给它的参数转换为原始值: typeof Number(1); // "number" typeof Number("1"); // "number" @@ -565,60 +538,58 @@ JavaScript中有五种原始类型:数字、字符串、布尔值、null和und typeof String(1); // "string" typeof Boolean(1); // "boolean" - -## Error 对象 +## 错误处理对象 -JavaScript中有很多内置的Error构造函数,比如Error()、SyntaxError(),TypeError()等等,这些“错误”通常和throw语句一起使用。这些构造函数创建的错误对象包含这些属性: +JavaScript中有很多内置的错误处理构造函数,比如`Error()`、`SyntaxError()`,`TypeError()`等等,它们通常和`throw`语句一起被使用。这些构造函数创建的错误对象包含这些属性: -**name** +- `name` -name属性是指创建这个对象的构造函数的名字,通常是“Errora”,有时会有特定的名字比如“RangeError” + name属性是指产生这个对象的构造函数的名字,通常是“Error”,有时会有特定的名字比如“RangeError” -**message** +- `message` -创建这个对象时传入构造函数的字符串 + 创建这个对象时传入构造函数的字符串 -错误对象还有其他一些属性,比如产生错误的行号和文件名,但这些属性是浏览器自行实现的,不同浏览器的实现也不一致,因此出于兼容性考虑,并不推荐使用这些属性。 +错误对象还有一些其他的属性,比如产生错误的行号和文件名,但这些属性是浏览器自行实现的,不同浏览器的实现也不一致,因此出于兼容性考虑,并不推荐使用这些属性。 -另一方面,throw可以抛出任何对象,并不限于“错误对象”,因此你可以根据需要抛出自定义的对象。这些对象包含属性“name”和“message”或其他你希望传递给异常处理逻辑的信息,异常处理逻辑由catch语句指定。你可以灵活运用抛出的错误对象,将程序从错误状态恢复至正常状态。 +`throw`可以抛出任何对象,并不限于“错误对象”,因此你可以根据需要抛出自定义的对象。这些对象包含属性“name”和“message”或其他你希望传递给异常处理逻辑的信息,异常处理逻辑由`catch`语句指定。你可以灵活运用抛出的错误对象,将程序从错误状态恢复至正常状态。 try { - // something bad happened, throw an error + // 一些不好的事情发生了,抛出错误 throw { - name: "MyErrorType", // custom error type + name: "MyErrorType", // 自定义错误类型 message: "oops", extra: "This was rather embarrassing", - remedy: genericErrorHandler // who should handle it + remedy: genericErrorHandler // 应该由谁处理 }; } catch (e) { - // inform the user + // 通知用户 alert(e.message); // "oops" - // gracefully handle the error - e.remedy(); // calls genericErrorHandler() + // 优雅地处理错误 + e.remedy(); // 调用genericErrorHandler() } -通过new调用和省略new调用错误构造函数是一模一样的,他们都返回相同的错误对象。 +使用`new`调用和省略`new`调用错误构造函数是一模一样的,他们都返回相同的错误对象。 - ## 小结 -在本章里,我们讨论了多种直接量模式,它们是使用构造函数写法的替代方案,本章讲述了这些内容: +在本章里,我们讨论了多种字面量模式,它们是使用构造函数写法的替代方案,本章讲述了这些内容: -- 对象直接量写法——一种简洁优雅的定义对象的方法,名值对之间用逗号分隔,通过花括号包装起来 -- 构造函数——内置构造函数(内置构造函数通常都有对应的直接量语法)和自定义构造函数。 -- 一种强制函数以构造函数的模式执行(不管用不用new调用构造函数,都始终返回new出来的实例)的技巧 -- 数组直接量写法——数组元素之间使用逗号分隔,通过方括号括起来 -- JSON——是一种轻量级的数据交换格式 -- 正则表达式直接量 -- 避免使用其他的内置构造函数:String()、Number()、Boolean()以及不同种类的Error()构造器 +- 对象字面量写法——一种简洁优雅的定义对象的方法,通过花括号包裹,名值对之间用逗号分隔 +- 构造函数——内置构造函数(内置构造函数通常都有对应的字面量语法)和自定义构造函数 +- 一种强制函数以构造函数的模式运行行(不管用不用`new`调用构造函数,都始终返回`new`出来的实例)的技巧 +- 数组字面量写法——通过方括号包裹,数组元素之间使用逗号分隔 +- JSON——一种轻量级的数据交换格式 +- 正则表达式字面量 +- 避免使用其他的内置构造函数:`String()`、`Number()`、`Boolean()`以及不同种类的`Error()`构造函数 -通常除了Date()构造函数之外,其他的内置构造函数并不常用,下面的表格中对这些构造函数以及它们的直接量语法做了整理。 +通常情况下,除了`Date()`之外,其他的内置构造函数并不常用,下面的表格对这些构造函数以及它们的字面量语法做了整理。 - + diff --git a/chapter4.markdown b/chapter4.markdown index 50c2a81..5300699 100644 --- a/chapter4.markdown +++ b/chapter4.markdown @@ -1,10 +1,10 @@ # 函数 -熟练运用函数是JavaScript程序员的必备技能,因为在JavaScript中函数实在是太常用了。它能够完成的任务种类非常之多,而在其他语言中则需要很多特殊的语法支持才能达到这种能力。 +熟练运用函数是JavaScript程序员的必备技能,因为在JavaScript中函数实在是太常用了。它能够完成各种类型的任务,而在其他语言中则需要很多特殊的语法支持才能拥有这种能力。 -在本章将会介绍在JavaScript中定义函数的多种方式,包括函数表达式和函数声明、以及局部作用域和变量声明提前的工作原理。然后会介绍一些有用的模式,帮助你设计API(为你的函数提供更好的接口)、搭建代码架构(使用尽可能少的全局对象)、并优化性能(避免不必要的操作)。 +本章将会介绍在JavaScript中定义函数的几种方式,包括函数表达式和函数声明以及局部作用域和变量声明提前的工作原理。然后会介绍一些有用的模式,帮助你设计API(为你的函数提供更好的接口)、搭建代码架构(使用尽可能少的全局对象)、并优化性能(避免不必要的操作)。 -现在让我们来一起揭秘JavaScript函数,我们首先从一些背景知识开始说起。 +现在让我们来一起揭秘JavaScript函数,首先从一些背景知识开始说起。 ## 背景知识 @@ -13,125 +13,119 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 函数是对象,那么: - 可以在程序执行时动态创建函数 -- 可以将函数赋值给变量,可以将函数的引用拷贝至另一个变量,可以扩充函数,除了某些特殊场景外均可被删除。 -- 可以将函数作为参数传入另一个函数,也可以被当作返回值返回。 +- 可以将函数赋值给变量,可以将函数的引用拷贝至另一个变量,可以扩充函数,除了某些特殊场景外均可被删除 +- 可以将函数作为参数传入另一个函数,也可以被当作返回值返回 - 函数可以包含自己的属性和方法 -对于一个函数A来说,首先它是对象,拥有属性和方法,其中某个属性碰巧是另一个函数B,B可以接受函数作为参数,假设这个函数参数为C,当执行B的时候,返回另一个函数D。乍一看这里有一大堆相互关联的函数。当你开始习惯函数的许多用法时,你会惊叹原来函数是如此强大、灵活并富有表现力。通常说来,一说到JavaScript的函数,我们首先认为它是对象,它具有一个可以“执行”的特性,也就是说我们可以“调用”这个函数。 +有可能会有这样的情况:一个函数A,它也是一个对象,拥有属性和方法,其中某个属性是另一个函数B,B可以接受函数作为参数,假设这个函数参数为C,当执行B的时候,返回另一个函数D。乍一看这里有一大堆相互关联的函数,但当你开始习惯函数的许多用法时,你会惊叹原来函数是如此灵活、强大且富有表现力。通常说来,一说到JavaScript的函数,我们首先认为它是一个对象,具有一个可以“执行”的特性,也就是说我们可以“调用”这个函数。 -我们通过new Function()构造器来生成一个函数,这时可以明显看出函数是对象: +我们通过`new Function()`构造函数来创建一个函数,这时可以明显看出函数是对象: - // antipattern - // for demo purposes only + // 反模式,仅用于演示 var add = new Function('a, b', 'return a + b'); - add(1, 2); // returns 3 + add(1, 2); // 返回 3 -在这段代码中,毫无疑问add()是一个对象,毕竟它是由构造函数创建的。这里并不推荐使用Function()构造器创建函数(和eval()一样糟糕),因为程序逻辑代码是以字符串的形式传入构造器的。这样的代码可读性差,写起来也很费劲,你不得不对逻辑代码中的引号做转义处理,并需要特别关注为了让代码保持一定的可读性而保留的空格和缩进。 +在这段代码中,毫无疑问`add()`是一个对象,因为它是由构造函数创建的。这里并不推荐使用`Function()`构造函数来创建函数(和`eval()`一样糟糕),因为程序逻辑代码是以字符串的形式传入构造器的。这样的代码可读性差,写起来也很费劲,你还要对代码中的引号做转义处理,并需要特别关注为了保持可读性而保留的空格和缩进。 -函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里作者的表述只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过var声明的变量都是局部变量,在函数外部是不可见的。刚才所指花括号无法提供作用域支持的意思是说,如果在if条件句内、或在for或while循环体内用var定义了变量,这个变量并不是属于if语句或for(while)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量的作用域的不二之选。 +函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过`let`来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过`var`声明的变量都是局部变量,在函数外部是不可见的。刚才所说的花括号无法提供作用域支持的意思是说,如果在`if`条件句、`for`或`while`循环体内用`var`定义了变量,这个变量并不是属于`if`语句或`for`(`while`)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量作用域的最佳选择。 -### 术语释义 +### 术语 -首先我们先简单讨论下创建函数相关的术语,因为精确无歧义的术语约定和我们所讨论的各种模式一样重要。 +首先我们先简单讨论下与创建函数相关的术语,因为精确无歧义的术语约定非常重要。 看下这个代码片段: - // named function expression + // 具名函数表达式 var add = function add(a, b) { return a + b; }; -这段代码描述了一个函数,这种描述称为“带有命名的函数表达式”。 +这段代码描述了一个函数,这种描述称为“具名函数表达式”。 -如果函数表达式将名字省略掉(比如下面的示例代码),这时它是“无名字的函数表达式”,通常我们称之为“匿名函数”,比如: +如果省略掉函数表达式中的名字(比如下面的示例代码),这时它是“匿名函数表达式”,通常我们称之为“匿名函数”,比如: - // function expression, a.k.a. anonymous function + // 匿名函数表达式,又称匿名函数 var add = function (a, b) { return a + b; }; -因此“函数表达式”是一个更广义的概念,“带有命名的函数表达式”是函数表达式的一种特殊形式,仅仅当需要给函数定义一个可选的名字时使用。 +因此“函数表达式”是一个更广义的概念,“具名函数表达式”是函数表达式的一种特殊形式,仅仅当需要给函数定义一个可选的名字时使用。 -当省略第二个add,它就成了无名字的函数表达式,这不会对函数定义和调用语法造成任何影响。带名字和不带名字唯一的区别是函数对象的name属性是否是一个空字符串。name属性属于语言的扩展(未在ECMA标准中定义),但很多环境都实现了。如果不省略第二个add,那么属性add.name则是"add",name属性在用Firebug的调试过程中非常有用,还能让函数递归调用自身,其他情况可以省略它。 +当省略第二个`add`,它就成了没有名字的函数表达式,这不会对函数定义和调用语法造成任何影响。带名字和不带名字唯一的区别是函数对象的`name`属性是否为空字符串。`name`属性属于语言的扩展(未在ECMA标准中定义),但很多环境都实现了。如果不省略第二个`add`,那么`add.name`是"add",`name`属性在用像Firebug之类的调试工具进行调试的过程中非常有用,它也可以让函数递归调用自身,如果是其他情况,则可以省略它。 最后来看一下“函数声明”,函数声明的语法和其他语言中的语法非常类似: function foo() { - // function body goes here + // 函数体 } -从语法角度讲,带有命名的函数表达式和函数声明非常像,特别是当不需要将函数表达式赋值给一个变量的时候(在本章后面所讲到的回调模式中有类似的例子)。多数情况下,函数声明和带命名的函数表达式在外观上没有多少不同,只是它们在函数执行时对上下文的影响有所区别,下一小节会讲到。 +从语法上来看,具名函数表达式和函数声明非常像,特别是当不需要将函数表达式赋值给一个变量的时候(在本章后面所讲到的回调模式中有类似的例子)。多数情况下,函数声明和具名函数表达式在外观上没有多少不同,只是它们在函数执行时对上下文的影响有所区别,下一小节会讲到。 两种语法的一个区别是末尾的分号。函数声明末尾不需要分号,而函数表达式末尾是需要分号的。推荐你始终不要丢掉函数表达式末尾的分号,即便JavaScript可以进行分号补全,也不要冒险这样做。 ->另外我们经常看到“函数直接量”。它用来表示函数表达式或带命名的函数表达式。由于这个术语是有歧义的,所以最好不要用它。 - +> 另外我们经常看到“函数字面量”。它用来表示函数表达式或具名函数表达式。由于这个术语是有歧义的,所以最好不要用它。 ### 声明 vs 表达式:命名与提前 -那么,到底应该用哪个呢?函数声明还是函数表达式?在不能使用函数声明语法的场景下,只能使用函数表达式了。下面这个例子中,我们给函数传入了另一个函数对象作为参数,以及给对象定义方法: +那么,到底应该用哪个呢?函数声明还是函数表达式?在不能使用函数声明语法的场景下,就只能使用函数表达式了,将函数作为参数传递、在对象字面量中定义方法都是这样的例子: - // this is a function expression, - // pased as an argument to the function `callMe` + // 作为参数传递给callMe的函数表达式 callMe(function () { - // I am an unnamed function expression - // also known as an anonymous function + // 我是匿名函数表达式,也叫匿名函数 }); - // this is a named function expression + // 这是一个具名函数表达式 callMe(function me() { - // I am a named function expression - // and my name is "me" + // 我是具名函数表达式,我的名字是“me” }); - // another function expression + // 另一个函数表达式 var myobject = { say: function () { - // I am a function expression + // 我是函数表达式 } }; -函数声明只能出现在“程序代码”中,也就是说在别的函数体内或在全局。这个定义不能赋值给变量或属性,同样不能作为函数调用的参数。下面这个例子是函数声明的合法用法,这里所有的函数foo(),bar()和local()都使用函数声明来定义: +函数声明只能出现在“程序代码”中,也就是说在别的函数体内或在全局。这个定义不能赋值给变量或属性,同样不能作为函数调用的参数。(译注:注意这里说的是函数声明的语句,而不是通过声明语句定义出来的函数本身。任何函数都是可以被赋值给变量和属性的,也可以被作为参数传递。)下面这个例子是函数声明的合法用法,这里所有的函数`foo()`,`bar()`和`local()`都使用函数声明来定义: - // global scope + // 全局作用域 function foo() {} function local() { - // local scope + // 本地作用域 function bar() {} return bar; } ### 函数的name属性 -选择函数定义模式的另一个考虑是只读属性name的可用性。尽管标准规范中并未规定,但很多运行环境都实现了name属性,在函数声明和带有名字的函数表达式中是有name的属性定义的。在匿名函数表达式中,则不一定有定义,这个是和实现相关的,在IE中是无定义的,在Firefox和Safari中是有定义的,但是值为空字符串。 +选择用哪种模式定义函数时的另一个考虑是只读属性`name`的可用性。尽管标准规范中并未定义,但很多运行环境都实现了`name`属性,在函数声明和具名函数表达式中是有`name`属性的。在匿名函数表达式中,则不一定有定义,这个是和实现相关的,在IE中是无定义的,在Firefox和Safari中是有定义的,但是值为空字符串。 - function foo() {} // declaration - var bar = function () {}; // expression - var baz = function baz() {}; // named expression + function foo() {} // 函数声明 + var bar = function () {}; // 匿名函数表达式 + var baz = function baz() {}; // 具名函数表达式 foo.name; // "foo" bar.name; // "" baz.name; // "baz" -在Firebug或其他工具中调试程序时name属性非常有用,它可以用来显示当前正在执行的函数。同样可以通过name属性来递归的调用函数自身。如果你对这些场景不感兴趣,那么请尽可能的使用匿名函数表达式,这样会更简单、且冗余代码更少。 +在Firebug或其他工具中调试程序时`name`属性非常有用,它可以用来显示当前正在执行的函数。同样可以通过`name`属性来递归地调用函数自身。如果你对这些场景不感兴趣,那么请尽可能地使用匿名函数表达式,这样会更简单、且冗余代码更少。 -和函数声明相比而言,函数表达式的语法更能说明函数是一种对象,而不是某种特别的语言写法。 +相对函数声明而言,函数表达式的语法更能说明函数是一种和其它对象类似的对象,而不是语言中某种特别的组成部分。 ->我们可以将一个带名字的函数表达式赋值给变量,变量名和函数名不同,这在技术上是可行的。比如:`var foo = function bar(){};`。然而,这种用法的行为在浏览器中的兼容性不佳(特别是IE中),因此并不推荐大家使用这种模式。 +> 我们可以将一个带名字的函数表达式赋值给变量,变量名和函数名不同,这在技术上是可行的。比如:`var foo = function bar(){};`。然而,这种用法的行为在浏览器中的兼容性不好(特别是IE中),因此并不推荐大家使用这种模式。 -### 函数提前 +### 声明提前 -通过前面的讲解,你可能以为函数声明和带名字的函数表达式是完全等价的。事实上不是这样,主要区别在于“声明提前”的行为。 +通过前面的讲解,你可能以为函数声明和具名函数表达式是完全等价的。事实上并不是这样,主要区别在于“声明提前”的行为。 ->术语“提前”并未在ECMAScript中定义,但是并没有其他更好的方法来描述这种行为了。 +> 术语“提前”并未在ECMAScript中定义,但是它是一种很好地描述这种行为的方法。 我们知道,不管在函数内何处声明变量,变量都会自动提前至函数体的顶部。对于函数来说亦是如此,因为他们也是一种对象,赋值给了变量。需要注意的是,函数声明定义的函数不仅能让声明提前,还能让定义提前,看一下这段示例代码: - // antipattern - // for illustration only + // 反模式,仅用于演示 - // global functions + // 全局函数 function foo() { alert('global foo'); } @@ -147,64 +141,63 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 foo(); // "local foo" bar(); // TypeError: bar is not a function - // function declaration: - // variable 'foo' and its implementation both get hoisted + // 函数声明: + // 变量foo和它的定义实现都被提前了 function foo() { alert('local foo'); } - // function expression: - // only variable 'bar' gets hoisted - // not the implementation + // 函数表达式: + // 只有变量bar被提前,它的定义实现没有被提前 var bar = function () { alert('local bar'); }; } hoistMe(); -在这段代码中,和普通的变量一样,hoistMe()函数中的foo和bar被“搬运”到了顶部,覆盖了全局的foo和bar。不同之处在于,局部的foo()定义提前至顶部并能正常工作,尽管定义它的位置并不靠前。bar()的定义并未提前,只是声明提前了。因此当程序执行到bar()定义的位置之前,它的值都是undefined,并不是函数(防止当前上下文查找到作用域链上的全局的bar(),也就“覆盖”了全局的bar())。 +在这段代码中,和普通的变量一样,`hoistMe()`函数中的`foo`和`bar`被“搬运”到了顶部,覆盖了全局的`foo()`和`bar()`。不同之处在于,本地的`foo()`的位置并不在前面,但它的定义却被提前到了顶部并能正常工作,而`bar()`的定义并未提前,只有声明提前了。因此当程序执行到`bar()`定义的位置之前,它的值都不是函数,而是`undefined`(在此期间全局的`bar()`都是被本地覆盖的)。 -到目前为止我们介绍了必要的背景知识和函数定义相关的术语,下面开始介绍一些JavaScript所提供的函数相关的好的模式,我们从回调模式开始。同样,再次强调JavaScript函数的两个特殊特性,掌握这两点至关重要: +到目前为止我们介绍了必要的背景知识和函数定义相关的术语,下面开始介绍一些JavaScript所提供的函数相关的模式,我们从回调模式开始。再次强调JavaScript函数的两个特性,掌握这两点至关重要: - 函数是对象 -- 函数提供局部变量作用域 +- 函数提供本地变量作用域 ## 回调模式 -函数是对象,也就意味着函数可以当作参数传入另外一个函数中。当你给函数writeCode()传入一个函数参数introduceBugs(),在某个时刻writeCode()执行了(或调用了)introduceBugs()。在这种情况下,我们说introduceBugs()是一个“回调函数”,简称“回调”: +函数是对象,也就意味着函数可以当作参数传入另外一个函数中。给函数`writeCode()`传入一个函数参数`introduceBugs()`,在某个时刻`writeCode()`执行了(或调用了)`introduceBugs()`,在这种情况下,我们称`introduceBugs()`是一个“回调函数”,简称“回调”: function writeCode(callback) { - // do something... + // 做点什么…… callback(); - // ... + // …… } function introduceBugs() { - // ... make bugs + // …… } writeCode(introduceBugs); -注意introduceBugs()是如何作为参数传入writeCode()的,当作参数的函数不带括号。括号的意思是执行函数,而这里我们希望传入一个引用,让writeCode()在合适的时机执行它(调用它)。 +注意`introduceBugs()`作为参数传入`writeCode()`时,函数后面是不带括号的。括号的意思是执行函数,而这里我们希望传入一个引用,让`writeCode()`在合适的时机执行它(调用它)。 -### 一个回调的例子 +### 回调的例子 -我们从一个例子开始,首先介绍无回调的情况,然后在作修改。假设你有一个通用的函数,用来完成某种复杂的逻辑并返回一大段数据。假设我们用findNodes()来命名这个通用函数,这个函数用来对DOM树进行遍历,并返回我所感兴趣的页面节点: +我们从一个例子开始,首先介绍无回调的情况,然后再进行修改。假设你有一个通用的函数,用来完成某种复杂的逻辑并返回一大段数据。假设这个通用函数叫`findNodes()`,用来对DOM树进行遍历,并返回页面节点: var findNodes = function () { - var i = 100000, // big, heavy loop - nodes = [], // stores the result - found; // the next node found + var i = 100000, // 大量耗时的循环 + nodes = [], // 存储结果 + found; // 标示下找到的节点 while (i) { i -= 1; - // complex logic here... + // 这里是复杂的逻辑…… nodes.push(found); } return nodes; }; -保持这个函数的功能的通用性并一贯返回DOM节点组成的数组,并不会发生对节点的实际操作,这是一个不错的注意。可以将操作节点的逻辑放入另外一个函数中,比如放入一个hide()函数中,这个函数用来隐藏页面中的节点元素: +保持这个函数的功能的通用性,让它只返回DOM节点组成的数组,而不对节点进行操作是一个很好的思想。可以将操作节点的逻辑放入另外一个函数中,比如`hide()`函数,这个函数用来隐藏页面中的节点元素: var hide = function (nodes) { var i = 0, max = nodes.length; @@ -213,27 +206,27 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 } }; - // executing the functions + // 执行函数 hide(findNodes()); -这个实现的效率并不高,因为它将findNodes()所返回的节点数组重新遍历了一遍。最好在findNodes()中选择元素的时候就直接应用hide()操作,这样就能避免第二次的遍历,从而提高效率。但如果将hide()的逻辑写死在findNodes()的函数体内,findNodes()就变得不再通用了(译注:如果我将hide()的逻辑替换成其他逻辑怎么办呢?),因为修改逻辑和遍历逻辑耦合在一起了。如果使用回调模式,则可以将隐藏节点的逻辑写入回调函数,将其传入findNodes()中适时执行: +这个实现的效率并不高,因为它将`findNodes()`所返回的节点数组重新遍历了一遍。更高效的办法是在`findNodes()`中选择元素的时候就直接应用`hide()`操作,这样就能避免第二次的遍历,从而提高效率。但如果将`hide()`的逻辑写死在`findNodes()`的函数体内,`findNodes()`就变得不再通用了,因为修改逻辑和遍历逻辑耦合在一起了。这时候如果使用回调模式,就可以将隐藏节点的逻辑写入回调函数,将其传入`findNodes()`中适时执行: - // refactored findNodes() to accept a callback + // 重构后的findNodes()接受一个回调函数 var findNodes = function (callback) { var i = 100000, nodes = [], found; - // check if callback is callable + // 检查回调函数是否可以执行 if (typeof callback !== "function") { callback = false; } while (i) { i -= 1; - // complex logic here... + // 这里是复杂的逻辑…… - // now callback: + // 回调: if (callback) { callback(found); } @@ -243,21 +236,21 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 return nodes; }; -这里的实现比较直接,findNodes()多作了一个额外工作,就是检查回调函数是否存在,如果存在的话就执行它。回调函数是可选的,因此修改后的findNodes()也是和之前一样使用,是可以兼容旧代码和旧API的。 +这里的实现比较直接,`findNodes()`多作了一个额外工作,就是检查回调函数是否存在,如果存在的话就执行它。回调函数是可选的,因此修改后的`findNodes()`仍然可以和之前一样使用,是可以兼容旧代码和旧API的。 -这时hide()的实现就非常简单了,因为它不用对元素列表做任何遍历了: +这时`hide()`的实现就非常简单了,因为它不用对元素列表做任何遍历了: - // a callback function + // 回调函数 var hide = function (node) { node.style.display = "none"; }; - // find the nodes and hide them as you go + // 找到节点并隐藏它们 findNodes(hide); -正如代码中所示,回调函数可以是事先定义好的,也可以是一个匿名函数,你也可以将其称作main函数,比如这段代码,我们利用同样的通用函数findNodes()来完成显示元素的操作: +回调函数可以是事先定义好的,像上面的代码一样,也可以是一个在调用函数时创建的匿名函数,比如这段代码,我们利用同样的通用函数`findNodes()`来完成显示元素的操作: - // passing an anonymous callback + // 传入匿名回调函数 findNodes(function (node) { node.style.display = "block"; }); @@ -268,9 +261,9 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 callback(parameters); -尽管这种写法可以适用大多数的情况,而且足够简单,但还有一些场景,回调函数不是匿名函数或者全局函数,而是对象的方法。如果回调函数中使用this指向它所属的对象,则回调逻辑往往并不像我们希望的那样执行。 +尽管这种写法很简单,而且可以适用于大多数的情况,但还有一些场景,回调函数不是匿名函数或者全局函数,而是对象的方法,如果这种情况下回调函数中使用了`this`指向它所属的对象,则回调逻辑就可能不是我们期望的那样。 -假设回调函数是paint(),它是myapp的一个方法: +假设回调函数是`paint()`,它是`myapp`的一个方法: var myapp = {}; myapp.color = "green"; @@ -288,13 +281,13 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 // ... }; -当你调用findNodes(myapp.paint),运行结果和我们期望的不一致,因为this.color未定义。因为findNodes()是全局函数,this指向的是全局对象。如果findNodes()是dom对象的方法(类似dom.findNodes()),那么回调函数内的this则指向dom,而不是myapp。 +当你调用`findNodes(myapp.paint)`时,运行结果和我们期望的不一致,因为`this.color`未定义。这时候`this`指向的是全局对象,因为`findNodes()`是全局函数。如果`findNodes()`是dom对象的方法(类似`dom.findNodes()`),那么回调函数内的`this`指向该dom,而不是`myapp`。 解决办法是,除了传入回调函数,还需将回调函数所属的对象当作参数传进去: findNodes(myapp.paint, myapp); -同样需要修改findNodes()的逻辑,增加对传入的对象的绑定: +同样需要修改`findNodes()`的逻辑,增加对传入的对象的绑定: var findNodes = function (callback, callback_obj) { //... @@ -304,9 +297,9 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 // ... }; -在后续的章节会对call()和apply()有更详细的讲述。 +在后续的章节会对`call()`和`apply()`有更详细的讲述。 -其实还有一种替代写法,就是将函数当作字符串传入findNodes(),这样就不必再写一次对象了,换句话说: +其实还有一种替代写法,就是将函数名称以字符串传入`findNodes()`,这样就不必再写一次对象了,也就是说: findNodes(myapp.paint, myapp); @@ -314,7 +307,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 findNodes("paint", myapp); -在findNodes()中的逻辑则需要修改为: +在`findNodes()`中的逻辑则需要修改为: var findNodes = function (callback, callback_obj) { @@ -331,34 +324,35 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 ### 异步事件监听 -JavaScript中的回调模式已经是我们的家常便饭了,比如,如果你给网页中的元素绑定事件,则需要提供回调函数的引用,以便事件发生时能调用到它。这里有一个简单的例子,我们将console.log()作为回调函数绑定了document的点击事件: +JavaScript中的回调模式已经是我们的家常便饭了,比如,如果你给网页中的元素绑定事件,则需要提供回调函数的引用,以便事件发生时能调用到它。这里有一个简单的例子,我们将`console.log()`作为回调函数绑定到了`document`的点击事件上: document.addEventListener("click", console.log, false); -客户端浏览器中的大多数编程都是事件驱动的,当网页下载完成,则触发load事件,当用户和页面产生交互时也会触发多种事件,比如click、keypress、mouseover、mousemove等等。正是由于回调模式的灵活性,JavaScript天生适于事件驱动编程。回调模式能够让程序“异步”执行,换句话说,就是让程序不按顺序执行。 +客户端浏览器中的大多数编程都是事件驱动的,当网页下载完成,则触发`load`事件,当用户和页面产生交互时也会触发多种事件,比如`click`、`keypress`、`mouseover`、`mousemove`等等。JavaScript天生适合事件驱动编程,因为回调模式能够让程序“异步”执行,换句话说,就是让程序不按顺序执行。 -“不要打电话给我,我会打给你”,这是好莱坞很有名的一句话,很多电影都有这句台词。电影中的主角不可能同时应答很多个电话呼叫。在JavaScript的异步事件模型中也是同样的道理。电影中是留下电话号码,JavaScript中是提供一个回调函数,当时机成熟时就触发回调。有时甚至提供了很多回调,有些回调压根是没用的,但由于这个事件可能永远不会发生,因此这些回调的逻辑也不会执行。比如,假设你从此不再用“鼠标点击”,那么你之前绑定的鼠标点击的回调函数则永远也不会执行。 +“不要打电话给我,我会打给你”,这是好莱坞很有名的一句台词,可能很多人会对同一个角色说这句话,而电影中的主角不可能同时应答这些人的电话呼叫。在JavaScript的异步事件模型中也是同样的道理,不同的是,电影中是留下电话号码,JavaScript中是提供一个在适当的时机被调用的回调函数。有时甚至可以提供比实际需要更多的回调函数,因为可能某个特定的事件永远不会发生。比如,假设用户一直不点击“购买”,那么你之前写的用来验证信用卡号格式的函数就永远不会被调用执行。(译注:这段话有点不好翻译,前面的比喻看不懂。后面有两个方面的意思,一方面指回调函数并不一定会被执行,如果事件不发生,那么回调函数就永远不会被执行;另一方面指可以通过多个事件来绑定同一个回调函数,因为你无法确定用户会触发哪一个事件,比如到底是键盘操作还是鼠标操作。) -### 超时 +### 延时 -另外一个最常用的回调模式是在调用超时函数时,超时函数是浏览器window对象的方法,共有两个:setTimeout()和setInterval()。这两个方法的参数都是回调函数。 +另外一个最常用的回调模式是在调用延时函数的时候。延时函数是浏览器`window`对象的方法,共有两个:`setTimeout()`和`setInterval()`。这两个方法的参数都是回调函数。 var thePlotThickens = function () { console.log('500ms later...'); }; setTimeout(thePlotThickens, 500); -再次需要注意,函数thePlotThickens是作为变量传入setTimeout的,它不带括号,如果带括号的话则立即执行了,这里只是用到这个函数的引用,以便在setTimeout的逻辑中调用到它。也可以传入字符串“thePlotThickens()”,但这是一种反模式,和eval()一样不推荐使用。 +再次提醒,函数名`thePlotThickens`是作为变量传入`setTimeout`的,它不带括号,如果带括号的话就被立即执行了,而这里只是用到这个函数的引用,以便在`setTimeout()`的逻辑中调用它。也可以传入字符串`"thePlotThickens()"`,但这是一种反模式,和`eval()`一样不推荐使用。 -### 库中的回调 +### 类库中的回调 + +回调模式非常简单,但又很强大,可以信手拈来灵活运用,因此这种模式在类库的设计中也非常得宠。类库的代码要尽可能保持通用和可复用,而回调模式则可帮助库的作者达成这个目标。你不必预料并实现你所想到的所有情形,这会让类库变得臃肿,而且大多数用户并不需要这些多余的特性支持。相反,你将精力放在核心功能的实现上,提供回调的入口作为“钩子”,可以让类库的方法变得可扩展、可定制。 -回调模式非常简单,但又很强大。可以随手拈来灵活运用,因此这种模式在库的设计中也非常得宠。库的代码要尽可能的保持通用和重用,而回调模式则可帮助库的作者完成这个目标。你不必预料和实现你所想到的所有情形,因为这会让库变的膨胀而臃肿,而且大多数用户并不需要这些多余的特性支持。相反,你将精力放在核心功能的实现上,提供回调的入口作为“钩子”,可以让库的方法变得可扩展、可定制。 ## 返回函数 -函数是对象,因此当然可以作为返回值。也就是说,函数不一定非要返回一坨数据,函数可以返回另外一个定制好的函数,或者可以根据输入的不同按需创造另外一个函数。 +函数是对象,因此可以作为返回值。也就是说,函数不一定非要返回一坨数据,函数也可以返回另外一个函数,或者可以根据输入的不同按需创造另外一个函数。 -这里有一个简单的例子:一个函数完成了某种功能,可能是一次性初始化,然后都基于这个返回值进行操作,这个返回值恰巧是另一个函数: +这里有一个简单的例子:一个函数完成了某种功能,可能是一次性初始化,然后做了一些对返回值的操作,而这个返回值恰巧是另一个函数: var setup = function () { alert(1); @@ -367,11 +361,11 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 }; }; - // using the setup function + // 使用setup()函数 var my = setup(); // alerts 1 my(); // alerts 2 -因为setup()把返回的函数作了包装,它创建了一个闭包,我们可以用这个闭包来存储一些私有数据,这些私有数据可以通过返回的函数进行操作,但在函数外部不能直接读取到这些私有数据。比如这个例子中提供了一个计数器,每次调用这个函数计数器都会加一: +因为`setup()`包裹了返回的函数,因此它创建了一个闭包,我们可以用这个闭包来存储一些私有数据,这些私有数据可以通过返回的函数进行操作,但在函数外部不能直接读取到这些私有数据。比如这个例子中提供了一个计数器,每次调用这个函数时,计数器都会加一: var setup = function () { var count = 0; @@ -380,15 +374,16 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 }; }; - // usage + // 使用 var next = setup(); - next(); // returns 1 + next(); // 返回 1 next(); // 2 next(); // 3 -## 自定义函数 -我们动态定义函数,并将函数赋值给变量。如果将你定义的函数赋值给已经存在的函数变量的话,则新函数会覆盖旧函数。这样做的结果是,旧函数的引用就丢弃掉了,变量中所存储的引用值替换成了新的。这样看起来这个变量指代的函数逻辑就发生了变化,或者说函数进行了“重新定义”或“重写”。说起来有些拗口,实际上并不复杂,来看一个例子: +## 重定义函数 + +函数可以被动态定义,也可以被赋值给变量。如果将你定义的函数赋值给已经存在的函数变量的话,则新函数会覆盖旧函数。这样做的结果是,旧函数的引用被丢弃掉,变量中所存储的引用值替换成了新的函数。这样看起来这个变量指代的函数逻辑就发生了变化,或者说函数进行了“重新定义”或“重写”。听起来很麻烦,但实际上并不复杂,来看一个例子: var scareMe = function () { alert("Boo!"); @@ -396,52 +391,621 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 alert("Double boo!"); }; }; - // using the self-defining function + // 使用重定义函数 scareMe(); // Boo! scareMe(); // Double boo! -当函数中包含一些初始化操作,并希望这些初始化只执行一次,那么这种模式是非常适合这个场景的。因为能避免的重复执行则尽量避免,函数的一部分可能再也不会执行到。在这个场景中,函数执行一次后就被重写为另外一个函数了。 +当函数中包含一些初始化操作,并希望这些初始化操作只执行一次,那么这种模式是非常合适的,因为我们要避免重复执行不需要的代码。在这个场景中,函数执行一次后就被重写为另外一个函数了。 -使用这种模式可以帮助提高应用的执行效率,因为重新定义的函数执行的更少。 +使用这种模式可以帮助提高应用的执行效率,因为重新定义的函数执行的代码量更少。 ->这种模式的另外一个名字是“函数的懒惰定义”,因为直到函数执行一次后才重新定义,可以说它是“某个时间点之后才存在”,简称“懒惰定义”。 +> 这种模式的另外一个名字是“函数的懒惰定义”,因为直到函数执行一次后才重新定义,可以说它是“某个时间点之后才存在”,简称“懒惰定义”。 -这种模式有一种明显的缺陷,就是之前给原函数添加的功能在重定义之后都丢失了。如果将这个函数定义为不同的名字,函数赋值给了很多不同的变量,或作为对象的方法使用,那么新定义的函数有可能不会执行,原始的函数会照旧执行(译注:由于函数的赋值是引用的赋值,函数赋值给多个变量只是将引用赋值给了多个变量,当某一个变量定义了新的函数,也只是变量的引用值发生变化,原函数本身依旧存在,当程序中存在某个变量的引用还是旧函数的话,旧函数还是会依旧执行)。 +这种模式有一个明显的缺陷,就是之前给原函数添加的功能在重定义之后都丢失了。同时,如果这个函数被重定义为不同的名字,被赋值给不同的变量,或者是作为对象的方法使用,那么重定义的部分并不会生效,原来的函数依然会被执行。 -让我们来看一个例子,scareMe()函数在这里作为一等对象来使用: +让我们来看一个例子,`scareMe()`函数在这里作为一等对象来使用: 1. 给他增加了一个属性 -2. 函数对象赋值给一个新变量 -3. 函数依旧可以作为方法来调用 +2. 函数对象被赋值给一个新变量 +3. 函数还被作为方法来调用 看一下这段代码: - // 1. adding a new property + // 1. 添加一个新属性 scareMe.property = "properly"; - // 2. assigning to a different name + // 2. 被赋值给一个不同名的变量 var prank = scareMe; - // 3. using as a method + // 3. 作为方法使用 var spooky = { boo: scareMe }; - // calling with a new name + // 使用新名字调用 prank(); // "Boo!" prank(); // "Boo!" console.log(prank.property); // "properly" - // calling as a method + // 作为方法调用 spooky.boo(); // "Boo!" spooky.boo(); // "Boo!" - console.log(spooky.boo.property); + console.log(spooky.boo.property); // "properly" - // "properly" - // using the self-defined function + // 使用重定义函数 scareMe(); // Double boo! scareMe(); // Double boo! console.log(scareMe.property); // undefined +从结果来看,当重定义函数被赋值给一个新的变量的时候,这段使用重定义函数的代码的执行结果与我们期望的结果可能并不一样。每当`prank()`被调用的时候,它都弹出“Boo!”。同时它也重写了`scareMe()`函数,但是`prank()`自己仍然能够使用之前的定义,包括属性`property`。在这个函数被作为`spooky`对象的`boo()`方法调用的时候,结果也一样。所有的这些调用,在第一次的时候就已经修改了全局的`scareMe()`的指向,所以当它最终被调用的时候,它的函数体已经被修改为弹出“Double boo”,也就不能获取到新添加的属性`scareMe.property`。 + + +## 即时函数 + +即时函数是一种语法模式,它会使函数在定义后立即执行。看这个例子: + + (function () { + alert('watch out!'); + }()); + +这种模式本质上只是一个在创建后就被执行的函数表达式(具名或者匿名)。“即时函数”这种说法并没有在ECMAScript标准中被定义,但它作为一个名词,有助于我们的描述和讨论。 + +这种模式由以下几个部分组成: + +- 使用函数表达式定义一个函数。(不能使用函数声明。) +- 在最后加入一对括号,这会使函数立即被执行。 +- 把整个函数包裹到一对括号中(只在没有将函数赋值给变量时需要)。 + +下面这种语法也很常见(注意右括号的位置),但是JSLint倾向于第一种: + + (function () { + alert('watch out!'); + })(); + +这种模式很有用,它为我们提供一个作用域的沙箱,可以在执行一些初始化代码的时候使用。设想这样的场景:当页面加载的时候,你需要运行一些代码,比如绑定事件、创建对象等等。所有的这些代码都只需要运行一次,所以没有必要创建一个带有名字的函数。但是这些代码需要一些临时变量,而这些变量在初始化完之后又不会再次被用到。显然,把这些变量作为全局变量声明是不合适的。正因为如此,我们才需要即时函数。它可以把你所有的代码包裹到一个作用域里面,而不会暴露任何变量到全局作用域中: + + (function () { + + var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + today = new Date(), + msg = 'Today is ' + days[today.getDay()] + ', ' + today.getDate(); + + alert(msg); + + }()); // "Today is Fri, 13" + +如果这段代码没有被包裹到立即执行函数中,那么变量`days`、`today`、`msg`都会是全局变量,而这些变量仅仅是因为初始化而遗留下来的垃圾,没有任何用处。 + + +### 即时函数的参数 + +即时函数也可以接受参数,看这个例子: + + // 打印出: + // I met Joe Black on Fri Aug 13 2010 23:26:59 GMT-0800 (PST) + + (function (who, when) { + + console.log("I met " + who + " on " + when); + + }("Joe Black", new Date())); + +通常我们会把全局对象当作一个参数传给即时函数,以保证在函数内部也可以访问到全局对象,而不是使用`window`对象,这样可以使得代码在非浏览器环境中使用时更具可移植性。 + +值得注意的是,一般情况下尽量不要给即时函数传入太多的参数,否则会有一件麻烦的事情,就是你在阅读代码的时候需要频繁地上下滚动代码。 + +### 即时函数的返回值 + +和其它的函数一样,即时函数也可以返回值,并且这些返回值也可以被赋值给变量: + + var result = (function () { + return 2 + 2; + }()); + +如果省略括号的话也可以达到同样的目的,因为如果需要将返回值赋给变量,那么第一对括号就不是必需的。省略括号的代码是这样子: + + var result = function () { + return 2 + 2; + }(); + +这种写法更简洁,但是同时也容易造成误解。如果有人在阅读代码的时候忽略了最后的一对括号,那么他会以为`result`指向了一个函数。而事实上`result`是指向这个函数运行后的返回值,在这个例子中是4。 + +还有一种写法也可以得到同样的结果: + + var result = (function () { + return 2 + 2; + })(); + +前面的例子中,即时函数返回的是一个基本类型的数值。但事实上,一个即时函数可以返回任意类型的值,甚至返回一个函数都可以。你可以利用即时函数的作用域来存储一些私有的数据,这些数据只能在返回的内层函数中被访问。 + +在下面的例子中,即时函数的返回值是一个函数,这个函数会简单地返回`res`的值,并且这个值被赋给了变量`getResult`。而`res`是一个预先计算好的变量,它被存储在即时函数的闭包中: + + var getResult = (function () { + var res = 2 + 2; + return function () { + return res; + }; + }()); + +在定义一个对象属性的时候也可以使用即时函数。设想一下这样的场景:你需要定义一个对象的属性,这个属性在对象的生命周期中都不会改变,但是在定义之前,你需要做一些计算来得到它的值。这种情况下你就可以使用即时函数来包裹那些额外的计算工作,然后将它的返回值作为对象属性的值。下面是一个例子: + + var o = { + message: (function () { + var who = "me", + what = "call"; + return what + " " + who; + }()), + getMsg: function () { + return this.message; + } + }; + + // 使用对象 + o.getMsg(); // "call me" + o.message; // "call me" + + +在这个例子中,`o.message`是一个字符串,而不是一个函数,但是它需要一个函数在脚本载入后通过计算得到这个属性值。 + +### 好处和用法 + +即时函数应用很广泛。它可以帮助我们做一些不想留下全局变量的工作。所有定义的变量都只是即时函数的本地变量,你完全不用担心临时变量会污染全局对象。 + +> 即时函数还有一些名字,比如“自调用函数”或者“自执行函数”,因为这些函数会在被定义后立即执行自己。 + +这种模式也经常被用到书签代码中,因为书签代码有可能会运行在任何一个页面中,所以需要非常苛刻地保持全局命名空间干净。 + +这种模式也可以让你包裹一些独立的特性到一个封闭的模块中。设想你的页面是静态的,在没有JavaScript的时候工作正常,然后,本着渐进增强的精神,你给页面加入了一点增强代码。这时候,你就可以把你的代码(也可以叫“模块”或者“特性”)放到一个即时函数中并且保证页面在有没有它的时候都可以正常工作。然后你就可以加入更多的增强特性,或者对它们进行移除、进行独立测试或者允许用户禁用等等。 + +你可以使用下面的模板定义一段函数代码,我们叫它module1: + + // 在module1.js中定义module1 + (function () { + + // 所有module 1的代码…… + + }()); + +你可以套用这个模板来编写其它的模块,然后在发布到线上的时候,再决定在这个时间节点上哪些特性是稳定可用的,然后使用发布脚本将它们打包上线。 + + +## 对象即时初始化 + +还有另外一种可以避免污染全局作用域的方法,和前面描述的即时函数相似,叫做“对象即时初始化”模式。这种模式使用一个带有`init()`方法的对象来实现,这个方法在对象被创建后立即执行。初始化的工作由`init()`函数来完成。 + +下面是一个对象即时初始化模式的例子: + + ({ + // 这里可以定义一些设置项,比如常量 + maxwidth: 600, + maxheight: 400, + + // 你也可以定义一些方法 + gimmeMax: function () { + return this.maxwidth + "x" + this.maxheight; + }, + + // 初始化 + init: function () { + console.log(this.gimmeMax()); + // 更多的初始化任务…… + } + }).init(); + +在语法上,当你使用这种模式的时候就像在使用对象字面量创建一个普通对象一样。不同之处在于,需要将对象字面量用括号括起来,这样能让JavaScript引擎知道这是一个对象字面量,而不是一个代码块(`if`或者`for`循环之类)。在括号后面,紧接着就执行了`init()`方法。 + +你也可以将对象字面量和`init()`调用一起写到括号里面。简单地说,下面两种语法都是有效的: + + ({...}).init(); + ({...}.init()); + +这种模式的好处和即时函数模式是一样的:在做一些一次性的初始化工作的时候保护全局作用域不被污染。从语法上看,这种模式似乎比即时函数要复杂一些,但是如果你的初始化工作比较复杂(这种情况很常见),它会给整个初始化工作一个比较清晰的结构。比如,一些私有的辅助性函数可以被很轻易地看出来,因为它们是这个临时对象的属性,但是如果是在即时函数模式中,它们很可能只是一些散落的函数。 + +这种模式的一个弊端是,JavaScript压缩工具可能不能像压缩一段包裹在函数中的代码一样有效地压缩这种模式的代码。这些私有的属性和方法不被会重命名为一些更短的名字,因为从压缩工具的角度来看,保证压缩的可靠性更重要。在写作本书的时候,Google出品的Closure Compiler的“advanced”模式是唯一会重命名立即初始化的对象的属性的压缩工具。一个压缩后的样例是这样: + + ({d:600,c:400,a:function(){return this.d+"x"+this.c},b:function(){console.log(this.a())}}).b(); + +> 这种模式主要用于一些一次性的工作,并且在`init()`方法执行完后就无法再次访问到这个对象。如果希望在这些工作完成后保持对对象的引用,只需要简单地在`init()`的末尾加上`return this;`即可。 + + +## 条件初始化 + +条件初始化(也叫条件加载)是一种优化模式。当你知道某种条件在整个程序生命周期中都不会变化的时候,那么对这个条件的探测只做一次就很有意义。浏览器探测(或者特征检测)是一个典型的例子。 + +举例说明,当你探测到`XMLHttpRequest`被作为一个本地对象支持时,就知道浏览器不会在程序执行过程中改变这一情况,也不会出现突然需要去处理ActiveX对象的情况。当环境不发生变化的时候,你的代码就没有必要在需要在每次初始化XHR对象时探测一遍(并且得到同样的结果)。 + +另外一些可以从条件初始化中获益的场景是获得一个DOM元素的computed styles或者是绑定事件处理函数。大部分程序员在他们的编程生涯中都编写过事件绑定和取消绑定相关的组件,像下面的例子: + + // 优化前的代码 + var utils = { + addListener: function (el, type, fn) { + if (typeof window.addEventListener === 'function') { + el.addEventListener(type, fn, false); + } else if (typeof document.attachEvent === 'function') { // IE + el.attachEvent('on' + type, fn); + } else { // 老的浏览器 + el['on' + type] = fn; + } + }, + removeListener: function (el, type, fn) { + // 和上面很类似的代码…… + } + }; + +这段代码的问题就是效率不高。每当你执行`utils.addListener()`或者`utils.removeListener()`时,同样的检查都会被重复执行。 + +如果使用条件初始化,那么浏览器探测的工作只需要在初始化代码的时候执行一次。在初始化的时候,代码探测一次环境,然后重新定义这个函数在剩下来的程序生命周期中应该怎样工作。下面是一个例子,看看如何达到这个目的: + + // 优化后的代码 + + // 接口 + var utils = { + addListener: null, + removeListener: null + }; + + // 实现 + if (typeof window.addEventListener === 'function') { + utils.addListener = function (el, type, fn) { + el.addEventListener(type, fn, false); + }; + utils.removeListener = function (el, type, fn) { + el.removeEventListener(type, fn, false); + }; + } else if (typeof document.attachEvent === 'function') { // IE + utils.addListener = function (el, type, fn) { + el.attachEvent('on' + type, fn); + }; + utils.removeListener = function (el, type, fn) { + el.detachEvent('on' + type, fn); + }; + } else { // older browsers + utils.addListener = function (el, type, fn) { + el['on' + type] = fn; + }; + utils.removeListener = function (el, type, fn) { + el['on' + type] = null; + }; + } + +说到这里,要特别提醒一下关于浏览器探测的事情。当你使用这个模式的时候,不要对浏览器特性过度假设。举个例子,如果你探测到浏览器不支持`window.addEventListener`时,不要假设这个浏览器是IE,也不要认为它不支持原生的`XMLHttpRequest`,虽然这个结论在整个浏览器历史上的某个时间点是正确的。当然,也有一些情况是可以放心地做一些特性假设的,比如`.addEventListener`和`.removeEventListerner`,但是通常来讲,浏览器的特性在发生变化时都是独立的。最好的策略就是分别探测每个特性,然后使用条件初始化,使这种探测只做一次。 + + +## 函数属性——记忆模式(Memoization) + +函数也是对象,所以它们可以有属性。事实上,函数也确实本来就有一些属性。比如,对一个函数来说,不管是用什么语法创建的,它会自动拥有一个`length`属性来标识这个函数期待接受的参数个数: + + function func(a, b, c) {} + console.log(func.length); // 3 + +任何时候都可以给函数添加自定义属性。添加自定义属性的一个有用场景是缓存函数的执行结果(返回值),这样下次同样的函数被调用的时候就不需要再做一次那些可能很复杂的计算。缓存一个函数的运行结果也就是为大家所熟知的记忆模式。 + +在下面的例子中,`myFunc`函数创建了一个`cache`属性,可以通过`myFunc.cache`访问到。这个`cache`属性是一个对象(hash表),传给函数的参数会作为对象的key,函数执行结果会作为对象的值。函数的执行结果可以是任何的复杂数据结构: + + var myFunc = function (param) { + if (!myFunc.cache[param]) { + var result = {}; + // ……复杂的计算…… + myFunc.cache[param] = result; + } + return myFunc.cache[param]; + }; + + // 缓存 + myFunc.cache = {}; + +上面的代码假设函数只接受一个参数`param`,并且这个参数是原始类型(比如字符串)。如果你有更多更复杂的参数,则通常需要对它们进行序列化。比如,你需要将`arguments`对象序列化为JSON字符串,然后使用JSON字符串作为`cache`对象的key: + + var myFunc = function () { + + var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)), + result; + + if (!myFunc.cache[cachekey]) { + result = {}; + // ……复杂的计算…… + myFunc.cache[cachekey] = result; + } + return myFunc.cache[cachekey]; + }; + + // 缓存 + myFunc.cache = {}; + +需要注意的是,在序列化的过程中,对象的“标识”将会丢失。如果你有两个不同的对象,却碰巧有相同的属性,那么他们会共享同样的缓存内容。 + +前面代码中的函数名还可以使用`arguments.callee`来替代,这样就不用将函数名硬编码。不过尽管现阶段这个办法可行,但是仍然需要注意,`arguments.callee`在ECMAScript5的严格模式中是不被允许的: + + var myFunc = function (param) { + + var f = arguments.callee, + result; + + if (!f.cache[param]) { + result = {}; + // ……复杂的计算…… + f.cache[param] = result; + } + return f.cache[param]; + }; + + // 缓存 + myFunc.cache = {}; + + +## 配置对象 + +配置对象模式是一种为自己的代码提供更简洁的API的方法,如果你正在写一个即将被其它程序调用的类库之类的代码的时候就特别有用。 + +软件在开发和维护过程中需要不断改变是一个不争的事实。这样的事情总是以一些有限的需求开始,但是随着开发的进行,越来越多的功能会不断被加进来。 + +设想一下你正在写一个名为`addPerson()`的函数,它接受一个姓和一个名,然后在列表中加入一个人: + + function addPerson(first, last) {...} + +然后你意识到,生日也必须要存储,此外,性别和地址也作为可选项存储。所以你修改了函数,添加了一些新的参数(还得非常小心地将可选参数放到最后): + + function addPerson(first, last, dob, gender, address) {...} + +这个时候,函数已经显得有点长了。然后,你又被告知需要添加一个用户名,并且不是可选的。现在这个函数的调用者需要将所有的可选参数传进来,并且得非常小心地保证不弄混参数的顺序: + + addPerson("Bruce", "Wayne", new Date(), null, null, "batman"); + +这样传一大串的参数真的很不方便。一个更好的办法就是将它们替换成一个参数对象,我们叫它`conf`,是“configuration”(配置)的缩写: + + addPerson(conf); + +然后这个函数的使用者就可以这样: + + var conf = { + username: "batman", + first: "Bruce", + last: "Wayne" + }; + addPerson(conf); + +配置对象模式的好处是: + +- 不需要记住参数的顺序 +- 可以很安全地跳过可选参数 +- 拥有更好的可读性和可维护性 +- 更容易添加和移除参数 + +配置对象模式的坏处是: + +- 需要记住参数的名字 +- 参数名字不能被压缩 + +在实践中,这个模式对创建DOM元素的函数或者是给元素设定CSS样式的函数会非常实用,因为元素和CSS样式可能会有很多属性,但是大部分属性是可选的。 + +## 柯里化 (Curry) + +在本章剩下的部分,我们将讨论一下关于柯里化和部分应用的话题。但是在我们开始这个话题之前,先看一下什么是函数应用。 + +### 函数应用 + +在一些纯粹的函数式编程语言中,对函数的描述不是被调用(`called`或者`invoked`),而是被应用(`applied`)。在JavaScript中也有同样的东西——我们可以使用`Function.prototype.apply()`来应用一个函数,因为在JavaScript中,函数实际上是对象,并且他们拥有方法。 + +下面是一个函数应用的例子: + + // 定义函数 + var sayHi = function (who) { + return "Hello" + (who ? ", " + who : "") + "!"; + }; + + // 调用函数 + sayHi(); // "Hello" + sayHi('world'); // "Hello, world!" + + // 应用函数 + sayHi.apply(null, ["hello"]); // "Hello, hello!" + +从上面的例子中可以看出来,调用一个函数和应用一个函数有相同的结果。`apply()`接受两个参数:第一个是在函数内部绑定到`this`上的对象,第二个是一个参数数组,参数数组会在函数内部变成一个类似数组的`arguments`对象。如果第一个参数为`null`,那么`this`将指向全局对象,这正是当你调用一个函数(且这个函数不是某个对象的方法)时发生的事情。 + +当一个函数是一个对象的方法时,我们不再像前面的例子一样传入`null`。(译注:主要是为了保证方法中的`this`绑定到一个有效的对象而不是全局对象。)在下面的例子中,对象被作为第一个参数传给`apply()`: + + var alien = { + sayHi: function (who) { + return "Hello" + (who ? ", " + who : "") + "!"; + } + }; + + alien.sayHi('world'); // "Hello, world!" + sayHi.apply(alien, ["humans"]); // "Hello, humans!" + +在这个例子中,`sayHi()`中的`this`指向`alien`。而在上一个例子中,`this`是指向的全局对象。(译注:这个例子的代码有误,最后一行的`sayHi`并不能访问到`alien`的`sayHi`方法,需要使用`alien.sayHi.apply(alien, ["humans"])`才可正确运行。另外,在`sayHi`中也没有出现`this`。) + +正如上面两个例子所展现出来的一样,我们将所谓的函数调用当作函数应用的一种语法糖来理解也没有什么太大的问题。 + +需要注意的是,除了`apply()`之外,`Function.prototype`对象还有一个`call()`方法,但是它仍然只是`apply()`的一种语法糖。(译注:这两个方法的区别在于,`apply()`只接受两个参数,第二个参数为需要传给函数的参数数组,而`call()`则接受任意多个参数,从第二个开始将参数依次传给函数。)不过有种情况下使用这个语法糖会更好:当你的函数只接受一个参数的时候,你可以省去为唯一的一个元素创建数组的工作: + + // 第二种更高效,因为节省了一个数组 + sayHi.apply(alien, ["humans"]); // "Hello, humans!" + sayHi.call(alien, "humans"); // "Hello, humans!" + +### 部分应用 + +现在我们知道了,调用一个函数实际上就是给它应用一堆参数,那是否能够只传一部分参数而不传全部呢?这实际上跟我们手工处理数学函数非常类似。 + +假设已经有了一个`add()`函数,它的工作是把`x`和`y`两个数加到一起。下面的代码片段展示了当`x`为5、`y`为4时的计算步骤: + + // 并不是合法的JavaScript代码,仅用于演示 + + // 假设有一个add()函数 + function add(x, y) { + return x + y; + } + + // 给定参数 + add(5, 4); + + // 第一步 传入一个参数 + function add(5, y) { + return 5 + y; + } + + // 第二步 传入另一个参数 + function add(5, 4) { + return 5 + 4; + } + +在这个代码片段中,第一步和第二步并不是有效的JavaScript代码,但是它展示了我们手工计算的过程。首先获得第一个参数的值,然后在函数中将未知的`x`值替换为5。然后重复这个过程,直到替换掉所有的参数。 + +第一步是一个所谓的部分应用的例子:我们只应用了第一个参数。当你执行一个部分应用的时候并不能获得结果(或者是解决方案),取而代之的是另一个函数。 + +下面的代码片段展示了一个虚拟的`partialApply()`方法的用法: + + var add = function (x, y) { + return x + y; + }; + + // 完整应用 + add.apply(null, [5, 4]); // 9 + + // 部分应用 + var newadd = add.partialApply(null, [5]); + // 为新函数传入一个参数 + newadd.apply(null, [4]); // 9 + +正如你所看到的一样,部分应用给了我们另一个函数,这个函数可以在稍后调用的时候接受其它的参数。这实际上跟`add(5)(4)`是等价的,因为`add(5)`返回了一个函数,这个函数可以使用`(4)`来调用。我们又一次看到,熟悉的`add(5, 4)`也差不多是`add(5)(4)`的一种语法糖。 + +现在,让我们回到地球:并不存在这样的一个`partialApply()`函数,并且函数的默认表现也不会像上面的例子中那样。但是你完全可以自己去写,因为JavaScript的动态特性完全可以做到这样。 + +让函数理解并且处理部分应用的过程,叫柯里化(Currying)。 + +### 柯里化(Currying) + +柯里化这个名字来自数学家Haskell Curry。(Haskell编程语言也是因他而得名。)柯里化是一个变换函数的过程。柯里化的另外一个名字也叫schönfinkelisation,来自另一位数学家——Moses Schönfinkelisation——这种变换的最初发明者。 + +所以我们怎样对一个函数进行柯里化呢?其它的函数式编程语言也许已经原生提供了支持并且所有的函数已经默认柯里化了。在JavaScript中我们可以修改一下`add()`函数使它柯里化,然后支持部分应用。 + +来看一个例子: + + // 柯里化过的add()方法,可以接受部分参数 + function add(x, y) { + var oldx = x, oldy = y; + if (typeof oldy === "undefined") { // 部分应用 + return function (newy) { + return oldx + newy; + }; + } + // 完整应用 + return x + y; + } + + // 测试 + typeof add(5); // "function" + add(3)(4); // 7 + + // 创建并保存函数 + var add2000 = add(2000); + add2000(10); // 2010 + +在这段代码中,第一次调用`add()`时,在返回的内层函数那里创建了一个闭包。这个闭包将原来的`x`和`y`的值存储到了`oldx`和`oldy`中。当内层函数执行的时候,`oldx`会被使用。如果没有部分应用,即`x`和`y`都传了值,那么这个函数会简单地将他们相加。这个`add()`函数的实现显得有些冗余,仅仅是为了更好地说明问题。下面的代码片段中展示了一个更简洁的版本,没有`oldx`和`oldy`,因为原始的`x`已经被存储到了闭包中,此外我们复用了`y`作为本地变量,而不用像之前那样新定义一个变量`newy`: + + // 柯里化过的add()方法,可以接受部分参数 + function add(x, y) { + if (typeof y === "undefined") { // 部分应用 + return function (y) { + return x + y; + }; + } + // 完整应用 + return x + y; + } + +在这些例子中,`add()`函数自己处理了部分应用。有没有可能用一种更为通用的方式来做同样的事情呢?换句话说,我们能不能对任意一个函数进行处理,得到一个新函数,使它可以处理部分参数?下面的代码片段展示了一个通用函数的例子,我们叫它`schonfinkelize()`,它正是用来做这个的。我们使用`schonfinkelize()`这个名字,一部分原因是它比较难发音,另一部分原因是它听起来比较像动词(使用“curry”则不是那么明确),而我们刚好需要一个动词来表明这是一个函数转换的过程。 + +这是一个通用的柯里化函数: + + function schonfinkelize(fn) { + var slice = Array.prototype.slice, + stored_args = slice.call(arguments, 1); + return function () { + var new_args = slice.call(arguments), + args = stored_args.concat(new_args); + return fn.apply(null, args); + }; + } + +这个`schonfinkelize()`可能显得比较复杂了,只是因为在JavaScript中`arguments`不是一个真的数组。从`Array.prototype`中借用`slice()`方法帮助我们将`arguments`转换成数组,以便能更好地对它进行操作。当`schonfinkelize()`第一次被调用的时候,它使用`slice`变量存储了对`slice()`方法的引用,同时也存储了调用时的除去第一个之外的参数(`stored_args`),因为第一个参数是要被柯里化的函数。`schonfinkelize()`返回了一个函数,当这个返回的函数被调用的时候,它可以(通过闭包)访问到已经存储的参数`stored_args`和`slice`。新的函数只需要合并老的部分应用的参数(`stored_args`)和新的参数(`new_args`),然后将它们应用到原来的函数`fn`(也可以在闭包中访问到)即可。 + +现在有了通用的柯里化函数,就可以做一些测试了: + + // 普通函数 + function add(x, y) { + return x + y; + } + + // 柯里化得到新函数 + var newadd = schonfinkelize(add, 5); + newadd(4); // 9 + + // 另一种选择 直接调用新函数 + schonfinkelize(add, 6)(7); // 13 + +用来做函数转换的`schonfinkelize()`并不局限于单个参数或者单步的柯里化。这里有些更多用法的例子: + + // 普通函数 + function add(a, b, c, d, e) { + return a + b + c + d + e; + } + + // 参数个数可以随意分割 + schonfinkelize(add, 1, 2, 3)(5, 5); // 16 + + // 两步柯里化 + var addOne = schonfinkelize(add, 1); + addOne(10, 10, 10, 10); // 41 + var addSix = schonfinkelize(addOne, 2, 3); + addSix(5, 5); // 16 + +### 什么时候使用柯里化 + +当你发现自己在调用同样的函数并且传入的参数大部分都相同的时候,就是考虑柯里化的理想场景了。你可以通过传入一部分的参数动态地创建一个新的函数。这个新函数会存储那些重复的参数(所以你不需要再每次都传入),然后再在调用原始函数的时候将整个参数列表补全。 + +##小结 + +在JavaScript中,对开发者在函数这个话题的理解和运用的要求是比较苛刻的。在本章中,主要讨论了有关函数的一些背景知识和术语。介绍了JavaScript函数中两个重要的特性,也就是: + +1. 函数是一等对象,他们可以被作为值传递,也可以拥有属性和方法。 +2. 函数拥有本地作用域,而大括号不产生块级作用域。另外需要注意的是,变量的声明会被提前到本地作用域顶部。 + +创建一个函数的语法有: + +1. 具名函数表达式 +2. 匿名函数表达式(和上一种一样,但是没有名字),也就是为大家熟知的“匿名函数” +3. 函数声明,与其它语言的函数语法相似 + +在介绍完背景和函数的语法后,介绍了一些有用的模式,按分类列出: + +1. API模式,它们帮助我们为函数给出更干净的接口,包括: + - 回调模式 + + 传入一个函数作为参数 + - 配置对象 + + 帮助保持函数的参数数量可控 + - 返回函数 + + 函数的返回值是另一个函数 + - 柯里化 + + 新函数在已有函数的基础上再加上一部分参数构成 +2. 初始化模式,这些模式帮助我们用一种干净的、结构化的方法来做一些初始化工作(在web页面和应用中非常常见),通过一些临时变量来保证不污染全局命名空间。这些模式包括: + - 即时函数 + + 当它们被定义后立即执行 + - 对象即时初始化 + + 初始化工作被放入一个匿名对象,这个对象提供一个可以立即被执行的方法 + - 条件初始化 + + 使分支代码只在初始化的时候执行一次,而不是在整个程序生命周期中反复执行 +3. 性能模式,这些模式帮助提高代码的执行速度,包括: + - 记忆模式 + 利用函数的属性,使已经计算过的值不用再次计算 + - 自定义函数 + 重写自身的函数体,使第二次及后续的调用做更少的工作 diff --git a/chapter5.markdown b/chapter5.markdown new file mode 100644 index 0000000..baca060 --- /dev/null +++ b/chapter5.markdown @@ -0,0 +1,1054 @@ +# 对象创建模式 + +在JavaScript中创建对象是件很容易的事情,直接通过对象字面量或者构造函数就可以。本章将在此基础上介绍一些常用的对象创建模式。 + +JavaScript语言本身很简单、直观,也没有其他语言的一些语言特性:命名空间、模块、包、私有属性以及静态成员。本章将介绍一些常用的模式,以此实现这些语言特性。 + +我们将对命名空间、依赖声明、模块模式以及沙箱模式进行初探——它们可以帮助我们更好地组织应用程序的代码,有效地减少全局污染的问题。除此之外,还会讨论私有和特权成员、静态和私有静态成员、对象常量、链式调用以及一种像类式语言一样定义构造函数的方法等话题。 + +## 命名空间模式 + +使用命名空间可以减少全局变量的数量,与此同时,还能有效地避免命名冲突和前缀的滥用。 + +JavaScript没有原生的命名空间语法,但很容易可以实现这个特性。为了避免产生全局污染,你可以为应用或者类库创建一个(通常是唯一一个)全局对象,然后将所有的功能都添加到这个对象上,而不是到处声明大量的全局函数、全局对象以及其他的全局变量。 + +看如下例子: + + // 重构前:5个全局变量 + // 注意:反模式 + // 构造函数 + function Parent() {} + function Child() {} + // 一个变量 + var some_var = 1; + + // 一些对象 + var module1 = {}; + module1.data = {a: 1, b: 2}; + var module2 = {}; + +可以通过创建一个全局对象(通常代表应用名)比如`MYAPP`来重构上述这类代码,然后将上述例子中的函数和变量都变为该全局对象的属性: + + // 重构后:一个全局变量 + // 全局对象 + var MYAPP = {}; + + // 构造函数 + MYAPP.Parent = function () {}; + MYAPP.Child = function () {}; + + // 一个变量 + MYAPP.some_var = 1; + + // 一个对象容器 + MYAPP.modules = {}; + + // 嵌套的对象 + MYAPP.modules.module1 = {}; + MYAPP.modules.module1.data = {a: 1, b: 2}; + MYAPP.modules.module2 = {}; + +这里的`MYAPP`就是命名空间对象,对象名可以随便取,可以是应用名、类库名、域名或者是公司名都可以。开发者经常约定全局变量都采用大写(所有字母都大写),这样可以显得比较突出(不过要记住,大写的变量也常用于表示常量)。 + +这种模式是一种很好的提供命名空间的方式,避免了自身代码的命名冲突,同时还避免了同一个页面上自身代码和第三方代码(比如JavaScript类库或者widget)的冲突。这种模式在大多数情况下非常适用,但也有它的缺点: + +- 代码量稍有增加;在每个函数和变量前加上这个命名空间对象的前缀,会增加代码量,增大文件大小 +- 该全局实例可以被随时修改 +- 命名的深度嵌套会减慢属性值的查询 + +本章后续要介绍的沙箱模式则可以避免这些缺点。 + +### 通用命名空间函数 + +随着程序复杂度的提高,代码会被分拆在不同的文件中以按照页面需要来加载,这样一来,就不能保证你的代码一定是第一个定义命名空间或者某个属性的,甚至会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示: + + // 不安全的做法 + var MYAPP = {}; + // 更好的做法 + if (typeof MYAPP === "undefined") { + var MYAPP = {}; + } + // 简写 + var MYAPP = MYAPP || {}; + +如上所示,如果每次做类似操作都要这样检查一下就会有很多重复的代码。例如,要声明`MYAPP.modules.module2`,就要重复三次这样的检查。所以,我们需要一个可复用的`namespace()`函数来专门处理这些检查工作,然后用它来创建命名空间,如下所示: + + // 使用命名空间函数 + MYAPP.namespace('MYAPP.modules.module2'); + + // 等价于: + // var MYAPP = { + // modules: { + // module2: {} + // } + // }; + +下面是上述`namespace`函数的实现示例。这种实现是非破坏性的,意味着如果要创建的命名空间已经存在,则不会再重复创建: + + var MYAPP = MYAPP || {}; + MYAPP.namespace = function (ns_string) { + var parts = ns_string.split('.'), + parent = MYAPP, + i; + + // 去除不必要的全局变量层 + // 译注:因为namespace已经属于MYAPP + if (parts[0] === "MYAPP") { + parts = parts.slice(1); + } + + for (i = 0; i < parts.length; i += 1) { + // 如果属性不存在则创建它 + if (typeof parent[parts[i]] === "undefined") { + parent[parts[i]] = {}; + } + parent = parent[parts[i]]; + } + return parent; + }; + +上述实现支持如下几种用法: + + // 将返回值赋给本地变量 + var module2 = MYAPP.namespace('MYAPP.modules.module2'); + module2 === MYAPP.modules.module2; // true + + // 省略全局命名空间`MYAPP` + MYAPP.namespace('modules.module51'); + + // 长命名空间 + MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property'); + +图5-1 展示了上述代码创建的命名空间对象在Firebug下的可视化结果 + +![MYAPP命名空间在Firebug下的可视结果](./Figure/chapter5/5-1.jpg) + +图5-1 MYAPP命名空间在Firebug下的可视化结果 + +## 依赖声明 + +JavaScript库往往是模块化而且有用到命名空间的,这使得你可以只使用你需要的模块。比如在YUI2中,全局变量`YAHOO`就是一个命名空间,各个模块都是全局变量的属性,比如`YAHOO.util.Dom`(DOM模块)、`YAHOO.util.Event`(事件模块)。 + +将你的代码依赖在函数或者模块的顶部进行声明是一个好主意。声明就是创建一个本地变量,指向你需要用到的模块: + + var myFunction = function () { + // 依赖 + var event = YAHOO.util.Event, + dom = YAHOO.util.Dom; + + // 在函数后面的代码中使用event和dom…… + }; + +这是一个相当简单的模式,但是有很多的好处: + +- 明确的依赖声明是告知使用你代码的开发者,需要保证指定的脚本文件被包含在页面中。 +- 将声明放在函数顶部使得依赖很容易被查找和解析。 +- 本地变量(如`dom`)永远会比全局变量(如`YAHOO`)要快,甚至比全局变量的属性(如`YAHOO.util.Dom`)还要快,这样会有更好的性能。使用了依赖声明模式之后,全局变量的解析在函数中只会进行一次,在此之后将会使用更快的本地变量。 +- 一些高级的代码压缩工具比如YUI Compressor和Google Closure compiler会重命名本地变量(比如`event`可能会被压缩成一个字母,如`A`),这会使代码更精简,但这个操作不会对全局变量进行,因为这样做不安全。 + +下面的代码片段是关于是否使用依赖声明模式对压缩影响的展示。尽管使用了依赖声明模式的`test2()`看起来复杂,因为需要更多的代码行数和一个额外的变量,但在压缩后它的代码量却会更小,意味着用户只需要下载更少的代码: + + function test1() { + alert(MYAPP.modules.m1); + alert(MYAPP.modules.m2); + alert(MYAPP.modules.m51); + } + + /* + test1()压缩后的函数体: + alert(MYAPP.modules.m1);alert(MYAPP.modules.m2);alert(MYAPP.modules.m51) + */ + + function test2() { + var modules = MYAPP.modules; + alert(modules.m1); + alert(modules.m2); + alert(modules.m51); + } + + /* + test2()压缩后的函数体: + var a=MYAPP.modules;alert(a.m1);alert(a.m2);alert(a.m51) + */ + + +## 私有属性和方法 + +JavaScript不像Java或者其它语言,它没有专门的提供私有、保护、公有属性和方法的语法。所有的对象成员都是公有的: + + var myobj = { + myprop: 1, + getProp: function () { + return this.myprop; + } + }; + console.log(myobj.myprop); // myprop是公有的 + console.log(myobj.getProp()); // getProp()也是公有的 + +当你使用构造函数创建对象的时候也是一样的,所有的成员都是公有的: + + function Gadget() { + this.name = 'iPod'; + this.stretch = function () { + return 'iPad'; + }; + } + var toy = new Gadget(); + console.log(toy.name); // name是公有的 + console.log(toy.stretch()); // stretch()也是公有的 + +### 私有成员 + +尽管语言并没有用于私有成员的专门语法,但你可以通过闭包来实现。在构造函数中创建一个闭包,任何在这个闭包中的部分都不会暴露到构造函数之外。但是,这些私有变量却可以被公有方法访问,也就是在构造函数中定义的并且作为返回对象一部分的那些方法。我们来看一个例子,`name`是一个私有成员,在构造函数之外不能被访问: + + function Gadget() { + // 私有成员 + var name = 'iPod'; + // 公有函数 + this.getName = function () { + return name; + }; + } + var toy = new Gadget(); + + // name是是私有的 + console.log(toy.name); // undefined + // 公有方法可以访问到name + console.log(toy.getName()); // "iPod" + +如你所见,在JavaScript创建私有成员很容易。你需要做的只是将私有成员放在一个函数中,保证它是函数的本地变量,也就是说让它在函数之外不可以被访问。 + +### 特权方法 + +特权方法的概念不涉及到任何语法,它只是一个给可以访问到私有成员的公有方法的名字(就好像它们有更多权限一样)。 + +在前面的例子中,`getName()`就是一个特权方法,因为它有访问`name`属性的特殊权限。 + +### 私有成员失效 + +当你使用私有成员时,需要考虑一些极端情况: + +- 在Firefox的一些早期版本中,允许通过给`eval()`传递第二个参数的方法来指定上下文对象,从而允许访问函数的私有作用域。比如在Mozilla Rhino(译注:一个JavaScript引擎)中,允许使用`__parent__`来访问私有作用域。这些极端情况现在并没有广泛存在于浏览器中。 +- 当你直接通过特权方法返回一个私有变量,而这个私有变量恰好是一个对象或者数组时,外部的代码可以修改这个私有变量,因为它是按引用传递的。 + +我们来看一下第二种情况。下面的`Gadget`的实现看起来没有问题: + + function Gadget() { + // 私有成员 + var specs = { + screen_width: 320, + screen_height: 480, + color: "white" + }; + + // 公有函数 + this.getSpecs = function () { + return specs; + }; + } + +这里的问题是`getSpecs()`返回了一个`specs`对象的引用。这使得`Gadget()`的使用者可以修改貌似隐藏起来的私有成员`specs`: + + var toy = new Gadget(), + specs = toy.getSpecs(); + + specs.color = "black"; + specs.price = "free"; + + console.dir(toy.getSpecs()); + +在Firebug控制台中打印出来的结果如图5-2: + +![图5-2 私有对象被修改了](./Figure/chapter5/5-2.jpg) + +图5-2 私有对象被修改了 + +这个问题有点出乎意料,解决方法就是不要将你想保持私有的对象或者数组的引用传递出去。达到这个目标的一种方法是让`getSpecs()`返回一个新对象,这个新对象只包含对象的使用者需要的数据。这也是众所周知的“最低授权原则”(Principle of Least Authority,简称POLA),指永远不要给出比真实需要更多的东西。在这个例子中,如果`Gadget()`的使用者关注它是否适应一个特定的盒子,它只需要知道尺寸即可。所以你应该创建一个`getDimensions()`,用它返回一个只包含`width`和`height`的新对象,而不是把什么都给出去。也就是说,也许你根本不需要实现`getSpecs()`方法。 + +当你需要传递所有的数据时,有另外一种方法,就是使用通用的对象复制函数创建`specs`对象的一个副本。下一章提供了两个这样的函数——一个叫`extend()`,它会浅复制一个给定的对象(只复制顶层的成员),另一个叫`extendDeep()`,它会做深复制,遍历所有的属性和嵌套的属性。 + +### 对象字面量和私有成员 + +到目前为止,我们只看了使用构建函数创建私有成员的示例。如果使用对象字面量创建对象时会是什么情况呢?是否有可能含有私有成员? + +如你前面所看到的那样,私有数据使用一个函数来包裹。所以在使用对象字面量时,你也可以使用一个即时函数创建的闭包。例如: + + var myobj; // 一个对象 + (function () { + // 私有成员 + var name = "my, oh my"; + + // 实现公有部分,注意没有var + myobj = { + // 特权方法 + getName: function () { + return name; + } + }; + }()); + + myobj.getName(); // "my, oh my" + +还有一个原理一样但看起来不一样的实现示例: + + var myobj = (function () { + // 私有成员 + var name = "my, oh my"; + + // 实现公有部分 + return { + getName: function () { + return name; + } + }; + }()); + + myobj.getName(); // "my, oh my" + +这个例子也是所谓的“模块模式”的基础,我们稍后将讲到它。 + +### 原型和私有成员 + +使用构造函数创建私有成员的一个弊端是,每一次调用构造函数创建对象时这些私有成员都会被创建一次。 + +这对在构建函数中添加到`this`的成员来说是一个问题。为了避免重复劳动,节省内存,你可以将共用的属性和方法添加到构造函数的`prototype`(原型)属性中。这样的话这些公共的部分会在使用同一个构造函数创建的所有实例中共享。你也同样可以在这些实例中共享私有成员,甚至可以将两种模式联合起来达到这个目的,同时使用构造函数中的私有属性和对象字面量中的私有属性。因为`prototype`属性也只是一个对象,可以使用对象字面量创建。 + +这是一个示例: + + function Gadget() { + // 私有成员 + var name = 'iPod'; + // 公有函数 + this.getName = function () { + return name; + }; + } + + Gadget.prototype = (function () { + // 私有成员 + var browser = "Mobile Webkit"; + // 公有函数 + return { + getBrowser: function () { + return browser; + } + }; + }()); + + var toy = new Gadget(); + console.log(toy.getName()); // 自有的特权方法 + console.log(toy.getBrowser()); // 来自原型的特权方法 + +### 将私有函数暴露为公有方法 + +“暴露模式”是指将已经有的私有函数暴露为公有方法,它在你希望尽量保护对象内的一些方法不被外部修改干扰的时候很有用。你希望能提供一些功能给外部访问,因为它们会被用到,如果你把这些方法公开,就会使得它们不再健壮,因为你的API的使用者可能修改它们。在ECMAScript5中,你可以选择冻结一个对象,但在之前的版本中这种方法不可用。下面进入暴露模式(原来是由Christian Heilmann创造的模式,叫“暴露模块模式”)。 + +我们来看一个例子,它建立在对象字面量的私有成员模式之上: + + var myarray; + + (function () { + + var astr = "[object Array]", + toString = Object.prototype.toString; + + function isArray(a) { + return toString.call(a) === astr; + } + + function indexOf(haystack, needle) { + var i = 0, + max = haystack.length; + for (; i < max; i += 1) { + if (haystack[i] === needle) { + return i; + } + } + return −1; + } + + myarray = { + isArray: isArray, + indexOf: indexOf, + inArray: indexOf + }; + + }()); + +这里有两个私有变量(私有函数)——`isArray()`和`indexOf()`。在包裹函数的最后,用那些允许被从外部访问的函数填充`myarray`对象。在这个例子中,同一个私有函数 `indexOf()`同时被暴露为ECMAScript5风格的`indexOf()`和PHP风格的`inArry()`。测试一下`myarray`对象: + + myarray.isArray([1,2]); // true + myarray.isArray({0: 1}); // false + myarray.indexOf(["a", "b", "z"], "z"); // 2 + myarray.inArray(["a", "b", "z"], "z"); // 2 + +现在假如有一些意外的情况发生在暴露的`indexOf()`方法上,私有的`indexOf()`方法仍然是安全的,因此`inArray()`仍然可以正常工作: + + myarray.indexOf = null; + myarray.inArray(["a", "b", "z"], "z"); // 2 + +## 模块模式 + +模块模式使用得很广泛,因为它可以为代码提供特定的结构,帮助组织日益增长的代码。不像其它语言,JavaScript没有专门的“包”(package)的语法,但模块模式提供了用于创建独立解耦的代码片段的工具,这些代码可以被当成黑盒,当你正在写的软件需求发生变化时,这些代码可以被添加、替换、移除。 + +模块模式是我们目前讨论过的好几种模式的组合,即: + +- 命名空间模式 +- 即时函数模式 +- 私有和特权成员模式 +- 依赖声明模式 + +第一步是初始化一个命名空间。我们使用本章前面部分的`namespace()`函数,创建一个提供数组相关方法的套件模块: + + MYAPP.namespace('MYAPP.utilities.array'); + +下一步是定义模块。使用一个即时函数来提供私有作用域供私有成员使用。即时函数返回一个对象,也就是带有公有接口的真正的模块,可以供其它代码使用: + + MYAPP.utilities.array = (function () { + return { + // todo... + }; + }()); + +下一步,给公有接口添加一些方法: + + MYAPP.utilities.array = (function () { + return { + inArray: function (needle, haystack) { + // ... + }, + isArray: function (a) { + // ... + } + }; + }()); + +如果需要的话,你可以在即时函数提供的闭包中声明私有属性和私有方法。同样,依赖声明放置在函数顶部,在变量声明的下方可以选择性地放置辅助初始化模块的一次性代码。函数最终返回的是一个包含模块公共API的对象: + + MYAPP.namespace('MYAPP.utilities.array'); + MYAPP.utilities.array = (function () { + + // 依赖声明 + var uobj = MYAPP.utilities.object, + ulang = MYAPP.utilities.lang, + + // 私有属性 + array_string = "[object Array]", + ops = Object.prototype.toString; + + // 私有方法 + // …… + + // 结束变量声明 + + // 选择性放置一次性初始化的代码 + // …… + + // 公有API + return { + + inArray: function (needle, haystack) { + for (var i = 0, max = haystack.length; i < max; i += 1) { + if (haystack[i] === needle) { + return true; + } + } + }, + + isArray: function (a) { + return ops.call(a) === array_string; + } + // ……更多的方法和属性 + }; + }()); + +模块模式被广泛使用,是一种值得强烈推荐的模式,它可以帮助我们组织代码,尤其是代码量在不断增长的时候。 + +### 暴露模块模式 + +我们在本章中讨论私有成员模式时已经讨论过暴露模式。模块模式也可以用类似的方法来组织,将所有的方法保持私有,只在最后暴露需要使用的方法来初始化API。 + +上面的例子可以变成这样: + + MYAPP.utilities.array = (function () { + + // 私有属性 + var array_string = "[object Array]", + ops = Object.prototype.toString, + + // 私有方法 + inArray = function (haystack, needle) { + for (var i = 0, max = haystack.length; i < max; i += 1) { + if (haystack[i] === needle) { + return i; + } + } + return −1; + }, + isArray = function (a) { + return ops.call(a) === array_string; + }; + // 结束变量定义 + + // 暴露公有API + return { + isArray: isArray, + indexOf: inArray + }; + }()); + +### 创建构造函数的模块 + +前面的例子创建了一个对象`MYAPP.utilities.array`,但有时候使用构造函数来创建对象会更方便。你也可以同样使用模块模式来做。唯一的区别是包裹模块的即时函数会在最后返回一个函数,而不是一个对象。 + +看下面的模块模式的例子,创建了一个构造函数`MYAPP.utilities.Array`: + + MYAPP.namespace('MYAPP.utilities.Array'); + + MYAPP.utilities.Array = (function () { + + // 依赖声明 + var uobj = MYAPP.utilities.object, + ulang = MYAPP.utilities.lang, + + // 私有属性和方法…… + Constr; + + // 结束变量定义 + + // 选择性放置一次性初始化代码 + // …… + + // 公有API——构造函数 + Constr = function (o) { + this.elements = this.toArray(o); + }; + // 公有API——原型 + Constr.prototype = { + constructor: MYAPP.utilities.Array, + version: "2.0", + toArray: function (obj) { + for (var i = 0, a = [], len = obj.length; i < len; i += 1) { + a[i] = obj[i]; + } + return a; + } + }; + + // 返回构造函数 + return Constr; + + }()); + +像这样使用这个新的构造函数: + + var arr = new MYAPP.utilities.Array(obj); + +### 在模块中引入全局上下文 + +作为这种模式的一个常见的变种,你可以给包裹模块的即时函数传递参数。你可以传递任何值,但通常情况下会传递全局变量甚至是全局对象本身。引入全局上下文可以加快函数内部的全局变量的解析,因为引入之后会作为函数的本地变量: + + MYAPP.utilities.module = (function (app, global) { + + // 全局对象和全局命名空间都作为本地变量存在 + + }(MYAPP, this)); + +## 沙箱模式 + +沙箱模式主要着眼于命名空间模式的短处,即: + +- 依赖一个全局变量成为应用的全局命名空间。在命名空间模式中,没有办法在同一个页面中运行同一个应用或者类库的不同版本,因为它们都会需要同一个全局变量名,比如`MYAPP`。 +- 代码中以点分隔的名字比较长,无论写代码还是解析都需要处理这个很长的名字,比如`MYAPP.utilities.array`。 + +顾名思义,沙箱模式为模块提供了一个环境,模块在这个环境中的任何行为都不会影响其它的模块和其它模块的沙箱。 + +这个模式在YUI3中用得很多,但是需要记住的是,下面的讨论只是一些示例实现,并不讨论YUI3中的沙箱是如何实现的。 + +### 全局构造函数 + +在命名空间模式中 ,有一个全局对象,而在沙箱模式中,唯一的全局变量是一个构造函数,我们把它命名为`Sandbox()`。我们使用这个构造函数来创建对象,同时也要传入一个回调函数,这个函数会成为代码运行的独立空间。 + +使用沙箱模式是像这样: + + new Sandbox(function (box) { + // 你的代码…… + }); + +`box`对象和命名空间模式中的`MYAPP`类似,它包含了所有你的代码需要用到的功能。 + +我们要多做两件事情: + +- 通过一些手段(第3章中的强制使用`new`的模式),你可以在创建对象的时候不要求一定有`new`。 +- 让`Sandbox()`构造函数可以接受一个(或多个)额外的配置参数,用于指定这个对象需要用到的模块名字。我们希望代码是模块化的,因此绝大部分`Sandbox()`提供的功能都会被包含在模块中。 + +有了这两个额外的特性之后,我们来看一下实例化对象的代码是什么样子。 + +你可以在创建对象时省略`new`并像这样使用已有的`ajax`和`event`模块: + + Sandbox(['ajax', 'event'], function (box) { + // console.log(box); + }); + +下面的例子和前面的很像,但是模块名字是作为独立的参数传入的: + + Sandbox('ajax', 'dom', function (box) { + // console.log(box); + }); + +使用通配符“*”来表示“使用所有可用的模块”是个不错的想法,为了方便,我们也假设没有任何模块传入时,沙箱使用“*”。所以有两种使用所有可用模块的方法: + + Sandbox('*', function (box) { + // console.log(box); + }); + + Sandbox(function (box) { + // console.log(box); + }); + +下面的例子展示了如何实例化多个沙箱对象,你甚至可以将它们嵌套起来而互不影响: + + Sandbox('dom', 'event', function (box) { + + // 使用dom和event模块 + + Sandbox('ajax', function (box) { + // 另一个沙箱中的box,这个box和外面的box不一样 + + //... + + // 使用ajax模块的代码到此为止 + + }); + + // 这里的代码与ajax模块无关 + }); + +从这些例子中看到,使用沙箱模式可以通过将代码包裹在回调函数中的方式来保护全局命名空间。 + +如果需要的话,你也可以利用函数也是对象这一事实,将一些数据作为静态属性存放到`Sandbox()`构造函数。 + +最后,你可以根据需要的模块类型创建不同的实例,这些实例都是相互独立的。 + +现在我们来看一下如何实现`Sandbox()`构造函数和它的模块来支持上面讲到的所有功能。 + +### 添加模块 + +在动手实现构造函数之前,我们先来看一下如何添加模块。 + +`Sandbox()`构造函数也是一个对象,所以可以给它添加一个`modules`静态属性。这个属性也是一个包含名值(key-value)对的对象,其中key是模块的名字,value是模块的功能实现。 + + Sandbox.modules = {}; + + Sandbox.modules.dom = function (box) { + box.getElement = function () {}; + box.getStyle = function () {}; + box.foo = "bar"; + }; + + Sandbox.modules.event = function (box) { + // 如果有需要的话可以访问Sandbox的原型 + // box.constructor.prototype.m = "mmm"; + box.attachEvent = function () {}; + box.dettachEvent = function () {}; + }; + + Sandbox.modules.ajax = function (box) { + box.makeRequest = function () {}; + box.getResponse = function () {}; + }; + +在这个例子中我们添加了`dom`、`event`和`ajax`模块,这些模块在每个类库或者复杂的web应用中都很常见。 + +每个模块功能函数接受一个实例`box`作为参数,并给这个实例添加属性和方法。 + +### 实现构造函数 + +最后,我们来实现`Sandbox()`构造函数(你可能会很自然地想将这类构造函数命名为对你的类库或者应用有意义的名字): + + function Sandbox() { + // 将参数转换为数组 + var args = Array.prototype.slice.call(arguments), + // 最后一个参数是回调函数 + callback = args.pop(), + // 参数可以作为数组或者单独的参数传递 + modules = (args[0] && typeof args[0] === "string") ? args : args[0], i; + + // 保证函数是作为构造函数被调用 + if (!(this instanceof Sandbox)) { + return new Sandbox(modules, callback); + } + + // 根据需要给this添加属性 + this.a = 1; + this.b = 2; + + // 给this对象添加模块 + // 未指明模块或者*都表示“使用所有模块” + if (!modules || modules[0] === '*') { + modules = []; + for (i in Sandbox.modules) { + if (Sandbox.modules.hasOwnProperty(i)) { + modules.push(i); + } + } + } + + // 初始化指定的模块 + for (i = 0; i < modules.length; i += 1) { + Sandbox.modules[modules[i]](this); + } + + // 调用回调函数 + callback(this); + } + + // 需要添加在原型上的属性 + Sandbox.prototype = { + name: "My Application", + version: "1.0", + getName: function () { + return this.name; + } + }; + +这个实现中的一些关键点: + +- 有一个检查`this`是否是`Sandbox()`实例的过程,如果不是(也就是调用`Sandbox()`时没有加`new`),我们将这个函数作为构造函数再调用一次。 +- 你可以在构造函数中给`this`添加属性,也可以给构造函数的原型添加属性。 +- 被依赖的模块可以以数组的形式传递,也可以作为单独的参数传递,甚至以`*`通配符(或者省略)来表示加载所有可用的模块。值得注意的是,我们在这个示例实现中并没有考虑从外部文件中加载模块,但明显这是一个值得考虑的事情。比如YUI3就支持这种情况,你可以只加载最基本的模块(作为“种子”),其余需要的任何模块都通过将模块名和文件名对应的方式从外部文件中加载。 +- 当我们知道依赖的模块之后就初始化它们,也就是调用实现每个模块的函数。 +- 构造函数的最后一个参数是回调函数。这个回调函数会在最后使用新创建的实例来调用。事实上这个回调函数就是用户的沙箱,它被传入一个`box`对象,这个对象包含了所有依赖的功能。 + +## 静态成员 + +静态属性和方法是指那些在所有的实例中都一样的成员。在基于类的语言中,静态成员是用专门的语法来创建,使用时就像是类自己的成员一样。比如`MathUtils`类的`max()`方法会被像这样调用:`MathUtils.max(3, 5)`。这是一个公有静态成员的示例,即可以在不实例化类的情况下使用。同样也可以有私有的静态方法,即对类的使用者不可见,而在类的所有实例间是共享的。我们来看一下如何在JavaScript中实现公有和私有静态成员。 + +### 公有静态成员 + +在JavaScript中没有专门用于静态成员的语法。但通过给构造函数添加属性的方法,可以拥有和基于类的语言一样的使用语法。之所以可以这样做是因为构造函数和其它的函数一样,也是对象,可以拥有属性。前一章讨论过的记忆模式也使用了同样的方法,即给函数添加属性。 + +下面的例子定义了一个构造函数`Gadget()`,它有一个静态方法`isShiny()`和一个实例方法`setPrice()`。`isShiny()`是一个静态方法,因为它不需要指定一个具体的对象就能工作(你不需要先拿到一个特定的小工具(gadget)才知道所有小工具是不是有光泽的(shiny))。但setPrice()却需要一个对象,因为小工具可能有不同的定价: + + // 构造函数 + var Gadget = function () {}; + + // 静态方法 + Gadget.isShiny = function () { + return "you bet"; + }; + + // 添加到原型的普通方法 + Gadget.prototype.setPrice = function (price) { + this.price = price; + }; + +现在我们来调用这些方法。静态方法`isShiny()`可以直接在构造函数上调用,但其它的方法需要一个实例: + + // 调用静态方法 + Gadget.isShiny(); // "you bet" + + // 创建实例并调用方法 + var iphone = new Gadget(); + iphone.setPrice(500); + +使用静态方法的调用方式去调用实例方法并不能正常工作,同样,用调用实例方法的方式来调用静态方法也不能正常工作: + + typeof Gadget.setPrice; // "undefined" + typeof iphone.isShiny; // "undefined" + +有时候让静态方法也能用在实例上会很方便。我们可以通过在原型上加一个新方法来很容易地做到这点,这个新方法作为原来的静态方法的一个包装: + + Gadget.prototype.isShiny = Gadget.isShiny; + iphone.isShiny(); // "you bet" + +在这种情况下,你需要很小心地处理静态方法内的`this`。当你运行`Gadget.isShiny()`时,在`isShiny()`内部的`this`指向`Gadget`构造函数。而如果你运行`iphone.isShiny()`,那么`this`会指向`iphone`。 + +下面的例子展示了同一个方法被静态调用和非静态调用时明显不同的行为,这取决于调用的方式。这里的`instanceof`用于获取方法是如何被调用的: + + // 构造函数 + var Gadget = function (price) { + this.price = price; + }; + + // 静态方法 + Gadget.isShiny = function () { + + // 这句始终正常工作 + var msg = "you bet"; + + if (this instanceof Gadget) { + // 这句只有在非静态方式调用时正常工作 + msg += ", it costs $" + this.price + '!'; + } + + return msg; + }; + + // 原型上添加的方法 + Gadget.prototype.isShiny = function () { + return Gadget.isShiny.call(this); + }; + +测试一下静态方法调用: + + Gadget.isShiny(); // "you bet" + +测试一下实例中的非静态调用: + + var a = new Gadget('499.99'); + a.isShiny(); // "you bet, it costs $499.99!" + +### 私有静态成员 + +到目前为止,我们都只讨论了公有的静态方法,现在我们来看一下如何实现私有静态成员。所谓私有静态成员是指: + +- 被所有由同一构造函数创建的对象共享 +- 不允许在构造函数外部访问 + +我们来看一个例子,`counter`是`Gadget()`构造函数的一个私有静态属性。在本章中我们已经讨论过私有属性,这里的做法也是一样,需要一个函数提供的闭包来包裹私有成员。然后让这个包裹函数立即执行并返回一个新的函数。将这个返回的函数赋值给`Gadget()`作为构造函数。 + + var Gadget = (function () { + + // 静态变量/属性 + var counter = 0; + + // 返回构造函数的新实现 + return function () { + console.log(counter += 1); + }; + + }()); // 立即执行 + +这个`Gadget()`构造函数只简单地增加私有变量`counter`的值然后打印出来。用多个实例测试的话你会看到`counter`在实例之间是共享的: + + var g1 = new Gadget();// logs 1 + var g2 = new Gadget();// logs 2 + var g3 = new Gadget();// logs 3 + +因为我们在创建每个实例的时候`counter`的值都会加1,所以它实际上成了唯一标识使用`Gadget`构造函数创建的对象的ID。这个唯一标识可能会很有用,那为什么不把它通过一个特权方法暴露出去呢?(译注:严格来讲,这里不能叫ID,只是一个记录有多少个实例的数字而已,因为如果有多个实例被创建的话,没有办法取到除了最后一个之外的实例的标识。)下面的例子是基于前面的例子,增加了用于访问私有静态属性的`getLastId()`方法: + + // 构造函数 + var Gadget = (function () { + + // 静态变量/属性 + var counter = 0, + NewGadget; + + // 这将是Gadget的新实现 + NewGadget = function () { + counter += 1; + }; + + // 特权方法 + NewGadget.prototype.getLastId = function () { + return counter; + }; + + // 重写构造函数 + return NewGadget; + + }()); // 立即执行 + +测试这个新的实现: + + var iphone = new Gadget(); + iphone.getLastId(); // 1 + var ipod = new Gadget(); + ipod.getLastId(); // 2 + var ipad = new Gadget(); + ipad.getLastId(); // 3 + +静态属性(包括私有和公有)有时候会非常方便,它们可以包含和具体实例无关的方法和数据,而不用在每次实例中再创建一次。当我们在第七章中讨论单例模式时,你可以看到使用静态属性实现类式单例构造函数的例子。 + +## 对象常量 + +在一些比较现代的环境中可能会提供`const`来创建常量,但在其它的环境中,JavaScript是没有常量的。 + +一种常用的解决办法是通过命名规范,让不应该变化的变量使用全大写。这个规范实际上也用在JavaScript原生对象中: + + Math.PI; // 3.141592653589793 + Math.SQRT2; // 1.4142135623730951 + Number.MAX_VALUE; // 1.7976931348623157e+308 + +你自己的常量也可以用这种规范,然后将它们作为静态属性加到构造函数中: + + // 构造函数 + var Widget = function () { + // 实现…… + }; + + // 常量 + Widget.MAX_HEIGHT = 320; + Widget.MAX_WIDTH = 480; + +同样的规范也适用于使用字面量创建的对象,常量会是使用大写名字的属性。 + +如果你真的希望有一个不能被改变的值,那么可以创建一个私有属性,然后提供一个取值的方法(getter),但不给赋值的方法(setter)。这种方法在很多可以用命名规范解决的情况下可能有些矫枉过正,但不失为一种选择。 + +下面是一个通用的`constant`对象的实现,它提供了这些方法: + +- set(name, value) + + 定义一个新的常量 + +- isDefined(name) + + 检查一个常量是否存在 + +- get(name) + + 取常量的值 + +在这个实现中,只允许基本类型的值成为常量。同时还要使用`hasOwnProperty()`小心地处理那些恰好是原生属性的常量名,比如`toString`或者`hasOwnProperty`,然后给所有的常量名加上一个随机生成的前缀: + + var constant = (function () { + var constants = {}, + ownProp = Object.prototype.hasOwnProperty, + allowed = { + string: 1, + number: 1, + boolean: 1 + }, + prefix = (Math.random() + "_").slice(2); + return { + set: function (name, value) { + if (this.isDefined(name)) { + return false; + } + if (!ownProp.call(allowed, typeof value)) { + return false; + } + constants[prefix + name] = value; + return true; + }, + isDefined: function (name) { + return ownProp.call(constants, prefix + name); + }, + get: function (name) { + if (this.isDefined(name)) { + return constants[prefix + name]; + } + return null; + } + }; + }()); + +测试这个实现: + + // 检查是否定义 + constant.isDefined("maxwidth"); // false + + // 定义 + constant.set("maxwidth", 480); // true + + // 再次检查 + constant.isDefined("maxwidth"); // true + + // 尝试重定义 + constant.set("maxwidth", 320); // false + + // 看看这个值是否被改变 + constant.get("maxwidth"); // 480 + +## 链式调用模式 + +使用链式调用模式可以让你在一对个象上连续调用多个方法,不需要将前一个方法的返回值赋给变量,也不需要将多个方法调用分散在多行: + + myobj.method1("hello").method2().method3("world").method4(); + +当你创建了一个没有有意义的返回值的方法时,你可以让它返回`this`,也就是这些方法所属的对象。这使得对象的使用者可以将下一个方法的调用和前一次调用链起来: + + var obj = { + value: 1, + increment: function () { + this.value += 1; + return this; + }, + add: function (v) { + this.value += v; + return this; + }, + shout: function () { + alert(this.value); + } + }; + + // 链式方法调用 + obj.increment().add(3).shout(); // 5 + + // 单独调用每个方法 + obj.increment(); + obj.add(3); + obj.shout(); // 5 + +### 链式调用模式的利弊 + +使用链式调用模式的一个好处就是可以节省代码量,使得代码更加简洁和易读,读起来就像在读句子一样。 + +另外一个好处就是帮助你思考如何拆分你的函数,创建更小、更有针对性的函数,而不是一个什么都做的函数。长时间来看,这会提升代码的可维护性。 + +一个弊端是调试这样写的代码会更困难。你可能知道一个错误出现在某一行,但这一行要做很多的事情。当链式调用的方法中的某一个出现问题而又没报错时,你无法知晓到底是哪一个出问题了。《代码整洁之道》的作者Robert Martion甚至叫这种模式为“train wreck”模式。(译注:直译为“火车事故”,指负面影响比较大。) + +不管怎样,认识这种模式总是好的,当你写的方法没有明显的有意义的返回值时,你就可以返回`this`。这个模式应用得很广泛,比如jQuery库。如果你去看DOM的API的话,你会发现它也会以这样的形式倾向于链式调用: + + document.getElementsByTagName('head')[0].appendChild(newnode); + +## method()方法 + +JavaScript对于习惯于用类来思考的人来说可能会比较费解,这也是很多开发者希望将JavaScript代码变得更像基于类的语言的原因。其中的一种尝试就是由Douglas Crockford提出来的`method()`方法。其实,他也承认将JavaScript变得像基于类的语言是不推荐的方法,但不管怎样,这都是一种有意思的模式,你可能会在一些应用中见到。 + +使用构造函数就像Java中使用类一样。它也允许你在构造函数体的`this`中添加实例属性。但是在`this`中添加方法却是不高效的,因为最终这些方法会在每个实例中被重新创建一次,这样会花费更多的内存。这也是为什么可重用的方法应该被放到构造函数的`prototype`属性(原型)中的原因。但对很多开发者来说,`prototype`可能跟个外星人一样陌生,所以你可以通过一个方法将它隐藏起来。 + +> 给语言添加一个使用起来更方便的方法一般叫作“语法糖”。在这个例子中,你可以将`method()`方法称为一个语法糖方法。 + +使用这个语法糖方法`method()`来定义一个“类”是像这样: + + var Person = function (name) { + this.name = name; + }. + method('getName', function () { + return this.name; + }). + method('setName', function (name) { + this.name = name; + return this; + }); + +注意构造函数和调用`method()`是如何链起来的,接下来又链式调用了下一个`method()`方法。这就是我们前面讨论的链式调用模式,可以帮助我们用一个语句完成对整个“类”的定义。 + +`method()`方法接受两个参数: + +- 新方法的名字 +- 新方法的实现 + +然后这个新方法被添加到`Person`“类”。新方法的实现也只是一个函数,在这个函数里面`this`指向由`Person()`创建的对象,正如我们期望的那样。 + +下面是使用`Person()`创建和使用新对象的代码: + + var a = new Person('Adam'); + a.getName(); // 'Adam' + a.setName('Eve').getName(); // 'Eve' + +同样地注意链式调用,因为`setName()`返回了`this`就可以链式调用了。 + +最后是`method()`方法的实现: + + if (typeof Function.prototype.method !== "function") { + Function.prototype.method = function (name, implementation) { + this.prototype[name] = implementation; + return this; + }; + } + +在`method()`的实现中,我们首先检查这个方法是否已经被实现过,如果没有则继续,将传入的参数`implementation`加到构造函数的原型中。在这里`this`指向构造函数,而我们要增加的功能正好在这个构造函数的原型上。 + +## 小结 + +在本章中你看到了好几种除了字面量和构造函数之外的创建对象的方法。 + +你看到了使用命名空间模式来保持全局空间干净和帮助组织代码,看到了简单而又有用的依赖声明模式。然后我们详细讨论了有关私有成员的模式,包括私有成员、特权方法以及一些涉及私有成员的极端情况,还有使用对象字面量创建私有成员以及将私有方法暴露为公有方法。所有这些模式都是搭建起现在流行而强大的模块模式的积木。 + +然后你看到了使用沙箱模式作为长命名空间的另一种选择,它可以为你的代码和模块提供独立的环境。 + +在最后,我们深入讨论了对象常量、静态成员(公有和私有)、链式调用模式,以及神奇的`method()`方法。 \ No newline at end of file diff --git a/chapter6.markdown b/chapter6.markdown new file mode 100644 index 0000000..f149d9f --- /dev/null +++ b/chapter6.markdown @@ -0,0 +1,799 @@ +# 代码复用模式 + +代码复用是一个既重要又有趣的话题。如果你面对自己或者别人已经写好的代码,而这些代码又是经过测试的、可维护的、可扩展的、有文档的,这时候你只想写尽量少且可以被复用的代码就是一个再自然不过的想法。 + +当我们说到代码复用的时候,想到的第一件事就是继承,本章会有很大篇幅讲述这个话题,你将看到好多种方法来实现“类式(classical)”和一些其它方式的继承。但是,最最重要的事情,是你需要记住终极目标——代码复用。继承是达到这个目标的一种方法,但是不是唯一的。在本章,你将看到怎样基于其它对象来构建新对象,怎样使用混元,以及怎样在不使用继承的情况下只复用你需要的功能。 + +在做代码复用的工作的时候,谨记Gang of Four在书中给出的关于对象创建的建议:“优先使用对象创建而不是类继承”。(译注:《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)是一本设计模式的经典书籍,该书作者为Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides,被称为“Gang of Four”,简称“GoF”。) + +## 类式继承 vs 现代继承模式 + +在讨论JavaScript的继承这个话题的时候,经常会听到“类式继承”的概念,那我们先看一下什么是类式(classical)继承。classical一词并不是来自某些古老的、固定的或者是被广泛接受的解决方案,而仅仅是来自单词“class”。(译注:classical也有“经典”的意思。) + +很多编程语言都有原生的类的概念,以此作为对象的蓝本。在这些语言中,每个对象都是一个指定类的实例(instance),并且(以Java为例)一个对象不能在不存在对应的类的情况下存在。在JavaScript中,因为没有类,所以类的实例的概念没什么意义。JavaScript的对象仅仅是简单的键值对,这些键值对都可以动态创建或者是改变。 + +但是JavaScript拥有构造函数(constructor functions),并且有语法和使用类非常相似的`new`运算符。 + +在Java中你可能会这样写: + + Person adam = new Person(); + +在JavaScript中你可以这样: + + var adam = new Person(); + +除了Java是强类型语言需要给`adam`添加类型`Person`外,其它的语法看起来是一样的。JavaScript的构造函数调用方式看起来让人感觉`Person()`是一个类,但事实上,`Person()`仅仅是一个函数。语法上的相似使得非常多的开发者陷入对JavaScript类的思考,并且给出了很多模拟类的继承方案。这样的实现方式,我们叫它“类式继承”。顺便也提一下,所谓“现代”继承模式是指那些不需要你去想类这个概念的模式。 + +当需要给项目选择一个继承模式时,有不少的备选方案。你应该尽量选择那些现代继承模式,除非团队已经觉得“无类不欢”。 + +本章先讨论类式继承,然后再关注现代继承模式。 + +## 类式继承的期望结果 + +实现类式继承的目标是基于构造函数`Child()`来创建一个对象,然后从另一个构造函数`Parent()`获得属性。 + +> 尽管我们是在讨论类式继承,但还是尽量避免使用“类”这个词。“构造函数”或者“constructor”虽然更长,但是更准确,不会让人迷惑。通常情况下,应该努力避免在跟团队沟通的时候使用“类”这个词,因为在JavaScript中,很可能每个人都会有不同的理解。 + +下面是定义两个构造函数`Parent()`和`Child()`的例子: + + //Parent构造函数 + function Parent(name) { + this.name = name || 'Adam'; + } + + //给原型增加方法 + Parent.prototype.say = function () { + return this.name; + }; + + //空的Child构造函数 + function Child(name) {} + + //继承 + inherit(Child, Parent); + +上面的代码定义了两个构造函数`Parent()`和`Child()`,`say()`方法被添加到了`Parent()`构建函数的原型(`prototype`)中,`inherit()`函数完成了继承的工作。`inherit()`函数并不是原生提供的,需要自己实现。让我们来看一看比较常见的实现它的几种方法。 + +## 类式继承1——默认模式 + +最常用的一种模式是使用`Parent()`构造函数来创建一个对象,然后把这个对象设为`Child()`的原型。这是可复用的`inherit()`函数的第一种实现方法: + + function inherit(C, P) { + C.prototype = new P(); + } + +需要强调的是原型(`prototype`属性)应该指向一个对象,而不是函数,所以它需要指向由被继承的构造函数创建的实例(对象),而不是构造函数自己。换句话说,请注意`new`运算符,有了它这种模式才可以正常工作。 + +之后在应用中使用`new Child()`创建对象的时候,它将通过原型拥有`Parent()`实例的功能,像下面的例子一样: + + var kid = new Child(); + kid.say(); // "Adam" + +### 跟踪原型链 + +在这种模式中,子对象既继承了(父对象的)“自有属性”(添加给`this`的实例属性,比如`name`),也继承了原型中的属性和方法(比如`say()`)。 + +我们来看一下在这种继承模式中原型链是怎么工作的。为了讨论方便,我们假设对象是内存中的一块空间,它包含数据和指向其它空间的引用。当使用`new Parent()`创建一个对象时,这样的一块空间就被分配了(图6-1中的2号),它保存着`name`属性的数据。如果你尝试访问`say()`方法(比如通过`(new Parent).say()`),2号空间中并没有这个方法。但是在通过隐藏的链接`__proto__`指向`Parent()`构建函数的原型`prototype`属性时,就可以访问到包含`say()`方法的1号空间(`Parent.prototype`)了。所有的这一块都是在幕后发生的,不需要任何额外的操作,但是知道它是怎样工作的有助于让你明白你正在访问或者修改的数据在哪,这是很重要的。注意,`__proto__`在这里只是为了解释原型链而存在,这个属性在语言本身中是不可用的,尽管有一些环境提供了(比如Firefox)。 + +![图6-1 Parent()构造函数的原型链](./Figure/chapter6/6-1.jpg) + +图6-1 Parent()构造函数的原型链 + +现在我们来看一下在使用`inherit()`函数之后再使用`var kid = new Child()`创建一个新对象时会发生什么。见图6-2。 + +![图6-2 继承后的原型链](./Figure/chapter6/6-2.jpg) + +图6-2 继承后的原型链 + +`Child()`构造函数是空的,也没有属性添加到`Child.prototype`上,这样,使用`new Child()`创建出来的对象都是空的,除了有隐藏的链接`__proto__`。在这个例子中,`__proto__`指向在`inherit()`函数中创建的`new Parent()`对象。 + +现在使用`kid.say()`时会发生什么?3号对象没有这个方法,所以通过原型链找到2号。2号对象也没有这个方法,所以也通过原型链找到1号,刚好有这个方法。接下来`say()`方法引用了`this.name`,这个变量也需要解析,于是沿原型链查找的过程又走了一遍。在这个例子中,`this`指向3号对象,它没有`name`属性,然后2号对象被访问,并且有`name`属性,值为“Adam”。 + +最后,我们看一点额外的东西,假如我们有如下的代码: + + var kid = new Child(); + kid.name = "Patrick"; + kid.say(); // "Patrick" + +图6-3展现了这个例子的原型链: + +![图6-3 继承并且给子对象添加属性后的原型链](./Figure/chapter6/6-3.jpg) + +图6-3 继承并且给子对象添加属性后的原型链 + +设定`kid.name`并没有改变2号对象的`name`属性,但是却直接在3号对象上添加了自有的`name`属性。当`kid.say()`执行时,`say()`方法会依次在3号对象中找,然后是2号,最后到1号,像前面说的一样。但是这一次在找`this.name`(和`kid.name`一样)时很快,因为这个属性在3号对象中就被找到了。 + +如果通过`delete kid.name`的方式移除新添加的属性,那么2号对象的`name`属性就将被暴露出来并且在查找的时候被找到。 + +### 这种模式的缺点 + +这种模式的一个缺点是既继承了(父对象的)“自有属性”,也继承了原型中的属性。大部分情况下你可能并不需要“自有属性”,因为它们更可能是为实例对象添加的,并不用于复用。 + +> 一个在构造函数上常用的规则是,用于复用的成员(译注:属性和方法)应该被添加到原型上。 + +在使用这个`inherit()`函数时另外一个不便是它不能够让你传参数给子构造函数,这些参数有可能是想再传给父构造函数的。考虑下面的例子: + + var s = new Child('Seth'); + s.say(); // "Adam" + +这并不是我们期望的结果。事实上传递参数给父构造函数是可能的,但这样需要在每次需要一个子对象时再做一次继承,很不方便,因为需要不断地创建父对象。 + +## 类式继承2——借用构造函数 + +下面这种模式解决了从子对象传递参数到父对象的问题。它借用了父对象的构造函数,将子对象绑定到`this`,同时传入参数: + + function Child(a, c, b, d) { + Parent.apply(this, arguments); + } + +使用这种模式时,只能继承在父对象的构造函数中添加到`this`的属性,不能继承原型上的成员。 + +使用借用构造函数的模式,子对象通过复制的方式继承父对象的成员,而不是像类式继承1中那样通过引用的方式。下面的例子展示了这两者的不同: + + //父构造函数 + function Article() { + this.tags = ['js', 'css']; + } + var article = new Article(); + + //BlogPost通过类式继承1(默认模式)从article继承 + function BlogPost() {} + BlogPost.prototype = article; + var blog = new BlogPost(); + //注意你不需要使用`new Article()`,因为已经有一个实例了 + + //StaticPage通过借用构造函数的方式从Article继承 + function StaticPage() { + Article.call(this); + } + var page = new StaticPage(); + + alert(article.hasOwnProperty('tags')); // true + alert(blog.hasOwnProperty('tags')); // false + alert(page.hasOwnProperty('tags')); // true + +在上面的代码片段中,`Article()`被用两种方式分别继承。默认模式使`blog`可以通过原型链访问到`tags`属性,所以它自己并没有`tags`属性,`hasOwnProperty()`返回`false`。`page`对象有自己的`tags`属性,因为它是使用借用构造函数的方式继承,复制(而不是引用)了`tags`属性。 + +注意在修改继承后的`tags`属性时的不同表现: + + blog.tags.push('html'); + page.tags.push('php'); + alert(article.tags.join(', ')); // "js, css, html" + +在这个例子中,`blog`对象修改了`tags`属性,同时,它也修改了父对象,因为实际上`blog.tags`和`article.tags`是引向同一个数组。而对`pages.tags`的修改并不影响父对象`article`,因为`pages.tags`在继承的时候是一份独立的拷贝。 + +### 原型链 + +我们来看一下当我们使用熟悉的Parent()和Child()构造函数和这种继承模式时原型链是什么样的。为了使用这种继承模式,`Child()`有明显变化: + + //父构造函数 + function Parent(name) { + this.name = name || 'Adam'; + } + + //在原型上添加方法 + Parent.prototype.say = function () { + return this.name; + }; + + //子构造函数 + function Child(name) { + Parent.apply(this, arguments); + } + + var kid = new Child("Patrick"); + kid.name; // "Patrick" + typeof kid.say; // "undefined" + +如果看一下图6-4,就能发现`new Child()`对象和`Parent()`之间不再有链接。这是因为`Child.prototype`根本就没有被使用,它指向一个空对象。使用这种模式,`kid`拥有了自有的`name`属性,但是并没有继承`say()`方法,如果尝试调用它的话会出错。这种继承方式只是一种一次性地将父对象的属性复制为子对象的属性,并没有`__proto__`链接。 + +![图6-4 使用借用构造函数模式时没有被关联的原型链](./Figure/chapter6/6-4.jpg) + +图6-4 使用借用构造函数模式时没有被关联的原型链 + +### 利用借用构造函数模式实现多继承 + +使用借用构造函数模式,可以通过借用多个构造函数的方式来实现多继承: + + function Cat() { + this.legs = 4; + this.say = function () { + return "meaowww"; + } + } + + function Bird() { + this.wings = 2; + this.fly = true; + } + + function CatWings() { + Cat.apply(this); + Bird.apply(this); + } + + var jane = new CatWings(); + console.dir(jane); + +结果如图6-5,任何重复的属性都会以最后的一个值为准。 + +![图6-5 在Firebug中查看CatWings对象](./Figure/chapter6/6-5.jpg) + +图6-5 在Firebug中查看CatWings对象 + +### 借用构造函数的利与弊 + +这种模式的一个明显的弊端就是无法继承原型。如前面所说,原型往往是添加可复用的方法和属性的地方,这样就不用在每个实例中再创建一遍。 + +这种模式的一个好处是获得了父对象自有成员的拷贝,不存在子对象意外改写父对象属性的风险。 + +那么,在上一个例子中,怎样使一个子对象也能够继承原型属性呢?怎样能使`kid`可以访问到`say()`方法呢?下一种继承模式解决了这个问题。 + +## 类式继承3——借用并设置原型 + +综合以上两种模式,首先借用父对象的构造函数,然后将子对象的原型设置为父对象的一个新实例: + + function Child(a, c, b, d) { + Parent.apply(this, arguments); + } + Child.prototype = new Parent(); + +这样做的好处是子对象获得了父对象的自有成员,也获得了父对象中可复用的(在原型中实现的)方法。子对象也可以传递任何参数给父构造函数。这种行为可能是最接近Java的,子对象继承了父对象的所有东西,同时可以安全地修改自己的属性而不用担心修改到父对象。 + +一个弊端是父构造函数被调用了两次,所以不是很高效。最后,(父对象的)自有属性(比如这个例子中的`name`)也被继承了两次。 + +我们来看一下代码并做一些测试: + + //父构造函数 + function Parent(name) { + this.name = name || 'Adam'; + } + + //在原型上添加方法 + Parent.prototype.say = function () { + return this.name; + }; + + //子构造函数 + function Child(name) { + Parent.apply(this, arguments); + } + Child.prototype = new Parent(); + + var kid = new Child("Patrick"); + kid.name; // "Patrick" + kid.say(); // "Patrick" + delete kid.name; + kid.say(); // "Adam" + +跟前一种模式不一样,现在`say()`方法被正确地继承了。可以看到`name`也被继承了两次,在删除掉自己的拷贝后,在原型链上的另一个就被暴露出来了。 + +图6-6展示了这些对象之间的关系。这些关系有点像图6-3中展示的,但是获得这种关系的方法是不一样的。 + +![图6-6 除了继承“自己的属性”外,原型链也被保留了](./Figure/chapter6/6-6.jpg) + +图6-6 除了继承“自己的属性”外,原型链也被保留了 + +## 类式继承4——共享原型 + +不像前一种类式继承模式需要调用两次父构造函数,下面这种模式根本不会涉及到调用父构造函数的问题。 + +一般的经验是将可复用的成员放入原型中而不是`this`。从继承的角度来看,则是任何应该被继承的成员都应该放入原型中。这样你只需要设定子对象的原型和父对象的原型一样即可: + + function inherit(C, P) { + C.prototype = P.prototype; + } + +这种模式的原型链很短并且查找很快,因为所有的对象实际上共享着同一个原型。但是这样也有弊端,那就是如果子对象或者在继承关系中的某个地方的任何一个子对象修改这个原型,将影响所有的继承关系中的父对象。(译注:指会影响到所有从这个原型中继承的对象所依赖的共享原型上的成员。) + +如图6-7,子对象和父对象共享同一个原型,都可以访问`say()`方法。但是,子对象不继承`name`属性。 + +![图6-7 (父子对象)共享原型时的关系](./Figure/chapter6/6-7.jpg) + +图6-7 (父子对象)共享原型时的关系 + +## 类式继承5——临时构造函数 + +下一种模式通过打断父对象和子对象原型的直接链接解决了共享原型时的问题,同时还从原型链中获得其它的好处。 + +下面是这种模式的一种实现方式,`F()`函数是一个空函数,它充当了子对象和父对象的代理。`F()`的`prototype`属性指向父对象的原型。子对象的原型是这个空函数的一个实例: + + function inherit(C, P) { + var F = function () {}; + F.prototype = P.prototype; + C.prototype = new F(); + } + +这种模式有一种和默认模式(类式继承1)明显不一样的行为,因为在这里子对象只继承原型中的属性(图6-8)。 + +![图6-8 使用临时(代理)构造函数F()实现类式继承](./Figure/chapter6/6-8.jpg) + +图6-8 使用临时(代理)构造函数F()实现类式继承 + +这种模式通常情况下都是一种很棒的选择,因为原型本来就是存放复用成员的地方。在这种模式中,父构造函数添加到`this`中的任何成员都不会被继承。 + +我们来创建一个子对象并且检查一下它的行为: + + var kid = new Child(); + +如果你访问`kid.name`将得到`undefined`。在这个例子中,`name`是父对象自己的属性,而在继承的过程中我们并没有调用`new Parent()`,所以这个属性并没有被创建。当访问`kid.say()`时,它在3号对象中不可用,所以在原型链中查找,4号对象也没有,但是1号对象有,它在内存中的位置会被所有从`Parent()`创建的构造函数和子对象所共享。 + +### 存储父类(Superclass) + +在上一种模式的基础上,还可以添加一个指向原始父对象的引用。这很像其它语言中访问超类(superclass)的情况,有时候很方便。 + +我们将这个属性命名为“uber”,因为“super”是一个保留字,而“superclass”则可能误导别人认为JavaScript拥有类。下面是这种类式继承模式的一个改进版实现: + + function inherit(C, P) { + var F = function () {}; + F.prototype = P.prototype; + C.prototype = new F(); + C.uber = P.prototype; + } + +### 重置构造函数引用 + +这个近乎完美的模式上还需要做的最后一件事情就是重置构造函数(`constructor`)的指向,以便未来在某个时刻能被正确地使用。 + +如果不重置构造函数的指向,那所有的子对象都会认为`Parent()`是它们的构造函数,而这个结果完全没有用。使用前面的`inherit()`的实现,你可以观察到这种行为: + + // Parent,Child,实现继承 + function Parent() {} + function Child() {} + inherit(Child, Parent); + + // 测试 + var kid = new Child(); + kid.constructor.name; // "Parent" + kid.constructor === Parent; // true + +`constructor`属性很少被用到,但是在运行时检查对象很方便。你可以重新将它指向期望的构造函数而不影响功能,因为这个属性更多是“信息性”的。(译注:即它更多的时候是在提供信息而不是参与到函数功能中。) + +最终,这种类式继承的Holy Grail版本看起来是这样的: + + function inherit(C, P) { + var F = function () {}; + F.prototype = P.prototype; + C.prototype = new F(); + C.uber = P.prototype; + C.prototype.constructor = C; + } + +类似这样的函数也存在于YUI库(也许还有其它库)中,它将类式继承的方法带给了没有类的语言。如果你决定使用类式继承,那么这是最好的方法。 + +> “代理函数”或者“代理构造函数”也是指这种模式,因为临时构造函数是被用作获取父构造函数原型的代理。 + +一种常见的对Holy Grail模式的优化是避免每次需要继承的时候都创建一个临时(代理)构造函数。事实上创建一次就足够了,以后只需要修改它的原型即可。你可以用一个即时函数来将代理函数存储到闭包中: + + var inherit = (function () { + var F = function () {}; + return function (C, P) { + F.prototype = P.prototype; + C.prototype = new F(); + C.uber = P.prototype; + C.prototype.constructor = C; + } + }()); + +## Klass + +有很多JavaScript类库模拟了类,创造了新的语法糖。这些类库具体的实现方式可能会不一样,但是基本上都有一些共性,包括: + +- 有一个约定好的方法,如`initialize`、`_init`或者其它相似的名字,会被自动调用,来充当类的构造函数 +- 类可以从其它类继承 +- 在子类中可以访问到父类(superclass) + +> 我们在这里做一点变化,在本章的这部分自由地使用“class”这个词,因为主题就是模拟类。 + +为避免讨论太多细节,我们来看一下JavaScript中一种模拟类的实现。首先,看一下这种方案将如何被使用? + + var Man = klass(null, { + __construct: function (what) { + console.log("Man's constructor"); + this.name = what; + }, + getName: function () { + return this.name; + } + }); + +这种语法糖的形式是一个名为`klass()`的函数。在一些其它的实现方式中,它可能是`Klass()`构造函数或者是增强的`Object.prototype`,但是在这个例子中,我们让它只是一个简单的函数。 + +这个函数接受两个参数:一个被继承的类和通过对象字面量提供的新类的实现。受PHP的影响,我们约定类的构造函数必须是一个名为`__construct()`的方法。在前面的代码片段中,建立了一个名为`Man`的新类,并且它不继承任何类(意味着继承自`Object`)。`Man`类有一个在`__construct()`建立的自有属性`name`和一个方法`getName()`。这个类是一个构造函数,所以下面的代码将正常工作(并且看起来像类实例化的过程): + + var first = new Man('Adam'); // logs "Man's constructor" + first.getName(); // "Adam" + +现在我们来扩展这个类,创建一个`SuperMan`类: + + var SuperMan = klass(Man, { + __construct: function (what) { + console.log("SuperMan's constructor"); + }, + getName: function () { + var name = SuperMan.uber.getName.call(this); + return "I am " + name; + } + }); + +这里,`klass()`的第一个参数是将被继承的`Man`类。值得注意的是,在`getName()`中,父类的`getName()`方法首先通过`SuperMan`类的`uber`静态属性被调用。我们来测试一下: + + var clark = new SuperMan('Clark Kent'); + clark.getName(); // "I am Clark Kent" + +第一行在console中记录了“Man's constructor”,然后是“Superman's constructor”,在一些语言中,父类的构造函数在子类构造函数被调用的时候会自动执行,这个特性也被模拟了。 + +用`instanceof`运算符测试返回希望的结果: + + clark instanceof Man; // true + clark instanceof SuperMan; // true + +最后,我们来看一下`klass()`函数是怎样实现的: + + var klass = function (Parent, props) { + + var Child, F, i; + + // 1. 构造函数 + Child = function () { + if (Child.uber && Child.uber.hasOwnProperty("__construct")) { + Child.uber.__construct.apply(this, arguments); + } + if (Child.prototype.hasOwnProperty("__construct")) { + Child.prototype.__construct.apply(this, arguments); + } + }; + + // 2. 继承 + Parent = Parent || Object; + F = function () {}; + F.prototype = Parent.prototype; + Child.prototype = new F(); + Child.uber = Parent.prototype; + Child.prototype.constructor = Child; + + // 3. 添加方法实现 + for (i in props) { + if (props.hasOwnProperty(i)) { + Child.prototype[i] = props[i]; + } + } + + // 返回“类” + return Child; + }; + +这个`klass()`实现有三个明显的部分: + +1. 创建`Child()`构造函数,这也是最后返回的将被作为类使用的函数。在这个函数里面,如果`__construct()`方法存在的话将被调用,同样,如果父类的`__construct()`存在,也将被调用(通过使用静态属性`uber`)。也可能存在`uber`没有定义的情况——比如从`Object`继承,前例中`Man`类即是如此。 +2. 第二部分主要完成继承。只是简单地使用前面章节讨论过的Holy Grail类式继承模式。只有一个东西是新的:如果`Parent`没有传值的话,设定`Parent`为`Object`。 +3. 最后一部分是真正定义类的地方,遍历需要实现的方法(如例子中的`__constructor()`和`getName()`),并将它们添加到`Child()`的原型中。 + +什么时候使用这种模式呢?其实,最好是能避免则避免,因为它带来了在这门语言中不存在的完整的类的概念,会让人疑惑。使用它需要学习新的语法和新的规则,也就是说,如果你或者你的团队习惯于使用类并且对原型感到不习惯,这种模式可能是一个可以探索的方向。这种模式允许你完全忘掉原型,好处就是你可以使用像其它语言那样的(变种)语法。 + +## 原型继承 + +现在,让我们从一个叫作“原型继承”的模式来讨论没有类的现代继承模式。在这种模式中,没有任何类牵涉进来,一个对象继承自另外一个对象。你可以这样理解它:你有一个想复用的对象,然后你想创建第二个对象,并且获得第一个对象的功能。下面是这种模式的用法: + + // 需要继承的对象 + var parent = { + name: "Papa" + }; + + // 新对象 + var child = object(parent); + + // 测试 + alert(child.name); // "Papa" + +在这个代码片段中,有一个已经存在的使用对象字面量创建的对象叫`parent`,我们想创建一个和`parent`有相同的属性和方法的对象叫`child`。`child`对象使用`object()`函数创建。这个函数在JavaScript中并不存在(不要与构造函数`Object()`混淆),所以我们来看看怎样定义它。 + +与Holy Grail类式继承相似,可以使用一个空的临时构造函数`F()`,然后设定`F()`的原型为`parent`对象。最后,返回一个临时构造函数的新实例。 + + function object(o) { + function F() {} + F.prototype = o; + return new F(); + } + +图6-9展示了使用原型继承时的原型链。这样创建的`child`总是一个空对象,它没有自有属性但通过原型链(`__proto__`)拥有父对象的所有功能。 + +![图6-9 原型继承模式](./Figure/chapter6/6-9.jpg) + +图6-9 原型继承模式 + +### 讨论 + +在原型继承模式中,`parent`不一定需要使用对象字面量来创建(尽管这是一种常用的方式),也可以使用构造函数来创建。注意,如果你这样做,那么自有属性和原型上的属性都将被继承: + + // 父构造函数 + function Person() { + // 自有属性 + this.name = "Adam"; + } + // 原型上的属性 + Person.prototype.getName = function () { + return this.name; + }; + + // 使用Person()创建一个新对象 + var papa = new Person(); + // 继承 + var kid = object(papa); + + // 测试:自有属性和原型上的属性都被继承了 + kid.getName(); // "Adam" + +也可以使用这种模式的一个变种,只继承已存在的构造函数的原型对象。记住,对象继承自对象,而不管父对象是怎么创建的。这是前面例子的一个修改版本: + + // 父构造函数 + function Person() { + // 自有属性 + this.name = "Adam"; + } + // 原型上的属性 + Person.prototype.getName = function () { + + }; + + // 继承 + var kid = object(Person.prototype); + + typeof kid.getName; // "function",因为它在原型中 + typeof kid.name; // "undefined",因为只有原型中的成员被继承了 + +### ECMAScript5中的原型继承 + +在ECMAScript5中,原型继承已经正式成为语言的一部分。这种模式使用`Object.create()`方法来实现。换句话说,你不再需要自己去写类似`object()`的函数,它是语言原生的部分了: + + var child = Object.create(parent); + +`Object.create()`接收一个额外的参数——一个对象。这个额外对象中的属性将被作为自有属性添加到返回的子对象中。这让我们可以很方便地将继承和创建子对象在一个方法调用中实现。例如: + + var child = Object.create(parent, { + age: { value: 2 } // ES5中的属性描述符 + }); + child.hasOwnProperty("age"); // true + +你可能也会发现原型继承模式已经在一些JavaScript类库中实现了,比如,在YUI3中,它是`Y.Object()`方法: + + YUI().use('*', function (Y) { + var child = Y.Object(parent); + }); + +## 通过复制属性继承 + +让我们来看一下另外一种继承模式——通过复制属性继承。在这种模式中,一个对象通过简单地复制另一个对象来获得功能。下面是一个简单的实现这种功能的`extend()`函数: + + function extend(parent, child) { + var i; + child = child || {}; + for (i in parent) { + if (parent.hasOwnProperty(i)) { + child[i] = parent[i]; + } + } + return child; + } + +这是一个简单的实现,仅仅是遍历了父对象的成员然后复制它们。在这个实现中,`child`是可选参数,如果它没有被传入一个已有的对象,那么一个全新的对象将被创建并返回: + + var dad = {name: "Adam"}; + var kid = extend(dad); + kid.name; // "Adam" + +上面给出的实现叫作对象的“浅拷贝”(shallow copy),与之相对,“深拷贝”是指检查准备复制的属性本身是否是对象或者数组,如果是,也遍历它们的属性并复制。如果使用浅拷贝的话(因为在JavaScript中对象是按引用传递),如果你改变子对象的一个属性,而这个属性恰好是一个对象,那么你也会改变父对象。实际上这对方法来说可能很好(因为函数也是对象,也是按引用传递),但是当遇到其它的对象和数组的时候可能会有些意外情况。考虑这种情况: + + var dad = { + counts: [1, 2, 3], + reads: {paper: true} + }; + var kid = extend(dad); + kid.counts.push(4); + dad.counts.toString(); // "1,2,3,4" + dad.reads === kid.reads; // true + +现在让我们来修改一下`extend()`函数以便实现深拷贝。你需要做的事情只是检查一个属性的类型是否是对象,如果是,则递归遍历它的属性。另外一个需要做的检查是这个对象是真的对象还是数组,可以使用第三章讨论过的数组检查方式。最终深拷贝版的`extend()`是这样的: + + function extendDeep(parent, child) { + var i, + toStr = Object.prototype.toString, + astr = "[object Array]"; + + child = child || {}; + + for (i in parent) { + if (parent.hasOwnProperty(i)) { + if (typeof parent[i] === "object") { + child[i] = (toStr.call(parent[i]) === astr) ? [] : {}; + extendDeep(parent[i], child[i]); + } else { + child[i] = parent[i]; + } + } + } + return child; + } + +现在测试时这个新的实现给了我们对象的真实拷贝,所以子对象不会修改父对象: + + var dad = { + counts: [1, 2, 3], + reads: {paper: true} + }; + var kid = extendDeep(dad); + + kid.counts.push(4); + kid.counts.toString(); // "1,2,3,4" + dad.counts.toString(); // "1,2,3" + + dad.reads === kid.reads; // false + kid.reads.paper = false; + kid.reads.web = true; + dad.reads.paper; // true + +通过复制属性继承的模式很简单且应用很广泛。例如Firebug(JavaScript写的Firefox扩展)有一个方法叫`extend()`做浅拷贝,jQuery的`extend()`方法做深拷贝。YUI3提供了一个叫作`Y.clone()`的方法,它创建一个深拷贝并且通过绑定到子对象的方式复制函数。(本章后面将有更多关于绑定的内容。) + +这种模式并不高深,因为根本没有原型牵涉进来,而只跟对象和它们的属性有关。 + +## 混元(Mix-ins) + +既然谈到了通过复制属性来继承,就让我们顺便多说一点,来讨论一下“混元”模式。除了前面说的从一个对象复制,你还可以从任意多数量的对象中复制属性,然后将它们混在一起组成一个新对象。 + +实现很简单,只需要遍历传入的每个参数然后复制它们的每个属性: + + function mix() { + var arg, prop, child = {}; + for (arg = 0; arg < arguments.length; arg += 1) { + for (prop in arguments[arg]) { + if (arguments[arg].hasOwnProperty(prop)) { + child[prop] = arguments[arg][prop]; + } + } + } + return child; + } + +现在我们有了一个通用的混元函数,我们可以传递任意数量的对象进去,返回的结果将是一个包含所有传入对象属性的新对象。下面是用法示例: + + var cake = mix( + {eggs: 2, large: true}, + {butter: 1, salted: true}, + {flour: "3 cups"}, + {sugar: "sure!"} + ); + +图6-10展示了在Firebug的控制台中用`console.dir(cake)`展示出来的混元后`cake`对象的属性。 + +![图6-10 在Firebug中查看cake对象](./Figure/chapter6/6-10.jpg) + +图6-10 在Firebug中查看cake对象 + +> 如果你习惯了某些将混元作为原生部分的语言,那么你可能期望修改一个或多个父对象时也影响子对象。但在这个实现中这是不会发生的事情。这里我们只是简单地遍历、复制自有属性,并没有与父对象有任何链接。 + +## 借用方法 + +有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。得益于函数的`call()`和`apply()`方法,可以通过借用方法模式实现它。在本书中,你其实已经见过这种模式了,甚至在本章`extendDeep()`的实现中也有用到。 + +在JavaScript中函数也是对象,它们有一些有趣的方法,比如`call()`和`apply()`。这两个方法的唯一区别是后者接受一个参数数组以传入正在调用的方法,而前者只接受一个一个的参数。你可以使用这两个方法来从已有的对象中借用方法: + + // call()示例 + notmyobj.doStuff.call(myobj, param1, p2, p3); + // apply()示例 + notmyobj.doStuff.apply(myobj, [param1, p2, p3]); + +在这个例子中有一个对象`myobj`,而且`notmyobj`有一个用得着的方法叫`doStuff()`。你可以简单地临时借用`doStuff()`方法,而不用处理继承然后得到一堆`myobj`中无关的方法。 + +你传一个对象和任意的参数,这个被借用的方法会将`this`绑定到你传递的对象上。简单地说,你的对象会临时假装成另一个对象以使用它的方法。这就像实际上获得了继承但又免除了“继承税”(译注:指不需要的属性和方法)。 + +### 例:从数组借用 + +这种模式的一种常见用法是从数组借用方法。 + +数组有很多很有用但是一些“类数组”对象(如`arguments`)不具备的方法。所以`arguments`可以借用数组的方法,比如`slice()`。这是一个例子: + + function f() { + var args = [].slice.call(arguments, 1, 3); + return args; + } + + // 示例 + f(1, 2, 3, 4, 5, 6); // returns [2,3] + +在这个例子中,有一个空数组被创建了,因为要借用它的方法。也可以使用一种看起来代码更长的方法来做,那就是直接从数组的原型中借用方法,使用`Array.prototype.slice.call(...)`。这种方法代码更长一些,但是不用创建一个空数组。 + +### 借用并绑定 + +当借用方法的时候,不管是通过`call()`/`apply()`还是通过简单的赋值,方法中的`this`指向的对象都是基于调用的表达式来决定的。但是有时候最好的使用方式是将`this`的值锁定或者提前绑定到一个指定的对象上。 + +我们来看一个例子。这是一个对象`one`,它有一个`say()`方法: + + var one = { + name: "object", + say: function (greet) { + return greet + ", " + this.name; + } + }; + + // 测试 + one.say('hi'); // "hi, object" + +现在另一个对象`two`没有`say()`方法,但是它可以从one借用: + + var two = { + name: "another object" + }; + + one.say.apply(two, ['hello']); // "hello, another object" + +在这个例子中,`say()`方法中的`this`指向了`two`,`this.name`是“another object”。但是如果在某些场景下你将函数赋值给了全局变量或者是将这个函数作为回调,会发生什么?在客户端编程中有非常多的事件和回调,所以这种情况经常发生: + + // 赋值给变量,this会指向全局对象 + var say = one.say; + say('hoho'); // "hoho, undefined" + + // 作为回调 + var yetanother = { + name: "Yet another object", + method: function (callback) { + return callback('Hola'); + } + }; + yetanother.method(one.say); // "Holla, undefined" + +在这两种情况中`say()`中的`this`都指向了全局对象,所以代码并不像我们想象的那样正常工作。要修复(绑定)一个方法的对象,我们可以用一个简单的函数,像这样: + + function bind(o, m) { + return function () { + return m.apply(o, [].slice.call(arguments)); + }; + } + +这个`bind()`函数接受一个对象`o`和一个方法`m`,然后把它们绑定在一起,再返回另一个函数。返回的函数通过闭包可以访问到`o`和`m`,也就是说,即使在`bind()`返回之后,内层的函数仍然可以访问到`o`和`m`,而`o`和`m`会始终指向原来的对象和方法。让我们用`bind()`来创建一个新函数: + + var twosay = bind(two, one.say); + twosay('yo'); // "yo, another object" + +正如你看到的,尽管`twosay()`是作为一个全局函数被创建的,但`this`并没有指向全局对象,而是指向了通过`bind()`传入的对象`two`。不论如何调用`twosay()`,`this`将始终指向`two`。 + +绑定是奢侈的,你需要付出的代价是一个额外的闭包。 + +### Function.prototype.bind() + +ECMAScript5在`Function.prototype`中添加了一个方法叫`bind()`,使用时和`apply()`/`call()`一样简单。所以你可以这样写: + + var newFunc = obj.someFunc.bind(myobj, 1, 2, 3); + +这意味着将`someFunc()`和`myobj`绑定了,并且还传入了`someFunc()`的前三个参数。这也是一个在第4章讨论过的部分应用的例子。 + +让我们来看一下当你的程序跑在低于ES5的环境中时如何实现`Function.prototype.bind()`: + + if (typeof Function.prototype.bind === "undefined") { + Function.prototype.bind = function (thisArg) { + var fn = this, + slice = Array.prototype.slice, + args = slice.call(arguments, 1); + + return function () { + return fn.apply(thisArg, args.concat(slice.call(arguments))); + }; + }; + } + +这个实现可能看起来有点熟悉,它使用了部分应用,将传入`bind()`的参数串起来(除了第一个参数),然后在被调用时传给`bind()`返回的新函数。这是用法示例: + + var twosay2 = one.say.bind(two); + twosay2('Bonjour'); // "Bonjour, another object" + +在这个例子中,除了绑定的对象外,我们没有传任何参数给`bind()`。下一个例子中,我们来传一个用于部分应用的参数: + + var twosay3 = one.say.bind(two, 'Enchanté'); + twosay3(); // "Enchanté, another object" + +##小结 + +在JavaScript中,继承有很多种方案可以选择,在本章中你看到了很多类式继承和现代继承的方案。学习和理解不同的模式是有好处的,因为这可以增强你对这门语言的掌握能力。 + +但是,也许在开发过程中继承并不是你经常面对的一个问题。一部分是因为这个问题已经被使用某种方式或者某个你使用的类库解决了,另一部分是因为你不需要在JavaScript中建立很长很复杂的继承链。在静态强类型语言中,继承可能是唯一可以复用代码的方法,但在JavaScript中有更多更简单更优化的方法,包括借用方法、绑定、复制属性、混元等。 + +记住,代码复用才是目标,继承只是达成这个目标的一种手段。 \ No newline at end of file diff --git a/chapter7.markdown b/chapter7.markdown new file mode 100644 index 0000000..250df4d --- /dev/null +++ b/chapter7.markdown @@ -0,0 +1,1349 @@ +# 设计模式 + +在GoF(Gang of Four)的书中提出的设计模式为面向对象的软件设计中遇到的一些普遍问题提供了解决方案。它们已经诞生很久了,而且被证实在很多情况下是很有效的。这正是你需要熟悉它的原因,也是我们要讨论它的原因。 + +尽管这些设计模式跟语言和具体的实现方式无关,但它们多年来被关注到的方面仍然主要是在强类型静态语言比如C++和Java中的应用。 + +JavaScript作为一种基于原型的弱类型动态语言,有些时候实现某些模式时相当简单,甚至不费吹灰之力。 + +让我们从第一个例子——单例模式——来看一下在JavaScript中和静态的基于类的语言有什么不同。 + +## 单例 + +单例模式的核心思想是让指定的类只存在唯一一个实例。这意味着当你第二次使用相同的类去创建对象的时候,你得到的应该和第一次创建的是同一个对象。 + +这如何应用到JavaScript中呢?在JavaScript中没有类,只有对象。当你创建一个对象时,事实上根本没有另一个对象和它一样,这个对象其实已经是一个单例。使用对象字面量创建一个简单的对象也是一种单例的例子: + + var obj = { + myprop: 'my value' + }; + +在JavaScript中,对象永远不会相等,除非它们是同一个对象,所以即使你创建一个看起来完全一样的对象,它也不会和前面的对象相等: + + var obj2 = { + myprop: 'my value' + }; + obj === obj2; // false + obj == obj2; // false + +所以你可以说当你每次使用对象字面量创建一个对象的时候就是在创建一个单例,并没有什么特别的语法牵涉进来。 + +> 需要注意的是,有的时候当人们在JavaScript中提出“单例”的时候,它们可能是在指第五章讨论过的“模块模式”。 + +### 使用new + +JavaScript没有类,所以一字一句地说单例的定义并没有什么意义。但是JavaScript有使用`new`、通过构造函数来创建对象的语法,有时候你可能需要这种语法下的一个单例实现。这也就是说当你使用`new`、通过同一个构造函数来创建多个对象的时候,你应该只是得到同一个对象的不同引用。 + +> 温馨提示:从一个实用模式的角度来说,下面的讨论并不是那么有用,只是更多地在模拟一些语言中关于这个模式的一些问题的解决方案。这些语言主要是(静态强类型的)基于类的语言,在这些语言中,函数并不是“一等公民”。 + +下面的代码片段展示了期望的结果(假设你忽略了多元宇宙的设想,接受了只有一个宇宙的观点): + + var uni = new Universe(); + var uni2 = new Universe(); + uni === uni2; // true + +在这个例子中,`uni`只在构造函数第一次被调用时创建。第二次(以及后续更多次)调用时,同一个`uni`对象被返回。这就是为什么`uni === uni2`的原因——因为它们实际上是同一个对象的两个引用。那么怎么在JavaScript达到这个效果呢? + +当对象实例`this`被创建时,你需要在`Universe()`构造函数中缓存它,以便在第二次调用的时候返回。有几种选择可以达到这种效果: + +- 你可以使用一个全局变量来存储实例。不推荐使用这种方法,因为通常我们认为使用全局变量是不好的。而且,任何人都可以改写全局变量的值,甚至可能是无意中改写。所以我们不再讨论这种方案。 +- 你也可以将对象实例缓存在构造函数的属性中。在JavaScript中,函数也是对象,所以它们也可以有属性。你可以写一些类似`Universe.instance`的属性来缓存对象。这是一种漂亮干净的解决方案,不足之处是`instance`属性仍然是可以被公开访问的,别人写的代码可能修改它,这样就会失去这个实例。 +- 你可以将实例包裹在闭包中。这可以保持实例是私有的,不会在构造函数之外被修改,代价是一个额外的闭包。 + +让我们来看一下第二种和第三种方案的实现示例。 + +### 将实例放到静态属性中 + +下面是一个将唯一的实例放入`Universe()`构造函数的一个静态属性中的例子: + + function Universe() { + + // 实例是否已经存在? + if (typeof Universe.instance === "object") { + return Universe.instance; + } + + // 处理普通逻辑 + this.start_time = 0; + this.bang = "Big"; + + // 缓存实例 + Universe.instance = this; + + // 隐式return: + // return this; + } + + // 测试 + var uni = new Universe(); + var uni2 = new Universe(); + uni === uni2; // true + +如你所见,这是一种直接有效的解决方案,唯一的缺陷是`instance`是可被公开访问的。一般来说它被其它代码误删改的可能是很小的(起码比全局变量`instance`要小得多),但是仍然是有可能的。 + +### 将实例放到闭包中 + +另一种实现基于类的单例模式的方法是使用一个闭包来保护这个唯一的实例。你可以通过第五章讨论过的“私有静态成员模式”来实现。唯一的秘密就是重写构造函数: + + function Universe() { + + // 缓存实例 + var instance = this; + + // 处理普通逻辑 + this.start_time = 0; + this.bang = "Big"; + + // 重写构造函数 + Universe = function () { + return instance; + }; + } + + // 测试 + var uni = new Universe(); + var uni2 = new Universe(); + uni === uni2; // true + +第一次调用时,原来的构造函数被调用并且正常返回`this`。在后续的调用中,被重写的构造函数被调用。被重写的这个构造函数可以通过闭包访问私有的`instance`变量并且将它返回。 + +这个实现实际上也是第四章讨论的重定义函数的又一个例子。如我们讨论过的一样,这种模式的缺点是被重写的函数(在这个例子中就是构造函数`Universe()`)将丢失那些在初始定义和重新定义之间添加的属性。在这个例子中,任何添加到`Universe()`的原型上的属性将不会被链接到使用原来的实现创建的实例上。(注:这里的“原来的实现”是指实例是由未被重写的构造函数创建的,而`Universe()`则是被重写的构造函数。) + +下面我们通过一些测试来展示这个问题: + + // 添加成员到原型 + Universe.prototype.nothing = true; + + var uni = new Universe(); + + // 在创建一个对象后再添加成员到原型 + Universe.prototype.everything = true; + + var uni2 = new Universe(); + + // 测试: + // 只有原始的原型被链接到对象上 + uni.nothing; // true + uni2.nothing; // true + uni.everything; // undefined + uni2.everything; // undefined + + // constructor看起来是对的 + uni.constructor.name; // "Universe" + + // 但其实不然 + uni.constructor === Universe; // false + +`uni.constructor`不再和`Universe()`相同的原因是`uni.constructor`仍然是指向原来的构造函数,而不是被重新定义的那个。 + +如果一定要让`prototype`和`constructor`的指向像我们期望的那样,可以通过一些调整来做到: + + function Universe() { + + // 缓存实例 + var instance; + + // 重写构造函数 + Universe = function Universe() { + return instance; + }; + + // 重写prototype属性 + Universe.prototype = this; + + // 创建实例 + instance = new Universe(); + + // 重写constructor属性 + instance.constructor = Universe; + + // 其它的功能代码 + instance.start_time = 0; + instance.bang = "Big"; + + return instance; + } + +现在所有的测试结果都可以像我们期望的那样了: + + // 修改原型,创建对象 + Universe.prototype.nothing = true; // true + var uni = new Universe(); + Universe.prototype.everything = true; // true + var uni2 = new Universe(); + + // 它们是同一个实例 + uni === uni2; // true + + // 所有的原型上的属性都正常工作,不管是什么时候在哪添加的 + uni.nothing && uni.everything && uni2.nothing && uni2.everything; // true + // 普通成员也可以正常工作 + uni.bang; // "Big" + // constructor指向正确 + uni.constructor === Universe; // true + +另一种可选的解决方案是将构造函数和实例包在一个即时函数中。当构造函数第一次被调用的时候,它返回一个对象并且将私有的`instance`指向它。在后续调用时,构造函数只是简单地返回这个私有变量。在这种新的实现下,前面所有的测试代码也会和期望的一样: + + var Universe; + + (function () { + + var instance; + + Universe = function Universe() { + + if (instance) { + return instance; + } + + instance = this; + + // 功能代码 + this.start_time = 0; + this.bang = "Big"; + + }; + + }()); + +## 工厂模式 + +使用工厂模式的目的就是创建对象。它通常被在类或者类的静态方法中实现,目的是: + +- 执行在建立相似的对象时进行的一些重复操作 +- 让工厂的使用者在编译阶段创建对象时不必知道它的特定类型(类) + +第二点在静态的基于类的语言中更重要,因为在(编译阶段)提前不知道类的情况下,创建类的实例是一件多少有些特殊的行为。但在JavaScript中,这部分的实现却是相当容易的事情。 + +使用工厂方法(或类)创建的对象被设计为从同一个父对象继承;它们是实现一些特定的功能的子类。有些时候这个共同的父对象就是包含工厂方法的同一个类。 + +我们来看一个示例实现,我们有: + +- 一个共同的父构造函数`CarMaker()`。 +- `CarMaker()`的一个静态方法叫`factory()`,用来创建`car`对象。 +- 特定的从`CarMaker()`继承而来的构造函数`CarMaker.Compact()`,`CarMaker.SUV()`,`CarMaker.Convertible()`。它们都被定义为父构造函数的静态属性以便保持全局空间干净,同时在需要的时候我们也知道在哪里找到它们。 + +我们来看一下已经完成的实现会怎么被使用: + + var corolla = CarMaker.factory('Compact'); + var solstice = CarMaker.factory('Convertible'); + var cherokee = CarMaker.factory('SUV'); + corolla.drive(); // "Vroom, I have 4 doors" + solstice.drive(); // "Vroom, I have 2 doors" + cherokee.drive(); // "Vroom, I have 17 doors" + +这一段: + + var corolla = CarMaker.factory('Compact'); + +可能是工厂模式中最为人熟知的。你有一个方法可以在运行时接受一个表示类型的字符串,然后它创建并返回了一个和请求的类型一样的对象。这里没有使用`new`的构造函数,也没有看到任何对象字面量,仅仅只有一个函数根据一个字符串指定的类型创建了对象。 + +这里是一个工厂模式的示例实现,它能让上面的代码片段工作: + + // 父构造函数 + function CarMaker() {} + + // 父构造函数的方法 + CarMaker.prototype.drive = function () { + return "Vroom, I have " + this.doors + " doors"; + }; + + // 静态工厂方法factory + CarMaker.factory = function (type) { + var constr = type, + newcar; + + // 如果指定类型的构造函数不存在则报错 + if (typeof CarMaker[constr] !== "function") { + throw { + name: "Error", + message: constr + " doesn't exist" + }; + } + + // 现在我们确认要用到的构造函数是存在的了 + // 让它继承自父构造函数,但只继承一次 + if (typeof CarMaker[constr].prototype.drive !== "function") { + CarMaker[constr].prototype = new CarMaker(); + } + // 创建一个新实例 + newcar = new CarMaker[constr](); + // 这里可以选择性地调用一些方法,然后返回实例 + return newcar; + }; + + // 创建特定类型的构造函数 + CarMaker.Compact = function () { + this.doors = 4; + }; + CarMaker.Convertible = function () { + this.doors = 2; + }; + CarMaker.SUV = function () { + this.doors = 24; + }; + +工厂模式的实现中没有什么是特别困难的,你需要做的仅仅是寻找请求类型的对象构造函数。在这个例子中,使用了一个简单的名字转换以便映射对象类型和创建对象的构造函数。继承的部分只是一个公共的重复代码片段的示例,它可以被放到工厂方法中而不是被每个构造函数的类型所重复。(译注:指原型继承的代码可以在`factory()`方法以外执行,而不是放到`factory()`中每调用一次都要执行一次。) + +### 内置对象工厂 + +为了说明工厂模式应用之广泛,我们来看一下内置的全局构造函数`Object()`。它的行为很像工厂,因为它根据不同的输入创建不同的对象。如果传入一个数字,它会使用`Number()`构造函数创建一个对象。在传入字符串和布尔值的时候也会发生类似的事情。任何其它的值(包括空值)将会创建一个正常的对象。 + +下面是这种行为的例子和测试,注意`Object()`调用时可以不用加`new`: + + var o = new Object(), + n = new Object(1), + s = Object('1'), + b = Object(true); + + // 测试 + o.constructor === Object; // true + n.constructor === Number; // true + s.constructor === String; // true + b.constructor === Boolean; // true + +`Object()`也是一个工厂这一事实可能没有太多实际用处,仅仅是觉得值得作为一个例子提一下,告诉我们工厂模式是随处可见的。 + +## 遍历模式 + +在遍历模式中,你有一些含有有序聚合数据的对象。这些数据可能在内部用一种复杂的结构存储着,但是你希望提供一种简单的方法来访问这种结构中的每个元素。数据的使用者不需要知道你是怎样组织你的数据的,他们只需要操作一个个独立的元素。 + +在遍历模式中,你的对象需要提供一个`next()`方法。按顺序调用`next()`方法必须返回序列中的下一个元素,但是“下一个”在你的特定的数据结构中指什么是由你自己来决定的。 + +假设你的对象叫`agg`,你可以通过简单地在循环中调用`next()`来访问每个数据元素,像这样: + + var element; + while (element = agg.next()) { + // 访问element…… + console.log(element); + } + +在遍历模式中,聚合对象通常也会提供一个方便的方法`hasNext()`,这样对象的使用者就可以知道他们已经获取到你数据的最后一个元素。当使用`hasNext()`来按顺序访问所有元素时,是像这样的: + + while (agg.hasNext()) { + // 访问element…… + console.log(agg.next()); + } + +## 装饰模式 + +在装饰模式中,一些额外的功能可以在运行时被动态地添加到一个对象中。在静态的基于类的语言中,处理这个问题可能是个挑战,但是在JavaScript中,对象本来就是可变的,所以给一个对象添加额外的功能本身并不是什么问题。 + +装饰模式的一个很方便的特性是可以对我们需要的特性进行定制和配置。刚开始时,我们有一个拥有基本功能的对象,然后可以从可用的装饰中去挑选一些需要用到的去增强这个对象,如果有必要的话,还可以指定增强的顺序。 + +### 用法 + +我们来看一下这个模式的用法示例。假设你正在做一个卖东西的web应用,每个新交易是一个新的`sale`对象。这个对象“知道”交易的价格并且可以通过调用`sale.getPrice()`方法返回。根据环境的不同,你可以开始用一些额外的功能来装饰这个对象。假设一个场景是这笔交易是发生在加拿大的一个省Québec,在这种情况下,购买者需要付联邦税和Québec省税。根据装饰模式的用法,你需要指明使用联邦税装饰器和Québec省税装饰器来装饰这个对象。然后你还可以给这个对象装饰一些价格格式的功能。这个场景的使用方式可能是像这样: + + var sale = new Sale(100); // 价格是100美元 + sale = sale.decorate('fedtax'); // 加上联邦税 + sale = sale.decorate('quebec'); // 加上省税 + sale = sale.decorate('money'); // 格式化 + sale.getPrice(); // "$112.88" + +在另一种场景下,购买者在一个不需要交省税的省,并且你想用加拿大元的格式来显示价格,你可以这样做: + + var sale = new Sale(100); // 价格是100美元 + sale = sale.decorate('fedtax'); // 加上联邦税 + sale = sale.decorate('cdn'); // 用加拿大元格式化 + sale.getPrice(); // "CDN$ 105.00" + +如你所见,这种方法可以在运行时很灵活地添加功能和调整对象。我们来看一下如何来实现这种模式。 + +### 实现 + +一种实现装饰模式的方法是让每个装饰器成为一个拥有应该被重写的方法的对象。每个装饰器实际上是继承自已经被前一个装饰器增强过的对象。装饰器的每个方法都会调用父对象(继承自的对象)的同名方法并取得值,然后做一些额外的处理。 + +最终的效果就是当你在第一个例子中调用`sale.getPrice()`时,实际上是在调用`money`装饰器的方法(图7-1)。但是因为每个装饰器会先调用父对象的方法,`money`的`getPrice()`先调用`quebec`的`getPrice()`,而它又会去调用`fedtax`的`getPrice()`方法,依次类推。这个链会一直走到原始的未经装饰的由`Sale()`构造函数实现的`getPrice()`。 + +![图7-1 装饰模式的实现](./Figure/chapter7/7-1.jpg) +图7-1 装饰模式的实现 + +这个实现以一个构造函数和一个原型方法开始: + + function Sale(price) { + this.price = price || 100; + } + Sale.prototype.getPrice = function () { + return this.price; + }; + +装饰器对象将都被作为构造函数的属性实现: + + Sale.decorators = {}; + +我们来看一个装饰器的例子。这是一个对象,实现了一个自定义的`getPrice()`方法。注意这个方法首先从父对象的方法中取值然后修改这个值: + + Sale.decorators.fedtax = { + getPrice: function () { + var price = this.uber.getPrice(); + price += price * 5 / 100; + return price; + } + }; + +使用类似的方法我们可以实现任意多个需要的装饰器。它们的实现方式像插件一样来扩展核心的`Sale()`的功能。它们甚至可以被放到额外的文件中,被第三方的开发者来开发和共享: + + Sale.decorators.quebec = { + getPrice: function () { + var price = this.uber.getPrice(); + price += price * 7.5 / 100; + return price; + } + }; + + Sale.decorators.money = { + getPrice: function () { + return "$" + this.uber.getPrice().toFixed(2); + } + }; + + Sale.decorators.cdn = { + getPrice: function () { + return "CDN$ " + this.uber.getPrice().toFixed(2); + } + }; + +最后我们来看`decorate()`这个神奇的方法,它把所有上面说的片段都串起来了。记住它是这样被调用的: + + sale = sale.decorate('fedtax'); + +字符串`'fedtax'`对应在`Sale.decorators.fedtax`中实现的对象。被装饰过的最新的对象`newobj`将从现在有的对象(也就是`this`对象,它要么是原始的对象,要么是经过最后一个装饰器装饰过的对象)中继承。实现这一部分需要用到前面章节中提到的临时构造函数模式。我们也设置一个`uber`属性给`newobj`以便子对象可以访问到父对象。然后我们从装饰器中复制所有额外的属性到被装饰的对象`newobj`中。最后,在我们的例子中,`newobj`被返回并且成为被更新过的`sale`对象。 + + Sale.prototype.decorate = function (decorator) { + var F = function () {}, + overrides = this.constructor.decorators[decorator], + i, newobj; + F.prototype = this; + newobj = new F(); + newobj.uber = F.prototype; + for (i in overrides) { + if (overrides.hasOwnProperty(i)) { + newobj[i] = overrides[i]; + } + } + return newobj; + }; + +### 使用列表实现 + +我们来看另一个明显不同的实现方法,得益于JavaScript的动态特性,它完全不需要使用继承。同时,我们也可以简单地将前一个方面的结果作为参数传给下一个方法,而不需要每一个方法都去调用前一个方法。 + +这样的实现方法还允许很容易地反装饰(undecorating)或者撤销一个装饰,这仅仅需要从一个装饰器列表中移除一个条目。 + +用法示例也会明显简单一些,因为我们不需要将`decorate()`的返回值赋值给对象。在这个实现中,`decorate()`不对对象做任何事情,它只是简单地将装饰器加入到一个列表中: + + var sale = new Sale(100); // 价格是100美元 + sale.decorate('fedtax'); // 加上联邦税 + sale.decorate('quebec'); // 加上省税 + sale.decorate('money'); // 格式化 + sale.getPrice(); // "$112.88" + +`Sale()`构造函数现在有了一个作为自己属性存在的装饰器列表: + + function Sale(price) { + this.price = price || 100; + this.decorators_list = []; + } + +可用的装饰器仍然被实现为`Sale.decorators`的属性。注意`getPrice()`方法现在更简单了,因为它们不需要调用父对象的`getPrice()`来获取结果,结果已经作为参数传递给它们了: + + Sale.decorators = {}; + + Sale.decorators.fedtax = { + getPrice: function (price) { + return price + price * 5 / 100; + } + }; + + Sale.decorators.quebec = { + getPrice: function (price) { + return price + price * 7.5 / 100; + } + }; + + Sale.decorators.money = { + getPrice: function (price) { + return "$" + price.toFixed(2); + } + }; + +最有趣的部分发生在父对象的`decorate()`和`getPrice()`方法上。在前一种实现方式中,`decorate()`还是多少有些复杂,而`getPrice()`十分简单。在这种实现方式中事情反过来了:`decorate()`只需要往列表中添加条目而`getPrice()`做了其它所有的工作,包括遍历现在添加的装饰器的列表,然后调用它们的`getPrice()`方法并将结果传递下去: + + Sale.prototype.decorate = function (decorator) { + this.decorators_list.push(decorator); + }; + + Sale.prototype.getPrice = function () { + var price = this.price, + i, + max = this.decorators_list.length, + name; + for (i = 0; i < max; i += 1) { + name = this.decorators_list[i]; + price = Sale.decorators[name].getPrice(price); + } + return price; + }; + +装饰模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中,`getPrice()`是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,`decorators_list`属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。 + +## 策略模式 + +策略模式允许在运行的时候选择算法。你的代码的使用者可以在处理特定任务的时候根据即将要做的事情的上下文来从一些可用的算法中选择一个。 + +使用策略模式的一个例子是解决表单验证的问题。你可以创建一个`validator`对象,有一个`validate()`方法。这个方法被调用时不用区分具体的表单类型,它总是会返回同样的结果——一个没有通过验证的列表和错误信息。 + +但是根据具体的需要验证的表单和数据,你代码的使用者可以选择进行不同类别的检查。你的`validator`选择最佳的策略来处理这个任务,然后将具体的数据检查工作交给合适的算法去做。 + +### 数据验证示例 + +假设你有一个下面这样的数据,它可能来自页面上的一个表单,你希望验证它是不是有效的数据: + + var data = { + first_name: "Super", + last_name: "Man", + age: "unknown", + username: "o_O" + }; + +对这个例子中的`validator`而言,它需要知道哪个是最佳策略,因此你需要先配置它,给它设定好规则以确定哪些是有效的数据。 + +假设你不需要姓,名字可以接受任何内容,但要求年龄是一个数字,并且用户名只允许包含字母和数字。配置可能是这样的: + + validator.config = { + first_name: 'isNonEmpty', + age: 'isNumber', + username: 'isAlphaNum' + }; + +现在`validator`对象已经有了用来处理数据的配置,你可以调用`validate()`方法,然后将验证错误打印到控制台上: + + validator.validate(data); + if (validator.hasErrors()) { + console.log(validator.messages.join("\n")); + } + +它可能会打印出这样的信息: + + Invalid value for *age*, the value can only be a valid number, e.g. 1, 3.14 or 2010 + Invalid value for *username*, the value can only contain characters and numbers, no special symbols + +现在我们来看一下这个`validator`是如何实现的。所有可用的用来验证的逻辑都是拥有一个`validate()`方法的对象,它们还有一行辅助信息用来显示错误信息: + + // 验证空值 + validator.types.isNonEmpty = { + validate: function (value) { + return value !== ""; + }, + instructions: "the value cannot be empty" + }; + + // 验证数字 + validator.types.isNumber = { + validate: function (value) { + return !isNaN(value); + }, + instructions: "the value can only be a valid number, e.g. 1, 3.14 or 2010" + }; + + // 验证是否只包含字母和数字 + validator.types.isAlphaNum = { + validate: function (value) { + return !/[^a-z0-9]/i.test(value); + }, + instructions: "the value can only contain characters and numbers, no special symbols" + }; + +最后,`validator`对象的核心是这样的: + + var validator = { + + // 所有可用的验证类型 + types: {}, + + // 本次验证所有的错误消息 + messages: [], + + // 本次验证的配置,格式为: + // name: validation type + config: {}, + + // 接口方法 + // `data` 是名值对 + validate: function (data) { + + var i, msg, type, checker, result_ok; + + // 重置所有的错误消息 + this.messages = []; + for (i in data) { + + if (data.hasOwnProperty(i)) { + + type = this.config[i]; + checker = this.types[type]; + + if (!type) { + continue; // 不需要验证 + } + if (!checker) { // 没有对应的验证类型 + throw { + name: "ValidationError", + message: "No handler to validate type " + type + }; + } + + result_ok = checker.validate(data[i]); + if (!result_ok) { + msg = "Invalid value for *" + i + "*, " + checker.instructions; + this.messages.push(msg); + } + } + } + return this.hasErrors(); + }, + + // 辅助方法 + hasErrors: function () { + return this.messages.length !== 0; + } + }; + +如你所见,`validator`对象是通用的,在所有的需要验证的场景下都可以保持这个样子。改进它的办法就是增加更多类型的检查。如果你将它用在很多页面上,那么很快你就会有一个非常好的验证类型的集合。然后在新的使用场景下使用时你需要做的仅仅是配置`validator`然后调用`validate()`方法。 + +## 外观模式 + +外观模式是一种很简单的模式,它只是为对象提供了更多的可供选择的接口。使方法保持短小而不是处理太多的工作是一种很好的实践。在这种实践的指导下,你会有一大堆的方法,而不是一个有着非常多参数的`uber`方法。有些时候,两个或者更多的方法会经常被一起调用。在这种情况下,创建另一个将这些重复调用包裹起来的方法就变得意义了。 + +例如,在处理浏览器事件的时候,有以下的方法: + +- `stopPropagation()` + + 阻止事件冒泡到父节点 +- `preventDefault()` + + 阻止浏览器执行默认动作(如打开链接或者提交表单) + +这是两个有不同目的的相互独立的方法,他们也应该被保持独立,但与此同时,他们也经常被一起调用。所以为了不在应用中到处重复调用这两个方法,你可以创建一个外观方法来调用它们: + + var myevent = { + // …… + stop: function (e) { + e.preventDefault(); + e.stopPropagation(); + } + // …… + }; + +外观模式也适用于一些浏览器脚本的场景,即将浏览器的差异隐藏在一个外观方法下面。继续前面的例子,你可以添加一些处理IE中事件API的代码: + + var myevent = { + // …… + stop: function (e) { + // 其它浏览器 + if (typeof e.preventDefault === "function") { + e.preventDefault(); + } + if (typeof e.stopPropagation === "function") { + e.stopPropagation(); + } + // IE + if (typeof e.returnValue === "boolean") { + e.returnValue = false; + } + if (typeof e.cancelBubble === "boolean") { + e.cancelBubble = true; + } + } + // …… + }; + +外观模式在做一些重新设计和重构工作时也很有用。当你想用一个不同的实现来替换某个对象的时候,你可能需要花相当长一段时间才能完成(一个复杂的对象),与此同时,一些使用这个新对象的代码也在被同步编写。你可以先想好新对象的API,然后在旧的对象前面使用新的API创建一个外观方法。使用这种方式,当你完全替换掉旧的对象的时候,你只需要修改少量的调用代码,因为新的代码已经是在使用新的API了。 + +## 代理模式 + +在代理模式中,一个对象充当了另一个对象的接口的角色。它和外观模式不一样,外观模式带来的方便仅限于将几个方法调用联合起来。而代理对象位于某个对象和它的使用者之间,可以保护对对象的访问。 + +这个模式看起来开销有点大,但在出于性能考虑时非常有用。代理对象可以作为目标对象的保护者,让目标对象做尽量少的工作。 + +一种示例用法是“懒初始化”(延迟初始化)。假设负责初始化的对象是开销很大的,并且正好使用者将它初始化后并不真正使用它。在这种情况下,代理对象可以作为目标对象的接口起到帮助作用。代理对象接收到初始化请求,但在目标对象真正被使用之前都不会将请求传递过去。 + +图7-2展示了这个场景,当使用目标对象的代码发出初始化请求时,代理对象回复一切就绪,但并没有将请求传递过去,只有在真正需要目标对象做些工作的时候才将两个请求一起传递过去。 + +![图7-2 通过代理对象时目标对象与使用者的关系](./Figure/chapter7/7-2.jpg) + +图7-2 通过代理对象时目标对象与使用者的关系 + +### 一个例子 + +在目标对象做某件工作开销很大时,代理模式很有用处。在web应用中,开销最大的操作之一就是网络请求,此时尽可能地合并HTTP请求是有意义的。我们来看一个这种场景下应用代理模式的实例。 + +#### 一个视频列表(expando) + +我们假设有一个用来播放选中视频的应用。你可以在这里看到真实的例子。 + +页面上有一个视频标题的列表,当用户点击视频标题的时候,标题下方的区域会展开并显示视频的更多信息,同时也使得视频可被播放。视频的详细信息和用来播放的URL并不是页面的一部分,它们需要通过网络请求来获取。服务端可以接受多个视频ID,这样我们就可以在合适的时候通过一次请求多个视频信息来减少HTTP请求以加快应用的速度。 + +我们的应用允许一次展开好几个(或全部)视频,所以这是一个合并网络请求的绝好机会。 + +![图7-3 真实的视频列表](./Figure/chapter7/7-3.jpg) + +图7-3 真实的视频列表 + +#### 没有代理对象的情况 + +这个应用中最主要的角色是两个对象: + +- `videos` + + 负责对信息区域展开/收起(`videos.getInfo()`方法)和播放视频的响应(`videos.getPlayer()`方法) +- `http` + + 负责通过`http.makeRequest()`方法与服务端通讯 + +当没有代理对象的时候,`videos.getInfo()`会为每个视频调用一次`http.makeRequest()`方法。当我们添加代理对象`proxy`后,它将位于`vidoes`和`http`中间,接手对`makeRequest()`的调用,并在可能的时候合并请求。 + +我们首先看一下没有代理对象的代码,然后添加代理对象来提升应用的响应速度。 + +#### HTML + +HTML代码仅仅是一个链接列表: + +

Toggle Checked

+
    +
  1. Gravedigger
  2. +
  3. Save Me
  4. +
  5. Crush
  6. +
  7. Don't Drink The Water
  8. +
  9. Funny the Way It Is
  10. +
  11. What Would You Say
  12. +
+ +#### 事件处理 + +现在我们来看一下事件处理的逻辑。首先我们定义一个方便的快捷函数`$`: + + var $ = function (id) { + return document.getElementById(id); + }; + +使用事件代理(第八章有更多关于这个模式的内容),我们将所有`id="vids"`的条目上的点击事件统一放到一个函数中处理: + + $('vids').onclick = function (e) { + var src, id; + + e = e || window.event; + src = e.target || e.srcElement; + + if (src.nodeName !== "A") { + return; + } + + if (typeof e.preventDefault === "function") { + e.preventDefault(); + } + e.returnValue = false; + + id = src.href.split('--')[1]; + + if (src.className === "play") { + src.parentNode.innerHTML = videos.getPlayer(id); + return; + } + + src.parentNode.id = "v" + id; + videos.getInfo(id); + }; + +#### `videos`对象 + +`videos`对象有三个方法: + +- `getPlayer()` + + 返回播放视频需要的HTML代码(跟我们讨论的无关) +- `updateList()` + + 网络请求的回调函数,接受从服务器返回的数据,然后生成用于视频详细信息的HTML代码。这一部分也没有什么需要关注的事情。 +- `getInfo()` + + 这个方法切换视频信息的可视状态,同时也调用`http`对象的方法,并传递`updaetList()`作为回调函数。 + +下面是这个对象的代码片段: + + var videos = { + + getPlayer: function (id) {...}, + updateList: function (data) {...}, + + getInfo: function (id) { + + var info = $('info' + id); + + if (!info) { + http.makeRequest([id], "videos.updateList"); + return; + } + + if (info.style.display === "none") { + info.style.display = ''; + } else { + info.style.display = 'none'; + } + + } + }; + +#### `http`对象 + +`http`对象只有一个方法,它向Yahoo!的YQL服务发起一个JSONP请求: + + var http = { + makeRequest: function (ids, callback) { + var url = 'http://query.yahooapis.com/v1/public/yql?q=', + sql = 'select * from music.video.id where ids IN ("%ID%")', + format = "format=json", + handler = "callback=" + callback, + script = document.createElement('script'); + + sql = sql.replace('%ID%', ids.join('","')); + sql = encodeURIComponent(sql); + + url += sql + '&' + format + '&' + handler; + script.src = url; + + document.body.appendChild(script); + } + }; + +> YQL(Yahoo! Query Language)是一种web service,它提供了使用类似SQL的语法来调用很多其它web service的能力,使得使用者不需要学习每个service的API。 + +当所有的六个视频都被选中后,将会向服务端发起六个独立的像这样的YQL请求: + + select * from music.video.id where ids IN ("2158073") + +#### 代理对象 + +前面的代码工作得很好,但我们可以让它工作得更好。`proxy`对象就在这样的场景中出现,并接管了`http`和`videos`对象之间的通讯。它将使用一个简单的逻辑来尝试合并请求:50ms的延迟。`videos`对象并不直接调用后台接口,而是调用`proxy`对象的方法。`proxy`对象在转发这个请求前将会等待一段时间,如果在等待的50ms内有另一个来自`videos`的调用,则它们将被合并为同一个请求。50ms的延迟对用户来说几乎是无感知的,但是却可以用来合并请求以提升点击“toggle”时的体验,一次展开多个视频。它也可以显著降低服务器的负载,因为web服务器只需要处理更少量的请求。 + +合并后查询两个视频信息的YQL大概是这样: + + select * from music.video.id where ids IN ("2158073", "123456") + +在修改后的代码中,唯一的变化是`videos.getInfo()`现在调用的是`proxy.makeRequest()`而不是`http.makeRequest()`,像这样: + + proxy.makeRequest(id, videos.updateList, videos); + +`proxy`对象创建了一个队列来收集50ms之内接受到的视频ID,然后将这个队列传递给`http`对象,并提供回调函数,因为`videos.updateList()`只能处理一个接收到的视频信息。 + +下面是`proxy`对象的代码: + + var proxy = { + ids: [], + delay: 50, + timeout: null, + callback: null, + context: null, + makeRequest: function (id, callback, context) { + // 添加到队列 + this.ids.push(id); + + this.callback = callback; + this.context = context; + + // 设置延时 + if (!this.timeout) { + this.timeout = setTimeout(function () { + proxy.flush(); + }, this.delay); + } + }, + flush: function () { + + http.makeRequest(this.ids, "proxy.handler"); + + // 清除延时和队列 + this.timeout = null; + this.ids = []; + + }, + handler: function (data) { + var i, max; + + // 单个视频 + if (parseInt(data.query.count, 10) === 1) { + proxy.callback.call(proxy.context, data.query.results.Video); + return; + } + + // 多个视频 + for (i = 0, max = data.query.results.Video.length; i < max; i += 1) { + proxy.callback.call(proxy.context, data.query.results.Video[i]); + } + } + }; + +使用代理模式可以在只改动一处原来代码的情况下,将多个web service请求合并为一个。 + +图7-4和7-5展示了使用代理模式将与服务器三次数据交互(不用代理模式时)变为一次交互的过程。 + +![图7-4 与服务器三次数据交互](./Figure/chapter7/7-4.jpg) + +图7-4 与服务器三次数据交互 + +![图7-5 通过一个代理对象合并请求,减少与服务器数据交互](./Figure/chapter7/7-5.jpg) + +图7-5 通过一个代理对象合并请求,减少与服务器数据交互 + + +### 使用代理对象做缓存 + +在这个例子中,目标对象的使用者(`videos`)已经可以做到不对同一个对象重复发出请求,但现实情况中并不总是这样。其实这个代理对象还可以通过缓存之前的请求结果到`cache`属性中来进一步保护`http`对象(图7-6)。然后当`videos`对象需要对同一个ID的视频请求第二次时,`proxy`对象可以直接从缓存中取出,从而避免一次网络交互。 + +![图7-6 代理缓存](./Figure/chapter7/7-6.jpg) + +图7-6 代理缓存 + +## 中介者模式 + +一个应用不论大小,都是由一些彼此独立的对象组成的。所有的对象都需要一个通讯方式来保持可维护性,即你可以安全地修改应用的一部分而不破坏其它部分。随着应用的开发和维护,会有越来越多的对象。然后,在重构代码的时候,对象可能会被移除或者被重新设计。当对象知道其它对象的太多信息并且直接通讯(直接调用彼此的方法或者修改属性)时,会导致我们不愿意看到的紧耦合。当对象耦合很紧时,要修改一个对象而不影响其它的对象是很困难的。此时甚至连一个最简单的修改都变得不那么容易,甚至连一个修改需要用多长时间都难以评估。 + +中介者模式就是一个缓解此问题的办法,它通过解耦来提升代码的可维护性(见图7-7)。在这个模式中,各个彼此合作的对象并不直接通讯,而是通过一个`mediator`(中介者)对象通讯。当一个对象改变了状态后,它就通知中介者,然后中介者再将这个改变告知给其它应该知道这个变化的对象。 + +![图7-7 中介者模式中的对象关系](./Figure/chapter7/7-7.jpg) + +图7-7 中介者模式中的对象关系 + +### 中介者示例 + +我们来看一个使用中介者模式的实例。这个应用是一个游戏,它的玩法是比较两位游戏者在半分钟内按下按键的次数,次数多的获胜。玩家1需要按的是1,玩家2需要按的是0(这样他们的手指不会搅在一起)。当前分数会显示在一个计分板上。 + +对象列表如下: + +- `Player1` +- `Player2` +- `Scoreboard` +- `Mediator` + +中介者`Mediator`知道所有的对象,它与输入设备(键盘)打交道,处理`keypress`事件,决定现在是哪位玩家玩的,然后通知这个玩家(见图7-8)。玩家负责玩(即给自己的分数加一分),然后通知中介者他这一轮已经玩完。中介者再告知计分板最新的分数,计分板更新显示。 + +除了中介者之外,其它的对象都不知道有别的对象存在。这样就使得更新这个游戏变得很简单,比如要添加一位玩家或者是添加另外一个显示剩余时间的地方。 + +你可以在这里看到这个游戏的在线演示。 + +![图7-8 游戏涉及的对象](./Figure/chapter7/7-8.jpg) + +图7-8 游戏涉及的对象 + +玩家对象是通过`Player()`构造函数来创建的,有自己的`points`和`name`属性。原型上的`play()`方法负责给自己加一分然后通知中介者: + + function Player(name) { + this.points = 0; + this.name = name; + } + Player.prototype.play = function () { + this.points += 1; + mediator.played(); + }; + +`scoreboard`对象(计分板)有一个`update()`方法,它会在每次玩家玩完后被中介者调用。计分板根本不知道玩家的任何信息,也不保存分数,它只负责显示中介者给过来的分数: + + var scoreboard = { + + // 被更新的HTML元素 + element: document.getElementById('results'), + + // 更新分数显示 + update: function (score) { + + var i, msg = ''; + for (i in score) { + + if (score.hasOwnProperty(i)) { + msg += '

' + i + '<\/strong>: '; + msg += score[i]; + msg += '<\/p>'; + } + } + this.element.innerHTML = msg; + } + }; + +现在我们来看一下`mediator`对象(中介者)。在游戏初始化的时候,在`setup()`方法中创建玩家,然后放入`players`属性以便后续使用。`played()`方法会被玩家在每轮玩完后调用,它更新`score`哈希然表然后将它传给`scoreboard`用于显示。最后一个方法是`keypress()`,负责处理键盘事件,决定是哪位玩家玩的,并且通知它: + + var mediator = { + + // 所有的玩家 + players: {}, + + // 初始化 + setup: function () { + var players = this.players; + players.home = new Player('Home'); + players.guest = new Player('Guest'); + + }, + + // 玩家玩完后更新分数 + played: function () { + var players = this.players, + score = { + Home: players.home.points, + Guest: players.guest.points + }; + + scoreboard.update(score); + }, + + // 处理用户交互 + keypress: function (e) { + e = e || window.event; // IE + if (e.which === 49) { // 按键“1” + mediator.players.home.play(); + return; + } + if (e.which === 48) { // 按键“0” + mediator.players.guest.play(); + return; + } + } + }; + +最后一件事是初始化和结束游戏: + + // 开始 + mediator.setup(); + window.onkeypress = mediator.keypress; + + // 游戏在30秒后结束 + setTimeout(function () { + window.onkeypress = null; + alert('Game over!'); + }, 30000); + + +## 观察者模式 + +观察者模式被广泛地应用于JavaScript客户端编程中。所有的浏览器事件(`mouseover`,`keypress`等)都是使用观察者模式的例子。这种模式的另一个名字叫“自定义事件”,意思是这些事件是被编写出来的,和浏览器触发的事件相对。它还有另外一个名字叫“订阅者/发布者”模式(Pub/Sub)。 + +使用这个模式的最主要目的就是促进代码解耦。在观察者模式中,一个对象订阅另一个对象的指定活动并得到通知,而不是调用另一个对象的方法。订阅者也被叫作观察者,被观察的对象叫作发布者或者被观察者。当一个特定的事件发生的时候,发布者会通知(调用)所有的订阅者,同时还可能以事件对象的形式传递一些消息。 + +### 例1:杂志订阅 + +为了理解观察者模式的实现方式,我们来看一个具体的例子。我们假设有一个发布者`paper`,它发行一份日报和一份月刊。无论是日报还是月刊发行,有一个名叫`joe`的订阅者都会收到通知。 + +`paper`对象有一个`subscribers`属性,它是一个数组,用来保存所有的订阅者。订阅的过程就仅仅是将订阅者放到这个数组中而已。当一个事件发生时,`paper`遍历这个订阅者列表,然后通知它们。通知的意思也就是调用订阅者对象的一个方法。因此,在订阅过程中,订阅者需要提供一个方法给`paper`对象的`subscribe()`。 + +`paper`对象也可以提供`unsubscribe()`方法,它可以将订阅者从数组中移除。`paper`对象的最后一个重要的方法是`publish()`,它负责调用订阅者的方法。总结一下,一个发布者对象需要有这些成员: + +- `subscribers` + + 一个数组 +- `subscribe()` + + 将订阅者加入数组 +- `unsubscribe()` + + 从数组中移除订阅者 +- `publish()` + + 遍历订阅者并调用它们订阅时提供的方法 + +所有三个方法都需要一个`type`参数,因为一个发布者可能触发好几种事件(比如同时发布杂志和报纸),而订阅者可以选择性地订阅其中的一种或几种。 + +因为这些成员对任何对象来说都是通用的,因此将它们作为一个单独的对象提取出来是有意义的。然后,我们可以(通过混元模式)将它们复制到任何一个对象中,将这些对象转换为订阅者。 + +下面是这些发布者通用功能的一个示例实现,它定义了上面列出来的所有成员,还有一个辅助的`visitSubscribers()`方法: + + var publisher = { + subscribers: { + any: [] // 对应事件类型的订阅者 + }, + subscribe: function (fn, type) { + type = type || 'any'; + if (typeof this.subscribers[type] === "undefined") { + this.subscribers[type] = []; + } + this.subscribers[type].push(fn); + }, + unsubscribe: function (fn, type) { + this.visitSubscribers('unsubscribe', fn, type); + }, + publish: function (publication, type) { + this.visitSubscribers('publish', publication, type); + }, + visitSubscribers: function (action, arg, type) { + var pubtype = type || 'any', + subscribers = this.subscribers[pubtype], + i, + max = subscribers.length; + + for (i = 0; i < max; i += 1) { + if (action === 'publish') { + subscribers[i](arg); + } else { + if (subscribers[i] === arg) { + subscribers.splice(i, 1); + } + } + } + } + }; + +下面这个函数接受一个对象作为参数,并通过复制通用发布者的方法将这个对象转变成发布者: + + function makePublisher(o) { + var i; + for (i in publisher) { + if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") { + o[i] = publisher[i]; + } + } + o.subscribers = {any: []}; + } + +现在我们来实现`paper`对象,它能做的事情就是发布日报和月刊: + + var paper = { + daily: function () { + this.publish("big news today"); + }, + monthly: function () { + this.publish("interesting analysis", "monthly"); + } + }; + +将`paper`对象变成发布者: + + makePublisher(paper); + +现在我们有了一个发布者,让我们再来看一下订阅者对象`joe`,它有两个方法: + + var joe = {
 + drinkCoffee: function (paper) { + console.log('Just read ' + paper); + }, + sundayPreNap: function (monthly) { + console.log('About to fall asleep reading this ' + monthly); + } + }; + +现在让`joe`来订阅`paper`: + + paper.subscribe(joe.drinkCoffee); + paper.subscribe(joe.sundayPreNap, 'monthly'); + +如你所见,`joe`提供了一个当默认的`any`事件发生时被调用的方法,还提供了另一个当`monthly`事件发生时被调用的方法。现在让我们来触发一些事件: + + paper.daily(); + paper.daily(); + paper.daily(); + paper.monthly(); + +这些发布行为都会调用joe的对应方法,控制台中输出的结果是: + + Just read big news today + Just read big news today + Just read big news today + About to fall asleep reading this interesting analysis + +这里值得称道的地方就是`paper`对象并没有硬编码写上`joe`,而`joe`也同样没有硬编码写上`paper`。这里也没有知道所有事情的中介者对象。所有涉及到的对象都是松耦合的,而且在不修改代码的前提下,我们可以给`paper`添加更多的订阅者,同时`joe`也可以在任何时候取消订阅。 + +让我们更进一步,将`joe`也变成一个发布者。(毕竟,在博客和微博上,任何人都可以是发布者。)这样,`joe`变成发布者之后就可以在Twitter上更新状态: + + makePublisher(joe); + joe.tweet = function (msg) { + this.publish(msg); + }; + +现在假设`paper`的公关部门准备通过`Twitter`收集读者反馈,于是它订阅了`joe`,提供了一个方法`readTweets()`: + + paper.readTweets = function (tweet) { + alert('Call big meeting! Someone ' + tweet); + }; + joe.subscribe(paper.readTweets); + +这样每当`joe`发出消息时,`paper`就会弹出警告窗口: + + joe.tweet("hated the paper today"); + +结果是一个警告窗口:“Call big meeting! Someone hated the paper today”。 + +你可以在看到完整的源代码,并且在控制台中运行这个实例。 + +### 例2:按键游戏 + +我们来看另一个例子。我们将实现一个和中介者模式的示例一样的按钮游戏,但这次使用观察者模式。为了让它看起来更高档,我们允许接受无限个玩家,而不限于2个。我们仍然保留用来产生玩家的`Player()`构造函数,也保留`scoreboard`对象,只有`mediator`会变成`game`对象。 + +在中介者模式中,`mediator`对象知道所有涉及到的对象,并且调用它们的方法。而观察者模式中的`game`对象不是这样,它会让对象来订阅它们感兴趣的事件。比如,`scoreboard`会订阅`game`对象的`scorechange`事件。 + +首先我们重新看一下通用的`publisher`对象,并且将它的接口做一点小修改以更贴近浏览器的情况: + +- 将`publish()`,`subscribe()`,`unsubscribe()`分别改为`fire()`,`on()`,`remove()` +- 事件的`type`每次都会被用到,所以把它变成三个方法的第一个参数 +- 可以给订阅者的方法额外加一个`context`参数,以便回调方法可以用`this`指向它自己所属的对象 + +新的`publisher`对象是这样: + + var publisher = { + subscribers: { + any: [] + }, + on: function (type, fn, context) { + type = type || 'any'; + fn = typeof fn === "function" ? fn : context[fn]; + + if (typeof this.subscribers[type] === "undefined") { + this.subscribers[type] = []; + } + this.subscribers[type].push({fn: fn, context: context || this}); + }, + remove: function (type, fn, context) { + this.visitSubscribers('unsubscribe', type, fn, context); + }, + fire: function (type, publication) { + this.visitSubscribers('publish', type, publication); + }, + visitSubscribers: function (action, type, arg, context) { + var pubtype = type || 'any', + subscribers = this.subscribers[pubtype], + i, + max = subscribers ? subscribers.length : 0; + + for (i = 0; i < max; i += 1) { + if (action === 'publish') { + subscribers[i].fn.call(subscribers[i].context, arg); + } else { + if (subscribers[i].fn === arg && subscribers[i].context === context) { + subscribers.splice(i, 1); + } + } + } + } + }; + + +新的`Player()`构造函数是这样: + + function Player(name, key) { + this.points = 0; + this.name = name; + this.key = key; + this.fire('newplayer', this); + } + + Player.prototype.play = function () { + this.points += 1; + this.fire('play', this); + }; + +变动的部分是这个构造函数接受`key`,代表这个玩家在键盘上用来按之后得分的按键。(这些键预先被硬编码过。)每次创建一个新玩家的时候,一个`newplayer`事件也会被触发。类似的,每次有一个玩家玩的时候,会触发`play`事件。 + +`scoreboard`对象和原来一样,它只是简单地将当前分数显示出来。 + +`game`对象会关注所有的玩家,这样它就可以给出分数并且触发`scorechange`事件。它也会订阅浏览器中所有的·keypress·事件,这样它就会知道按钮对应的玩家: + + var game = { + + keys: {}, + + addPlayer: function (player) { + var key = player.key.toString().charCodeAt(0); + this.keys[key] = player; + }, + + handleKeypress: function (e) { + e = e || window.event; // IE + if (game.keys[e.which]) { + game.keys[e.which].play(); + } + }, + + handlePlay: function (player) { + var i, + players = this.keys, + score = {}; + + for (i in players) { + if (players.hasOwnProperty(i)) { + score[players[i].name] = players[i].points; + } + } + this.fire('scorechange', score); + } + }; + +用于将任意对象转变为订阅者的`makePublisher()`还是和之前一样。`game`对象会变成发布者(这样它才可以触发`scorechange`事件),`Player.prototype`也会变成发布者,以使得每个玩家对象可以触发`play`和`newplayer`事件: + + makePublisher(Player.prototype); + makePublisher(game); + +`game`对象订阅`play`和`newplayer`事件(以及浏览器的`keypress`事件),`scoreboard`订阅`scorechange`事件: + + Player.prototype.on("newplayer", "addPlayer", game); + Player.prototype.on("play", "handlePlay", game); + game.on("scorechange", scoreboard.update, scoreboard); + window.onkeypress = game.handleKeypress; + +如你所见,`on()`方法允许订阅者通过函数(`scoreboard.update`)或者是字符串(`"addPlayer"`)来指定回调函数。当有提供`context`(如`game`)时,才能通过字符串来指定回调函数。 + +初始化的最后一点工作就是动态地创建玩家对象(以及它们对象的按键),用户想要多少个就可以创建多少个: + + var playername, key; + while (1) { + playername = prompt("Add player (name)"); + if (!playername) { + break; + } + while (1) { + key = prompt("Key for " + playername + "?"); + if (key) { + break; + } + } + new Player(playername, key); + } + + +这就是游戏的全部。你可以在看到完整的源代码并且试玩一下。 + +值得注意的是,在中介者模式中,`mediator`对象必须知道所有的对象,然后在适当的时机去调用对应的方法。而这个例子中,`game`对象会显得笨一些(译注:指知道的信息少一些),游戏依赖于对象去观察特定的事件然后触发相应的动作:如`scoreboard`观察`scorechange`事件。这使得对象之间的耦合更松了(对象间知道彼此的信息越少越好),而代价则是弄清事件和订阅者之间的对应关系会更困难一些。在这个例子中,所有的订阅行为都发生在代码中的同一个地方,而随着应用规模的境长,`on()`可能会被在各个地方调用(如在每个对象的初始化代码中)。这使得调试更困难一些,因为没有一个集中的地方来看这些代码并理解正在发生什么事情。在观察者模式中,你将不再能看到那种从开头一直跟到结尾的顺序执行方式。 + +## 小结 + +在这章中你学习到了若干种流行的设计模式,并且也知道了如何在JavaScript中实现它们。我们讨论过的设计模式有: + +- 单例模式 + + 只创建类的唯一一个实例。我们看了好几种可以不通过构造函数和类Java语法达成单例的方法。从另一方面来说,JavaScript中所有的对象都是单例。有时候开发者说的单例是指通过模块化模式创建的对象。 +- 工厂模式 + + 一种在运行时通过指定字符串来创建指定类型对象的方法。 +- 遍历模式 + + 通过提供API来实现复杂的自定义数据结构中的遍历和导航。 +- 装饰模式 + + 在运行时通过从预先定义好的装饰器对象来给被装饰对象动态添加功能。 +- 策略模式 + + 保持接口一致的情况下选择最好的策略来完成特定类型的任务。 +- 外观模式 + + 通过包装通用的(或者设计得很差的)方法来提供一个更方便的API。 +- 代理模式 + + 包装一个对象以控制对它的访问,通过合并操作或者是只在真正需要时执行来尽量避免开销太大的操作。 +- 中介者模式 + + 通过让对象不彼此沟通,只通过一个中介者对象沟通的方法来促进解耦。 +- 观察者模式 + + 通过创建“可被观察的对象”使它在某个事件发生时通知订阅者的方式来解耦。(也叫“订阅者/发布者”或者“自定义事件”。) diff --git a/chapter8.markdown b/chapter8.markdown new file mode 100644 index 0000000..1b3ce3e --- /dev/null +++ b/chapter8.markdown @@ -0,0 +1,966 @@ +# DOM和浏览器中的模式 + +在本书的前面几章中,我们主要关注了JavaScript核心(ECMAScript),并没有涉及太多关于在浏览器中使用JavaScript的内容。在本章,我们将探索一些在浏览器环境中的模式,因为这是最常见的JavaScript程序环境。浏览器脚本编程也是大部分不喜欢JavaScript的人对这门语言的认知。这当然是可以理解,因为在浏览器中有非常多不一致的宿主对象和DOM实现。很明显,任何能够减轻客户端脚本编程的痛楚的最佳实践都是大有益处的。 + +在本章中,你会看到一些零散的模式,包括DOM编程、事件处理、远程脚本、页面脚本的加载策略以及将JavaScript部署到生产环境的步骤。 + +但首先,让我们来简要讨论一下如何做客户端脚本编程。 + +## 分离 + +在web应用开发中主要关注的有三种东西: + +- 内容 + + 即HTML文档 +- 表现 + + 指定文档样式的CSS +- 行为 + + JavaScript,用来处理用户交互和页面的动态变化 + +尽可能地将这三者分离可以加强应用在各种用户代理(译注:user agent,即为用户读取页面并呈现的软件,一般指浏览器)的可到达性(译注:delivery,指可被用户代理接受并理解的程度),比如图形浏览器、纯文本浏览器、用于残障人士的辅助技术、移动设备等等。分离常常是和渐进增强的思想一起实现的,我们从一个最基础的体验(纯HTML)开始,它将被用于最简单的用户代理,当用户代理的兼容性提升时再添加更多的可以为体验加分的东西。如果浏览器支持CSS,那么用户会看到文档更好的呈现。如果浏览器支持JavaScript,那文档会更像一个应用,有更多用来增强用户体验的特性。 + +在实践中,分离意味者: + +- 在关掉CSS的情况下测试页面,看页面是否仍然可用,内容是否可以呈现和阅读 +- 在关掉JavaScript的情况下测试页面,确保页面仍然可以完成它的主要功能,所有的链接都可以正常工作(没有href="#"的链接),表单仍然可以正常填写和提交 +- 不要使用内联的事件处理(如onclick)或者是内联的style属性,因为它们不属于内容层 +- 使用语义化的HTML元素,比如头部和列表等 + +JavaScript(行为)层的地位不应该很显赫,也就是说它不应该成为页面正常工作必须依赖的东西,不应该使得用户在使用不支持的浏览器操作时存在障碍。它只应该被用来增强页面。 + +通常比较优雅的用来处理浏览器差异的方法是特性检测,它的思想是你不应该使用浏览器类型检测来决定代码的逻辑,而是应该检测在当前环境中你需要使用的某个方法或者是属性是否存在。浏览器检测一般认为是一种“反模式”,虽然有的情况下不可避免要使用,但它应该是最后才考虑的选择,并且应该只在特性检测没有办法给出明确答案(或者造成明显性能问题)的时候使用: + + // 反模式 + if (navigator.userAgent.indexOf('MSIE') !== −1) { + document.attachEvent('onclick', console.log); + } + + // 更好的方式 + if (document.attachEvent) { + document.attachEvent('onclick', console.log); + } + + // 或者还可以再具体一点 + if (typeof document.attachEvent !== "undefined") { + document.attachEvent('onclick', console.log); + } + +分离也有助于开发、维护,减少升级一个现有应用的难度,因为当出现问题的时候,你知道去看哪一块。当出现一个JavaScript错误的时候,你不需要去看HTML或者是CSS就能修复它。 + +## DOM编程 + +操作页面的DOM树是在客户端JavaScript编程中最普遍的行为。这也是导致开发者头疼的最主要原因(这也导致了JavaScript名声不好),因为DOM方法在不同的浏览器中实现得有很多差异。这也是为什么使用一个抽象了浏览器差异的JavaScript库能显著提高开发速度的原因。 + +我们来看一些在访问和修改DOM树时推荐的模式,主要考虑性能方面。 + +### DOM访问 + +DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能不好是因为浏览器的DOM实现通常是和JavaScript引擎分离的。从浏览器的角度来讲,这样做是很有意义的,因为有可能一个JavaScript应用根本不需要DOM,而除了JavaScript之外的其它语言(如IE的VBScript)也可以用来操作页面中的DOM。 + +一个原则就是DOM访问的次数应该被减少到最低,这意味者: + +- 避免在循环中访问DOM +- 将DOM引用赋给本地变量,然后操作本地变量 +- 当可能的时候使用selectors API +- 遍历HTML collections时缓存`length`(见第二章) + +看下面例子中的第二个循环,尽管它看起来更长一些,但却要快上几十上百倍(取决于具体浏览器): + + // 反模式 + for (var i = 0; i < 100; i += 1) { + document.getElementById("result").innerHTML += i + ", "; + } + + // 更好的方式 - 更新本地变量 + var i, content = ""; + for (i = 0; i < 100; i += 1) { + content += i + ","; + } + document.getElementById("result").innerHTML += content; + +在下一个代码片段中,第二个例子(使用了本地变量`style`)更好,尽管它需要多写一行代码,还需要多定义一个变量: + + // 反模式 + var padding = document.getElementById("result").style.padding, + margin = document.getElementById("result").style.margin; + + // 更好的方式 + var style = document.getElementById("result").style, + padding = style.padding, + margin = style.margin; + +使用selectors API是指使用这个方法: + + document.querySelector("ul .selected"); + document.querySelectorAll("#widget .class"); + +这两个方法接受一个CSS选择器字符串,返回匹配这个选择器的DOM列表(译注:`querySelector`只返回第一个匹配的DOM)。selectors API在现代浏览器(以及IE8+)中可用,它总是会比你使用其它DOM方法来做同样的选择要快。主流的JavaScript库的最新版本都已经使用了这个API,所以你应该去检查你的项目,确保使用的是最新版本。 + +给你经常访问的元素加上一个`id`属性也是有好处的,因为`document.getElementById(myid)`是找到一个DOM元素最容易也是最快的方法。 + +### DOM操作 + +除了访问DOM元素之外,你可能经常需要改变它们、删除其中的一些或者是添加新的元素。更新DOM会导致浏览器重绘(repaint)屏幕,也经常导致重排(reflow,重新计算元素的位置),这些操作代价是很高的。 + +还是那句话,原则是尽量少地更新DOM,这意味着我们可以将变化集中到一起,然后在“活动的”(live)文档树之外去执行这些变化。 + +当你需要添加一棵相对较大的子树的时候,你应该在完成这棵树的构建之后再放到文档树中。为了达到这个目的,你可以使用文档碎片(document fragment)来包含你的节点。 + +不要这样添加节点: + + // 反模式 + // 在节点创建后就插入文档 + + var p, t; + + p = document.createElement('p'); + t = document.createTextNode('first paragraph'); + p.appendChild(t); + document.body.appendChild(p); + + p = document.createElement('p'); + t = document.createTextNode('second paragraph'); + p.appendChild(t); + document.body.appendChild(p); + +一个更好的版本是创建一个文档碎片,然后“离线地”(译注:即不在文档树中)更新它,当它准备好之后再将它加入文档树中。当你将文档碎片添加到DOM树中时,碎片的内容将会被添加进去,而不是碎片本身。这个特性非常好用。所以当有好几个没有被包裹在同一个父元素的节点时,文档碎片是一个很好的包裹方式。 + +下面是使用文档碎片的例子: + + var p, t, frag; + + frag = document.createDocumentFragment(); + + p = document.createElement('p'); + t = document.createTextNode('first paragraph'); + p.appendChild(t); + frag.appendChild(p); + + p = document.createElement('p'); + t = document.createTextNode('second paragraph'); + p.appendChild(t); + frag.appendChild(p); + + document.body.appendChild(frag); + +这个例子和前面例子中每段更新一次相比,文档树只被更新了一次,只导致一次重排/重绘。 + +当你添加新的节点到文档中时,文档碎片很有用。当你需要更新已有的节点时,你也可以将这些变化集中。你可以将你要修改的子树的父节点克隆一份,然后对克隆的这份做修改,完成之后再去替换原来的元素。 + + var oldnode = document.getElementById('result'), + clone = oldnode.cloneNode(true); + + // 修改克隆后的节点…… + + // 结束修改之后: + oldnode.parentNode.replaceChild(clone, oldnode); + +## 事件 + +在浏览器脚本编程中,另一块充满兼容性问题并且带来很多不愉快的区域就是浏览器事件,比如`click`,`mouseover`等等。同样的,一个JavaScript库可以解决支持IE(9以下)和W3C标准实现带来的双倍工作量。 + +我们来看一下一些主要的点,因为你在做一些简单的页面或者快速开发的时候可能不会使用已有的库,当然,也有可能你正在写你自己的库。 + +### 事件处理 + +麻烦是从给元素绑定事件开始的。假设你有一个按钮,点击它的时候增加计数器的值。你可以添加一个内联的`onclick`属性,这在所有的浏览器中都能正常工作,但是会违反分离和渐进增强的思想。所以你应该尽量在JavaScript中来做绑定,而不是在标签中。 + +假设你有下面的标签: + + + +你可以将一个函数赋给节点的onclick属性,但你只能这样做一次: + + // 不好的解决方案 + var b = document.getElementById('clickme'), + count = 0; + + b.onclick = function () { + count += 1; + b.innerHTML = "Click me: " + count; + }; + +如果你希望在按钮点击的时候执行好几个函数,那么在保持松耦合的情况下就不能用这种方法来做绑定。从技术上讲,你可以检测`onclick`是否已经包含一个函数,如果已经包含,就将它加到你自己的函数中,然后替换`onclick`的值为你的新函数。但是一个更干净的解决方案是使用`addEventListener()`方法。这个方法在IE8及以下版本中不存在,在这些浏览器中需要使用`attachEvent()`。 + +当我们回头看条件初始化模式(第四章)时,会发现其中的一个示例实现就是一个很好的解决跨浏览器事件监听的套件。现在我们不讨论细节,只看一下如何给我们的按钮绑定事件: + + var b = document.getElementById('clickme'); + if (document.addEventListener) { // W3C + b.addEventListener('click', myHandler, false); + } else if (document.attachEvent) { // IE + b.attachEvent('onclick', myHandler); + } else { // 为保险起见…… + b.onclick = myHandler; + } + +现在当按钮被点击时,`myHandler()`会被执行。我们来让这个函数实现增加按钮文字“Click me: 0”中的数字的功能。为了更有趣一点,我们假设有好几个按钮,一个`myHandler()`函数来处理所有的按钮点击。如果我们可以从每次点击的事件对象中获取节点和节点对应的计数器值,那为每个按钮保持一个引用和计数器就显得不高效了。 + +我们先看一下解决方案,稍后再来做些评论: + + function myHandler(e) { + + var src, parts; + + // 获取事件对象和事件来源 + e = e || window.event; + src = e.target || e.srcElement; + + // 真正工作的部分:更新文字 + parts = src.innerHTML.split(": "); + parts[1] = parseInt(parts[1], 10) + 1; + src.innerHTML = parts[0] + ": " + parts[1]; + + // 阻止冒泡 + if (typeof e.stopPropagation === "function") { + e.stopPropagation(); + } + if (typeof e.cancelBubble !== "undefined") { + e.cancelBubble = true; + } + + // 阻止默认行为 + if (typeof e.preventDefault === "function") { + e.preventDefault(); + } + if (typeof e.returnValue !== "undefined") { + e.returnValue = false; + } + + } + +在线的例子可以在找到。 + +在这个事件处理函数中,有四个部分: + +- 首先,我们需要访问事件对象,它包含事件的一些信息以及触发这个事件的页面元素。事件对象会被传到事件处理回调函数中,但是使用`onclick`属性时需要使用全局属性`window.event`来获取 +- 第二部分是真正用于更新文字的部分 +- 接下来是阻止事件冒泡。在这个例子中它不是必须的,但通常情况下,如果你不阻止的话,事件会一直冒泡到文档根元素甚至`window`对象。同样的,我们也需要用两种方法来阻止冒泡:W3C标准方式(`stopPropagation()`)和IE的方式(使用`cancelBubble`) +- 最后,如果需要的话,阻止默认行为。有一些事件(点击链接、提交表单)有默认的行为,但你可以使用`preventDefault()`(IE是通过设置`returnValue`的值为`false`的方式)来阻止这些默认行为 + +如你所见,这里涉及到了很多重复性的工作,所以使用第七章讨论过的外观模式创建自己的事件处理套件是很有意义的。 + +### 事件委托 + +事件委托是通过事件冒泡来实现的,它可以减少分散到各个节点上的事件处理函数的数量。如果有10个按钮在一个`div`元素中,你可以给`div`绑定一个事件处理函数,而不是给每个按钮都绑定一个。 + +我们来看一个实例,三个按钮放在一个`div`元素中(图8-1)。你可以在看到这个事件委托的实例。 + +> 译注: 上面的URL中的例子在IE下单击会没有反应,问题在于使用`document.attachEvernt()`时传递的第一个参数应该是`'onclick'`,而不是`'click'`。 + +![图8-1 事件委托示例:三个在点击时增加计数器值的按钮](./Figure/chapter8/8-1.jpg) + +图8-1 事件委托示例:三个在点击时增加计数器值的按钮 + +结构是这样的: + +

+ + + +
+ +你可以给包裹按钮的`div`绑定一个事件处理函数,而不是给每个按钮绑定一个。然后你可以使用和前面的示例中一样的`myHandler()`函数,但需要修改一个小地方:你需要将你不感兴趣的点击排除掉。在这个例子中,你只关注按钮上的点击,而在同一个`div`中产生的其它的点击应该被忽略掉。 + +`myHandler()`的改变就是检查事件来源的`nodeName`是不是`"button"`: + + // …… + // 获取事件对象和事件来源 + e = e || window.event; + src = e.target || e.srcElement; + + if (src.nodeName.toLowerCase() !== "button") { + return; + } + // ... + +事件委托的坏处是筛选容器中感兴趣的事件使得代码看起来更多了,但好处是性能的提升和更干净的代码,这个好处明显大于坏处,因此这是一种强烈推荐的模式。 + +主流的JavaScript库通过提供方便的API的方式使得使用事件委托变得很容易。比如YUI3中有`Y.delegate()`方法,它允许你指定两个CSS选择器,一个用来匹配包裹容器,一个用来匹配你感兴趣的节点。这很方便,因为如果事件发生在你不关心的元素上时,你的事件处理回调函数不会被调用。在这种情况下,绑定一个事件处理函数很简单: + + Y.delegate('click', myHandler, "#click-wrap", "button"); + +感谢YUI抽象了浏览器的差异,已经处理好了事件的来源,使得回调函数更简单了: + + function myHandler(e) { + + var src = e.currentTarget, + parts; + + parts = src.get('innerHTML').split(": "); + parts[1] = parseInt(parts[1], 10) + 1; + src.set('innerHTML', parts[0] + ": " + parts[1]); + + e.halt(); + } + +你可以在看到实例。 + +## 长时间运行的脚本 + +你可能注意到过,有时候浏览器会提示脚本运行时间过长,询问用户是否要停止执行。不管应用有多复杂,你都不希望这种情况发生在自己的应用中。 + +同时,如果脚本运行时间太长的话,浏览器的UI将变得没有响应,用户不能点击任何东西。这是一种很差的用户体验,应该尽量避免。 + +在JavaScript中没有线程,但你可以在浏览器中使用`setTimeout()`来模拟,或者在现代浏览器中使用web workers。 + +### setTimeout() + +它的思想是将一大堆工作分解成为一小段一小段,然后每隔1毫秒运行一段。使用1毫秒的延迟会导致整个任务完成得更慢,但是用户界面会保持可响应状态,用户会觉得浏览器没有失控,觉得更舒服。 + +> 1毫秒(甚至0毫秒)的延迟执行命令在实际运行的时候会延迟更多,这取决于浏览器和操作系统。设定0毫秒的延迟并不意味着马上执行,而是指“尽快执行”。比如,在IE中,最短的延迟是15毫秒。 + +### Web Workers + +现代浏览器为长时间运行的脚本提供了另一种解决方案:web workers。web workers在浏览器内部提供了后台线程支持,你可以将计算量很大的部分放到一个单独的文件中,比如`my_web_worker.js`,然后从主程序(页面)中这样调用它: + + var ww = new Worker('my_web_worker.js'); + ww.onmessage = function (event) { + document.body.innerHTML += + "

message from the background thread: " + event.data + "

"; + }; + +下面展示了一个做1亿次简单的数学运算的web worker: + + var end = 1e8, tmp = 1; + + postMessage('hello there'); + + while (end) { + end -= 1; + tmp += end; + if (end === 5e7) { // 5e7是1e8的一半 + postMessage('halfway there, `tmp` is now ' + tmp); + } + } + + postMessage('all done'); + +web worker使用`postMessage()`来和调用它的程序通讯,调用者通过`onmessage`事件来接受更新。`onmessage`事件处理函数接受一个事件对象作为参数,这个对象含有一个由web worker传过来`data`属性。类似的,调用者(在这个例子中)也可以使用`ww.postMessage()`来给web worker传递数据,web worker可以通过一个`onmessage`事件处理函数来接受这些数据。 + +上面的例子会在浏览器中打印出: + + message from the background thread: hello there + message from the background thread: halfway there, `tmp` is now 3749999975000001 message from the background thread: all done + +## 远程脚本编程 + +现代web应用经常会使用远程脚本编程和服务器通讯,而不刷新当前页面。这使得web应用更灵活,更像桌面程序。我们来看一下几种用JavaScript和服务器通讯的方法。 + +### XMLHttpRequest + +现在,`XMLHttpRequest`是一个特别的对象(构造函数),绝大多数浏览器都可以用,它使得我们可以从JavaScript来发送HTTP请求。发送一个请求有以下三步: + +1. 初始化一个`XMLHttpRequest`对象(简称XHR) +2. 提供一个回调函数,供请求对象状态改变时调用 +3. 发送请求 + +第一步很简单: + + var xhr = new XMLHttpRequest(); + +但是在IE7之前的版本中,XHR的功能是使用ActiveX对象实现的,所以需要做一下兼容处理。 + +第二步是给`readystatechange`事件提供一个回调函数: + + xhr.onreadystatechange = handleResponse; + +最后一步是使用`open()`和`send()`两个方法触发请求。`open()`方法用于初始化HTTP请求的方法(如GET,POST)和URL。`send()`方法用于传递POST的数据,如果是GET方法,则是一个空字符串。`open()`方法的最后一个参数用于指定这个请求是不是异步的。异步是指浏览器在等待响应的时候不会阻塞,这明显是更好的用户体验,因此除非必须要同步,否则异步参数应该使用true: + + xhr.open("GET", "page.html", true); + xhr.send(); + +下面是一个完整的示例,它获取新页面的内容,然后将当前页面的内容替换掉(可以在看到示例): + + var i, xhr, activeXids = [ + 'MSXML2.XMLHTTP.3.0', + 'MSXML2.XMLHTTP', + 'Microsoft.XMLHTTP' + ]; + + if (typeof XMLHttpRequest === "function") { // native XHR + xhr = new XMLHttpRequest(); + } else { // IE7以下 + for (i = 0; i < activeXids.length; i += 1) { + try { + xhr = new ActiveXObject(activeXids[i]); + break; + } catch (e) {} + } + } + + xhr.onreadystatechange = function () { + if (xhr.readyState !== 4) { + return false; + } + if (xhr.status !== 200) { + alert("Error, status code: " + xhr.status); + return false; + } + document.body.innerHTML += "
" + xhr.responseText + "<\/pre>"; };
+
+	xhr.open("GET", "page.html", true);
+	xhr.send("");
+
+代码中的一些说明:
+
+- 因为IE6及以下版本中,创建XHR对象有一点复杂,所以我们通过一个数组列出ActiveX的名字,然后遍历这个数组,使用`try-catch`块来尝试创建对象。
+- 回调函数会检查`xhr`对象的`readyState`属性。这个属性有0到4一共5个值,4代表“complete”(完成)。如果状态还没有完成,我们就继续等待下一次`readystatechange`事件。
+- 回调函数也会检查xhr对象的`status`属性。这个属性和HTTP状态码对应,比如200(OK)或者是404(Not found)。我们只对状态码200感兴趣,而将其它所有的都报为错误(为了简化示例,否则需要检查其它不代表出错的状态码)。
+- 上面的代码会在每次创建XHR对象时检查一遍支持情况。你可以使用前面提到过的模式(如条件初始化)来重写上面的代码,使得只需要做一次检查。
+
+### JSONP
+
+JSONP(JSON with padding)是另一种发起远程请求的方式。与XHR不同,它不受浏览器同源策略的限制,所以考虑到加载第三方站点内容的安全问题,使用它时应该很谨慎。
+
+一个XHR请求的返回可以是任何类型的文档:
+
+- XML文档(过去很常用)
+- HTML片段(很常用)
+- JSON数据(轻量、方便)
+- 简单的文本文件及其它
+
+而使用JSONP的话,返回的数据格式经常是被一个函数包裹的JSON,具体的函数名称在请求的时候提供。
+
+JSONP的请求URL通常是像这样:
+
+	http://example.org/getdata.php?callback=myHandler
+
+`getdata.php`可以是任何类型的页面或者脚本。`callback`参数指定用来处理响应的JavaScript函数(译注:也就是前面提到的包裹JSON的函数)。
+
+这个URL会被放到一个动态生成的`
+	// 第二种选择
+	
+
+但是,当你的目标是要构建一个高性能的web应用的时候,有些模式和考虑点还是应该知道的。
+
+作为题外话,来看一些比较常见的开发者会用在`
+		
+		
+		
+	
+	
+		……
+	
+	
+
+一个更好的选择是将所有的文件合并起来:
+
+	
+	
+	
+		My App
+		
+	
+	
+		……
+	
+	
+
+最好的选择是将合并后的脚本放到页面的尾部:
+
+	
+	
+	
+		My App
+	
+	
+		……
+		
+	
+	
+
+### HTTP分块
+
+HTTP协议支持“分块编码”,它允许将页面分成一块一块发送。所以如果你有一个很复杂的页面,你不需要将那些(静态)头部信息也等到所有的服务端工作都完成后再开始发送。
+
+一个简单的策略是在组装页面其余部分的时候将页面``的内容作为第一块发送。也就是像这样子:
+
+	
+	
+	
+		My App
+	
+	
+	
+		……
+		 
+	
+	
+
+这种情况下可以做一个简单的改动,将JavaScript移回``,随着第一块一起发送。
+
+这样的话可以让浏览器在拿到`head`区内容后就开始下载脚本文件,而此时页面的其它部分在服务端还尚未就绪:
+
+	
+	
+	
+		My App
+		 
+	
+	
+	
+		……
+	
+	
+
+一个更好的办法是使用第三块内容,让它在页面尾部,只包含脚本。如果有一些每个页面都用到的静态的头部,也可以将这部分随第一块一起发送:
+
+	 
+	
+		My App 
+	
+		
+		
+
+		... The full body of the page ...
+
+		
+		
+	
+	
+	
+
+这种方法很适合使用渐进增强思想的网站(关键业务不依赖JavaScript)。当HTML的第二块发送完毕的时候,浏览器已经有了一个加载、显示完毕并且可用的页面,就像禁用JavaScript时的情况。当JavaScript随着第三块到达时,它会进一步增强页面,为页面锦上添花。
+
+### 动态script元素实现非阻塞下载
+
+前面已经说到过,JavaScript会阻塞后面文件的下载,但有一些模式可以防止阻塞:
+
+- 使用XHR加载脚本,然后作为一个字符串使用`eval()`来执行。这种方法受同源策略的限制,而且引入了`eval()`这种“反模式”`
+- 使用`defer`和`async`属性,但有浏览器兼容性问题
+- 使用动态`
+
+	// 修改后的:
+	
+
+最后一步是使用异步加载的脚本遍历这个数组,然后执行函数:
+
+	var i, scripts = mynamespace.inline_scripts, max = scripts.length;
+	for (i = 0; i < max; max += 1) {
+		scripts[i]();
+	}
+
+#### 插入`
+		
+	
+	
+	
+
+对很多应用来说,延迟加载的部分大部分情况下会比核心部分要大,因为我们关注的“行为”(比如拖放、XHR、动画)只在用户初始化之后才会发生。
+
+### 按需加载
+
+前面的模式会在页面加载后无条件加载其它的JavaScript,并假设这些代码很可能会被用到。但我们是否可以做得更好,分部分加载,在真正需要使用的时候才加载那一部分?
+
+假设你页面的侧边栏上有一些tabs。点击tab会发出一个XHR请求获取内容,然后更新tab的内容,然后有一个更新的动画。如果这是页面上唯一需要XHR和动画库的地方,而用户又不点击tab的话会怎样?
+
+下面介绍按需加载模式。你可以创建一个`require()`函数或者方法,它接受一个需要被加载的脚本文件的文件名,还有一个在脚本被加载完毕后执行的回调函数。
+
+`require()`函数可以被这样使用:
+
+	require("extra.js", function () {
+		functionDefinedInExtraJS();
+	});
+
+我们来看一下如何实现这样一个函数。加载脚本很简单——你只需要按照动态`
内置构造函数(不推荐)直接量语法和原始值(推荐)字面量语法和原始值(推荐)
var o = new Object();