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/README.markdown b/README.markdown index 1bdab2b..ad89f86 100644 --- a/README.markdown +++ b/README.markdown @@ -5,111 +5,111 @@ **“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 的实际使用场景出发,提炼了不少可以让前端们偷懒的实用招式。模式的探索、创新,将永远是程序员自我提升的一条修炼之道。值得一读。 # 目录 -## [第一章 概述](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) -- 立即执行的函数 - - 立即执行的函数的参数 - - 立即执行的函数的返回值 - - 好处和用法 -- 立即初始化的对象 -- 启动时间程序 -- 函数属性——一种备忘录模式 -- 对象的配置 -- 柯里化 (Curry) - - 函数应用 - - 部分应用 - - 柯里化 - - 什么时候使用柯里化 -- 小节 +## [第一章 概述](javascript.patterns/blob/master/chapter1.markdown) + +- [模式](javascript.patterns/blob/master/chapter1.markdown) +- [JavaScript:概念](javascript.patterns/blob/master/chapter1.markdown#a2) + - [面向对象](javascript.patterns/blob/master/chapter1.markdown#a3) + - [无类](javascript.patterns/blob/master/chapter1.markdown#a4) + - [原型](javascript.patterns/blob/master/chapter1.markdown#a5) + - [运行环境](javascript.patterns/blob/master/chapter1.markdown#a6) +- [ECMAScript 5](javascript.patterns/blob/master/chapter1.markdown#a7) +- [JSLint](javascript.patterns/blob/master/chapter1.markdown#a8) +- [控制台工具](javascript.patterns/blob/master/chapter1.markdown#a9) + +## [第二章 高质量JavaScript基本要点](javascript.patterns/blob/master/chapter2.markdown) + +- [编写可维护的代码](javascript.patterns/blob/master/chapter2.markdown#a2) +- [减少全局对象](javascript.patterns/blob/master/chapter2.markdown#a3) + - [全局对象带来的困扰](javascript.patterns/blob/master/chapter2.markdown#a4) + - [忘记var时的副作用](javascript.patterns/blob/master/chapter2.markdown#a5) + - [访问全局对象](javascript.patterns/blob/master/chapter2.markdown#a6) + - [单 var 模式](javascript.patterns/blob/master/chapter2.markdown#a7) + - [声明提前:分散的 var 带来的问题](javascript.patterns/blob/master/chapter2.markdown#a8) +- [for 循环](javascript.patterns/blob/master/chapter2.markdown#a9) +- [for-in 循环](javascript.patterns/blob/master/chapter2.markdown#a10) +- [(不)扩充内置原型](javascript.patterns/blob/master/chapter2.markdown#a11) +- [switch 模式](javascript.patterns/blob/master/chapter2.markdown#a12) +- [避免隐式类型转换](javascript.patterns/blob/master/chapter2.markdown#a13) + - [避免使用 eval()](javascript.patterns/blob/master/chapter2.markdown#a14) +- [使用parseInt()进行数字转换](javascript.patterns/blob/master/chapter2.markdown#a15) +- [编码风格](javascript.patterns/blob/master/chapter2.markdown#a16) + - [缩进](javascript.patterns/blob/master/chapter2.markdown#a17) + - [花括号](javascript.patterns/blob/master/chapter2.markdown#a18) + - [左花括号的放置](javascript.patterns/blob/master/chapter2.markdown#a19) + - [空格](javascript.patterns/blob/master/chapter2.markdown#a20) +- [命名规范](javascript.patterns/blob/master/chapter2.markdown#a21) + - [构造器命名中的大小写](javascript.patterns/blob/master/chapter2.markdown#a22) + - [单词分隔](javascript.patterns/blob/master/chapter2.markdown#a23) + - [其他命名风格](javascript.patterns/blob/master/chapter2.markdown#a24) +- [书写注释](javascript.patterns/blob/master/chapter2.markdown#a25) +- [书写API文档](javascript.patterns/blob/master/chapter2.markdown#a26) + - [一个例子:YUIDoc](javascript.patterns/blob/master/chapter2.markdown#a27) +- [编写易读的代码](javascript.patterns/blob/master/chapter2.markdown#a28) +- [相互评审](javascript.patterns/blob/master/chapter2.markdown#a29) +- [生产环境中的代码压缩(Minify)](javascript.patterns/blob/master/chapter2.markdown#a30) +- [运行JSLint](javascript.patterns/blob/master/chapter2.markdown#a31) +- [小结](javascript.patterns/blob/master/chapter2.markdown#a32) + +## [第三章 直接量和构造函数](javascript.patterns/blob/master/chapter3.markdown) + +- [对象直接量](javascript.patterns/blob/master/chapter3.markdown#a2) + - [对象直接量语法](javascript.patterns/blob/master/chapter3.markdown#a3) + - [通过构造函数创建对象](javascript.patterns/blob/master/chapter3.markdown#a4) + - [获得对象的构造器](javascript.patterns/blob/master/chapter3.markdown#a5) +- [自定义构造函数](javascript.patterns/blob/master/chapter3.markdown#a6) + - [构造函数的返回值](javascript.patterns/blob/master/chapter3.markdown#a7) +- [强制使用new的模式](javascript.patterns/blob/master/chapter3.markdown#a8) + - [命名约定](javascript.patterns/blob/master/chapter3.markdown#a9) + - [使用that](javascript.patterns/blob/master/chapter3.markdown#a10) + - [调用自身的构造函数](javascript.patterns/blob/master/chapter3.markdown#a11) +- [数组直接量](javascript.patterns/blob/master/chapter3.markdown#a12) + - [数组直接量语法](javascript.patterns/blob/master/chapter3.markdown#a13) + - [有意思的数组构造器](javascript.patterns/blob/master/chapter3.markdown#a14) + - [检查是不是数组](javascript.patterns/blob/master/chapter3.markdown#a15) +- [JSON](javascript.patterns/blob/master/chapter3.markdown#a16) + - [使用JSON](javascript.patterns/blob/master/chapter3.markdown#a17) +- [正则表达式直接量](javascript.patterns/blob/master/chapter3.markdown#a18) + - [正则表达式直接量语法](javascript.patterns/blob/master/chapter3.markdown#a19) +- [原始值的包装对象](javascript.patterns/blob/master/chapter3.markdown#a20) +- [Error对象](javascript.patterns/blob/master/chapter3.markdown#a21) +- [小结](javascript.patterns/blob/master/chapter3.markdown#a22) + +## [第四章 函数](javascript.patterns/blob/master/chapter4.markdown) + +- [背景知识](javascript.patterns/blob/master/chapter4.markdown#a2) + - [术语释义](javascript.patterns/blob/master/chapter4.markdown#a3) + - [声明 vs 表达式:命名与提前](javascript.patterns/blob/master/chapter4.markdown#a4) + - [函数的name属性](javascript.patterns/blob/master/chapter4.markdown#a5) + - [函数提前](javascript.patterns/blob/master/chapter4.markdown#a6) +- [回调模式](javascript.patterns/blob/master/chapter4.markdown#a7) + - [一个回调的例子](javascript.patterns/blob/master/chapter4.markdown#a8) + - [回调和作用域](javascript.patterns/blob/master/chapter4.markdown#a9) + - [异步事件监听](javascript.patterns/blob/master/chapter4.markdown#a10) + - [超时](javascript.patterns/blob/master/chapter4.markdown#a11) + - [库中的回调](javascript.patterns/blob/master/chapter4.markdown#a12) +- [返回函数](javascript.patterns/blob/master/chapter4.markdown#a12) +- [自定义函数](javascript.patterns/blob/master/chapter4.markdown#a14) +- [立即执行的函数](javascript.patterns/blob/master/chapter4.markdown#a15) + - [立即执行的函数的参数](javascript.patterns/blob/master/chapter4.markdown#a16) + - [立即执行的函数的返回值](javascript.patterns/blob/master/chapter4.markdown#a17) + - [好处和用法](javascript.patterns/blob/master/chapter4.markdown#a18) +- [立即初始化的对象](javascript.patterns/blob/master/chapter4.markdown#a19) +- [条件初始化](javascript.patterns/blob/master/chapter4.markdown#a20) +- [函数属性——Memoization模式](javascript.patterns/blob/master/chapter4.markdown#a21) +- [配置对象](javascript.patterns/blob/master/chapter4.markdown#a22) +- [柯里化 (Curry)](javascript.patterns/blob/master/chapter4.markdown#a23) + - [函数应用](javascript.patterns/blob/master/chapter4.markdown#a24) + - [部分应用](javascript.patterns/blob/master/chapter4.markdown#a25) + - [柯里化](javascript.patterns/blob/master/chapter4.markdown#a26) + - [什么时候使用柯里化](javascript.patterns/blob/master/chapter4.markdown#a27) +- [小结](javascript.patterns/blob/master/chapter4.markdown#a28) ## 第五章 对象创建模式 @@ -140,33 +140,33 @@ - method() 方法 - 小节 -## 第六章 代码重用模式 - -- 类式继承 vs 现代继承模式 -- 类式继承的期望结果 -- 经典模式 1 ——默认模式 - - 使用原型链 - - 模式 1 的缺陷 -- 经典模式 2 ——借用构造器 - - 原型连 - - 通过借用构造函数实现多重继承 - - 借用构造器模式的利弊 -- 经典模式 3 ——借用并设置原型 -- 经典模式 4 ——共享原型 -- 经典模式 5 —— 临时构造器 - - 存储父类 - - 重置构造器引用 -- Klass -- 原型继承 - - 讨论 - - 除了ECMAScript5之外 -- 通过拷贝属性继承 -- 混元 -- 借用方法 - - 例子:从数组借用 - - 借用和绑定 - - Function.prototype.bind() -- 小节 +## [第六章 代码复用模式](javascript.patterns/blob/master/chapter6.markdown#a1) + +- [类式继承 vs 现代继承模式](javascript.patterns/blob/master/chapter6.markdown#a2) +- [类式继承的期望结果](javascript.patterns/blob/master/chapter6.markdown#a3) +- [类式继承 1 ——默认模式](javascript.patterns/blob/master/chapter6.markdown#a4) + - [跟踪原型链](javascript.patterns/blob/master/chapter6.markdown#a5) + - [这种模式的缺点](javascript.patterns/blob/master/chapter6.markdown#a6) +- [类式继承 2 ——借用构造函数](javascript.patterns/blob/master/chapter6.markdown#a7) + - [原型链](javascript.patterns/blob/master/chapter6.markdown#a8) + - [利用借用构造函数模式实现多继承](javascript.patterns/blob/master/chapter6.markdown#a9) + - [借用构造函数的利与弊](javascript.patterns/blob/master/chapter6.markdown#a10) +- [类式继承 3 ——借用并设置原型](javascript.patterns/blob/master/chapter6.markdown#a11) +- [经典模式 4 ——共享原型](javascript.patterns/blob/master/chapter6.markdown#a12) +- [经典模式 5 —— 临时构造函数](javascript.patterns/blob/master/chapter6.markdown#a13) + - [存储父类](javascript.patterns/blob/master/chapter6.markdown#a14) + - [重置构造函数引用](javascript.patterns/blob/master/chapter6.markdown#a15) +- [Klass](javascript.patterns/blob/master/chapter6.markdown#a16) +- [原型继承](javascript.patterns/blob/master/chapter6.markdown#a17) + - [讨论](javascript.patterns/blob/master/chapter6.markdown#a18) + - [例外的ECMAScript 5](javascript.patterns/blob/master/chapter6.markdown#a19) +- [通过复制属性继承](javascript.patterns/blob/master/chapter6.markdown#a20) +- [混元(Mix-ins)](javascript.patterns/blob/master/chapter6.markdown#a21) +- [借用方法](javascript.patterns/blob/master/chapter6.markdown#a22) + - [例:从数组借用](javascript.patterns/blob/master/chapter6.markdown#a23) + - [借用并绑定](javascript.patterns/blob/master/chapter6.markdown#a24) + - [Function.prototype.bind()](javascript.patterns/blob/master/chapter6.markdown#a25) +- [小结](javascript.patterns/blob/master/chapter6.markdown#a26) ## 第七章 设计模式 diff --git a/chapter2.markdown b/chapter2.markdown index 2913054..e4516e8 100644 --- a/chapter2.markdown +++ b/chapter2.markdown @@ -111,6 +111,8 @@ JavaScript 使用函数来管理作用域,在一个函数内定义的变量称 也就是说,隐式全局变量并不算是真正的变量,但他们是全局对象的属性成员。属性是可以通过delete运算符删除的,而变量不可以被删除: +>(译注:在浏览器环境中,所有 JavaScript 代码都是在 window 作用域内的,所以在这种情况下,我们所说的全局变量其实都是 window 下的一个属性,故可以用 delete 删除,但在如 nodejs 或 gjs 等非浏览器环境下,显式声明的全局变量无法用 delete 删除。) + // define three globals var global_var = 1; global_novar = 2; // antipattern diff --git a/chapter3.markdown b/chapter3.markdown index 55c32b0..f32a30e 100644 --- a/chapter3.markdown +++ b/chapter3.markdown @@ -1,7 +1,7 @@ # 第三章 直接量和构造函数 -JavaScript中的直接量模式更加简洁、富有表现力,且在定义对象时不容易出错。本章将对直接量展开讨论,包括对象、数组和正则表达式直接量,以及为什么要使用等价的内置构造器函数来创建它们,比如Object()和Array()等。本章同样会介绍JSON格式,JSON是使用数组和对象直接量的形式定义的一种数据转换格式。本章还会讨论自定义构造函数,包括如何强制使用new以确保构造函数的正确执行。 +JavaScript中的直接量模式更加简洁、富有表现力,且在定义对象时不容易出错。本章将对直接量展开讨论,包括对象、数组和正则表达式直接量,以及为什么要优先使用它们而不是如`Object()`和`Array()`这些等价的内置构造器函数。本章同样会介绍JSON格式,JSON是使用数组和对象直接量的形式定义的一种数据转换格式。本章还会讨论自定义构造函数,包括如何强制使用new以确保构造函数的正确执行。 本章还会补充讲述一些基础知识,比如内置包装对象Number()、String()和Boolean(),以及如何将它们和原始值(数字、字符串和布尔值)比较。最后,快速介绍一下Error()构造函数的用法。 @@ -338,7 +338,7 @@ ECMAScript5中修正了这种非正常的行为逻辑。在严格模式中,thi console.log(a.length); // 3 console.log(typeof a[0]); // "undefined" -尽管构造器的行为并不像我们想象的那样,当给new Array()传入一个浮点数时情况就更糟糕了。这时结果就会出错(译注:给new Array()传入浮点数会报“范围错误”RangError,new Array(3.00)则不会报错),因为数组长度不可能是浮点数。 +或许上面的情况看起来还不算是太严重的问题,但当 `new Array()` 的参数是一个浮点数而不是整数时则会导致严重的错误,这是因为数组的长度不可能是浮点数。 // using array literal var a = [3.14]; diff --git a/chapter4.markdown b/chapter4.markdown index 50c2a81..92c18dd 100644 --- a/chapter4.markdown +++ b/chapter4.markdown @@ -1,3 +1,4 @@ + # 函数 熟练运用函数是JavaScript程序员的必备技能,因为在JavaScript中函数实在是太常用了。它能够完成的任务种类非常之多,而在其他语言中则需要很多特殊的语法支持才能达到这种能力。 @@ -6,6 +7,8 @@ 现在让我们来一起揭秘JavaScript函数,我们首先从一些背景知识开始说起。 + + ## 背景知识 JavaScript的函数具有两个主要特性,正是这两个特性让它们与众不同。第一个特性是,函数是一等对象(first-class object),第二个是函数提供作用域支持。 @@ -30,6 +33,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 函数的第二个重要特性是它能提供作用域支持。在JavaScript中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript中只有函数作用域(译注:这里作者的表述只针对函数而言,此外JavaScript还有全局作用域)。在函数内所有通过var声明的变量都是局部变量,在函数外部是不可见的。刚才所指花括号无法提供作用域支持的意思是说,如果在if条件句内、或在for或while循环体内用var定义了变量,这个变量并不是属于if语句或for(while)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。在第二章里提到我们要减少对全局命名空间的污染,那么使用函数则是控制变量的作用域的不二之选。 + ### 术语释义 首先我们先简单讨论下创建函数相关的术语,因为精确无歧义的术语约定和我们所讨论的各种模式一样重要。 @@ -66,7 +70,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 >另外我们经常看到“函数直接量”。它用来表示函数表达式或带命名的函数表达式。由于这个术语是有歧义的,所以最好不要用它。 - + ### 声明 vs 表达式:命名与提前 那么,到底应该用哪个呢?函数声明还是函数表达式?在不能使用函数声明语法的场景下,只能使用函数表达式了。下面这个例子中,我们给函数传入了另一个函数对象作为参数,以及给对象定义方法: @@ -102,6 +106,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 return bar; } + ### 函数的name属性 选择函数定义模式的另一个考虑是只读属性name的可用性。尽管标准规范中并未规定,但很多运行环境都实现了name属性,在函数声明和带有名字的函数表达式中是有name的属性定义的。在匿名函数表达式中,则不一定有定义,这个是和实现相关的,在IE中是无定义的,在Firefox和Safari中是有定义的,但是值为空字符串。 @@ -120,6 +125,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 >我们可以将一个带名字的函数表达式赋值给变量,变量名和函数名不同,这在技术上是可行的。比如:`var foo = function bar(){};`。然而,这种用法的行为在浏览器中的兼容性不佳(特别是IE中),因此并不推荐大家使用这种模式。 + ### 函数提前 通过前面的讲解,你可能以为函数声明和带名字的函数表达式是完全等价的。事实上不是这样,主要区别在于“声明提前”的行为。 @@ -170,6 +176,8 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 - 函数是对象 - 函数提供局部变量作用域 + + ## 回调模式 函数是对象,也就意味着函数可以当作参数传入另外一个函数中。当你给函数writeCode()传入一个函数参数introduceBugs(),在某个时刻writeCode()执行了(或调用了)introduceBugs()。在这种情况下,我们说introduceBugs()是一个“回调函数”,简称“回调”: @@ -188,6 +196,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 注意introduceBugs()是如何作为参数传入writeCode()的,当作参数的函数不带括号。括号的意思是执行函数,而这里我们希望传入一个引用,让writeCode()在合适的时机执行它(调用它)。 + ### 一个回调的例子 我们从一个例子开始,首先介绍无回调的情况,然后在作修改。假设你有一个通用的函数,用来完成某种复杂的逻辑并返回一大段数据。假设我们用findNodes()来命名这个通用函数,这个函数用来对DOM树进行遍历,并返回我所感兴趣的页面节点: @@ -262,6 +271,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 node.style.display = "block"; }); + ### 回调和作用域 在上一个例子中,执行回调函数的写法是: @@ -329,6 +339,7 @@ JavaScript的函数具有两个主要特性,正是这两个特性让它们与 // ... }; + ### 异步事件监听 JavaScript中的回调模式已经是我们的家常便饭了,比如,如果你给网页中的元素绑定事件,则需要提供回调函数的引用,以便事件发生时能调用到它。这里有一个简单的例子,我们将console.log()作为回调函数绑定了document的点击事件: @@ -339,6 +350,7 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 “不要打电话给我,我会打给你”,这是好莱坞很有名的一句话,很多电影都有这句台词。电影中的主角不可能同时应答很多个电话呼叫。在JavaScript的异步事件模型中也是同样的道理。电影中是留下电话号码,JavaScript中是提供一个回调函数,当时机成熟时就触发回调。有时甚至提供了很多回调,有些回调压根是没用的,但由于这个事件可能永远不会发生,因此这些回调的逻辑也不会执行。比如,假设你从此不再用“鼠标点击”,那么你之前绑定的鼠标点击的回调函数则永远也不会执行。 + ### 超时 另外一个最常用的回调模式是在调用超时函数时,超时函数是浏览器window对象的方法,共有两个:setTimeout()和setInterval()。这两个方法的参数都是回调函数。 @@ -350,10 +362,13 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 再次需要注意,函数thePlotThickens是作为变量传入setTimeout的,它不带括号,如果带括号的话则立即执行了,这里只是用到这个函数的引用,以便在setTimeout的逻辑中调用到它。也可以传入字符串“thePlotThickens()”,但这是一种反模式,和eval()一样不推荐使用。 + ### 库中的回调 回调模式非常简单,但又很强大。可以随手拈来灵活运用,因此这种模式在库的设计中也非常得宠。库的代码要尽可能的保持通用和重用,而回调模式则可帮助库的作者完成这个目标。你不必预料和实现你所想到的所有情形,因为这会让库变的膨胀而臃肿,而且大多数用户并不需要这些多余的特性支持。相反,你将精力放在核心功能的实现上,提供回调的入口作为“钩子”,可以让库的方法变得可扩展、可定制。 + + ## 返回函数 函数是对象,因此当然可以作为返回值。也就是说,函数不一定非要返回一坨数据,函数可以返回另外一个定制好的函数,或者可以根据输入的不同按需创造另外一个函数。 @@ -386,6 +401,8 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 next(); // 2 next(); // 3 + + ## 自定义函数 我们动态定义函数,并将函数赋值给变量。如果将你定义的函数赋值给已经存在的函数变量的话,则新函数会覆盖旧函数。这样做的结果是,旧函数的引用就丢弃掉了,变量中所存储的引用值替换成了新的。这样看起来这个变量指代的函数逻辑就发生了变化,或者说函数进行了“重新定义”或“重写”。说起来有些拗口,实际上并不复杂,来看一个例子: @@ -402,7 +419,7 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 当函数中包含一些初始化操作,并希望这些初始化只执行一次,那么这种模式是非常适合这个场景的。因为能避免的重复执行则尽量避免,函数的一部分可能再也不会执行到。在这个场景中,函数执行一次后就被重写为另外一个函数了。 -使用这种模式可以帮助提高应用的执行效率,因为重新定义的函数执行的更少。 +使用这种模式可以帮助提高应用的执行效率,因为重新定义的函数执行更少的代码。 >这种模式的另外一个名字是“函数的懒惰定义”,因为直到函数执行一次后才重新定义,可以说它是“某个时间点之后才存在”,简称“懒惰定义”。 @@ -435,13 +452,602 @@ JavaScript中的回调模式已经是我们的家常便饭了,比如,如果 // 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”。它也就不能获取到新添加的属性“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都会是全局变量,而这些变量仅仅是由因为初始化而遗留下来的垃圾,没有任何用处。 + + + +### 立即执行的函数的参数 + +立即执行的函数也可以接受参数,看这个例子: + + // prints: + // 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; + } + }; + + // usage + o.getMsg(); // "call me" + o.message; // "call me" + + +在这个例子中,o.message是一个字符串,而不是一个函数,但是它需要一个函数在脚本载入后来得到这个属性值。 + + +### 好处和用法 + +立即执行的函数应用很广泛。它可以帮助我们做一些不想留下全局变量的工作。所有定义的变量都只是立即执行的函数的本地变量,你完全不用担心临时变量会污染全局对象。 + +> 立即执行的函数还有一些名字,比如“自调用函数”或者“自执行函数”,因为这些函数会在被定义后立即执行自己。 + +这种模式也经常被用到书签代码中,因为书签代码会在任何一个页面运行,所以需要非常苛刻地保持全局命名空间干净。 + +这种模式也可以让你包裹一些独立的特性到一个封闭的模块中。设想你的页面是静态的,在没有JavaScript的时候工作正常,然后,本着渐进增强的精神,你给页面加入了一点增加代码。这时候,你就可以把你的代码(也可以叫“模块”或者“特性”)放到一个立即执行的函数中并且保证页面在有没有它的时候都可以正常工作。然后你就可以加入更多的增强特性,或者对它们进行移除、进行独立测试或者允许用户禁用等等。 + +你可以使用下面的模板定义一段函数代码,我们叫它module1: + + // module1 defined in module1.js + (function () { + + // all the module 1 code ... + + }()); + +套用这个模板,你就可以编写其它的模块。然后在发布到线上的时候,你就可以决定在这个时间节点上哪些特性是可以使用的,然后使用发布脚本将它们打包上线。 + + + +## 立即初始化的对象 + +还有另外一种可以避免污染全局作用域的方法,和前面描述的立即执行的函数相似,叫做“立即初始化的对象”模式。这种模式使用一个带有init()方法的对象来实现,这个方法在对象被创建后立即执行。初始化的工作由init()函数来完成。 + +下面是一个立即初始化的对象模式的例子: + + ({ + // here you can define setting values + // a.k.a. configuration constants + maxwidth: 600, + maxheight: 400, + + // you can also define utility methods + gimmeMax: function () { + return this.maxwidth + "x" + this.maxheight; + }, + + // initialize + init: function () { + console.log(this.gimmeMax()); + // more init tasks... + } + }).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或者是绑定事件处理函数。大部分程序员在他们的客户端编程生涯中都编写过事件绑定和取消绑定相关的组件,像下面的例子: + + // BEFORE + 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 { // older browsers + el['on' + type] = fn; + } + }, + removeListener: function (el, type, fn) { + // pretty much the same... + } + }; + +这段代码的问题就是效率不高。每当你执行utils.addListener()或者utils.removeListener()时,同样的检查都会被重复执行。 + +如果使用条件初始化,那么浏览器探测的工作只需要在初始化代码的时候执行一次。在初始化的时候,代码探测一次环境,然后重新定义这个函数在剩下来的程序生命周期中应该怎样工作。下面是一个例子,看看如何达到这个目的: + + // AFTER + + // the interface + var utils = { + addListener: null, + removeListener: null + }; + + // the implementation + 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 + +任何时候都可以给函数添加自定义属性。添加自定义属性的一个有用场景是缓存函数的执行结果(返回值),这样下次同样的函数被调用的时候就不需要再做一次那些可能很复杂的计算。缓存一个函数的运行结果也就是为大家所熟知的Memoization。 + +在下面的例子中,myFunc函数创建了一个cache属性,可以通过myFunc.cache访问到。这个cache属性是一个对象(hash表),传给函数的参数会作为对象的key,函数执行结果会作为对象的值。函数的执行结果可以是任何的复杂数据结构: + + var myFunc = function (param) { + if (!myFunc.cache[param]) { + var result = {}; + // ... expensive operation ... + myFunc.cache[param] = result; + } + return myFunc.cache[param]; + }; + + // cache storage + 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 = {}; + // ... expensive operation ... + myFunc.cache[cachekey] = result; + } + return myFunc.cache[cachekey]; + }; + + // cache storage + myFunc.cache = {}; + +需要注意的是,在序列化的过程中,对象的“标识”将会丢失。如果你有两个不同的对象,却碰巧有相同的属性,那么他们会共享同样的缓存内容。 + +前面代码中的函数名还可以使用arguments.callee来替代,这样就不用将函数名硬编码。不过尽管现阶段这个办法可行,但是仍然需要注意,arguments.callee在ECMAScript 5的严格模式中是不被允许的: + + var myFunc = function (param) { + + var f = arguments.callee, + result; + + if (!f.cache[param]) { + result = {}; + // ... expensive operation ... + f.cache[param] = result; + } + return f.cache[param]; + }; + + // cache storage + 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中,函数实际上是对象,并且他们拥有方法。 + +下面是一个函数应用的例子: + + // define a function + var sayHi = function (who) { + return "Hello" + (who ? ", " + who : "") + "!"; + }; + + // invoke a function + sayHi(); // "Hello" + sayHi('world'); // "Hello, world!" + + // apply a function + 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()则接受任意多个参数,从第二个开始将参数依次传给函数。)不过有种情况下使用这个语法糖会更好:当你的函数只接受一个参数的时候,你可以省去为唯一的一个元素创建数组的工作: + + // the second is more efficient, saves an array + sayHi.apply(alien, ["humans"]); // "Hello, humans!" + sayHi.call(alien, "humans"); // "Hello, humans!" + + +### 部分应用 + +现在我们知道了,调用一个函数实际上就是给它应用一堆参数,那是否能够只传一部分参数而不传全部呢?这实际上跟我们手工处理数学函数非常类似。 + +假设已经有了一个add()函数,它的工作是把x和y两个数加到一起。下面的代码片段展示了当x为5、y为4时的计算步骤: + + // for illustration purposes + // not valid JavaScript + + // we have this function + function add(x, y) { + return x + y; + } + + // and we know the arguments + add(5, 4); + + // step 1 -- substitute one argument + function add(5, y) { + return 5 + y; + } + + // step 2 -- substitute the other argument + function add(5, 4) { + return 5 + 4; + } + +在这个代码片段中,step 1和step 2并不是有效的JavaScript代码,但是它展示了我们手工计算的过程。首先获得第一个参数的值,然后将未知的x和已知的值5替换到函数中。然后重复这个过程,直到替换掉所有的参数。 + +step 1是一个所谓的部分应用的例子:我们只应用了第一个参数。当你执行一个部分应用的时候并不能获得结果(或者是解决方案),取而代之的是另一个函数。 + +下面的代码片段展示了一个虚拟的partialApply()方法的用法: + + var add = function (x, y) { + return x + y; + }; + + // full application + add.apply(null, [5, 4]); // 9 + + // partial application + var newadd = add.partialApply(null, [5]); + // applying an argument to the new function + 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()函数使它柯里化,然后支持部分应用。 + +来看一个例子: + + // a curried add() + // accepts partial list of arguments + function add(x, y) { + var oldx = x, oldy = y; + if (typeof oldy === "undefined") { // partial + return function (newy) { + return oldx + newy; + }; + } + // full application + return x + y; + } + + // test + typeof add(5); // "function" + add(3)(4); // 7 + + // create and store a new function + var add2000 = add(2000); + add2000(10); // 2010 + +在这段代码中,第一次调用add()时,在返回的内层函数那里创建了一个闭包。这个闭包将原来的x和y的值存储到了oldx和oldy中。当内层函数执行的时候,oldx会被使用。如果没有部分应用,即x和y都传了值,那么这个函数会简单地将他们相加。这个add()函数的实现跟实际情况比起来有些冗余,仅仅是为了更好地说明问题。下面的代码片段中展示了一个更简洁的版本,没有oldx和oldy,因为原始的x已经被存储到了闭包中,此外我们复用了y作为本地变量,而不用像之前那样新定义一个变量newy: + + // a curried add + // accepts partial list of arguments + function add(x, y) { + if (typeof y === "undefined") { // partial + return function (y) { + return x + y; + }; + } + // full application + 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(也可以在闭包中访问到)即可。 + +现在有了通用的柯里化函数,就可以做一些测试了: + + // a normal function + function add(x, y) { + return x + y; + } + + // curry a function to get a new function + var newadd = schonfinkelize(add, 5); + newadd(4); // 9 + + // another option -- call the new function directly + schonfinkelize(add, 6)(7); // 13 + +用来做函数转换的schonfinkelize()并不局限于单个参数或者单步的柯里化。这里有些更多用法的例子: + + // a normal function + function add(a, b, c, d, e) { + return a + b + c + d + e; + } + + // works with any number of arguments + schonfinkelize(add, 1, 2, 3)(5, 5); // 16 + + // two-step currying + 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. 性能模式,这些模式帮助提高代码的执行速度,包括: + - Memoization + 利用函数的属性,使已经计算过的值不用再次计算 + - 自定义函数 + 重写自身的函数体,使第二次及后续的调用做更少的工作 \ No newline at end of file diff --git a/chapter6.markdown b/chapter6.markdown new file mode 100644 index 0000000..395653d --- /dev/null +++ b/chapter6.markdown @@ -0,0 +1,831 @@ + +# 代码复用模式 + +代码复用是一个既重要又有趣的话题,因为努力在自己或者别人写的代码上写尽量少且可以复用的代码是件很自然的事情,尤其当这些代码是经过测试的、可维护的、可扩展的、有文档的时候。 + +当我们说到代码复用的时候,想到的第一件事就是继承,本章会有很大篇幅讲述这个话题。你将看到好多种方法来实现“类式(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, inheritance + function Parent() {} + function Child() {} + inherit(Child, Parent); + + // testing the waters + 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. + // new constructor + 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. + // inherit + Parent = Parent || Object; + F = function () {}; + F.prototype = Parent.prototype; + Child.prototype = new F(); + Child.uber = Parent.prototype; + Child.prototype.constructor = Child; + + // 3. + // add implementation methods + for (i in props) { + if (props.hasOwnProperty(i)) { + Child.prototype[i] = props[i]; + } + } + + // return the "class" + return Child; + }; + +这个klass()实现有三个明显的部分: + +1. 创建Child()构造函数,这也是最后返回的将被作为类使用的函数。在这个函数里面,如果\_\_construct方法存在的话将被调用。同样是在父类的\_\_construct(如果存在)被调用前使用静态的uber属性。也可能存在uber没有定义的情况——比如从Object继承,因为它是在Man类中被定义的。 +2. 第二部分主要完成继承。只是简单地使用前面章节讨论过的Holy Grail类式继承模式。只有一个东西是新的:如果Parent没有传值的话,设定Parent为Object。 +3. 最后一部分是类真正定义的地方,循环需要实现的方法(如例子中的\_\_constructt和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不需要使用对象字面量来创建。(尽管这是一种更觉的方式。)可以使用构造函数来创建parent。注意,如果你这样做,那么自己的属性和原型上的属性都将被继承: + + // parent constructor + function Person() { + // an "own" property + this.name = "Adam"; + } + // a property added to the prototype + Person.prototype.getName = function () { + return this.name; + }; + + // create a new person + var papa = new Person(); + // inherit + var kid = object(papa); + + // test that both the own property + // and the prototype property were inherited + kid.getName(); // "Adam" + +在这种模式的另一个变种中,你可以选择只继承已存在的构造函数的原型对象。记住,对象继承自对象,不管父对象是怎么创建的。这是前面例子的一个修改版本: + + // parent constructor + function Person() { + // an "own" property + this.name = "Adam"; + } + // a property added to the prototype + Person.prototype.getName = function () { + + }; + + // inherit + var kid = object(Person.prototype); + + typeof kid.getName; // "function", because it was in the prototype + typeof kid.name; // "undefined", because only the prototype was inherited + + +###例外的ECMAScript 5 + +在ECMAScript 5中,原型继承已经正式成为语言的一部分。这种模式使用Object.create方法来实现。换句话说,你不再需要自己去写类似object()的函数,它是语言原生的了: + + var child = Object.create(parent); + +Object.create()接收一个额外的参数——一个对象。这个额外对象中的属性将被作为自己的属性添加到返回的子对象中。这让我们可以很方便地将继承和创建子对象在一个方法调用中实现。例如: + + var child = Object.create(parent, { + age: { value: 2 } // ECMA5 descriptor + }); + 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()函数以便做深拷贝。所有你需要做的事情只是检查一个属性的类型是否是对象,如果是,则递归遍历它的属性。另外一个需要做的检查是这个对象是真的对象还是数组。我们可以使用第3章讨论过的数组检查方式。最终深拷贝版的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() example + notmyobj.doStuff.call(myobj, param1, p2, p3); + // apply() example + 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; + } + + // example + 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; + } + }; + + // test + 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”。但是如果在某些场景下你将th函数赋值给了全局变量或者是将这个函数作为回调,会发生什么?在客户端编程中有非常多的事件和回调,所以这种情况经常发生: + + // assigning to a variable + // `this` will point to the global object + var say = one.say; + say('hoho'); // "hoho, undefined" + + // passing as a callback + 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中你可能有更多更简单更优化的方法,包括借用方法、绑定、复制属性、混元等。 + +记住,代码复用才是目标,继承只是达成这个目标的一种手段。 + diff --git a/chapter7.markdown b/chapter7.markdown new file mode 100644 index 0000000..251daca --- /dev/null +++ b/chapter7.markdown @@ -0,0 +1,33 @@ + +# 设计模式 + +在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中提出“单例”的时候,它们可能是在指第5章讨论过的“模块模式”。 +