From 9321145440da615115133ee6dba799b3d4228d3c Mon Sep 17 00:00:00 2001 From: TooBug Date: Thu, 25 Apr 2013 10:03:19 +0800 Subject: [PATCH 01/58] =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=20=E4=BB=8B=E7=BB=8D=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 7bc94f6..a547138 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -1,65 +1,65 @@ # 对象创建模式 -在JavaScript中创建对象很容易——可以通过使用对象直接量或者构造函数。本章将在此基础上介绍一些常用的对象创建模式。 +在JavaScript中创建对象是件很容易的事情,直接通过对象字面量或者构造函数就可以。本章将在此基础上介绍一些常用的对象创建模式。 -JavaScript语言本身简单、直观,通常也没有其他语言那样的语法特性:命名空间、模块、包、私有属性以及静态成员。本章将介绍一些常用的模式,以此实现这些语法特性。 +JavaScript语言本身很简单、直观,也没有其他语言的一些语言特性:命名空间、模块、包、私有属性以及静态成员。本章将介绍一些常用的模式,以此实现这些语言特性。 -我们将对命名空间、依赖声明、模块模式以及沙箱模式进行初探——它们帮助更好地组织应用程序的代码,有效地减轻全局污染的问题。除此之外,还会对包括:私有和特权成员、静态和私有静态成员、对象常量、链以及类式函数定义方式在内的话题进行讨论。 +我们将对命名空间、依赖声明、模块模式以及沙箱模式进行初探——它们可以帮助我们更好地组织应用程序的代码,有效地减少全局污染的问题。除此之外,还会讨论私有和特权成员、静态和私有静态成员、对象常量、链式调用以及一种像类式语言一样定义构造函数的方法等话题。 -## 命名空间模式(Namespace Pattern) +## 命名空间模式 -命名空间可以帮助减少全局变量的数量,与此同时,还能有效地避免命名冲突、名称前缀的滥用。 +使用命名空间可以减少全局变量的数量,与此同时,还能有效地避免命名冲突和前缀的滥用。 -JavaScript默认语法并不支持命名空间,但很容易可以实现此特性。为了避免产生全局污染,你可以为应用或者类库创建一个(通常就一个)全局对象,然后将所有的功能都添加到这个对象上,而不是到处申明大量的全局函数、全局对象以及其他全局变量。 +JavaScript没有原生的命名空间语法,但很容易可以实现这个特性。为了避免产生全局污染,你可以为应用或者类库创建一个(通常是唯一一个)全局对象,然后将所有的功能都添加到这个对象上,而不是到处声明大量的全局函数、全局对象以及其他的全局变量。 看如下例子: - // BEFORE: 5 globals - // Warning: antipattern - // constructors + // 重构前:5个全局变量 + // 注意:反模式 + // 构造函数 function Parent() {} function Child() {} - // a variable + // 一个变量 var some_var = 1; - // some objects + // 一些对象 var module1 = {}; module1.data = {a: 1, b: 2}; var module2 = {}; -可以通过创建一个全局对象(通常代表应用名)来重构上述这类代码,比方说, MYAPP,然后将上述例子中的函数和变量都变为该全局对象的属性: +可以通过创建一个全局对象(通常代表应用名)比如`MYAPP`来重构上述这类代码,然后将上述例子中的函数和变量都变为该全局对象的属性: - // AFTER: 1 global - // global object + // 重构后:一个全局变量 + // 全局对象 var MYAPP = {}; - // constructors + // 构造函数 MYAPP.Parent = function () {}; MYAPP.Child = function () {}; - // a variable + // 一个变量 MYAPP.some_var = 1; - // an object container + // 一个对象容器 MYAPP.modules = {}; - // nested objects + // 嵌套的对象 MYAPP.modules.module1 = {}; MYAPP.modules.module1.data = {a: 1, b: 2}; MYAPP.modules.module2 = {}; -这里的MYAPP就是命名空间对象,对象名可以随便取,可以是应用名、类库名、域名或者是公司名都可以。开发者经常约定全局变量都采用大写(所有字母都大写),这样可以显得比较突出(不过,要记住,一般大写的变量都用于表示常量)。 +这里的`MYAPP`就是命名空间对象,对象名可以随便取,可以是应用名、类库名、域名或者是公司名都可以。开发者经常约定全局变量都采用大写(所有字母都大写),这样可以显得比较突出(不过要记住,大写的变量也常用于表示常量)。 -这种模式是一种很好的提供命名空间的方式,避免了自身代码的命名冲突,同时还避免了同一个页面上自身代码和第三方代码(比如:JavaScript类库或者小部件)的冲突。这种模式在大多数情况下非常适用,但也有它的缺点: +这种模式是一种很好的提供命名空间的方式,避免了自身代码的命名冲突,同时还避免了同一个页面上自身代码和第三方代码(比如JavaScript类库或者widget)的冲突。这种模式在大多数情况下非常适用,但也有它的缺点: -* 代码量稍有增加;在每个函数和变量前加上这个命名空间对象的前缀,会增加代码量,增大文件大小 -* 该全局实例可以被随时修改 -* 命名的深度嵌套会减慢属性值的查询 +- 代码量稍有增加;在每个函数和变量前加上这个命名空间对象的前缀,会增加代码量,增大文件大小 +- 该全局实例可以被随时修改 +- 命名的深度嵌套会减慢属性值的查询 本章后续要介绍的沙箱模式则可以避免这些缺点。 -###通用命名空间函数 +### 通用命名空间函数 随着程序复杂度的提高,代码会分置在不同的文件中以特定顺序来加载,这样一来,就不能保证你的代码一定是第一个申明命名空间或者改变量下的属性的。甚至还会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示: From 0b44548cc13847d184caf3c404ae4d7b0e5c7259 Mon Sep 17 00:00:00 2001 From: TooBug Date: Thu, 25 Apr 2013 10:29:48 +0800 Subject: [PATCH 02/58] =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index a547138..1d35ee2 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -58,33 +58,32 @@ JavaScript没有原生的命名空间语法,但很容易可以实现这个特 本章后续要介绍的沙箱模式则可以避免这些缺点。 - ### 通用命名空间函数 -随着程序复杂度的提高,代码会分置在不同的文件中以特定顺序来加载,这样一来,就不能保证你的代码一定是第一个申明命名空间或者改变量下的属性的。甚至还会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示: +随着程序复杂度的提高,代码会被分拆在不同的文件中以按照页面需要来加载,这样一来,就不能保证你的代码一定是第一个定义命名空间或者某个属性的,甚至会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示: - // unsafe + // 不安全的做法 var MYAPP = {}; - // better + // 更好的做法 if (typeof MYAPP === "undefined") { var MYAPP = {}; } - // or shorter + // 简写 var MYAPP = MYAPP || {}; -如上所示,不难看出,如果每次做类似操作都要这样检查一下就会有很多重复性的代码。比方说,要申明**MYAPP.modules.module2**,就要重复三次这样的检查。所以,我们需要一个重用的**namespace()**函数来专门处理这些检查工作,然后用它来创建命名空间,如下所示: +如上所示,如果每次做类似操作都要这样检查一下就会有很多重复的代码。例如,要声明`MYAPP.modules.module2`,就要重复三次这样的检查。所以,我们需要一个可复用的`namespace()`函数来专门处理这些检查工作,然后用它来创建命名空间,如下所示: - // using a namespace function + // 使用命名空间函数 MYAPP.namespace('MYAPP.modules.module2'); - // equivalent to: + // 等价于: // var MYAPP = { - // modules: { - // module2: {} - // } + // modules: { + // module2: {} + // } // }; -下面是上述namespace函数的实现案例。这种实现是无损的,意味着如果要创建的命名空间已经存在,则不会再重复创建: +下面是上述`namespace`函数的实现示例。这种实现是非破坏性的,意味着如果要创建的命名空间已经存在,则不会再重复创建: var MYAPP = MYAPP || {}; MYAPP.namespace = function (ns_string) { @@ -92,13 +91,14 @@ JavaScript没有原生的命名空间语法,但很容易可以实现这个特 parent = MYAPP, i; - // strip redundant leading global + // 去除不必要的全局变量层 + // 译注:因为namespace已经属于MYAPP if (parts[0] === "MYAPP") { parts = parts.slice(1); } for (i = 0; i < parts.length; i += 1) { - // create a property if it doesn't exist + // 如果属性不存在则创建它 if (typeof parent[parts[i]] === "undefined") { parent[parts[i]] = {}; } @@ -107,23 +107,23 @@ JavaScript没有原生的命名空间语法,但很容易可以实现这个特 return parent; }; -上述实现支持如下使用: +上述实现支持如下几种用法: - // assign returned value to a local var + // 将返回值赋给本地变量 var module2 = MYAPP.namespace('MYAPP.modules.module2'); module2 === MYAPP.modules.module2; // true - // skip initial `MYAPP` + // 省略全局命名空间`MYAPP` MYAPP.namespace('modules.module51'); - // long namespace + // 长命名空间 MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property'); -图5-1 展示了上述代码创建的命名空间对象在Firebug下的可视结果 +图5-1 展示了上述代码创建的命名空间对象在Firebug下的可视化结果 ![MYAPP命名空间在Firebug下的可视结果](./Figure/chapter5/5-1.jpg) -图5-1 MYAPP命名空间在Firebug下的可视结果 +图5-1 MYAPP命名空间在Firebug下的可视化结果 ## 声明依赖 From 9b698977751f0b7a60090b2ef5727a4b323c7791 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 10:29:37 +0800 Subject: [PATCH 03/58] =?UTF-8?q?=E5=A3=B0=E6=98=8E=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=88=90=20=E7=A7=81=E6=9C=89?= =?UTF-8?q?=E6=88=90=E5=91=98=E5=AE=8C=E6=88=90=E4=B8=80=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 90 +++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 1d35ee2..74bef9c 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -127,27 +127,26 @@ JavaScript没有原生的命名空间语法,但很容易可以实现这个特 ## 声明依赖 -JavaScript库往往是模块化而且有用到命名空间的,这使得你可以只使用你需要的模块。比如在YUI2中,全局变量YAHOO就是一个命名空间,各个模块作为全局变量的属性,比如YAHOO.util.Dom(DOM模块)、YAHOO.util.Event(事件模块)。 +JavaScript库往往是模块化而且有用到命名空间的,这使得你可以只使用你需要的模块。比如在YUI2中,全局变量`YAHOO`就是一个命名空间,各个模块都是全局变量的属性,比如`YAHOO.util.Dom`(DOM模块)、`YAHOO.util.Event`(事件模块)。 将你的代码依赖在函数或者模块的顶部进行声明是一个好主意。声明就是创建一个本地变量,指向你需要用到的模块: var myFunction = function () { - // dependencies + // 依赖 var event = YAHOO.util.Event, dom = YAHOO.util.Dom; - // use event and dom variables - // for the rest of the function... + // 在函数后面的代码中使用event和dom…… }; 这是一个相当简单的模式,但是有很多的好处: -- 明确的声明依赖是告知你代码的用户,需要保证指定的脚本文件被包含在页面中。 +- 明确的声明依赖是告知使用你代码的开发者,需要保证指定的脚本文件被包含在页面中。 - 将声明放在函数顶部使得依赖很容易被查找和解析。 -- 本地变量(如dom)永远会比全局变量(如YAHOO)要快,甚至比全局变量的属性(如YAHOO.util.Dom)还要快,这样会有更好的性能。使用了依赖声明模式之后,全局变量的解析在函数中只会进行一次,在此之后将会使用更快的本地变量。 -- 一些高级的代码压缩工具比如YUI Compressor和Google Closure compiler会重命名本地变量(比如event可能会被压缩成一个字母,如A),这会使代码更精简,但这个操作不会对全局变量进行,因为这样做不安全。 +- 本地变量(如`dom`)永远会比全局变量(如`YAHOO`)要快,甚至比全局变量的属性(如`YAHOO.util.Dom`)还要快,这样会有更好的性能。使用了依赖声明模式之后,全局变量的解析在函数中只会进行一次,在此之后将会使用更快的本地变量。 +- 一些高级的代码压缩工具比如YUI Compressor和Google Closure compiler会重命名本地变量(比如`event`可能会被压缩成一个字母,如`A`),这会使代码更精简,但这个操作不会对全局变量进行,因为这样做不安全。 -下面的代码片段是关于是否使用依赖声明模式对压缩影响的展示。尽管使用了依赖声明模式的test2()看起来复杂,因为需要更多的代码行数和一个额外的变量,但在压缩后它的代码量却会更小,意味着用户只需要下载更少的代码: +下面的代码片段是关于是否使用依赖声明模式对压缩影响的展示。尽管使用了依赖声明模式的`test2()`看起来复杂,因为需要更多的代码行数和一个额外的变量,但在压缩后它的代码量却会更小,意味着用户只需要下载更少的代码: function test1() { alert(MYAPP.modules.m1); @@ -156,7 +155,7 @@ JavaScript库往往是模块化而且有用到命名空间的,这使得你可 } /* - minified test1 body: + test1()压缩后的函数体: alert(MYAPP.modules.m1);alert(MYAPP.modules.m2);alert(MYAPP.modules.m51) */ @@ -168,7 +167,7 @@ JavaScript库往往是模块化而且有用到命名空间的,这使得你可 } /* - minified test2 body: + test2()压缩后的函数体: var a=MYAPP.modules;alert(a.m1);alert(a.m2);alert(a.m51) */ @@ -183,8 +182,8 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 return this.myprop; } }; - console.log(myobj.myprop); // `myprop` is publicly accessible - console.log(myobj.getProp()); // getProp() is public too + console.log(myobj.myprop); // myprop是公有的 + console.log(myobj.getProp()); // getProp()也是公有的 当你使用构造函数创建对象的时候也是一样的,所有的成员都是公有的: @@ -195,60 +194,60 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 }; } var toy = new Gadget(); - console.log(toy.name); // `name` is public - console.log(toy.stretch()); // stretch() is public + console.log(toy.name); // name是公有的 + console.log(toy.stretch()); // stretch()也是公有的 ### 私有成员 -尽管语言并没有用于私有成员的专门语法,但你可以通过闭包来实现。在构造函数中创建一个闭包,任何在这个闭包中的部分都不会暴露到构造函数之外。但是,这些私有变量却可以被公有方法访问,也就是在构造函数中定义的并且作为返回对象一部分的那些方法。我们来看一个例子,name是一个私有成员,在构造函数之外不能被访问: +尽管语言并没有用于私有成员的专门语法,但你可以通过闭包来实现。在构造函数中创建一个闭包,任何在这个闭包中的部分都不会暴露到构造函数之外。但是,这些私有变量却可以被公有方法访问,也就是在构造函数中定义的并且作为返回对象一部分的那些方法。我们来看一个例子,`name`是一个私有成员,在构造函数之外不能被访问: function Gadget() { - // private member + // 私有成员 var name = 'iPod'; - // public function + // 公有函数 this.getName = function () { return name; }; } var toy = new Gadget(); - // `name` is undefined, it's private + // name是是私有的 console.log(toy.name); // undefined - // public method has access to `name` + // 公有方法可以访问到name console.log(toy.getName()); // "iPod" 如你所见,在JavaScript创建私有成员很容易。你需要做的只是将私有成员放在一个函数中,保证它是函数的本地变量,也就是说让它在函数之外不可以被访问。 ### 特权方法 -特权方法的概念不涉及到任何语法,它只是一个给可以访问到私有成员的公有方法的名字(就像它们有更多权限一样)。 +特权方法的概念不涉及到任何语法,它只是一个给可以访问到私有成员的公有方法的名字(就好像它们有更多权限一样)。 -在前面的例子中,getName()就是一个特权方法,因为它有访问name属性的特殊权限。 +在前面的例子中,`getName()`就是一个特权方法,因为它有访问`name`属性的特殊权限。 ### 私有成员失效 当你使用私有成员时,需要考虑一些极端情况: -- 在Firefox的一些早期版本中,允许通过给eval()传递第二个参数的方法来指定上下文对象,从而允许访问函数的私有作用域。比如在Mozilla Rhino(译注:一个JavaScript引擎)中,允许使用`__parent__`来访问私有作用域。现在这些极端情况并没有被广泛应用到浏览器中。 +- 在Firefox的一些早期版本中,允许通过给`eval()`传递第二个参数的方法来指定上下文对象,从而允许访问函数的私有作用域。比如在Mozilla Rhino(译注:一个JavaScript引擎)中,允许使用`__parent__`来访问私有作用域。这些极端情况现在并没有广泛存在于浏览器中。 - 当你直接通过特权方法返回一个私有变量,而这个私有变量恰好是一个对象或者数组时,外部的代码可以修改这个私有变量,因为它是按引用传递的。 -我们来看一下第二种情况。下面的Gadget的实现看起来没有问题: +我们来看一下第二种情况。下面的`Gadget`的实现看起来没有问题: function Gadget() { - // private member + // 私有成员 var specs = { - screen_width: 320, - screen_height: 480, - color: "white" + screen_width: 320, + screen_height: 480, + color: "white" }; - // public function + // 公有函数 this.getSpecs = function () { return specs; }; } -这里的问题是getSpecs()返回了一个specs对象的引用。这使得Gadget的使用者可以修改貌似隐藏起来的私有成员specs: +这里的问题是`getSpecs()`返回了一个`specs`对象的引用。这使得`Gadget()`的使用者可以修改貌似隐藏起来的私有成员`specs`: var toy = new Gadget(), specs = toy.getSpecs(); @@ -264,25 +263,24 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 图5-2 私有对象被修改了 -这个意外的问题的解决方法就是不要将你想保持私有的对象或者数组的引用传递出去。达到这个目标的一种方法是让getSpecs()返回一个新对象,这个新对象只包含对象的使用者感兴趣的数据。这也是众所周知的“最低授权原则”(Principle of Least Authority,简称POLA),指永远不要给出比需求更多的东西。在这个例子中,如果Gadget的使用者关注它是否适应一个特定的盒子,它只需要知道尺寸即可。所以你应该创建一个getDimensions(),用它返回一个只包含width和height的新对象,而不是把什么都给出去。也就是说,也许你根本不需要实现getSpecs()方法。 +这个问题有点出乎意料,解决方法就是不要将你想保持私有的对象或者数组的引用传递出去。达到这个目标的一种方法是让`getSpecs()`返回一个新对象,这个新对象只包含对象的使用者需要的数据。这也是众所周知的“最低授权原则”(Principle of Least Authority,简称POLA),指永远不要给出比真实需要更多的东西。在这个例子中,如果`Gadget()`的使用者关注它是否适应一个特定的盒子,它只需要知道尺寸即可。所以你应该创建一个`getDimensions()`,用它返回一个只包含`width`和`height`的新对象,而不是把什么都给出去。也就是说,也许你根本不需要实现`getSpecs()`方法。 -当你需要传递所有的数据时,有另外一种方法,就是使用通用的对象复制函数创建specs对象的一个副本。下一章提供了两个这样的函数——一个叫extend(),它会浅复制一个给定的对象(只复制顶层的成员)。另一个叫extendDeep(),它会做深复制,遍历所有的属性和嵌套的属性。 +当你需要传递所有的数据时,有另外一种方法,就是使用通用的对象复制函数创建`specs`对象的一个副本。下一章提供了两个这样的函数——一个叫`extend()`,它会浅复制一个给定的对象(只复制顶层的成员),另一个叫`extendDeep()`,它会做深复制,遍历所有的属性和嵌套的属性。 ### 对象字面量和私有成员 到目前为止,我们只看了使用构建函数创建私有成员的示例。如果使用对象字面量创建对象时会是什么情况呢?是否有可能含有私有成员? -如你前面所看到的那样,私有数据使用一个函数来包裹。所以在使用对象字面量时,你也可以使用一个立即执行的匿名函数创建的闭包。例如: +如你前面所看到的那样,私有数据使用一个函数来包裹。所以在使用对象字面量时,你也可以使用一个即时函数创建的闭包。例如: - var myobj; // this will be the object + var myobj; // 一个对象 (function () { - // private members + // 私有成员 var name = "my, oh my"; - // implement the public part - // note -- no `var` + // 实现公有部分,注意没有var myobj = { - // privileged method + // 特权方法 getName: function () { return name; } @@ -294,10 +292,10 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 还有一个原理一样但看起来不一样的实现示例: var myobj = (function () { - // private members + // 私有成员 var name = "my, oh my"; - // implement the public part + // 实现公有部分 return { getName: function () { return name; @@ -313,23 +311,23 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 使用构造函数创建私有成员的一个弊端是,每一次调用构造函数创建对象时这些私有成员都会被创建一次。 -这对在构建函数中添加到`this`的成员来说是一个问题。为了避免重复劳动,节省内存,你可以将共用的属性和方法添加到构造函数的`prototype`(原型)属性中。这样的话这些公共的部分会在使用同一个构造函数创建的所有实例中共享。你也同样可以在这些实例中共享私有成员。你可以将两种模式联合起来达到这个目的:构造函数中的私有属性和对象字面量中的私有属性。因为`prototype`属性也只是一个对象,可以使用对象字面量创建。 +这对在构建函数中添加到`this`的成员来说是一个问题。为了避免重复劳动,节省内存,你可以将共用的属性和方法添加到构造函数的`prototype`(原型)属性中。这样的话这些公共的部分会在使用同一个构造函数创建的所有实例中共享。你也同样可以在这些实例中共享私有成员,甚至可以将两种模式联合起来达到这个目的,同时使用构造函数中的私有属性和对象字面量中的私有属性。因为`prototype`属性也只是一个对象,可以使用对象字面量创建。 这是一个示例: function Gadget() { - // private member + // 私有成员 var name = 'iPod'; - // public function + // 公有函数 this.getName = function () { return name; }; } Gadget.prototype = (function () { - // private member + // 私有成员 var browser = "Mobile Webkit"; - // public prototype members + // 公有函数 return { getBrowser: function () { return browser; @@ -338,8 +336,8 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 }()); var toy = new Gadget(); - console.log(toy.getName()); // privileged "own" method - console.log(toy.getBrowser()); // privileged prototype method + console.log(toy.getName()); // 自有的特权方法 + console.log(toy.getBrowser()); // 来自原型的特权方法 ### 将私有函数暴露为公有方法 From a7c20d628c211f3deefe57d8ec2c147af243ff99 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 16:06:16 +0800 Subject: [PATCH 04/58] =?UTF-8?q?=E6=9A=B4=E9=9C=B2=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 74bef9c..1d8abbc 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -341,7 +341,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 ### 将私有函数暴露为公有方法 -“暴露模式”是指将已经有的私有函数暴露为公有方法。当对对象进行操作时,所有功能代码都对这些操作很敏感,而你想尽量保护这些代码的时候很有用。(译注:指对来自外部的修改很敏感。)但同时,你又希望能提供一些功能的访问权限,因为它们会被用到。如果你把这些方法公开,就会使得它们不再健壮,你的API的使用者可能修改它们。在ECMAScript5中,你可以选择冻结一个对象,但在之前的版本中不可用。下面进入暴露模式(原来是由Christian Heilmann创造的模式,叫“暴露模块模式”)。 +“暴露模式”是指将已经有的私有函数暴露为公有方法,它在你希望尽量保护对象内的一些方法不被外部修改干扰的时候很有用。你希望能提供一些功能给外部访问,因为它们会被用到,如果你把这些方法公开,就会使得它们不再健壮,因为你的API的使用者可能修改它们。在ECMAScript5中,你可以选择冻结一个对象,但在之前的版本中这种方法不可用。下面进入暴露模式(原来是由Christian Heilmann创造的模式,叫“暴露模块模式”)。 我们来看一个例子,它建立在对象字面量的私有成员模式之上: @@ -375,7 +375,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 }()); -这里有两个私有变量和两个私有函数——`isArray()`和`indexOf()`。在包裹函数的最后,使用那些允许被从外部访问的函数填充`myarray`对象。在这个例子中,同一个私有函数 `indexOf()`同时被暴露为ECMAScript 5风格的`indexOf`和PHP风格的`inArry`。测试一下myarray对象: +这里有两个私有变量(私有函数)——`isArray()`和`indexOf()`。在包裹函数的最后,用那些允许被从外部访问的函数填充`myarray`对象。在这个例子中,同一个私有函数 `indexOf()`同时被暴露为ECMAScript5风格的`indexOf()`和PHP风格的`inArry()`。测试一下`myarray`对象: myarray.isArray([1,2]); // true myarray.isArray({0: 1}); // false From 34c134d82e38f2fb2f24b07534310d2d4a24280b Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 16:13:17 +0800 Subject: [PATCH 05/58] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=AC=AC=E4=B8=80=E9=83=A8=E5=88=86=20=E6=A0=A1=E5=AF=B9?= =?UTF-8?q?=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 44 ++++++++++++++------------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 1d8abbc..92e2d47 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -394,7 +394,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 模块模式是我们目前讨论过的好几种模式的组合,即: - 命名空间模式 -- 立即执行的函数模式 +- 即时函数模式 - 私有和特权成员模式 - 依赖声明模式 @@ -402,7 +402,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 MYAPP.namespace('MYAPP.utilities.array'); -下一步是定义模块。使用一个立即执行的函数来提供私有作用域供私有成员使用。立即执行的函数返回一个对象,也就是带有公有接口的真正的模块,可以供其它代码使用: +下一步是定义模块。使用一个即时函数来提供私有作用域供私有成员使用。即时函数返回一个对象,也就是带有公有接口的真正的模块,可以供其它代码使用: MYAPP.utilities.array = (function () { return { @@ -423,40 +423,28 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 }; }()); -如果需要的话,你可以在立即执行的函数提供的闭包中声明私有属性和私有方法。函数顶部也是声明依赖的地方。在变量声明的下方,你可以选择性地放置辅助初始化模块的一次性代码。函数最终返回的是一个包含模块公共API的对象: +如果需要的话,你可以在即时函数提供的闭包中声明私有属性和私有方法。同样,声明依赖放置在函数顶部,在变量声明的下方可以选择性地放置辅助初始化模块的一次性代码。函数最终返回的是一个包含模块公共API的对象: MYAPP.namespace('MYAPP.utilities.array'); MYAPP.utilities.array = (function () { -<<<<<<< HEAD - // dependencies + // 声明依赖 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, - // private properties + // 私有属性 array_string = "[object Array]", ops = Object.prototype.toString; -======= - // dependencies - var uobj = MYAPP.utilities.object, - ulang = MYAPP.utilities.lang, ->>>>>>> 合并改动 - // private properties - array_string = "[object Array]", - ops = Object.prototype.toString; + // 私有方法 + // …… -<<<<<<< HEAD -======= - // private methods - // ... - // end var + // 结束变量声明 ->>>>>>> 合并改动 - // optionally one-time init procedures - // ... + // 选择性放置一次性初始化的代码 + // …… - // public API + // 公有API return { inArray: function (needle, haystack) { @@ -466,19 +454,15 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 } } }, -<<<<<<< HEAD - -======= - ->>>>>>> 合并改动 + isArray: function (a) { return ops.call(a) === array_string; } - // ... more methods and properties + // ……更多的方法和属性 }; }()); -模块模式被广泛使用,这是一种值得强烈推荐的模式,它可以帮助组织代码,尤其是代码量在不断增长的时候。 +模块模式被广泛使用,是一种值得强烈推荐的模式,它可以帮助我们组织代码,尤其是代码量在不断增长的时候。 ### 暴露模块模式 From 3a7d8afdce47273672ce51e3c22ec14a38d8c0cc Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 16:20:51 +0800 Subject: [PATCH 06/58] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 55 ++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 92e2d47..65a97ec 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -472,25 +472,25 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 MYAPP.utilities.array = (function () { - // private properties + // 私有属性 var array_string = "[object Array]", ops = Object.prototype.toString, - // private methods - inArray = function (haystack, needle) { - for (var i = 0, max = haystack.length; i < max; i += 1) { - if (haystack[i] === needle) { - return i; + // 私有方法 + 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; - }; - // end var + return −1; + }, + isArray = function (a) { + return ops.call(a) === array_string; + }; + // 结束变量定义 - // revealing public API + // 暴露公有API return { isArray: isArray, indexOf: inArray @@ -499,7 +499,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 ### 创建构造函数的模块 -前面的例子创建了一个对象`MYAPP.utilities.array`,但有时候使用构造函数来创建对象会更方便。你也可以同样使用模块模式来做。唯一的区别是包裹模块的立即执行的函数会在最后返回一个函数,而不是一个对象。 +前面的例子创建了一个对象`MYAPP.utilities.array`,但有时候使用构造函数来创建对象会更方便。你也可以同样使用模块模式来做。唯一的区别是包裹模块的即时函数会在最后返回一个函数,而不是一个对象。 看下面的模块模式的例子,创建了一个构造函数`MYAPP.utilities.Array`: @@ -507,23 +507,23 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 MYAPP.utilities.Array = (function () { - // dependencies + // 声明依赖 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, - // private properties and methods... - Constr; + // 私有属性和方法…… + Constr; - // end var + // 结束变量定义 - // optionally one-time init procedures - // ... + // 选择性放置一次性初始化代码 + // …… - // public API -- constructor + // 公有API——构造函数 Constr = function (o) { this.elements = this.toArray(o); }; - // public API -- prototype + // 公有API——原型 Constr.prototype = { constructor: MYAPP.utilities.Array, version: "2.0", @@ -535,8 +535,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 } }; - // return the constructor - // to be assigned to the new namespace + // 返回构造函数 return Constr; }()); @@ -547,13 +546,11 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 ### 在模块中引入全局上下文 -作为这种模式的一个常见的变种,你可以给包裹模块的立即执行的函数传递参数。你可以传递任何值,但通常会传递全局变量甚至是全局对象本身。引入全局上下文可以加快函数内部的全局变量的解析,因为引入之后会作为函数的本地变量: +作为这种模式的一个常见的变种,你可以给包裹模块的即时函数传递参数。你可以传递任何值,但通常情况下会传递全局变量甚至是全局对象本身。引入全局上下文可以加快函数内部的全局变量的解析,因为引入之后会作为函数的本地变量: MYAPP.utilities.module = (function (app, global) { - // references to the global object - // and to the global app namespace object - // are now localized + // 全局对象和全局命名空间都作为本地变量存在 }(MYAPP, this)); From 98ac65bd753bffba8e1503eab4e9e1672a5adce5 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 16:31:26 +0800 Subject: [PATCH 07/58] =?UTF-8?q?=E6=B2=99=E7=AE=B1=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=BB=8B=E7=BB=8D=E5=92=8C=E4=BD=BF=E7=94=A8=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=20=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 65a97ec..7fd31b9 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -558,12 +558,12 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 沙箱模式主要着眼于命名空间模式的短处,即: -- 依赖一个全局变量成为应用的全局命名空间。在命名空间模式中,没有办法在同一个页面中运行同一个应用或者类库的不同版本,在为它们都会需要同一个全局变量名,比如`MYAPP`。 +- 依赖一个全局变量成为应用的全局命名空间。在命名空间模式中,没有办法在同一个页面中运行同一个应用或者类库的不同版本,因为它们都会需要同一个全局变量名,比如`MYAPP`。 - 代码中以点分隔的名字比较长,无论写代码还是解析都需要处理这个很长的名字,比如`MYAPP.utilities.array`。 顾名思义,沙箱模式为模块提供了一个环境,模块在这个环境中的任何行为都不会影响其它的模块和其它模块的沙箱。 -这个模式在YUI3中用得很多,但是需要记住的是,下面的讨论只是一些示例实现,并不讨论YUI3中的消息箱是如何实现的。 +这个模式在YUI3中用得很多,但是需要记住的是,下面的讨论只是一些示例实现,并不讨论YUI3中的沙箱是如何实现的。 ### 全局构造函数 @@ -572,19 +572,19 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 使用沙箱模式是像这样: new Sandbox(function (box) { - // your code here... + // 你的代码…… }); `box`对象和命名空间模式中的`MYAPP`类似,它包含了所有你的代码需要用到的功能。 我们要多做两件事情: -- 通过一些手段(第3章中的强制使用new的模式),你可以在创建对象的时候不要求一定有new。 +- 通过一些手段(第3章中的强制使用`new`的模式),你可以在创建对象的时候不要求一定有`new`。 - 让`Sandbox()`构造函数可以接受一个(或多个)额外的配置参数,用于指定这个对象需要用到的模块名字。我们希望代码是模块化的,因此绝大部分`Sandbox()`提供的功能都会被包含在模块中。 有了这两个额外的特性之后,我们来看一下实例化对象的代码是什么样子。 -你可以在创建对象时省略`new`并像这样使用已有的“ajax”和“event”模块: +你可以在创建对象时省略`new`并像这样使用已有的`ajax`和`event`模块: Sandbox(['ajax', 'event'], function (box) { // console.log(box); @@ -596,7 +596,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 // console.log(box); }); -使用通配符“*”来表示“使用所有可用的模块”如何?为了方便,我们也假设没有任何模块传入时,沙箱使用“*”。所以有两种使用所有可用模块的方法: +使用通配符“*”来表示“使用所有可用的模块”是个不错的想法,为了方便,我们也假设没有任何模块传入时,沙箱使用“*”。所以有两种使用所有可用模块的方法: Sandbox('*', function (box) { // console.log(box); @@ -606,23 +606,22 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 // console.log(box); }); -下面的例子展示了如何实例化多个消息箱对象,你甚至可以将它们嵌套起来而互不影响: +下面的例子展示了如何实例化多个沙箱对象,你甚至可以将它们嵌套起来而互不影响: Sandbox('dom', 'event', function (box) { - // work with dom and event + // 使用dom和event模块 Sandbox('ajax', function (box) { - // another sandboxed "box" object - // this "box" is not the same as - // the "box" outside this function + // 另一个沙箱中的box,这个box和外面的box不一样 //... - // done with Ajax + // 使用ajax模块的代码到此为止 + }); - // no trace of Ajax module here + // 这里的代码与ajax模块无关 }); 从这些例子中看到,使用沙箱模式可以通过将代码包裹在回调函数中的方式来保护全局命名空间。 From c030d6657116c055d601efed47fc999d9fad8698 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 16:39:52 +0800 Subject: [PATCH 08/58] =?UTF-8?q?=E6=B2=99=E7=AE=B1=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 7fd31b9..4a8c4ed 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -5,7 +5,7 @@ JavaScript语言本身很简单、直观,也没有其他语言的一些语言特性:命名空间、模块、包、私有属性以及静态成员。本章将介绍一些常用的模式,以此实现这些语言特性。 我们将对命名空间、依赖声明、模块模式以及沙箱模式进行初探——它们可以帮助我们更好地组织应用程序的代码,有效地减少全局污染的问题。除此之外,还会讨论私有和特权成员、静态和私有静态成员、对象常量、链式调用以及一种像类式语言一样定义构造函数的方法等话题。 - +/Users/TooBug/github/javascript.patterns/chapter5.markdown ## 命名空间模式 使用命名空间可以减少全局变量的数量,与此同时,还能有效地避免命名冲突和前缀的滥用。 @@ -634,7 +634,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 ### 添加模块 -在动手实现构造函数之前,我们来看一下如何添加模块。 +在动手实现构造函数之前,我们先来看一下如何添加模块。 `Sandbox()`构造函数也是一个对象,所以可以给它添加一个`modules`静态属性。这个属性也是一个包含名值(key-value)对的对象,其中key是模块的名字,value是模块的功能实现。 @@ -647,7 +647,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 }; Sandbox.modules.event = function (box) { - // access to the Sandbox prototype if needed: + // 如果有需要的话可以访问Sandbox的原型 // box.constructor.prototype.m = "mmm"; box.attachEvent = function () {}; box.dettachEvent = function () {}; @@ -658,34 +658,33 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 box.getResponse = function () {}; }; -在这个例子中我们添加了`dom`、`event`和`ajax`模块,这些都是在每个类库或者复杂的web应用中很常见的代码片段。 +在这个例子中我们添加了`dom`、`event`和`ajax`模块,这些模块在每个类库或者复杂的web应用中都很常见。 -实现每个模块功能的函数接受一个实例`box`作为参数,并给这个实例添加属性和方法。 +每个模块功能函数接受一个实例`box`作为参数,并给这个实例添加属性和方法。 ### 实现构造函数 最后,我们来实现`Sandbox()`构造函数(你可能会很自然地想将这类构造函数命名为对你的类库或者应用有意义的名字): function Sandbox() { - // turning arguments into an array + // 将参数转换为数组 var args = Array.prototype.slice.call(arguments), - // the last argument is the callback + // 最后一个参数是回调函数 callback = args.pop(), - // modules can be passed as an array or as individual parameters + // 参数可以作为数组或者单独的参数传递 modules = (args[0] && typeof args[0] === "string") ? args : args[0], i; - // make sure the function is called - // as a constructor + // 保证函数是作为构造函数被调用 if (!(this instanceof Sandbox)) { return new Sandbox(modules, callback); } - // add properties to `this` as needed: + // 根据需要给this添加属性 this.a = 1; this.b = 2; - // now add modules to the core `this` object - // no modules or "*" both mean "use all modules" + // 给this对象添加模块 + // 未指明模块或者*都表示“使用所有模块” if (!modules || modules === '*') { modules = []; for (i in Sandbox.modules) { @@ -695,16 +694,16 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 } } - // initialize the required modules + // 初始化指定的模块 for (i = 0; i < modules.length; i += 1) { Sandbox.modules[modules[i]](this); } - // call the callback + // 调用回调函数 callback(this); } - // any prototype properties as needed + // 需要添加在原型上的属性 Sandbox.prototype = { name: "My Application", version: "1.0", @@ -715,7 +714,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 这个实现中的一些关键点: -- 有一个检查`this`是否是`Sandbox`实例的过程,如果不是(也就是调用`Sandbox()`时没有加`new`),我们将这个函数作为构造函数再调用一次。 +- 有一个检查`this`是否是`Sandbox()`实例的过程,如果不是(也就是调用`Sandbox()`时没有加`new`),我们将这个函数作为构造函数再调用一次。 - 你可以在构造函数中给`this`添加属性,也可以给构造函数的原型添加属性。 - 被依赖的模块可以以数组的形式传递,也可以作为单独的参数传递,甚至以`*`通配符(或者省略)来表示加载所有可用的模块。值得注意的是,我们在这个示例实现中并没有考虑从外部文件中加载模块,但明显这是一个值得考虑的事情。比如YUI3就支持这种情况,你可以只加载最基本的模块(作为“种子”),其余需要的任何模块都通过将模块名和文件名对应的方式从外部文件中加载。 - 当我们知道依赖的模块之后就初始化它们,也就是调用实现每个模块的函数。 From ef8ce2d14f2fae74a2d253b7c6bbd338de35fc28 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 16:57:59 +0800 Subject: [PATCH 09/58] =?UTF-8?q?=E5=85=AC=E6=9C=89=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=88=90=E5=91=98=20=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 4a8c4ed..ffca775 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -722,33 +722,33 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 ## 静态成员 -静态属性和方法是指那些在所有的实例中保持一致的成员。在基于类的语言中,静态成员是用专门的语法来创建,使用时就像是类自己的成员一样。比如`MathUtils`类的`max()`方法会被像这样调用:`MathUtils.max(3, 5)`。这是一个公有静态成员的示例,即可以在不实例化类的情况下使用。同样也可以有私有的静态方法,即对类的使用者不可见,而在类的所有实例间是共享的。我们来看一下如何在JavaScript中实现公有和私有静态成员。 +静态属性和方法是指那些在所有的实例中都一样的成员。在基于类的语言中,静态成员是用专门的语法来创建,使用时就像是类自己的成员一样。比如`MathUtils`类的`max()`方法会被像这样调用:`MathUtils.max(3, 5)`。这是一个公有静态成员的示例,即可以在不实例化类的情况下使用。同样也可以有私有的静态方法,即对类的使用者不可见,而在类的所有实例间是共享的。我们来看一下如何在JavaScript中实现公有和私有静态成员。 ### 公有静态成员 -在JavaScript中没有专门用于静态成员的语法。但通过给构造函数添加属性的方法,可以拥有和基于类的语言一样的使用语法。之所以可以这样做是因为构造函数和其它的函数一样,也是对象,可以拥有属性。前一章讨论过的Memoization模式也使用了同样的方法,即给函数添加属性。 +在JavaScript中没有专门用于静态成员的语法。但通过给构造函数添加属性的方法,可以拥有和基于类的语言一样的使用语法。之所以可以这样做是因为构造函数和其它的函数一样,也是对象,可以拥有属性。前一章讨论过的记忆模式也使用了同样的方法,即给函数添加属性。 -下面的例子定义了一个构造函数`Gadget`,它有一个静态方法`isShiny()`和一个实例方法`setPrice()`。`isShiny()`是一个静态方法,因为它不需要指定一个对象才能工作(就像你不需要先指定一个工具(gadget)才知道所有的工具是不是有光泽的(shiny))。但setPrice()却需要一个对象,因为工具可能有不同的定价: +下面的例子定义了一个构造函数`Gadget()`,它有一个静态方法`isShiny()`和一个实例方法`setPrice()`。`isShiny()`是一个静态方法,因为它不需要指定一个具体的对象就能工作(你不需要先拿到一个特定的小工具(gadget)才知道所有小工具是不是有光泽的(shiny))。但setPrice()却需要一个对象,因为小工具可能有不同的定价: - // constructor + // 构造函数 var Gadget = function () {}; - // a static method + // 静态方法 Gadget.isShiny = function () { return "you bet"; }; - // a normal method added to the prototype + // 添加到原型的普通方法 Gadget.prototype.setPrice = function (price) { this.price = price; }; -现在我们来调用这些方法。静态方法`isShiny()`可以直接在构造函数上调用,但其它的常规方法需要一个实例: +现在我们来调用这些方法。静态方法`isShiny()`可以直接在构造函数上调用,但其它的方法需要一个实例: - // calling a static method + // 调用静态方法 Gadget.isShiny(); // "you bet" - // creating an instance and calling a method + // 创建实例并调用方法 var iphone = new Gadget(); iphone.setPrice(500); @@ -764,28 +764,28 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 在这种情况下,你需要很小心地处理静态方法内的`this`。当你运行`Gadget.isShiny()`时,在`isShiny()`内部的`this`指向`Gadget`构造函数。而如果你运行`iphone.isShiny()`,那么`this`会指向`iphone`。 -最后一个例子展示了同一个方法被静态调用和非静态调用时明显不同的行为,这取决于调用的方式。这里的`instanceof`用于获方法是如何被调用的: +下面的例子展示了同一个方法被静态调用和非静态调用时明显不同的行为,这取决于调用的方式。这里的`instanceof`用于获取方法是如何被调用的: - // constructor + // 构造函数 var Gadget = function (price) { this.price = price; }; - // a static method + // 静态方法 Gadget.isShiny = function () { - // this always works + // 这句始终正常工作 var msg = "you bet"; if (this instanceof Gadget) { - // this only works if called non-statically + // 这句只有在非静态方式调用时正常工作 msg += ", it costs $" + this.price + '!'; } return msg; }; - // a normal method added to the prototype + // 原型上添加的方法 Gadget.prototype.isShiny = function () { return Gadget.isShiny.call(this); }; From b04ff343e19c93f13f0fc47fc1242ac75c962453 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 17:04:49 +0800 Subject: [PATCH 10/58] =?UTF-8?q?=E7=A7=81=E6=9C=89=E9=9D=99=E6=80=81?= =?UTF-8?q?=E6=88=90=E5=91=98=20=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index ffca775..9e1c1ed 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -806,51 +806,49 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 - 被所有由同一构造函数创建的对象共享 - 不允许在构造函数外部访问 -我们来看一个例子,`counter`是`Gadget`构造函数的一个私有静态属性。在本章中我们已经讨论过私有属性,这里的做法也是一样,需要一个函数提供的闭包来包裹私有成员。然后让这个包裹函数立即执行并返回一个新的函数。将这个返回的函数赋值给`Gadget`作为构造函数。 +我们来看一个例子,`counter`是`Gadget()`构造函数的一个私有静态属性。在本章中我们已经讨论过私有属性,这里的做法也是一样,需要一个函数提供的闭包来包裹私有成员。然后让这个包裹函数立即执行并返回一个新的函数。将这个返回的函数赋值给`Gadget()`作为构造函数。 var Gadget = (function () { - // static variable/property + // 静态变量/属性 var counter = 0; - // returning the new implementation - // of the constructor + // 返回构造函数的新实现 return function () { console.log(counter += 1); }; - }()); // execute immediately + }()); // 立即执行 -这个`Gadget`构造函数只简单地增加私有的`counter`的值然后打印出来。用多个实例测试的话你会看到`counter`在实例之间是共享的: +这个`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()`方法: +因为我们在创建每个实例的时候`counter`的值都会加1,所以它实际上成了唯一标识使用`Gadget`构造函数创建的对象的ID。这个唯一标识可能会很有用,那为什么不把它通过一个特权方法暴露出去呢?(译注:严格来讲,这里不能叫ID,只是一个记录有多少个实例的数字而已,因为如果有多个实例被创建的话,没有办法取到除了最后一个之外的实例的标识。)下面的例子是基于前面的例子,增加了用于访问私有静态属性的`getLastId()`方法: - // constructor + // 构造函数 var Gadget = (function () { - // static variable/property + // 静态变量/属性 var counter = 0, NewGadget; - // this will become the - // new constructor implementation + // 这将是Gadget的新实现 NewGadget = function () { counter += 1; }; - // a privileged method + // 特权方法 NewGadget.prototype.getLastId = function () { return counter; }; - // overwrite the constructor + // 重写构造函数 return NewGadget; - }()); // execute immediately + }()); // 立即执行 测试这个新的实现: From 4f4f7f622f05255c1b4ff4cf93cc9bf042fb53db Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 17:18:21 +0800 Subject: [PATCH 11/58] =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=B8=B8=E9=87=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 9e1c1ed..150b50b 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -863,7 +863,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 ## 对象常量 -JavaScript中是没有常量的,尽管在一些比较现代的环境中可能会提供`const`来创建常量。 +在一些比较现代的环境中可能会提供`const`来创建常量,但在其它的环境中,JavaScript是没有常量的。 一种常用的解决办法是通过命名规范,让不应该变化的变量使用全大写。这个规范实际上也用在JavaScript原生对象中: @@ -873,32 +873,34 @@ JavaScript中是没有常量的,尽管在一些比较现代的环境中可能 你自己的常量也可以用这种规范,然后将它们作为静态属性加到构造函数中: - // constructor + // 构造函数 var Widget = function () { - // implementation... + // 实现…… }; - // constants + // 常量 Widget.MAX_HEIGHT = 320; Widget.MAX_WIDTH = 480; -同样的规范也适用于使用字面量创建的对象,常量会是使用大写名字的普通名字。 +同样的规范也适用于使用字面量创建的对象,常量会是使用大写名字的属性。 如果你真的希望有一个不能被改变的值,那么可以创建一个私有属性,然后提供一个取值的方法(getter),但不给赋值的方法(setter)。这种方法在很多可以用命名规范解决的情况下可能有些矫枉过正,但不失为一种选择。 -下面是一个通过的`constant`对象的实现,它提供了这些方法: +下面是一个通用的`constant`对象的实现,它提供了这些方法: - set(name, value) 定义一个新的常量 + - isDefined(name) 检查一个常量是否存在 + - get(name) 取常量的值 -在这个实现中,只允许基本类型的值成为常量。同时还要使用`hasOwnproperty()`小心地处理那些恰好是原生属性的常量名,比如`toString`或者`hasOwnProperty`,然后给所有的常量名加上一个随机生成的前缀: +在这个实现中,只允许基本类型的值成为常量。同时还要使用`hasOwnProperty()`小心地处理那些恰好是原生属性的常量名,比如`toString`或者`hasOwnProperty`,然后给所有的常量名加上一个随机生成的前缀: var constant = (function () { var constants = {}, @@ -934,19 +936,19 @@ JavaScript中是没有常量的,尽管在一些比较现代的环境中可能 测试这个实现: - // check if defined + // 检查是否定义 constant.isDefined("maxwidth"); // false - // define + // 定义 constant.set("maxwidth", 480); // true - // check again + // 再次检查 constant.isDefined("maxwidth"); // true - // attempt to redefine + // 尝试重定义 constant.set("maxwidth", 320); // false - // is the value still intact? + // 看看这个值是否被改变 constant.get("maxwidth"); // 480 ## 链式调用模式 From 0bf54df8689ad1606c60ed82e47b6439fc7345d6 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 17:21:27 +0800 Subject: [PATCH 12/58] =?UTF-8?q?=20=E9=93=BE=E5=BC=8F=E8=B0=83=E7=94=A8?= =?UTF-8?q?=20=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 150b50b..6888af2 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -957,7 +957,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 myobj.method1("hello").method2().method3("world").method4(); -当你创建了一个没有有意义的返回值的方法时,你可以让它返回this,也就是这些方法所属的对象。这使得对象的使用者可以将下一个方法的调用和前一次调用链起来: +当你创建了一个没有有意义的返回值的方法时,你可以让它返回`this`,也就是这些方法所属的对象。这使得对象的使用者可以将下一个方法的调用和前一次调用链起来: var obj = { value: 1, @@ -974,10 +974,10 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 } }; - // chain method calls + // 链式方法调用 obj.increment().add(3).shout(); // 5 - // as opposed to calling them one by one + // 单独调用每个方法 obj.increment(); obj.add(3); obj.shout(); // 5 @@ -988,7 +988,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 另外一个好处就是帮助你思考如何拆分你的函数,创建更小、更有针对性的函数,而不是一个什么都做的函数。长时间来看,这会提升代码的可维护性。 -一个弊端是调用这样写的代码会更困难。你可能知道一个错误出现在某一行,但这一行要做很多的事情。当链式调用的方法中的某一个出现问题而又没报错时,你无法知晓到底是哪一个出问题了。《代码整洁之道》的作者Robert Martion甚至叫这种模式为“train wreck”模式。(译注:直译为“火车事故”,指负面影响比较大。) +一个弊端是调试这样写的代码会更困难。你可能知道一个错误出现在某一行,但这一行要做很多的事情。当链式调用的方法中的某一个出现问题而又没报错时,你无法知晓到底是哪一个出问题了。《代码整洁之道》的作者Robert Martion甚至叫这种模式为“train wreck”模式。(译注:直译为“火车事故”,指负面影响比较大。) 不管怎样,认识这种模式总是好的,当你写的方法没有明显的有意义的返回值时,你就可以返回`this`。这个模式应用得很广泛,比如jQuery库。如果你去看DOM的API的话,你会发现它也会以这样的形式倾向于链式调用: From a2b8d044eca606aac85a523906a3723eef274f67 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 26 Apr 2013 17:29:07 +0800 Subject: [PATCH 13/58] =?UTF-8?q?=E7=AC=AC=E4=BA=94=E7=AB=A0=20=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9=E5=AE=8C=E6=AF=95=20close=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter5.markdown | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/chapter5.markdown b/chapter5.markdown index 6888af2..3844d79 100644 --- a/chapter5.markdown +++ b/chapter5.markdown @@ -125,7 +125,7 @@ JavaScript没有原生的命名空间语法,但很容易可以实现这个特 图5-1 MYAPP命名空间在Firebug下的可视化结果 -## 声明依赖 +## 依赖声明 JavaScript库往往是模块化而且有用到命名空间的,这使得你可以只使用你需要的模块。比如在YUI2中,全局变量`YAHOO`就是一个命名空间,各个模块都是全局变量的属性,比如`YAHOO.util.Dom`(DOM模块)、`YAHOO.util.Event`(事件模块)。 @@ -141,7 +141,7 @@ JavaScript库往往是模块化而且有用到命名空间的,这使得你可 这是一个相当简单的模式,但是有很多的好处: -- 明确的声明依赖是告知使用你代码的开发者,需要保证指定的脚本文件被包含在页面中。 +- 明确的依赖声明是告知使用你代码的开发者,需要保证指定的脚本文件被包含在页面中。 - 将声明放在函数顶部使得依赖很容易被查找和解析。 - 本地变量(如`dom`)永远会比全局变量(如`YAHOO`)要快,甚至比全局变量的属性(如`YAHOO.util.Dom`)还要快,这样会有更好的性能。使用了依赖声明模式之后,全局变量的解析在函数中只会进行一次,在此之后将会使用更快的本地变量。 - 一些高级的代码压缩工具比如YUI Compressor和Google Closure compiler会重命名本地变量(比如`event`可能会被压缩成一个字母,如`A`),这会使代码更精简,但这个操作不会对全局变量进行,因为这样做不安全。 @@ -423,12 +423,12 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 }; }()); -如果需要的话,你可以在即时函数提供的闭包中声明私有属性和私有方法。同样,声明依赖放置在函数顶部,在变量声明的下方可以选择性地放置辅助初始化模块的一次性代码。函数最终返回的是一个包含模块公共API的对象: +如果需要的话,你可以在即时函数提供的闭包中声明私有属性和私有方法。同样,依赖声明放置在函数顶部,在变量声明的下方可以选择性地放置辅助初始化模块的一次性代码。函数最终返回的是一个包含模块公共API的对象: MYAPP.namespace('MYAPP.utilities.array'); MYAPP.utilities.array = (function () { - // 声明依赖 + // 依赖声明 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, @@ -507,7 +507,7 @@ JavaScript不像Java或者其它语言,它没有专门的提供私有、保护 MYAPP.utilities.Array = (function () { - // 声明依赖 + // 依赖声明 var uobj = MYAPP.utilities.object, ulang = MYAPP.utilities.lang, @@ -1022,7 +1022,7 @@ JavaScript对于习惯于用类来思考的人来说可能会比较费解,这 - 新方法的名字 - 新方法的实现 -然后这个新方法被添加到`Person`“类”。新方法的实现也只是一个函数,在这个函数里面`this`指向由`Person`创建的对象,正如我们期望的那样。 +然后这个新方法被添加到`Person`“类”。新方法的实现也只是一个函数,在这个函数里面`this`指向由`Person()`创建的对象,正如我们期望的那样。 下面是使用`Person()`创建和使用新对象的代码: @@ -1041,17 +1041,14 @@ JavaScript对于习惯于用类来思考的人来说可能会比较费解,这 }; } -在`method()`的实现中,我们首先检查这个方法是否已经被实现过,如果没有则继续,将传入的参数`implementation`加到构造函数的原型中。在这里`this`指向构造函数,而我们要增加的功能正在在这个构造函数的原型上。 +在`method()`的实现中,我们首先检查这个方法是否已经被实现过,如果没有则继续,将传入的参数`implementation`加到构造函数的原型中。在这里`this`指向构造函数,而我们要增加的功能正好在这个构造函数的原型上。 ## 小结 在本章中你看到了好几种除了字面量和构造函数之外的创建对象的方法。 -你看到了使用命名空间模式来保持全局空间干净和帮助组织代码。看到了简单而又有用的依赖声明模式。然后我们详细讨论了有关私有成员的模式,包括私有成员、特权方法以及一些涉及私有成员的极端情况,还有使用对象字面量创建私有成员以及将私有方法暴露为公有方法。所有这些模式都是搭建起现在流行而强大的模块模式的积木。 +你看到了使用命名空间模式来保持全局空间干净和帮助组织代码,看到了简单而又有用的依赖声明模式。然后我们详细讨论了有关私有成员的模式,包括私有成员、特权方法以及一些涉及私有成员的极端情况,还有使用对象字面量创建私有成员以及将私有方法暴露为公有方法。所有这些模式都是搭建起现在流行而强大的模块模式的积木。 然后你看到了使用沙箱模式作为长命名空间的另一种选择,它可以为你的代码和模块提供独立的环境。 -在最后,我们深入讨论了对象常量、静态成员(公有和私有)、链式调用模式,以及神奇的`method()`方法。 - - - +在最后,我们深入讨论了对象常量、静态成员(公有和私有)、链式调用模式,以及神奇的`method()`方法。 \ No newline at end of file From 4f22d6dd973955b970593bc5c279e1c8fff9e7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=92=B1=E5=BC=A0=E7=9B=9B?= Date: Sat, 27 Apr 2013 16:58:23 +0800 Subject: [PATCH 14/58] Update chapter3.markdown --- chapter3.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter3.markdown b/chapter3.markdown index e9963f0..68a245a 100644 --- a/chapter3.markdown +++ b/chapter3.markdown @@ -450,7 +450,7 @@ JavaScript中的正则表达式也是对象,可以通过两种方式创建它 var re = /pattern/gmi; -使用正则表达式字面量可以让代码更加简洁高效,比如当调用`String.prototype.prelace()`方法时,可以传入正则表达式参数: +使用正则表达式字面量可以让代码更加简洁高效,比如当调用`String.prototype.replace()`方法时,可以传入正则表达式参数: var no_letters = "abc123XYZ".replace(/[a-z]/gi, ""); console.log(no_letters); // 123 From 71b5bc8998e65c72344a11ea053a05bbd858eaa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=92=B1=E5=BC=A0=E7=9B=9B?= Date: Sat, 27 Apr 2013 16:59:21 +0800 Subject: [PATCH 15/58] Update chapter3.markdown --- chapter3.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapter3.markdown b/chapter3.markdown index e9963f0..68a245a 100644 --- a/chapter3.markdown +++ b/chapter3.markdown @@ -450,7 +450,7 @@ JavaScript中的正则表达式也是对象,可以通过两种方式创建它 var re = /pattern/gmi; -使用正则表达式字面量可以让代码更加简洁高效,比如当调用`String.prototype.prelace()`方法时,可以传入正则表达式参数: +使用正则表达式字面量可以让代码更加简洁高效,比如当调用`String.prototype.replace()`方法时,可以传入正则表达式参数: var no_letters = "abc123XYZ".replace(/[a-z]/gi, ""); console.log(no_letters); // 123 From 9754e336051193b1a565f196ac9ec54f0d17c8ad Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 3 May 2013 12:49:44 +0800 Subject: [PATCH 16/58] =?UTF-8?q?=E7=B1=BB=E5=BC=8F=E7=BB=A7=E6=89=BF1=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 54 +++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index e09321f..91fe61d 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -1,20 +1,18 @@ - # 代码复用模式 -代码复用是一个既重要又有趣的话题,因为努力在自己或者别人写的代码上写尽量少且可以复用的代码是件很自然的事情,尤其当这些代码是经过测试的、可维护的、可扩展的、有文档的时候。 +代码复用是一个既重要又有趣的话题。如果你面对自己或者别人已经写好的代码,而这些代码又是经过测试的、可维护的、可扩展的、有文档的,这时候你只想写尽量少且可以被复用的代码就是一个再自然不过的想法。 -当我们说到代码复用的时候,想到的第一件事就是继承,本章会有很大篇幅讲述这个话题。你将看到好多种方法来实现“类式(classical)”和一些其它方式的继承。但是,最最重要的事情,是你需要记住终极目标——代码复用。继承是达到这个目标的一种方法,但是不是唯一的。在本章,你将看到怎样基于其它对象来构建新对象,怎样使用混元,以及怎样在不使用继承的情况下只复用你需要的功能。 +当我们说到代码复用的时候,想到的第一件事就是继承,本章会有很大篇幅讲述这个话题,你将看到好多种方法来实现“类式(classical)”和一些其它方式的继承。但是,最最重要的事情,是你需要记住终极目标——代码复用。继承是达到这个目标的一种方法,但是不是唯一的。在本章,你将看到怎样基于其它对象来构建新对象,怎样使用混元,以及怎样在不使用继承的情况下只复用你需要的功能。 -在做代码复用的工作的时候,谨记Gang of Four 在书中给出的关于对象创建的建议:“优先使用对象创建而不是类继承”。(译注:《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)是一本设计模式的经典书籍,该书作者为Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides,被称为“Gang of Four”,简称“GoF”。) +在做代码复用的工作的时候,谨记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的对象仅仅是简单的键值对,这些键值对都可以动态创建或者是改变。 +很多编程语言都有原生的类的概念,以此作为对象的蓝本。在这些语言中,每个对象都是一个指定类的实例(instance),并且(以Java为例)一个对象不能在不存在对应的类的情况下存在。在JavaScript中,因为没有类,所以类的实例的概念没什么意义。JavaScript的对象仅仅是简单的键值对,这些键值对都可以动态创建或者是改变。 -但是JavaScript拥有构造函数(constructor functions),并且有语法和使用类非常相似的new运算符。 +但是JavaScript拥有构造函数(constructor functions),并且有语法和使用类非常相似的`new`运算符。 在Java中你可能会这样写: @@ -24,22 +22,21 @@ var adam = new Person(); -除了Java是强类型语言需要给adam添加类型Person外,其它的语法看起来是一样的。JavaScript的创建函数调用看起来感觉Person是一个类,但事实上,Person仅仅是一个函数。语法上的相似使得非常多的开发者陷入对JavaScript类的思考,并且给出了很多模拟类的继承方案。这样的实现方式,我们叫它“类式继承”。顺便也提一下,所谓“现代”继承模式是指那些不需要你去想类这个概念的模式。 +除了Java是强类型语言需要给`adam`添加类型`Person`外,其它的语法看起来是一样的。JavaScript的构造函数调用方式看起来让人感觉`Person()`是一个类,但事实上,`Person()`仅仅是一个函数。语法上的相似使得非常多的开发者陷入对JavaScript类的思考,并且给出了很多模拟类的继承方案。这样的实现方式,我们叫它“类式继承”。顺便也提一下,所谓“现代”继承模式是指那些不需要你去想类这个概念的模式。 当需要给项目选择一个继承模式时,有不少的备选方案。你应该尽量选择那些现代继承模式,除非团队已经觉得“无类不欢”。 本章先讨论类式继承,然后再关注现代继承模式。 - ## 类式继承的期望结果 -实现类式继承的目标是基于构造函数Child()来创建一个对象,然后从另一个构造函数Parent()获得属性。 +实现类式继承的目标是基于构造函数`Child()`来创建一个对象,然后从另一个构造函数`Parent()`获得属性。 > 尽管我们是在讨论类式继承,但还是尽量避免使用“类”这个词。“构造函数”或者“constructor”虽然更长,但是更准确,不会让人迷惑。通常情况下,应该努力避免在跟团队沟通的时候使用“类”这个词,因为在JavaScript中,很可能每个人都会有不同的理解。 -下面是定义两个构造函数Parent()和Child()的例子: +下面是定义两个构造函数`Parent()`和`Child()`的例子: - //parent构造函数 + //Parent构造函数 function Parent(name) { this.name = name || 'Adam'; } @@ -49,52 +46,50 @@ return this.name; }; - //空的child构造函数 + //空的Child构造函数 function Child(name) {} //继承 inherit(Child, Parent); -上面的代码定义了两个构造函数Parent()和Child(),say()方法被添加到了Parent()构建函数的原型(prototype)中,inherit()函数完成了继承的工作。inherit()函数并不是原生提供的,需要自己实现。让我们来看一看比较大众的实现它的几种方法。 +上面的代码定义了两个构造函数`Parent()`和`Child()`,`say()`方法被添加到了`Parent()`构建函数的原型(`prototype`)中,`inherit()`函数完成了继承的工作。`inherit()`函数并不是原生提供的,需要自己实现。让我们来看一看比较常见的实现它的几种方法。 - ## 类式继承1——默认模式 -最常用的一种模式是使用Parent()构造函数来创建一个对象,然后把这个对象设为Child()的原型。这是可复用的inherit()函数的第一种实现方法: +最常用的一种模式是使用`Parent()`构造函数来创建一个对象,然后把这个对象设为`Child()`的原型。这是可复用的`inherit()`函数的第一种实现方法: function inherit(C, P) { C.prototype = new P(); } -需要强调的是原型(prototype属性)应该指向一个对象,而不是函数,所以它需要指向由父构造函数创建的实例(对象),而不是构造函数自己。换句话说,请注意new运算符,有了它这种模式才可以正常工作。 +需要强调的是原型(`prototype`属性)应该指向一个对象,而不是函数,所以它需要指向由被继承的构造函数创建的实例(对象),而不是构造函数自己。换句话说,请注意`new`运算符,有了它这种模式才可以正常工作。 -之后在应用中使用new Child()创建对象的时候,它将通过原型拥有Parent()实例的功能,像下面的例子一样: +之后在应用中使用`new Child()`创建对象的时候,它将通过原型拥有`Parent()`实例的功能,像下面的例子一样: var kid = new Child(); kid.say(); // "Adam" - ### 跟踪原型链 -在这种模式中,子对象既继承了(父对象)“自己的属性”(添加给this的实例属性,比如name),也继承了原型中的属性和方法(比如say())。 +在这种模式中,子对象既继承了(父对象的)“自有属性”(添加给`this`的实例属性,比如`name`),也继承了原型中的属性和方法(比如`say()`)。 -我们来看一下在这种继承模式中原型链是怎么工作的。为了讨论方便,我们假设对象是内存中的一块空间,它包含数据和指向其它空间的引用。当使用new Parent()创建一个对象时,这样的一块空间就被分配了(图6-1中的2号)。它保存着name属性的数据。如果你尝试访问say()方法(比如通过(new Parent).say()),2号空间中并没有这个方法。但是在通过隐藏的链接__proto__指向Parent()构建函数的原型prototype属性时,就可以访问到包含say()方法的1号空间(Parent.prototype)了。所有的这一块都是在幕后发生的,不需要任何额外的操作,但是知道它是怎样工作的以及你正在访问或者修正的数据在哪是很重要的。注意,__proto__在这里只是为了解释原型链,这个属性在语言本身中是不可用的,尽管有一些环境提供了(比如Firefox)。 +我们来看一下在这种继承模式中原型链是怎么工作的。为了讨论方便,我们假设对象是内存中的一块空间,它包含数据和指向其它空间的引用。当使用`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。 +现在我们来看一下在使用`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()对象。 +`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”。 +现在使用`kid.say()`时会发生什么?3号对象没有这个方法,所以通过原型链找到2号。2号对象也没有这个方法,所以也通过原型链找到1号,刚好有这个方法。接下来`say()`方法引用了`this.name`,这个变量也需要解析,于是沿原型链查找的过程又走了一遍。在这个例子中,`this`指向3号对象,它没有`name`属性,然后2号对象被访问,并且有`name`属性,值为“Adam”。 -最后,我们多看一点东西,假如我们有如下的代码: +最后,我们看一点额外的东西,假如我们有如下的代码: var kid = new Child(); kid.name = "Patrick"; @@ -106,18 +101,17 @@ Child()构造函数是空的,也没有属性添加到Child.prototype上,这 图6-3 继承并且给子对象添加属性后的原型链 -设定kid.name并没有改变2号对象的name属性,但是它直接在3号对象上添加了自己的name属性。当kid.say()执行时,say方法在3号对象中找,然后是2号,最后到1号,像前面说的一样。但是这一次在找this.name(和kid.name一样)时很快,因为这个属性在3号对象中就被找到了。 +设定`kid.name`并没有改变2号对象的`name`属性,但是却直接在3号对象上添加了自有的`name`属性。当`kid.say()`执行时,`say()`方法会依次在3号对象中找,然后是2号,最后到1号,像前面说的一样。但是这一次在找`this.name`(和`kid.name`一样)时很快,因为这个属性在3号对象中就被找到了。 -如果通过delete kid.name的方式移除新添加的属性,那么2号对象的name属性将暴露出来并且在查找的时候被找到。 +如果通过`delete kid.name`的方式移除新添加的属性,那么2号对象的`name`属性就将被暴露出来并且在查找的时候被找到。 - ### 这种模式的缺点 -这种模式的一个缺点是既继承了(父对象)“自己的属性”,也继承了原型中的属性。大部分情况下你可能并不需要“自己的属性”,因为它们更可能是为实例对象添加的,并不用于复用。 +这种模式的一个缺点是既继承了(父对象的)“自有属性”,也继承了原型中的属性。大部分情况下你可能并不需要“自有属性”,因为它们更可能是为实例对象添加的,并不用于复用。 > 一个在构造函数上常用的规则是,用于复用的成员(译注:属性和方法)应该被添加到原型上。 -在使用这个inherit()函数时另外一个不便是它不能够让你传参数给子构造函数,这些参数有可能是想再传给父构造函数的。考虑下面的例子: +在使用这个`inherit()`函数时另外一个不便是它不能够让你传参数给子构造函数,这些参数有可能是想再传给父构造函数的。考虑下面的例子: var s = new Child('Seth'); s.say(); // "Adam" From 7501f536f62e9e2090ca5e7f58f057c2a655aeb8 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 3 May 2013 12:57:26 +0800 Subject: [PATCH 17/58] =?UTF-8?q?=E7=B1=BB=E5=BC=8F=E7=BB=A7=E6=89=BF2=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 91fe61d..b366a5e 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -118,18 +118,17 @@ 这并不是我们期望的结果。事实上传递参数给父构造函数是可能的,但这样需要在每次需要一个子对象时再做一次继承,很不方便,因为需要不断地创建父对象。 - ## 类式继承2——借用构造函数 -下面这种模式解决了从子对象传递参数到父对象的问题。它借用了父对象的构造函数,将子对象绑定到this,同时传入参数: +下面这种模式解决了从子对象传递参数到父对象的问题。它借用了父对象的构造函数,将子对象绑定到`this`,同时传入参数: function Child(a, c, b, d) { Parent.apply(this, arguments); } -使用这种模式时,只能继承在父对象的构造函数中添加到this的属性,不能继承原型上的成员。 +使用这种模式时,只能继承在父对象的构造函数中添加到`this`的属性,不能继承原型上的成员。 -使用借用构造函数的模式,子对象通过复制的方式继承父对象的成员,而不是像类式继承1中那样获得引用。下面的例子展示了这两者的不同: +使用借用构造函数的模式,子对象通过复制的方式继承父对象的成员,而不是像类式继承1中那样通过引用的方式。下面的例子展示了这两者的不同: //父构造函数 function Article() { @@ -153,20 +152,19 @@ alert(blog.hasOwnProperty('tags')); // false alert(page.hasOwnProperty('tags')); // true -在上面的代码片段中,Article()被两种方式分别继承。默认模式使blog可以通过原型链访问到tags属性,所以它自己并没有tags属性,hasOwnProperty()返回false。page对象有自己的tags属性,因为它是使用借用构造函数的方式继承,复制(而不是引用)了tags属性。 +在上面的代码片段中,`Article()`被用两种方式分别继承。默认模式使`blog`可以通过原型链访问到`tags`属性,所以它自己并没有`tags`属性,`hasOwnProperty()`返回`false`。`page`对象有自己的`tags`属性,因为它是使用借用构造函数的方式继承,复制(而不是引用)了`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在继承的时候是一份独立的拷贝。 +在这个例子中,`blog`对象修改了`tags`属性,同时,它也修改了父对象,因为实际上`blog.tags`和`article.tags`是引向同一个数组。而对`pages.tags`的修改并不影响父对象`article`,因为`pages.tags`在继承的时候是一份独立的拷贝。 - ### 原型链 -我们来看一下当我们使用熟悉的Parent()和Child()构造函数和这种继承模式时原型链是什么样的。为了使用这种继承模式,Child()有明显变化: +我们来看一下当我们使用熟悉的Parent()和Child()构造函数和这种继承模式时原型链是什么样的。为了使用这种继承模式,`Child()`有明显变化: //父构造函数 function Parent(name) { @@ -187,13 +185,12 @@ kid.name; // "Patrick" typeof kid.say; // "undefined" -如果看一下图6-4,就能发现new Child对象和Parent之间不再有链接。这是因为Child.prototype根本就没有被使用,它指向一个空对象。使用这种模式,kid拥有了自己的name属性,但是并没有继承say()方法,如果尝试调用它的话会出错。这种继承方式只是一种一次性地将父对象的属性复制为子对象的属性,并没有__proto__链接。 +如果看一下图6-4,就能发现`new Child()`对象和`Parent()`之间不再有链接。这是因为`Child.prototype`根本就没有被使用,它指向一个空对象。使用这种模式,`kid`拥有了自有的`name`属性,但是并没有继承`say()`方法,如果尝试调用它的话会出错。这种继承方式只是一种一次性地将父对象的属性复制为子对象的属性,并没有`__proto__`链接。 ![图6-4 使用借用构造函数模式时没有被关联的原型链](./Figure/chapter6/6-4.jpg) 图6-4 使用借用构造函数模式时没有被关联的原型链 - ### 利用借用构造函数模式实现多继承 使用借用构造函数模式,可以通过借用多个构造函数的方式来实现多继承: @@ -224,14 +221,13 @@ 图6-5 在Firebug中查看CatWings对象 - ### 借用构造函数的利与弊 这种模式的一个明显的弊端就是无法继承原型。如前面所说,原型往往是添加可复用的方法和属性的地方,这样就不用在每个实例中再创建一遍。 -这种模式的一个好处是获得了父对象自己成员的拷贝,不存在子对象意外改写父对象属性的风险。 +这种模式的一个好处是获得了父对象自有成员的拷贝,不存在子对象意外改写父对象属性的风险。 -那么,在上一个例子中,怎样使一个子对象也能够继承原型属性呢?怎样能使kid可以访问到say()方法呢?下一种继承模式解决了这个问题。 +那么,在上一个例子中,怎样使一个子对象也能够继承原型属性呢?怎样能使`kid`可以访问到`say()`方法呢?下一种继承模式解决了这个问题。 ## 类式继承3——借用并设置原型 From da31a822c6cc11977fa1caa6498f0265153f7a5d Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 3 May 2013 13:00:44 +0800 Subject: [PATCH 18/58] =?UTF-8?q?=E7=B1=BB=E5=BC=8F=E7=BB=A7=E6=89=BF3=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index b366a5e..51977aa 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -229,7 +229,6 @@ 那么,在上一个例子中,怎样使一个子对象也能够继承原型属性呢?怎样能使`kid`可以访问到`say()`方法呢?下一种继承模式解决了这个问题。 - ## 类式继承3——借用并设置原型 综合以上两种模式,首先借用父对象的构造函数,然后将子对象的原型设置为父对象的一个新实例: @@ -239,9 +238,9 @@ } Child.prototype = new Parent(); -这样做的好处是子对象获得了父对象自己的成员,也获得了父对象中可复用的(在原型中实现的)方法。子对象也可以传递任何参数给父构造函数。这种行为可能是最接近Java的,子对象继承了父对象的所有东西,同时可以安全地修改自己的属性而不用担心修改到父对象。 +这样做的好处是子对象获得了父对象的自有成员,也获得了父对象中可复用的(在原型中实现的)方法。子对象也可以传递任何参数给父构造函数。这种行为可能是最接近Java的,子对象继承了父对象的所有东西,同时可以安全地修改自己的属性而不用担心修改到父对象。 -一个弊端是父构造函数被调用了两次,所以不是很高效。最后,(父对象)自己的属性(比如这个例子中的name)也被继承了两次。 +一个弊端是父构造函数被调用了两次,所以不是很高效。最后,(父对象的)自有属性(比如这个例子中的`name`)也被继承了两次。 我们来看一下代码并做一些测试: @@ -267,7 +266,7 @@ delete kid.name; kid.say(); // "Adam" -跟前一种模式不一样,现在say()方法被正确地继承了。可以看到name也被继承了两次,在删除掉自己的拷贝后,在原型链上的另一个就被暴露出来了。 +跟前一种模式不一样,现在`say()`方法被正确地继承了。可以看到`name`也被继承了两次,在删除掉自己的拷贝后,在原型链上的另一个就被暴露出来了。 图6-6展示了这些对象之间的关系。这些关系有点像图6-3中展示的,但是获得这种关系的方法是不一样的。 @@ -275,7 +274,6 @@ 图6-6 除了继承“自己的属性”外,原型链也被保留了 - ## 类式继承4——共享原型 不像前一种类式继承模式需要调用两次父构造函数,下面这种模式根本不会涉及到调用父构造函数的问题。 From 876cc424ae7825dafb9b93cc3ca006e424192c70 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 3 May 2013 13:03:54 +0800 Subject: [PATCH 19/58] =?UTF-8?q?=E7=B1=BB=E5=BC=8F=E7=BB=A7=E6=89=BF4=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 51977aa..00b436d 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -278,15 +278,15 @@ 不像前一种类式继承模式需要调用两次父构造函数,下面这种模式根本不会涉及到调用父构造函数的问题。 -一般的经验是将可复用的成员放入原型中而不是this。从继承的角度来看,则是任何应该被继承的成员都应该放入原型中。这样你只需要设定子对象的原型和父对象的原型一样即可: +一般的经验是将可复用的成员放入原型中而不是`this`。从继承的角度来看,则是任何应该被继承的成员都应该放入原型中。这样你只需要设定子对象的原型和父对象的原型一样即可: function inherit(C, P) { C.prototype = P.prototype; } -这种模式的原型链很短并且查找很快,因为所有的对象实际上共享着同一个原型。但是这样也有弊端,那就是如果子对象或者在继承关系中的某个地方的任何一个子对象修改这个原型,将影响所有的继承关系中的父对象。(译注:这里应该是指会影响到所有从这个原型中继承的对象。) +这种模式的原型链很短并且查找很快,因为所有的对象实际上共享着同一个原型。但是这样也有弊端,那就是如果子对象或者在继承关系中的某个地方的任何一个子对象修改这个原型,将影响所有的继承关系中的父对象。(译注:指会影响到所有从这个原型中继承的对象所依赖的共享原型上的成员。) -如图6-7,子对象和父对象共享同一个原型,都可以访问say()方法。但是,子对象不继承name属性。 +如图6-7,子对象和父对象共享同一个原型,都可以访问`say()`方法。但是,子对象不继承`name`属性。 ![图6-7 (父子对象)共享原型时的关系](./Figure/chapter6/6-7.jpg) From 431361581a71b820f3b7eb20380d62a74aec3125 Mon Sep 17 00:00:00 2001 From: TooBug Date: Fri, 3 May 2013 13:16:12 +0800 Subject: [PATCH 20/58] =?UTF-8?q?=E7=B1=BB=E5=BC=8F=E7=BB=A7=E6=89=BF?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 00b436d..3042d02 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -292,12 +292,11 @@ 图6-7 (父子对象)共享原型时的关系 - ## 类式继承5——临时构造函数 下一种模式通过打断父对象和子对象原型的直接链接解决了共享原型时的问题,同时还从原型链中获得其它的好处。 -下面是这种模式的一种实现方式,F()函数是一个空函数,它充当了子对象和父对象的代理。F()的prototype属性指向父对象的原型。子对象的原型是一这个空函数的一个实例: +下面是这种模式的一种实现方式,`F()`函数是一个空函数,它充当了子对象和父对象的代理。`F()`的`prototype`属性指向父对象的原型。子对象的原型是这个空函数的一个实例: function inherit(C, P) { var F = function () {}; @@ -311,15 +310,14 @@ 图6-8 使用临时(代理)构造函数F()实现类式继承 -这种模式通常情况下都是一种很棒的选择,因为原型本来就是存放复用成员的地方。在这种模式中,父构造函数添加到this中的任何成员都不会被继承。 +这种模式通常情况下都是一种很棒的选择,因为原型本来就是存放复用成员的地方。在这种模式中,父构造函数添加到`this`中的任何成员都不会被继承。 我们来创建一个子对象并且检查一下它的行为: var kid = new Child(); -如果你访问kid.name将得到undefined。在这个例子中,name是父对象自己的属性,而在继承的过程中我们并没有调用new Parent(),所以这个属性并没有被创建。当访问kid.say()时,它在3号对象中不可用,所以在原型链中查找,4号对象也没有,但是1号对象有,它在内在中的位置会被所有从Parent()创建的构造函数和子对象所共享。 +如果你访问`kid.name`将得到`undefined`。在这个例子中,`name`是父对象自己的属性,而在继承的过程中我们并没有调用`new Parent()`,所以这个属性并没有被创建。当访问`kid.say()`时,它在3号对象中不可用,所以在原型链中查找,4号对象也没有,但是1号对象有,它在内存中的位置会被所有从`Parent()`创建的构造函数和子对象所共享。 - ### 存储父类(Superclass) 在上一种模式的基础上,还可以添加一个指向原始父对象的引用。这很像其它语言中访问超类(superclass)的情况,有时候很方便。 @@ -333,24 +331,23 @@ C.uber = P.prototype; } - ### 重置构造函数引用 -这个近乎完美的模式上还需要做的最后一件事情就是重置构造函数(constructor)的指向,以便未来在某个时刻能被正确地使用。 +这个近乎完美的模式上还需要做的最后一件事情就是重置构造函数(`constructor`)的指向,以便未来在某个时刻能被正确地使用。 -如果不重置构造函数的指向,那所有的子对象都会认为Parent()是它们的构造函数,而这个结果完全没有用。使用前面的inherit()的实现,你可以观察到这种行为: +如果不重置构造函数的指向,那所有的子对象都会认为`Parent()`是它们的构造函数,而这个结果完全没有用。使用前面的`inherit()`的实现,你可以观察到这种行为: - // parent, child, inheritance + // Parent,Child,实现继承 function Parent() {} function Child() {} inherit(Child, Parent); - // testing the waters + // 测试 var kid = new Child(); kid.constructor.name; // "Parent" kid.constructor === Parent; // true -constructor属性很少用,但是在运行时检查对象很方便。你可以重新将它指向期望的构造函数而不影响功能,因为这个属性更多是“信息性”的。(译注:即它更多的时候是在提供信息而不是参与到函数功能中。) +`constructor`属性很少被用到,但是在运行时检查对象很方便。你可以重新将它指向期望的构造函数而不影响功能,因为这个属性更多是“信息性”的。(译注:即它更多的时候是在提供信息而不是参与到函数功能中。) 最终,这种类式继承的Holy Grail版本看起来是这样的: @@ -366,7 +363,7 @@ constructor属性很少用,但是在运行时检查对象很方便。你可以 > “代理函数”或者“代理构造函数”也是指这种模式,因为临时构造函数是被用作获取父构造函数原型的代理。 -一种常见的对Holy Grail模式的优化是避免每次需要继承的时候都创建一个临时(代理)构造函数。事实上创建一次就足够了,以后只需要修改它的原型即可。你可以用一个立即执行的函数来将代理函数存储到闭包中: +一种常见的对Holy Grail模式的优化是避免每次需要继承的时候都创建一个临时(代理)构造函数。事实上创建一次就足够了,以后只需要修改它的原型即可。你可以用一个即时函数来将代理函数存储到闭包中: var inherit = (function () { var F = function () {}; @@ -378,7 +375,6 @@ constructor属性很少用,但是在运行时检查对象很方便。你可以 } }()); - ## Klass 有很多JavaScript类库模拟了类,创造了新的语法糖。具体的实现方式可能会不一样,但是基本上都有一些共性,包括: From 9bf28bae8073671c6ce28c27470610b89b974af1 Mon Sep 17 00:00:00 2001 From: TooBug Date: Sat, 4 May 2013 17:02:47 +0800 Subject: [PATCH 21/58] =?UTF-8?q?Klass=20=E6=A0=A1=E5=AF=B9=E5=AE=8C?= =?UTF-8?q?=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 3042d02..dab19c2 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -377,15 +377,15 @@ ## Klass -有很多JavaScript类库模拟了类,创造了新的语法糖。具体的实现方式可能会不一样,但是基本上都有一些共性,包括: +有很多JavaScript类库模拟了类,创造了新的语法糖。这些类库具体的实现方式可能会不一样,但是基本上都有一些共性,包括: -- 有一个约定好名字的方法,如initialize、_init或者其它相似的名字,会被自动调用,来充当类的构造函数。 +- 有一个约定好的方法,如`initialize`、`_init`或者其它相似的名字,会被自动调用,来充当类的构造函数 - 类可以从其它类继承 - 在子类中可以访问到父类(superclass) -> 我们在这里做一下变化,在本章的这部分自由地使用“class”单词,因为主题就是模拟类。 +> 我们在这里做一点变化,在本章的这部分自由地使用“class”这个词,因为主题就是模拟类。 -为避免讨论太多细节,我们来看一下JavaScript中一种模拟类的实现。首先,这种解决方案从客户的角度来看将如何被使用? +为避免讨论太多细节,我们来看一下JavaScript中一种模拟类的实现。首先,看一下这种方案将如何被使用? var Man = klass(null, { __construct: function (what) { @@ -397,14 +397,14 @@ } }); -这种语法糖的形式是一个名为klass()的函数。在一些实现方式中,它可能是Klass()构造函数或者是增强的Object.prototype,但是在这个例子中,我们让它只是一个简单的函数。 +这种语法糖的形式是一个名为`klass()`的函数。在一些其它的实现方式中,它可能是`Klass()`构造函数或者是增强的`Object.prototype`,但是在这个例子中,我们让它只是一个简单的函数。 -这个函数接受两个参数:一个被继承的类和通过对象字面量提供的新类的实现。受PHP的影响,我们约定类的构造函数必须是一个名为\_\_construct的方法。在前面的代码片段中,建立了一个名为Man的新类,并且它不继承任何类(意味着继承自Object)。Man类有一个在\_\_construct建立的自己的属性name和一个方法getName()。这个类是一个构造函数,所以下面的代码将正常工作(并且看起来像类实例化的过程): +这个函数接受两个参数:一个被继承的类和通过对象字面量提供的新类的实现。受PHP的影响,我们约定类的构造函数必须是一个名为`__construct()`的方法。在前面的代码片段中,建立了一个名为`Man`的新类,并且它不继承任何类(意味着继承自`Object`)。`Man`类有一个在`__construct()`建立的自有属性`name`和一个方法`getName()`。这个类是一个构造函数,所以下面的代码将正常工作(并且看起来像类实例化的过程): var first = new Man('Adam'); // logs "Man's constructor" first.getName(); // "Adam" -现在我们来扩展这个类,创建一个SuperMan类: +现在我们来扩展这个类,创建一个`SuperMan`类: var SuperMan = klass(Man, { __construct: function (what) { @@ -416,26 +416,25 @@ } }); -这里,klass()的第一个参数是将被继承的Man类。值得注意的是,在getName()中,父类的getName()方法首先通过SuperMan类的uber静态属性被调用。我们来测试一下: +这里,`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”。在一些语言中,父类的构造函数在子类构造函数被调用的时候会自动执行,这个特性也可以模拟。 +第一行在console中记录了“Man's constructor”,然后是“Superman's constructor”,在一些语言中,父类的构造函数在子类构造函数被调用的时候会自动执行,这个特性也被模拟了。 -用instanceof运算符测试返回希望的结果: +用`instanceof`运算符测试返回希望的结果: clark instanceof Man; // true clark instanceof SuperMan; // true -最后,我们来看一下klass()函数是怎样实现的: +最后,我们来看一下`klass()`函数是怎样实现的: var klass = function (Parent, props) { var Child, F, i; - // 1. - // new constructor + // 1. 构造函数 Child = function () { if (Child.uber && Child.uber.hasOwnProperty("__construct")) { Child.uber.__construct.apply(this, arguments); @@ -445,8 +444,7 @@ } }; - // 2. - // inherit + // 2. 继承 Parent = Parent || Object; F = function () {}; F.prototype = Parent.prototype; @@ -454,25 +452,24 @@ Child.uber = Parent.prototype; Child.prototype.constructor = Child; - // 3. - // add implementation methods + // 3. 添加方法实现 for (i in props) { if (props.hasOwnProperty(i)) { Child.prototype[i] = props[i]; } } - // return the "class" + // 返回“类” return Child; }; -这个klass()实现有三个明显的部分: +这个`klass()`实现有三个明显的部分: -1. 创建Child()构造函数,这也是最后返回的将被作为类使用的函数。在这个函数里面,如果\_\_construct方法存在的话将被调用。同样是在父类的\_\_construct(如果存在)被调用前使用静态的uber属性。也可能存在uber没有定义的情况——比如从Object继承,因为它是在Man类中被定义的。 -2. 第二部分主要完成继承。只是简单地使用前面章节讨论过的Holy Grail类式继承模式。只有一个东西是新的:如果Parent没有传值的话,设定Parent为Object。 -3. 最后一部分是类真正定义的地方,循环需要实现的方法(如例子中的\_\_constructt和getName),并将它们添加到Child的原型中。 +1. 创建`Child()`构造函数,这也是最后返回的将被作为类使用的函数。在这个函数里面,如果`__construct()`方法存在的话将被调用,同样,如果父类的`__construct()`存在,也将被调用(通过使用静态属性`uber`)。也可能存在`uber`没有定义的情况——比如从`Object`继承,前例中`Man`类即是如此。 +2. 第二部分主要完成继承。只是简单地使用前面章节讨论过的Holy Grail类式继承模式。只有一个东西是新的:如果`Parent`没有传值的话,设定`Parent`为`Object`。 +3. 最后一部分是真正定义类的地方,遍历需要实现的方法(如例子中的`__constructor()`和`getName()`),并将它们添加到`Child()`的原型中。 -什么时候使用这种模式?其实,最好是能避免则避免,因为它带来了在这门语言中不存在的完整的类的概念,会让人疑惑。使用它需要学习新的语法和新的规则。也就是说,如果你或者你的团队对类感到习惯并且同时对原型感到不习惯,这种模式可能是一个可以探索的方向。这种模式允许你完全忘掉原型,好处就是你可以将语法变种得像其它你所喜欢的语言一样。 +什么时候使用这种模式呢?其实,最好是能避免则避免,因为它带来了在这门语言中不存在的完整的类的概念,会让人疑惑。使用它需要学习新的语法和新的规则,也就是说,如果你或者你的团队习惯于使用类并且对原型感到不习惯,这种模式可能是一个可以探索的方向。这种模式允许你完全忘掉原型,好处就是你可以使用像其它语言那样的(变种)语法。 ## 原型继承 From 3cc7f8d78197ab9a622f5c277e5aa839b0d6a255 Mon Sep 17 00:00:00 2001 From: TooBug Date: Sun, 5 May 2013 22:44:28 +0800 Subject: [PATCH 22/58] =?UTF-8?q?=E5=8E=9F=E5=9E=8B=E7=BB=A7=E6=89=BF=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 56 ++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index dab19c2..1714952 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -471,25 +471,24 @@ 什么时候使用这种模式呢?其实,最好是能避免则避免,因为它带来了在这门语言中不存在的完整的类的概念,会让人疑惑。使用它需要学习新的语法和新的规则,也就是说,如果你或者你的团队习惯于使用类并且对原型感到不习惯,这种模式可能是一个可以探索的方向。这种模式允许你完全忘掉原型,好处就是你可以使用像其它语言那样的(变种)语法。 - ## 原型继承 -现在,让我们从一个叫作“原型继承”的模式来讨论没有类的现代继承模式。在这种模式中,没有任何类进来,在这里,一个对象继承自另外一个对象。你可以这样理解它:你有一个想复用的对象,然后你想创建第二个对象,并且获得第一个对象的功能。下面是这种模式的用法: +现在,让我们从一个叫作“原型继承”的模式来讨论没有类的现代继承模式。在这种模式中,没有任何类牵涉进来,一个对象继承自另外一个对象。你可以这样理解它:你有一个想复用的对象,然后你想创建第二个对象,并且获得第一个对象的功能。下面是这种模式的用法: - //需要继承的对象 + // 需要继承的对象 var parent = { name: "Papa" }; - //新对象 + // 新对象 var child = object(parent); - //测试 + // 测试 alert(child.name); // "Papa" -在这个代码片段中,有一个已经存在的使用对象字面量创建的对象叫parent,我们想创建一个和parent有相同的属性和方法的对象叫child。child对象使用object()函数创建。这个函数在JavaScript中并不存在(不要与构造函数Object()混淆),所以我们来看看怎样定义它。 +在这个代码片段中,有一个已经存在的使用对象字面量创建的对象叫`parent`,我们想创建一个和`parent`有相同的属性和方法的对象叫`child`。`child`对象使用`object()`函数创建。这个函数在JavaScript中并不存在(不要与构造函数`Object()`混淆),所以我们来看看怎样定义它。 -与Holy Grail类式继承相似,可以使用一个空的临时构造函数F(),然后设定F()的原型为parent对象。最后,返回一个临时构造函数的新实例。 +与Holy Grail类式继承相似,可以使用一个空的临时构造函数`F()`,然后设定`F()`的原型为`parent`对象。最后,返回一个临时构造函数的新实例。 function object(o) { function F() {} @@ -497,69 +496,66 @@ return new F(); } -图6-9展示了使用原型继承时的原型链。在这里child总是以一个空对象开始,它没有自己的属性但通过原型链(\_\_proto\_\_)拥有父对象的所有功能。 +图6-9展示了使用原型继承时的原型链。这样创建的`child`总是一个空对象,它没有自有属性但通过原型链(`__proto__`)拥有父对象的所有功能。 ![图6-9 原型继承模式](./Figure/chapter6/6-9.jpg) 图6-9 原型继承模式 - ### 讨论 -在原型继承模式中,parent不需要使用对象字面量来创建。(尽管这是一种更常用的方式。)可以使用构造函数来创建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 + // 使用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 + typeof kid.getName; // "function",因为它在原型中 + typeof kid.name; // "undefined",因为只有原型中的成员被继承了 - -###例外的ECMAScript 5 +### ECMAScript5中的原型继承 -在ECMAScript 5中,原型继承已经正式成为语言的一部分。这种模式使用Object.create方法来实现。换句话说,你不再需要自己去写类似object()的函数,它是语言原生的了: +在ECMAScript5中,原型继承已经正式成为语言的一部分。这种模式使用`Object.create()`方法来实现。换句话说,你不再需要自己去写类似`object()`的函数,它是语言原生的部分了: var child = Object.create(parent); -Object.create()接收一个额外的参数——一个对象。这个额外对象中的属性将被作为自己的属性添加到返回的子对象中。这让我们可以很方便地将继承和创建子对象在一个方法调用中实现。例如: +`Object.create()`接收一个额外的参数——一个对象。这个额外对象中的属性将被作为自有属性添加到返回的子对象中。这让我们可以很方便地将继承和创建子对象在一个方法调用中实现。例如: var child = Object.create(parent, { - age: { value: 2 } // ECMA5 descriptor + age: { value: 2 } // ES5中的属性描述符 }); child.hasOwnProperty("age"); // true -你可能也会发现原型继承模式已经在一些JavaScript类库中实现了,比如,在YUI3中,它是Y.Object()方法: +你可能也会发现原型继承模式已经在一些JavaScript类库中实现了,比如,在YUI3中,它是`Y.Object()`方法: YUI().use('*', function (Y) { var child = Y.Object(parent); From 22df729b57192d457d968b9cb01ce5766dd34a7b Mon Sep 17 00:00:00 2001 From: TooBug Date: Sun, 5 May 2013 22:51:30 +0800 Subject: [PATCH 23/58] =?UTF-8?q?=E9=80=9A=E8=BF=87=E5=A4=8D=E5=88=B6?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E7=BB=A7=E6=89=BF=20=E5=92=8C=20=E6=B7=B7?= =?UTF-8?q?=E5=85=83=20=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 1714952..86cfeae 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -561,10 +561,9 @@ var child = Y.Object(parent); }); - ## 通过复制属性继承 -让我们来看一下另外一种继承模式——通过复制属性继承。在这种模式中,一个对象通过简单地复制另一个对象来获得功能。下面是一个简单的实现这种功能的extend()函数: +让我们来看一下另外一种继承模式——通过复制属性继承。在这种模式中,一个对象通过简单地复制另一个对象来获得功能。下面是一个简单的实现这种功能的`extend()`函数: function extend(parent, child) { var i; @@ -574,16 +573,16 @@ child[i] = parent[i]; } } - return child; + return child; } -这是一个简单的实现,仅仅是遍历了父对象的成员然后复制它们。在这个实现中,child是可选参数,如果它没有被传入一个已有的对象,那么一个全新的对象将被创建并被返回: +这是一个简单的实现,仅仅是遍历了父对象的成员然后复制它们。在这个实现中,`child`是可选参数,如果它没有被传入一个已有的对象,那么一个全新的对象将被创建并返回: var dad = {name: "Adam"}; var kid = extend(dad); kid.name; // "Adam" -上面给出的实现叫作对象的“浅拷贝”(shallow copy)。另一方面,“深拷贝”是指检查准备复制的属性本身是否是对象或者数组,如果是,也遍历它们的属性并复制。如果使用浅拷贝的话(因为在JavaScript中对象是按引用传递),如果你改变子对象的一个属性,而这个属性恰好是一个对象,那么你也会改变父对象。实际上这对方法来说可能很好(因为函数也是对象,也是按引用传递),但是当遇到其它的对象和数组的时候可能会有些意外情况。考虑这种情况: +上面给出的实现叫作对象的“浅拷贝”(shallow copy),与之相对,“深拷贝”是指检查准备复制的属性本身是否是对象或者数组,如果是,也遍历它们的属性并复制。如果使用浅拷贝的话(因为在JavaScript中对象是按引用传递),如果你改变子对象的一个属性,而这个属性恰好是一个对象,那么你也会改变父对象。实际上这对方法来说可能很好(因为函数也是对象,也是按引用传递),但是当遇到其它的对象和数组的时候可能会有些意外情况。考虑这种情况: var dad = { counts: [1, 2, 3], @@ -594,7 +593,7 @@ dad.counts.toString(); // "1,2,3,4" dad.reads === kid.reads; // true -现在让我们来修改一下extend()函数以便做深拷贝。所有你需要做的事情只是检查一个属性的类型是否是对象,如果是,则递归遍历它的属性。另外一个需要做的检查是这个对象是真的对象还是数组。我们可以使用第3章讨论过的数组检查方式。最终深拷贝版的extend()是这样的: +现在让我们来修改一下`extend()`函数以便实现深拷贝。你需要做的事情只是检查一个属性的类型是否是对象,如果是,则递归遍历它的属性。另外一个需要做的检查是这个对象是真的对象还是数组,可以使用第三章讨论过的数组检查方式。最终深拷贝版的`extend()`是这样的: function extendDeep(parent, child) { var i, @@ -633,11 +632,10 @@ kid.reads.web = true; dad.reads.paper; // true -通过复制属性继承的模式很简单且应用很广泛。例如Firebug(JavaScript写的Firefox扩展)有一个方法叫extend()做浅拷贝,jQuery的extend()方法做深拷贝。YUI3提供了一个叫作Y.clone()的方法,它创建一个深拷贝并且通过绑定到子对象的方式复制函数。(本章后面将有更多关于绑定的内容。) +通过复制属性继承的模式很简单且应用很广泛。例如Firebug(JavaScript写的Firefox扩展)有一个方法叫`extend()`做浅拷贝,jQuery的`extend()`方法做深拷贝。YUI3提供了一个叫作`Y.clone()`的方法,它创建一个深拷贝并且通过绑定到子对象的方式复制函数。(本章后面将有更多关于绑定的内容。) 这种模式并不高深,因为根本没有原型牵涉进来,而只跟对象和它们的属性有关。 - ## 混元(Mix-ins) 既然谈到了通过复制属性来继承,就让我们顺便多说一点,来讨论一下“混元”模式。除了前面说的从一个对象复制,你还可以从任意多数量的对象中复制属性,然后将它们混在一起组成一个新对象。 @@ -665,13 +663,13 @@ {sugar: "sure!"} ); -图6-10展示了在Firebug的控制台中用console.dir(cake)展示出来的混元后cake对象的属性。 +图6-10展示了在Firebug的控制台中用`console.dir(cake)`展示出来的混元后`cake`对象的属性。 ![图6-10 在Firebug中查看cake对象](./Figure/chapter6/6-10.jpg) 图6-10 在Firebug中查看cake对象 -> 如果你习惯了某些将混元作为原生部分的语言,那么你可能期望修改一个或多个父对象时也影响子对象。但在这个实现中这是不会发生的事情。这里我们只是简单地遍历、复制自己的属性,并没有与父对象的链接。 +> 如果你习惯了某些将混元作为原生部分的语言,那么你可能期望修改一个或多个父对象时也影响子对象。但在这个实现中这是不会发生的事情。这里我们只是简单地遍历、复制自有属性,并没有与父对象有任何链接。 ## 借用方法 From 64f74c6a5ef2f4acc715cf2a0f8f4f715ff570f7 Mon Sep 17 00:00:00 2001 From: TooBug Date: Mon, 6 May 2013 09:11:42 +0800 Subject: [PATCH 24/58] =?UTF-8?q?=E5=80=9F=E7=94=A8=E6=96=B9=E6=B3=95=20?= =?UTF-8?q?=E4=B9=8B=20=E4=BB=8E=E6=95=B0=E7=BB=84=E5=80=9F=E7=94=A8=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 86cfeae..4f77c95 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -671,38 +671,36 @@ > 如果你习惯了某些将混元作为原生部分的语言,那么你可能期望修改一个或多个父对象时也影响子对象。但在这个实现中这是不会发生的事情。这里我们只是简单地遍历、复制自有属性,并没有与父对象有任何链接。 - ## 借用方法 -有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。受益于函数方法call()和apply(),通过借用方法模式,这是可行的。在本书中,你其实已经见过这种模式了,甚至在本章extendDeep()的实现中也有用到。 +有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。得益于函数的`call()`和`apply()`方法,可以通过借用方法模式实现它。在本书中,你其实已经见过这种模式了,甚至在本章`extendDeep()`的实现中也有用到。 -如你所熟知的一样,在JavaScript中函数也是对象,它们有一些有趣的方法,比如call()和apply()。这两个方法的唯一区别是后者接受一个参数数组以传入正在调用的方法,而前者只接受一个一个的参数。你可以使用这两个方法来从已有的对象中借用方法: +在JavaScript中函数也是对象,它们有一些有趣的方法,比如`call()`和`apply()`。这两个方法的唯一区别是后者接受一个参数数组以传入正在调用的方法,而前者只接受一个一个的参数。你可以使用这两个方法来从已有的对象中借用方法: - //call() example + // call()示例 notmyobj.doStuff.call(myobj, param1, p2, p3); - // apply() example + // apply()示例 notmyobj.doStuff.apply(myobj, [param1, p2, p3]); -在这个例子中有一个对象myobj,而且notmyobj有一个用得着的方法叫doStuff()。你可以简单地临时借用doStuff()方法,而不用处理继承然后得到一堆myobj中你永远不会用的方法。 +在这个例子中有一个对象`myobj`,而且`notmyobj`有一个用得着的方法叫`doStuff()`。你可以简单地临时借用`doStuff()`方法,而不用处理继承然后得到一堆`myobj`中无关的方法。 -你传一个对象和任意的参数,这个被借用的方法会将this绑定到你自己的对象上。简单地说,你的对象会临时假装成另一个对象以使用它的方法。这就像实际上获得了继承但又免除了“继承税”(指你不需要的属性和方法)。 +你传一个对象和任意的参数,这个被借用的方法会将`this`绑定到你传递的对象上。简单地说,你的对象会临时假装成另一个对象以使用它的方法。这就像实际上获得了继承但又免除了“继承税”(译注:指不需要的属性和方法)。 - ### 例:从数组借用 这种模式的一种常见用法是从数组借用方法。 -数组有很多很有用但是一些“类数组”对象(如arguments)不具备的方法。所以arguments可以借用数组的方法,比如slice()。这是一个例子: +数组有很多很有用但是一些“类数组”对象(如`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(...)。这种方法代码更长一些,但是不用创建一个空数组。 +在这个例子中,有一个空数组被创建了,因为要借用它的方法。也可以使用一种看起来代码更长的方法来做,那就是直接从数组的原型中借用方法,使用`Array.prototype.slice.call(...)`。这种方法代码更长一些,但是不用创建一个空数组。 ### 借用并绑定 From 599b16c1de59f0e2256372694e03065e0dd7b252 Mon Sep 17 00:00:00 2001 From: TooBug Date: Mon, 6 May 2013 12:59:03 +0800 Subject: [PATCH 25/58] =?UTF-8?q?=E7=AC=AC=E5=85=AD=E7=AB=A0=20=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9=E5=AE=8C=E6=AF=95=20close=20#7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter6.markdown | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/chapter6.markdown b/chapter6.markdown index 4f77c95..f149d9f 100644 --- a/chapter6.markdown +++ b/chapter6.markdown @@ -702,12 +702,11 @@ 在这个例子中,有一个空数组被创建了,因为要借用它的方法。也可以使用一种看起来代码更长的方法来做,那就是直接从数组的原型中借用方法,使用`Array.prototype.slice.call(...)`。这种方法代码更长一些,但是不用创建一个空数组。 - ### 借用并绑定 -当借用方法的时候,不管是通过call()/apply()还是通过简单的赋值,方法中的this指向的对象都是基于调用的表达式来决定的。但是有时候最好的使用方式是将this的值锁定或者提前绑定到一个指定的对象上。 +当借用方法的时候,不管是通过`call()`/`apply()`还是通过简单的赋值,方法中的`this`指向的对象都是基于调用的表达式来决定的。但是有时候最好的使用方式是将`this`的值锁定或者提前绑定到一个指定的对象上。 -我们来看一个例子。这是一个对象one,它有一个say()方法: +我们来看一个例子。这是一个对象`one`,它有一个`say()`方法: var one = { name: "object", @@ -716,10 +715,10 @@ } }; - // test + // 测试 one.say('hi'); // "hi, object" -现在另一个对象two没有say()方法,但是它可以从one借用: +现在另一个对象`two`没有`say()`方法,但是它可以从one借用: var two = { name: "another object" @@ -727,14 +726,13 @@ one.say.apply(two, ['hello']); // "hello, another object" -在这个例子中,say()方法中的this指向了two,this.name是“another object”。但是如果在某些场景下你将函数赋值给了全局变量或者是将这个函数作为回调,会发生什么?在客户端编程中有非常多的事件和回调,所以这种情况经常发生: +在这个例子中,`say()`方法中的`this`指向了`two`,`this.name`是“another object”。但是如果在某些场景下你将函数赋值给了全局变量或者是将这个函数作为回调,会发生什么?在客户端编程中有非常多的事件和回调,所以这种情况经常发生: - // assigning to a variable - // `this` will point to the global object + // 赋值给变量,this会指向全局对象 var say = one.say; say('hoho'); // "hoho, undefined" - // passing as a callback + // 作为回调 var yetanother = { name: "Yet another object", method: function (callback) { @@ -743,7 +741,7 @@ }; yetanother.method(one.say); // "Holla, undefined" -在这两种情况中say()中的this都指向了全局对象,所以代码并不像我们想象的那样正常工作。要修复(换言之,绑定)一个方法的对象,我们可以用一个简单的函数,像这样: +在这两种情况中`say()`中的`this`都指向了全局对象,所以代码并不像我们想象的那样正常工作。要修复(绑定)一个方法的对象,我们可以用一个简单的函数,像这样: function bind(o, m) { return function () { @@ -751,25 +749,24 @@ }; } -这个bind()函数接受一个对象o和一个方法m,然后把它们绑定在一起,再返回另一个函数。返回的函数通过闭包可以访问到o和m。也就是说,即使在bind()返回之后,内层的函数仍然可以访问到o和m,而o和m会始终指向原始的对象和方法。让我们用bind()来创建一个新函数: +这个`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。 +正如你看到的,尽管`twosay()`是作为一个全局函数被创建的,但`this`并没有指向全局对象,而是指向了通过`bind()`传入的对象`two`。不论如何调用`twosay()`,`this`将始终指向`two`。 绑定是奢侈的,你需要付出的代价是一个额外的闭包。 - ### Function.prototype.bind() -ECMAScript5在Function.prototype中添加了一个方法叫bind(),使用时和apply和call()一样简单。所以你可以这样写: +ECMAScript5在`Function.prototype`中添加了一个方法叫`bind()`,使用时和`apply()`/`call()`一样简单。所以你可以这样写: var newFunc = obj.someFunc.bind(myobj, 1, 2, 3); -这意味着将someFunc()和myobj绑定了,并且还传入了someFunc()的前三个参数。这也是一个在第4章讨论过的部分应用的例子。 +这意味着将`someFunc()`和`myobj`绑定了,并且还传入了`someFunc()`的前三个参数。这也是一个在第4章讨论过的部分应用的例子。 -让我们来看一下当你的程序跑在低于ES5的环境中时如何实现Function.prototype.bind(): +让我们来看一下当你的程序跑在低于ES5的环境中时如何实现`Function.prototype.bind()`: if (typeof Function.prototype.bind === "undefined") { Function.prototype.bind = function (thisArg) { @@ -783,22 +780,20 @@ ECMAScript5在Function.prototype中添加了一个方法叫bind(),使用时和 }; } -这个实现可能看起来有点熟悉,它使用了部分应用,将传入bind()的参数串起来(除了第一个参数),然后在被调用时传给bind()返回的新函数。这是用法示例: +这个实现可能看起来有点熟悉,它使用了部分应用,将传入`bind()`的参数串起来(除了第一个参数),然后在被调用时传给`bind()`返回的新函数。这是用法示例: var twosay2 = one.say.bind(two); twosay2('Bonjour'); // "Bonjour, another object" -在这个例子中,除了绑定的对象外,我们没有传任何参数给bind()。下一个例子中,我们来传一个用于部分应用的参数: +在这个例子中,除了绑定的对象外,我们没有传任何参数给`bind()`。下一个例子中,我们来传一个用于部分应用的参数: var twosay3 = one.say.bind(two, 'Enchanté'); twosay3(); // "Enchanté, another object" - ##小结 -在JavaScript中,继承有很多种方案可以选择。学习和理解不同的模式是有好处的,因为这可以增强你对这门语言的掌握能力。在本章中你看到了很多类式继承和现代继承的方案。 - -但是,也许在开发过程中继承并不是你经常面对的一个问题。这一部分是因为这个问题已经被使用某种方式或者某个你使用的类库解决了,另一部分是因为你不需要在JavaScript中建立很长很复杂的继承链。在静态强类型语言中,继承可能是唯一可以利用代码的方法,但在JavaScript中你可能有更多更简单更优化的方法,包括借用方法、绑定、复制属性、混元等。 +在JavaScript中,继承有很多种方案可以选择,在本章中你看到了很多类式继承和现代继承的方案。学习和理解不同的模式是有好处的,因为这可以增强你对这门语言的掌握能力。 -记住,代码复用才是目标,继承只是达成这个目标的一种手段。 +但是,也许在开发过程中继承并不是你经常面对的一个问题。一部分是因为这个问题已经被使用某种方式或者某个你使用的类库解决了,另一部分是因为你不需要在JavaScript中建立很长很复杂的继承链。在静态强类型语言中,继承可能是唯一可以复用代码的方法,但在JavaScript中有更多更简单更优化的方法,包括借用方法、绑定、复制属性、混元等。 +记住,代码复用才是目标,继承只是达成这个目标的一种手段。 \ No newline at end of file From 4251240d0d45473475a7530ed1d77a38f40d68ca Mon Sep 17 00:00:00 2001 From: TooBug Date: Mon, 6 May 2013 13:30:20 +0800 Subject: [PATCH 26/58] =?UTF-8?q?=E5=8D=95=E4=BE=8B=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 114 +++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 61 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index 140bbe7..d2f3ce0 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -1,15 +1,13 @@ - # 设计模式 在GoF(Gang of Four)的书中提出的设计模式为面向对象的软件设计中遇到的一些普遍问题提供了解决方案。它们已经诞生很久了,而且被证实在很多情况下是很有效的。这正是你需要熟悉它的原因,也是我们要讨论它的原因。 尽管这些设计模式跟语言和具体的实现方式无关,但它们多年来被关注到的方面仍然主要是在强类型静态语言比如C++和Java中的应用。 -JavaScript作为一种基于原型的弱类型动态语言,使得有些时候实现某些模式时相当简单,甚至不费吹灰之力。 +JavaScript作为一种基于原型的弱类型动态语言,有些时候实现某些模式时相当简单,甚至不费吹灰之力。 让我们从第一个例子——单例模式——来看一下在JavaScript中和静态的基于类的语言有什么不同。 - ## 单例 单例模式的核心思想是让指定的类只存在唯一一个实例。这意味着当你第二次使用相同的类去创建对象的时候,你得到的应该和第一次创建的是同一个对象。 @@ -28,16 +26,15 @@ JavaScript作为一种基于原型的弱类型动态语言,使得有些时候 obj === obj2; // false obj == obj2; // false -所以你可以说当你每次使用对象字面量创建一个对象的时候就是在创建一个单例,并没有特别的语法迁涉进来。 +所以你可以说当你每次使用对象字面量创建一个对象的时候就是在创建一个单例,并没有什么特别的语法牵涉进来。 -> 需要注意的是,有的时候当人们在JavaScript中提出“单例”的时候,它们可能是在指第5章讨论过的“模块模式”。 +> 需要注意的是,有的时候当人们在JavaScript中提出“单例”的时候,它们可能是在指第五章讨论过的“模块模式”。 - ### 使用new -JavaScript没有类,所以一字一句地说单例的定义并没有什么意义。但是JavaScript有使用new、通过构造函数来创建对象的语法,有时候你可能需要这种语法下的一个单例实现。这也就是说当你使用new、通过同一个构造函数来创建多个对象的时候,你应该只是得到同一个对象的不同引用。 +JavaScript没有类,所以一字一句地说单例的定义并没有什么意义。但是JavaScript有使用`new`、通过构造函数来创建对象的语法,有时候你可能需要这种语法下的一个单例实现。这也就是说当你使用`new`、通过同一个构造函数来创建多个对象的时候,你应该只是得到同一个对象的不同引用。 -> 温馨提示:从一个实用模式的角度来说,下面的讨论并不是那么有用,只是更多地在实践模拟一些语言中关于这个模式的一些问题的解决方案。这些语言主要是(静态强类型的)基于类的语言,在这些语言中,函数并不是“一等公民”。 +> 温馨提示:从一个实用模式的角度来说,下面的讨论并不是那么有用,只是更多地在模拟一些语言中关于这个模式的一些问题的解决方案。这些语言主要是(静态强类型的)基于类的语言,在这些语言中,函数并不是“一等公民”。 下面的代码片段展示了期望的结果(假设你忽略了多元宇宙的设想,接受了只有一个宇宙的观点): @@ -45,126 +42,122 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 var uni2 = new Universe(); uni === uni2; // true -在这个例子中,uni只在构造函数第一次被调用时创建。第二次(以及后续更多次)调用时,同一个uni对象被返回。这就是为什么uni === uni2的原因——因为它们实际上是同一个对象的两个引用。那么怎么在JavaScript达到这个效果呢? +在这个例子中,`uni`只在构造函数第一次被调用时创建。第二次(以及后续更多次)调用时,同一个`uni`对象被返回。这就是为什么`uni === uni2`的原因——因为它们实际上是同一个对象的两个引用。那么怎么在JavaScript达到这个效果呢? -当对象实例this被创建时,你需要在Universe构造函数中缓存它,以便在第二次调用的时候返回。有几种选择可以达到这种效果: +当对象实例`this`被创建时,你需要在`Universe()`构造函数中缓存它,以便在第二次调用的时候返回。有几种选择可以达到这种效果: - 你可以使用一个全局变量来存储实例。不推荐使用这种方法,因为通常我们认为使用全局变量是不好的。而且,任何人都可以改写全局变量的值,甚至可能是无意中改写。所以我们不再讨论这种方案。 -- 你也可以将对象实例缓存在构造函数的属性中。在JavaScript中,函数也是对象,所以它们也可以有属性。你可以写一些类似Universe.instance的属性来缓存对象。这是一种漂亮干净的解决方案,不足之处是instance属性仍然是可以被公开访问的,别人写的代码可能修改它,这样就会失去这个实例。 +- 你也可以将对象实例缓存在构造函数的属性中。在JavaScript中,函数也是对象,所以它们也可以有属性。你可以写一些类似`Universe.instance`的属性来缓存对象。这是一种漂亮干净的解决方案,不足之处是`instance`属性仍然是可以被公开访问的,别人写的代码可能修改它,这样就会失去这个实例。 - 你可以将实例包裹在闭包中。这可以保持实例是私有的,不会在构造函数之外被修改,代价是一个额外的闭包。 让我们来看一下第二种和第三种方案的实现示例。 - ### 将实例放到静态属性中 -下面是一个将唯一的实例放入Universe构造函数的一个静态属性中的例子: +下面是一个将唯一的实例放入`Universe()`构造函数的一个静态属性中的例子: function Universe() { - // do we have an existing instance? - if (typeof Universe.instance === "object") { - return Universe.instance; - } - - // proceed as normal - this.start_time = 0; - this.bang = "Big"; - - // cache - Universe.instance = this; - - // implicit return: - // return this; + // 实例是否已经存在? + if (typeof Universe.instance === "object") { + return Universe.instance; + } + + // 处理普通逻辑 + this.start_time = 0; + this.bang = "Big"; + + // 缓存实例 + Universe.instance = this; + + // 隐式return: + // return this; } - // testing + // 测试 var uni = new Universe(); var uni2 = new Universe(); uni === uni2; // true -如你所见,这是一种直接有效的解决方案,唯一的缺陷是instance是可被公开访问的。一般来说它被其它代码误删改的可能是很小的(起码比全局变量instance要小得多),但是仍然是有可能的。 +如你所见,这是一种直接有效的解决方案,唯一的缺陷是`instance`是可被公开访问的。一般来说它被其它代码误删改的可能是很小的(起码比全局变量`instance`要小得多),但是仍然是有可能的。 - ### 将实例放到闭包中 -另一种实现基于类的单例模式的方法是使用一个闭包来保护这个唯一的实例。你可以通过第5章讨论过的“私有静态成员模式”来实现。唯一的秘密就是重写构造函数: +另一种实现基于类的单例模式的方法是使用一个闭包来保护这个唯一的实例。你可以通过第五章讨论过的“私有静态成员模式”来实现。唯一的秘密就是重写构造函数: function Universe() { - // the cached instance + // 缓存实例 var instance = this; - // proceed as normal + // 处理普通逻辑 this.start_time = 0; this.bang = "Big"; - // rewrite the constructor + // 重写构造函数 Universe = function () { return instance; }; } - // testing + // 测试 var uni = new Universe(); var uni2 = new Universe(); uni === uni2; // true -第一次调用时,原始的构造函数被调用并且正常返回this。在后续的调用中,被重写的构造函数被调用。被重写怕这个构造函数可以通过闭包访问私有的instance变量并且将它返回。 +第一次调用时,原来的构造函数被调用并且正常返回`this`。在后续的调用中,被重写的构造函数被调用。被重写的这个构造函数可以通过闭包访问私有的`instance`变量并且将它返回。 -这个实现实际上也是第4章讨论的自定义函数的又一个例子。如我们讨论过的一样,这种模式的缺点是被重写的函数(在这个例子中就是构造函数Universe())将丢失那些在初始定义和重新定义之间添加的属性。在这个例子中,任何添加到Universe()的原型上的属性将不会被链接到使用原来的实现创建的实例上。(注:这里的“原来的实现”是指实例是由未被重写的构造函数创建的,而Universe()则是被重写的构造函数。) +这个实现实际上也是第四章讨论的重定义函数的又一个例子。如我们讨论过的一样,这种模式的缺点是被重写的函数(在这个例子中就是构造函数`Universe()`)将丢失那些在初始定义和重新定义之间添加的属性。在这个例子中,任何添加到`Universe()`的原型上的属性将不会被链接到使用原来的实现创建的实例上。(注:这里的“原来的实现”是指实例是由未被重写的构造函数创建的,而`Universe()`则是被重写的构造函数。) 下面我们通过一些测试来展示这个问题: - // adding to the prototype + // 添加成员到原型 Universe.prototype.nothing = true; var uni = new Universe(); - // again adding to the prototype - // after the initial object is created + // 在创建一个对象后再添加成员到原型 Universe.prototype.everything = true; var uni2 = new Universe(); - Testing: - // only the original prototype was - // linked to the objects + // 测试: + // 只有原始的原型被链接到对象上 uni.nothing; // true uni2.nothing; // true uni.everything; // undefined uni2.everything; // undefined - // that sounds right: + // constructor看起来是对的 uni.constructor.name; // "Universe" - // but that's odd: + // 但其实不然 uni.constructor === Universe; // false -uni.constructor不再和Universe()相同的原因是uni.constructor仍然是指向原来的构造函数,而不是被重新定义的那个。 +`uni.constructor`不再和`Universe()`相同的原因是`uni.constructor`仍然是指向原来的构造函数,而不是被重新定义的那个。 -如果一定被要求让prototype和constructor的指向像我们期望的那样,可以通过一些调整来做到: +如果一定要让`prototype`和`constructor`的指向像我们期望的那样,可以通过一些调整来做到: function Universe() { - // the cached instance + // 缓存实例 var instance; - // rewrite the constructor + // 重写构造函数 Universe = function Universe() { return instance; }; - // carry over the prototype properties + // 重写prototype属性 Universe.prototype = this; - // the instance + // 创建实例 instance = new Universe(); - // reset the constructor pointer + // 重写constructor属性 instance.constructor = Universe; - // all the functionality + // 其它的功能代码 instance.start_time = 0; instance.bang = "Big"; @@ -173,24 +166,23 @@ uni.constructor不再和Universe()相同的原因是uni.constructor仍然是指 现在所有的测试结果都可以像我们期望的那样了: - // update prototype and create instance + // 修改原型,创建对象 Universe.prototype.nothing = true; // true var uni = new Universe(); Universe.prototype.everything = true; // true var uni2 = new Universe(); - // it's the same single instance + // 它们是同一个实例 uni === uni2; // true - // all prototype properties work - // no matter when they were defined + // 所有的原型上的属性都正常工作,不管是什么时候在哪添加的 uni.nothing && uni.everything && uni2.nothing && uni2.everything; // true - // the normal properties work + // 普通成员也可以正常工作 uni.bang; // "Big" - // the constructor points correctly + // constructor指向正确 uni.constructor === Universe; // true -另一种可选的解决方案是将构造函数和实例包在一个立即执行的函数中。当构造函数第一次被调用的时候,它返回一个对象并且将私有的instance指向它。在后续调用时,构造函数只是简单地返回这个私有变量。在这种新的实现下,前面所有的测试代码也会和期望的一样: +另一种可选的解决方案是将构造函数和实例包在一个即时函数中。当构造函数第一次被调用的时候,它返回一个对象并且将私有的`instance`指向它。在后续调用时,构造函数只是简单地返回这个私有变量。在这种新的实现下,前面所有的测试代码也会和期望的一样: var Universe; @@ -206,7 +198,7 @@ uni.constructor不再和Universe()相同的原因是uni.constructor仍然是指 instance = this; - // all the functionality + // 功能代码 this.start_time = 0; this.bang = "Big"; From 3ea8ab2949aa60d116c1d6efc2fce5adfb52482c Mon Sep 17 00:00:00 2001 From: TooBug Date: Tue, 7 May 2013 12:47:01 +0800 Subject: [PATCH 27/58] =?UTF-8?q?=E5=B7=A5=E5=8E=82=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index d2f3ce0..6c51f78 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -206,7 +206,6 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 }()); - ## 工厂模式 使用工厂模式的目的就是创建对象。它通常被在类或者类的静态方法中实现,目的是: @@ -214,15 +213,15 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 - 执行在建立相似的对象时进行的一些重复操作 - 让工厂的使用者在编译阶段创建对象时不必知道它的特定类型(类) -第二点在静态的基于类的语言中更重要,因为在(编译阶段)提前不知道类的情况下,创建类的实例是不普通的行为。但在JavaScript中,这部分的实现却是相当容易的事情。 +第二点在静态的基于类的语言中更重要,因为在(编译阶段)提前不知道类的情况下,创建类的实例是一件多少有些特殊的行为。但在JavaScript中,这部分的实现却是相当容易的事情。 -使用工厂方法(或类)创建的对象被设计为从同一个父对象继承;它们是特定的实现一些特殊功能的子类。有些时候这个共同的父对象就是包含工厂方法的同一个类。 +使用工厂方法(或类)创建的对象被设计为从同一个父对象继承;它们是实现一些特定的功能的子类。有些时候这个共同的父对象就是包含工厂方法的同一个类。 我们来看一个示例实现,我们有: -- 一个共同的父构造函数CarMaker。 -- CarMaker的一个静态方法叫factory(),用来创建car对象。 -- 特定的从CarMaker继承而来的构造函数CarMaker.Compact,CarMaker.SUV,CarMaker.Convertible。它们都被定义为父构造函数的静态属性以便保持全局空间干净,同时在需要的时候我们也知道在哪里找到它们。 +- 一个共同的父构造函数`CarMaker()`。 +- `CarMaker()`的一个静态方法叫`factory()`,用来创建`car`对象。 +- 特定的从`CarMaker()`继承而来的构造函数`CarMaker.Compact()`,`CarMaker.SUV()`,`CarMaker.Convertible()`。它们都被定义为父构造函数的静态属性以便保持全局空间干净,同时在需要的时候我们也知道在哪里找到它们。 我们来看一下已经完成的实现会怎么被使用: @@ -237,24 +236,24 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 var corolla = CarMaker.factory('Compact'); -可能是工厂模式中最知名的。你有一个方法可以在运行时接受一个表示类型的字符串,然后它创建并返回了一个和请求的类型一样的对象。这里没有使用new的构造函数,也没有看到任何对象字面量,仅仅只有一个函数根据一个字符串指定的类型创建了对象。 +可能是工厂模式中最为人熟知的。你有一个方法可以在运行时接受一个表示类型的字符串,然后它创建并返回了一个和请求的类型一样的对象。这里没有使用`new`的构造函数,也没有看到任何对象字面量,仅仅只有一个函数根据一个字符串指定的类型创建了对象。 这里是一个工厂模式的示例实现,它能让上面的代码片段工作: - // parent constructor + // 父构造函数 function CarMaker() {} - // a method of the parent + // 父构造函数的方法 CarMaker.prototype.drive = function () { return "Vroom, I have " + this.doors + " doors"; }; - // the static factory method + // 静态工厂方法factory CarMaker.factory = function (type) { var constr = type, - newcar; + newcar; - // error if the constructor doesn't exist + // 如果指定类型的构造函数不存在则报错 if (typeof CarMaker[constr] !== "function") { throw { name: "Error", @@ -262,18 +261,18 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 }; } - // at this point the constructor is known to exist - // let's have it inherit the parent but only once + // 现在我们确认要用到的构造函数是存在的了 + // 让它继承自父构造函数,但只继承一次 if (typeof CarMaker[constr].prototype.drive !== "function") { CarMaker[constr].prototype = new CarMaker(); } - // create a new instance + // 创建一个新实例 newcar = new CarMaker[constr](); - // optionally call some methods and then return... + // 这里可以选择性地调用一些方法,然后返回实例 return newcar; }; - // define specific car makers + // 创建特定类型的构造函数 CarMaker.Compact = function () { this.doors = 4; }; @@ -284,27 +283,26 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 this.doors = 24; }; -工厂模式的实现中没有什么是特别困难的。你需要做的仅仅是寻找请求类型的对象的构造函数。在这个例子中,使用了一个简单的名字转换以便映射对象类型和创建对象的构造函数。继承的部分只是一个公共的重复代码片段的示例,它可以被放到工厂方法中而不是被每个构造函数的类型重复。(译注:指通过原型继承的代码可以在factory方法以外执行,而不是放到factory中每调用一次都要执行一次。) +工厂模式的实现中没有什么是特别困难的,你需要做的仅仅是寻找请求类型的对象构造函数。在这个例子中,使用了一个简单的名字转换以便映射对象类型和创建对象的构造函数。继承的部分只是一个公共的重复代码片段的示例,它可以被放到工厂方法中而不是被每个构造函数的类型所重复。(译注:指原型继承的代码可以在`factory()`方法以外执行,而不是放到`factory()`中每调用一次都要执行一次。) - ### 内置对象工厂 -作为一个“野生的工厂”的例子,我们来看一下内置的全局构造函数Object()。它的行为很像工厂,因为它根据不同的输入创建不同的对象。如果传入一个数字,它会使用Number()构造函数创建一个对象。在传入字符串和布尔值的时候也会发生同样的事情。任何其它的值(包括空值)将会创建一个正常的对象。 +为了说明工厂模式应用之广泛,我们来看一下内置的全局构造函数`Object()`。它的行为很像工厂,因为它根据不同的输入创建不同的对象。如果传入一个数字,它会使用`Number()`构造函数创建一个对象。在传入字符串和布尔值的时候也会发生类似的事情。任何其它的值(包括空值)将会创建一个正常的对象。 -下面是这种行为的例子和测试,注意Object调用时可以不用加new: +下面是这种行为的例子和测试,注意`Object()`调用时可以不用加`new`: var o = new Object(), n = new Object(1), s = Object('1'), b = Object(true); - // test + // 测试 o.constructor === Object; // true n.constructor === Number; // true s.constructor === String; // true b.constructor === Boolean; // true -Object()也是一个工厂这一事实可能没有太多实际用处,仅仅是觉得值得作为一个例子提一下,告诉我们工厂模式是随处可见的。 +`Object()`也是一个工厂这一事实可能没有太多实际用处,仅仅是觉得值得作为一个例子提一下,告诉我们工厂模式是随处可见的。 From 07d5e5584c2d9d3add6b41bad8b8eb2cc5ee3cfb Mon Sep 17 00:00:00 2001 From: TooBug Date: Tue, 7 May 2013 12:49:22 +0800 Subject: [PATCH 28/58] =?UTF-8?q?=E8=BF=AD=E4=BB=A3=E5=99=A8=20=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index 6c51f78..53836fb 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -304,26 +304,24 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 `Object()`也是一个工厂这一事实可能没有太多实际用处,仅仅是觉得值得作为一个例子提一下,告诉我们工厂模式是随处可见的。 - - ## 迭代器 在迭代器模式中,你有一些含有有序聚合数据的对象。这些数据可能在内部用一种复杂的结构存储着,但是你希望提供一种简单的方法来访问这种结构中的每个元素。数据的使用者不需要知道你是怎样组织你的数据的,他们只需要操作一个个独立的元素。 -在迭代器模式中,你的对象需要提供一个next()方法。按顺序调用next()方法必须返回序列中的下一个元素,但是“下一个”在你的特定的数据结构中指什么是由你自己来决定的。 +在迭代器模式中,你的对象需要提供一个`next()`方法。按顺序调用`next()`方法必须返回序列中的下一个元素,但是“下一个”在你的特定的数据结构中指什么是由你自己来决定的。 -假设你的对象叫agg,你可以通过简单地在循环中调用next()来访问每个数据元素,像这样: +假设你的对象叫`agg`,你可以通过简单地在循环中调用`next()`来访问每个数据元素,像这样: var element; while (element = agg.next()) { - // do something with the element ... + // 访问element…… console.log(element); } -在迭代器模式中,聚合对象通常也会提供一个方便的方法hasNext(),这样对象的使用者就可以知道他们已经获取到你数据的最后一个元素。当使用另一种方法——hasNext()——来按顺序访问所有元素时,是像这样的: +在迭代器模式中,聚合对象通常也会提供一个方便的方法`hasNext()`,这样对象的使用者就可以知道他们已经获取到你数据的最后一个元素。当使用`hasNext()`来按顺序访问所有元素时,是像这样的: while (agg.hasNext()) { - // do something with the next element... + // 访问element…… console.log(agg.next()); } From d36dc03e7f826db001c1c28dd0416fcd355597c5 Mon Sep 17 00:00:00 2001 From: TooBug Date: Thu, 9 May 2013 13:00:55 +0800 Subject: [PATCH 29/58] =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=20=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 54 ++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index 53836fb..a383cf0 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -325,39 +325,36 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 console.log(agg.next()); } - ## 装饰器 在装饰器模式中,一些额外的功能可以在运行时被动态地添加到一个对象中。在静态的基于类的语言中,处理这个问题可能是个挑战,但是在JavaScript中,对象本来就是可变的,所以给一个对象添加额外的功能本身并不是什么问题。 -装饰器模式的一个很方便的特性是可以对我们需要的特性进行定制和配置。刚开始时,我们有一个拥有基本功能的对象,然后可以从可用的装饰器中去挑选一些需要用到的去增加这个对象,甚至如果顺序很重要的话,还可以指定增强的顺序。 +装饰器模式的一个很方便的特性是可以对我们需要的特性进行定制和配置。刚开始时,我们有一个拥有基本功能的对象,然后可以从可用的装饰器中去挑选一些需要用到的去增强这个对象,如果有必要的话,还可以指定增强的顺序。 - ### 用法 -我们来看一下这个模式的示例用法。假设你正在做一个卖东西的web应用,每个新交易是一个新的sale对象。这个对象“知道”交易的价格并且可以通过调用sale.getPrice()方法返回。根据环境的不同,你可以开始用一些额外的功能来装饰这个对象。假设一个场景是这笔交易是发生在加拿大的一个省Québec,在这种情况下,购买者需要付联邦税和Québec省税。根据装饰器模式的用法,你需要指明使用联邦税装饰器和Québec省税装饰器来装饰这个对象。然后你还可以给这个对象装饰一些价格格式的功能。这个场景的使用方式可能是像这样: +我们来看一下这个模式的用法示例。假设你正在做一个卖东西的web应用,每个新交易是一个新的`sale`对象。这个对象“知道”交易的价格并且可以通过调用`sale.getPrice()`方法返回。根据环境的不同,你可以开始用一些额外的功能来装饰这个对象。假设一个场景是这笔交易是发生在加拿大的一个省Québec,在这种情况下,购买者需要付联邦税和Québec省税。根据装饰器模式的用法,你需要指明使用联邦税装饰器和Québec省税装饰器来装饰这个对象。然后你还可以给这个对象装饰一些价格格式的功能。这个场景的使用方式可能是像这样: - var sale = new Sale(100); // the price is 100 dollars - sale = sale.decorate('fedtax'); // add federal tax - sale = sale.decorate('quebec'); // add provincial tax - sale = sale.decorate('money'); // format like money + 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); // the price is 100 dollars - sale = sale.decorate('fedtax'); // add federal tax - sale = sale.decorate('cdn'); // format using CDN + 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()。 +最终的效果就是当你在第一个例子中调用`sale.getPrice()`时,实际上是在调用`money`装饰器的方法(图7-1)。但是因为每个装饰器会先调用父对象的方法,`money`的`getPrice()`先调用`quebec`的`getPrice()`,而它又会去调用`fedtax`的`getPrice()`方法,依次类推。这个链会一直走到原始的未经装饰的由`Sale()`构造函数实现的`getPrice()`。 ![图7-1 装饰器模式的实现](./Figure/chapter7/7-1.jpg) 图7-1 装饰器模式的实现 @@ -375,7 +372,7 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 Sale.decorators = {}; -我们来看一个装饰器的例子。这是一个对象,实现了一个自定义的getPrice()方法。注意这个方法首先从父对象的方法中取值然后修改这个值: +我们来看一个装饰器的例子。这是一个对象,实现了一个自定义的`getPrice()`方法。注意这个方法首先从父对象的方法中取值然后修改这个值: Sale.decorators.fedtax = { getPrice: function () { @@ -385,7 +382,7 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 } }; -使用类似的方法我们可以实现任意多个需要的其它装饰器。他们的实现方式像插件一样来扩展核心的Sale()的功能。他们甚至可以被放到额外的文件中,被第三方的开发者来开发和共享: +使用类似的方法我们可以实现任意多个需要的装饰器。它们的实现方式像插件一样来扩展核心的`Sale()`的功能。它们甚至可以被放到额外的文件中,被第三方的开发者来开发和共享: Sale.decorators.quebec = { getPrice: function () { @@ -407,11 +404,11 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 } }; -最后我们来看decorate()这个神奇的方法,它把所有上面说的片段都串起来了。记得它是这样被调用的: +最后我们来看`decorate()`这个神奇的方法,它把所有上面说的片段都串起来了。记住它是这样被调用的: sale = sale.decorate('fedtax'); -字符串'fedtax'对应在Sale.decorators.fedtax中实现的对象。被装饰过的最新的对象newobj将从现在有的对象(也就是this对象,它要么是原始的对象,要么是经过最后一个装饰器装饰过的对象)中继承。实现这一部分需要用到前面章节中提到的临时构造函数模式。我们也设置一个uber属性给newobj以便子对象可以访问到父对象。然后我们从装饰器中复制所有额外的属性到被装饰的对象newobj中。最后,在我们的例子中,newobj被返回并且成为被更新过的sale对象。 +字符串`'fedtax'`对应在`Sale.decorators.fedtax`中实现的对象。被装饰过的最新的对象`newobj`将从现在有的对象(也就是`this`对象,它要么是原始的对象,要么是经过最后一个装饰器装饰过的对象)中继承。实现这一部分需要用到前面章节中提到的临时构造函数模式。我们也设置一个`uber`属性给`newobj`以便子对象可以访问到父对象。然后我们从装饰器中复制所有额外的属性到被装饰的对象`newobj`中。最后,在我们的例子中,`newobj`被返回并且成为被更新过的`sale`对象。 Sale.prototype.decorate = function (decorator) { var F = function () {}, @@ -428,29 +425,28 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 return newobj; }; - ### 使用列表实现 -我们来看另一个明显不同的实现方法,受益于JavaScript的动态特性,它完全不需要使用继承。同时,我们也可以简单地将前一个方面的结果作为参数传给下一个方法,而不需要每一个方法都去调用前一个方法。 +我们来看另一个明显不同的实现方法,得益于JavaScript的动态特性,它完全不需要使用继承。同时,我们也可以简单地将前一个方面的结果作为参数传给下一个方法,而不需要每一个方法都去调用前一个方法。 这样的实现方法还允许很容易地反装饰(undecorating)或者撤销一个装饰,这仅仅需要从一个装饰器列表中移除一个条目。 -用法示例也会明显简单一些,因为我们不需要将decorate()的返回值赋值给对象。在这个实现中,decorate()不对对象做任何事情,它只是简单地将装饰器加入到一个列表中: +用法示例也会明显简单一些,因为我们不需要将`decorate()`的返回值赋值给对象。在这个实现中,`decorate()`不对对象做任何事情,它只是简单地将装饰器加入到一个列表中: - var sale = new Sale(100); // the price is 100 dollars - sale.decorate('fedtax'); // add federal tax - sale.decorate('quebec'); // add provincial tax - sale.decorate('money'); // format like money + var sale = new Sale(100); // 价格是100美元 + sale.decorate('fedtax'); // 加上联邦税 + sale.decorate('quebec'); // 加上省税 + sale.decorate('money'); // 格式化 sale.getPrice(); // "$112.88" -Sale()构造函数现在有了一个作为自己属性的装饰器列表: +`Sale()`构造函数现在有了一个作为自己属性存在的装饰器列表: function Sale(price) { this.price = price || 100; this.decorators_list = []; } -可用的装饰器仍然被实现为Sale.decorators的属性。注意getPrice()方法现在更简单了,因为它们不需要调用父对象的getPrice()来获取结果,结果已经作为参数传递给它们了: +可用的装饰器仍然被实现为`Sale.decorators`的属性。注意`getPrice()`方法现在更简单了,因为它们不需要调用父对象的`getPrice()`来获取结果,结果已经作为参数传递给它们了: Sale.decorators = {}; @@ -472,7 +468,7 @@ Sale()构造函数现在有了一个作为自己属性的装饰器列表: } }; -最有趣的部分发生在父对象的decorate()和getPrice()方法上。在前一种实现方式中,decorate()还是多少有些复杂,而getPrice()十分简单。在这种实现方式中事情反过来了:decorate()只需要往列表中添加条目而getPrice()做了所有的工作。这些工作包括遍历现在添加的装饰器的列表,然后调用它们的getPrice()方法,并将结果传递给前一个: +最有趣的部分发生在父对象的`decorate()`和`getPrice()`方法上。在前一种实现方式中,`decorate()`还是多少有些复杂,而`getPrice()`十分简单。在这种实现方式中事情反过来了:`decorate()`只需要往列表中添加条目而`getPrice()`做了其它所有的工作,包括遍历现在添加的装饰器的列表,然后调用它们的`getPrice()`方法并将结果传递下去: Sale.prototype.decorate = function (decorator) { this.decorators_list.push(decorator); @@ -490,7 +486,7 @@ Sale()构造函数现在有了一个作为自己属性的装饰器列表: return price; }; -装饰器模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中,getPrice()是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,decorators_list属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。 +装饰器模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中,`getPrice()`是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,`decorators_list`属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。 ## 策略模式 From ce36b39c4d263f6417e4bb3549ae393044fd94a8 Mon Sep 17 00:00:00 2001 From: TooBug Date: Thu, 9 May 2013 13:17:16 +0800 Subject: [PATCH 30/58] =?UTF-8?q?=E7=AD=96=E7=95=A5=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 293 +++++++++++++++++++++++----------------------- 1 file changed, 145 insertions(+), 148 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index a383cf0..58789e1 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -488,42 +488,40 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 装饰器模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中,`getPrice()`是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,`decorators_list`属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。 - ## 策略模式 策略模式允许在运行的时候选择算法。你的代码的使用者可以在处理特定任务的时候根据即将要做的事情的上下文来从一些可用的算法中选择一个。 -使用策略模式的一个例子是解决表单验证的问题。你可以创建一个validator对象,有一个validate()方法。这个方法被调用时不用区分具体的表单类型,它总是会返回同样的结果——一个没有通过验证的列表和错误信息。 +使用策略模式的一个例子是解决表单验证的问题。你可以创建一个`validator`对象,有一个`validate()`方法。这个方法被调用时不用区分具体的表单类型,它总是会返回同样的结果——一个没有通过验证的列表和错误信息。 -但是根据具体的需要验证的表单和数据,你代码的使用者可以选择进行不同类别的检查。你的validator选择最佳的策略来处理这个任务,然后将具体的数据检查工作交给合适的算法去做。 +但是根据具体的需要验证的表单和数据,你代码的使用者可以选择进行不同类别的检查。你的`validator`选择最佳的策略来处理这个任务,然后将具体的数据检查工作交给合适的算法去做。 - ### 数据验证示例 假设你有一个下面这样的数据,它可能来自页面上的一个表单,你希望验证它是不是有效的数据: var data = { - first_name: "Super", - last_name: "Man", - age: "unknown", - username: "o_O" + first_name: "Super", + last_name: "Man", + age: "unknown", + username: "o_O" }; -对这个例子中的validator,它需要知道哪个是最佳策略,因此你需要先配置它,给它设定好规则以确定哪些是有效的数据。 +对这个例子中的`validator`而言,它需要知道哪个是最佳策略,因此你需要先配置它,给它设定好规则以确定哪些是有效的数据。 假设你不需要姓,名字可以接受任何内容,但要求年龄是一个数字,并且用户名只允许包含字母和数字。配置可能是这样的: validator.config = { - first_name: 'isNonEmpty', - age: 'isNumber', - username: 'isAlphaNum' + first_name: 'isNonEmpty', + age: 'isNumber', + username: 'isAlphaNum' }; -现在validator对象已经有了用来处理数据的配置,你可以调用validate()方法,然后将任何验证错误打印到控制台上: +现在`validator`对象已经有了用来处理数据的配置,你可以调用`validate()`方法,然后将验证错误打印到控制台上: validator.validate(data); if (validator.hasErrors()) { - console.log(validator.messages.join("\n")); + console.log(validator.messages.join("\n")); } 它可能会打印出这样的信息: @@ -531,89 +529,88 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 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`是如何实现的。所有可用的用来验证的逻辑都是拥有一个`validate()`方法的对象,它们还有一行辅助信息用来显示错误信息: - // checks for non-empty values + // 验证空值 validator.types.isNonEmpty = { - validate: function (value) { - return value !== ""; - }, - instructions: "the value cannot be empty" + validate: function (value) { + return value !== ""; + }, + instructions: "the value cannot be empty" }; - // checks if a value is a number + // 验证数字 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" + validate: function (value) { + return !isNaN(value); + }, + instructions: "the value can only be a valid number, e.g. 1, 3.14 or 2010" }; - // checks if the value contains only letters and numbers + // 验证是否只包含字母和数字 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" + validate: function (value) { + return !/[^a-z0-9]/i.test(value); + }, + instructions: "the value can only contain characters and numbers, no special symbols" }; -最后,validator对象的核心是这样的: +最后,`validator`对象的核心是这样的: var validator = { - // all available checks - types: {}, - - // error messages in the current - // validation session - messages: [], - - // current validation config - // name: validation type - config: {}, - - // the interface method - // `data` is key => value pairs - validate: function (data) { - - var i, msg, type, checker, result_ok; - - // reset all messages - this.messages = []; - for (i in data) { - - if (data.hasOwnProperty(i)) { - - type = this.config[i]; - checker = this.types[type]; - - if (!type) { - continue; // no need to validate - } - if (!checker) { // uh-oh - 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(); - }, - - // helper - hasErrors: function () { - return this.messages.length !== 0; - } + // 所有可用的验证类型 + 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()方法。 +如你所见,`validator`对象是通用的,在所有的需要验证的场景下都可以保持这个样子。改进它的办法就是增加更多类型的检查。如果你将它用在很多页面上,那么很快你就会有一个非常好的验证类型的集合。然后在新的使用场景下使用时你需要做的仅仅是配置`validator`然后调用`validate()`方法。 ## 外观模式 @@ -632,35 +629,35 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 这是两个有不同目的的相互独立的方法,他们也应该被保持独立,但与此同时,他们也经常被一起调用。所以为了不在应用中到处重复调用这两个方法,你可以创建一个外观方法来调用它们: var myevent = { - // ... - stop: function (e) { - e.preventDefault(); - e.stopPropagation(); - } - // ... + // ... + stop: function (e) { + e.preventDefault(); + e.stopPropagation(); + } + // ... }; 外观模式也适用于一些浏览器脚本的场景,即将浏览器的差异隐藏在一个外观方法下面。继续前面的例子,你可以添加一些处理IE中事件API的代码: var myevent = { - // ... - stop: function (e) { - // others - 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; - } - } - // ... + // ... + stop: function (e) { + // others + 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了。 @@ -852,48 +849,48 @@ proxy对象创建了一个队列来收集50ms之内接受到的视频ID,然后 下面是proxy对象的代码: var proxy = { - ids: [], - delay: 50, - timeout: null, - callback: null, - context: null, - makeRequest: function (id, callback, context) { - // add to the queue - this.ids.push(id); - - this.callback = callback; - this.context = context; - - // set up timeout - if (!this.timeout) { - this.timeout = setTimeout(function () { - proxy.flush(); - }, this.delay); - } - }, - flush: function () { - - http.makeRequest(this.ids, "proxy.handler"); - - // clear timeout and queue - this.timeout = null; - this.ids = []; - - }, - handler: function (data) { - var i, max; - - // single video - if (parseInt(data.query.count, 10) === 1) { - proxy.callback.call(proxy.context, data.query.results.Video); - return; - } - - // multiple videos - for (i = 0, max = data.query.results.Video.length; i < max; i += 1) { - proxy.callback.call(proxy.context, data.query.results.Video[i]); - } - } + ids: [], + delay: 50, + timeout: null, + callback: null, + context: null, + makeRequest: function (id, callback, context) { + // add to the queue + this.ids.push(id); + + this.callback = callback; + this.context = context; + + // set up timeout + if (!this.timeout) { + this.timeout = setTimeout(function () { + proxy.flush(); + }, this.delay); + } + }, + flush: function () { + + http.makeRequest(this.ids, "proxy.handler"); + + // clear timeout and queue + this.timeout = null; + this.ids = []; + + }, + handler: function (data) { + var i, max; + + // single video + if (parseInt(data.query.count, 10) === 1) { + proxy.callback.call(proxy.context, data.query.results.Video); + return; + } + + // multiple videos + 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请求合并为一个。 From 08ca72c28dfd66fd4867ceba15b429ee9b38c433 Mon Sep 17 00:00:00 2001 From: TooBug Date: Thu, 9 May 2013 13:21:44 +0800 Subject: [PATCH 31/58] =?UTF-8?q?=E5=A4=96=E8=A7=82=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index 58789e1..55cb4e6 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -612,37 +612,36 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 如你所见,`validator`对象是通用的,在所有的需要验证的场景下都可以保持这个样子。改进它的办法就是增加更多类型的检查。如果你将它用在很多页面上,那么很快你就会有一个非常好的验证类型的集合。然后在新的使用场景下使用时你需要做的仅仅是配置`validator`然后调用`validate()`方法。 - ## 外观模式 -外观模式是一种很简单的模式,它只是为对象提供了更多的可供选择的接口。使方法保持短小而不是处理太多的工作是一种很好的实践。在这种实践的指导下,你会有一大堆的方法,而不是一个有着非常多参数的uber方法。有些时候,两个或者更多的方法会经常被一起调用。在这种情况下,创建另一个将这些重复调用包裹起来的方法就变得意义了。 +外观模式是一种很简单的模式,它只是为对象提供了更多的可供选择的接口。使方法保持短小而不是处理太多的工作是一种很好的实践。在这种实践的指导下,你会有一大堆的方法,而不是一个有着非常多参数的`uber`方法。有些时候,两个或者更多的方法会经常被一起调用。在这种情况下,创建另一个将这些重复调用包裹起来的方法就变得意义了。 -例如,在处理浏览器事件的时候,有以下的事件: +例如,在处理浏览器事件的时候,有以下的方法: -- stopPropagation() +- `stopPropagation()` 阻止事件冒泡到父节点 -- preventDefault() +- `preventDefault()` 阻止浏览器执行默认动作(如打开链接或者提交表单) 这是两个有不同目的的相互独立的方法,他们也应该被保持独立,但与此同时,他们也经常被一起调用。所以为了不在应用中到处重复调用这两个方法,你可以创建一个外观方法来调用它们: var myevent = { - // ... + // …… stop: function (e) { e.preventDefault(); e.stopPropagation(); } - // ... + // …… }; 外观模式也适用于一些浏览器脚本的场景,即将浏览器的差异隐藏在一个外观方法下面。继续前面的例子,你可以添加一些处理IE中事件API的代码: var myevent = { - // ... + // …… stop: function (e) { - // others + // 其它浏览器 if (typeof e.preventDefault === "function") { e.preventDefault(); } @@ -657,10 +656,10 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 e.cancelBubble = true; } } - // ... + // …… }; -外观模式在做一些重新设计和重构工作时也很有用。当你想用一个不同的实现来替换某个对象的时候,你可能需要工作相当长一段时间(一个复杂的对象),与此同时,一些使用这个新对象的代码也在被同步编写。你可以先想好新对象的API,然后使用新的API创建一个外观方法在旧的对象前面。使用这种方式,当你完全替换到旧的对象的时候,你只需要修改少量客户代码,因为新的客户代码已经是在使用新的API了。 +外观模式在做一些重新设计和重构工作时也很有用。当你想用一个不同的实现来替换某个对象的时候,你可能需要花相当长一段时间才能完成(一个复杂的对象),与此同时,一些使用这个新对象的代码也在被同步编写。你可以先想好新对象的API,然后在旧的对象前面使用新的API创建一个外观方法。使用这种方式,当你完全替换掉旧的对象的时候,你只需要修改少量的调用代码,因为新的代码已经是在使用新的API了。 ## 代理模式 From dd37788bcabc6e3e33a48ae1a446a4b99dd58c8d Mon Sep 17 00:00:00 2001 From: TooBug Date: Sun, 12 May 2013 18:35:10 +0800 Subject: [PATCH 32/58] =?UTF-8?q?=E5=88=86=E7=A6=BB=20=E6=A0=A1=E5=AF=B9?= =?UTF-8?q?=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter8.markdown | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/chapter8.markdown b/chapter8.markdown index 57006b3..5b224bb 100644 --- a/chapter8.markdown +++ b/chapter8.markdown @@ -1,6 +1,6 @@ # DOM和浏览器中的模式 -在本书的前面几章中,我们主要关注了JavaScript核心(ECMAScript),并没有涉及太多关于在浏览器中使用JavaScript的内容。在本章,我们将探索一些在浏览器环境中的模式,因为这是最常见的JavaScript程序环境。浏览器脚本编程也是大部分不喜欢JavaScript的人对这门语言的认知。这当然是可以理解,因为在浏览器中有非常多不一致的宿主对象和DOM实现。很明显,任何能够减轻客户端脚本编程的痛楚的最佳初中都是大有益处的。 +在本书的前面几章中,我们主要关注了JavaScript核心(ECMAScript),并没有涉及太多关于在浏览器中使用JavaScript的内容。在本章,我们将探索一些在浏览器环境中的模式,因为这是最常见的JavaScript程序环境。浏览器脚本编程也是大部分不喜欢JavaScript的人对这门语言的认知。这当然是可以理解,因为在浏览器中有非常多不一致的宿主对象和DOM实现。很明显,任何能够减轻客户端脚本编程的痛楚的最佳实践都是大有益处的。 在本章中,你会看到一些零散的模式,包括DOM编程、事件处理、远程脚本、页面脚本的加载策略以及将JavaScript部署到生产环境的步骤。 @@ -20,7 +20,7 @@ JavaScript,用来处理用户交互和页面的动态变化 -尽可能地将这三者分离可以加强应用在各种用户代理(译注:user agent,即为用户读取页面并呈现的软件,一般指浏览器)的可到达性(译注:delivery,指可被用户代理接受并理解的程度),比如图形浏览器、纯文本浏览器、用于残障人士的辅助技术、移动设备等等。分离常常是和渐进增强的思想一起实现的,我们从一个给最简单的用户代理的最基础的体验(纯HTML)开始,当用户代理的兼容性提升时再添加更多的可以为体验加分的东西。如果浏览器支持CSS,那么用户会看到文档更好的呈现。如果浏览器支持JavaScript,那文档会更像一个应用,提供更多的特性来增强用户体验。 +尽可能地将这三者分离可以加强应用在各种用户代理(译注:user agent,即为用户读取页面并呈现的软件,一般指浏览器)的可到达性(译注:delivery,指可被用户代理接受并理解的程度),比如图形浏览器、纯文本浏览器、用于残障人士的辅助技术、移动设备等等。分离常常是和渐进增强的思想一起实现的,我们从一个最基础的体验(纯HTML)开始,它将被用于最简单的用户代理,当用户代理的兼容性提升时再添加更多的可以为体验加分的东西。如果浏览器支持CSS,那么用户会看到文档更好的呈现。如果浏览器支持JavaScript,那文档会更像一个应用,有更多用来增强用户体验的特性。 在实践中,分离意味者: @@ -29,21 +29,21 @@ - 不要使用内联的事件处理(如onclick)或者是内联的style属性,因为它们不属于内容层 - 使用语义化的HTML元素,比如头部和列表等 -JavaScript(行为)层的地位不应该很显赫,也就是说它不应该成为页面正常工作必须的东西,不应该使得用户在使用不支持的浏览器操作时存在障碍。它只应该被用来增强页面。 +JavaScript(行为)层的地位不应该很显赫,也就是说它不应该成为页面正常工作必须依赖的东西,不应该使得用户在使用不支持的浏览器操作时存在障碍。它只应该被用来增强页面。 -通常比较优雅的用来处理浏览器差异的方法是特性检测。它的思想是你不应该使用浏览器类型检测来决定代码的逻辑,而是应该检测在当前环境中你需要使用的某个方法或者是属性是否存在。浏览器检测一般认为是一种“反模式”(译注:anitpattern,指不好的模式)。虽然有的情况下不可避免要使用,但它应该是最后考虑的选择,并且应该只在特性检测没有办法给出明确答案(或者造成明显性能问题)的时候使用: +通常比较优雅的用来处理浏览器差异的方法是特性检测,它的思想是你不应该使用浏览器类型检测来决定代码的逻辑,而是应该检测在当前环境中你需要使用的某个方法或者是属性是否存在。浏览器检测一般认为是一种“反模式”,虽然有的情况下不可避免要使用,但它应该是最后才考虑的选择,并且应该只在特性检测没有办法给出明确答案(或者造成明显性能问题)的时候使用: - // antipattern + // 反模式 if (navigator.userAgent.indexOf('MSIE') !== −1) { document.attachEvent('onclick', console.log); } - // better + // 更好的方式 if (document.attachEvent) { document.attachEvent('onclick', console.log); } - // or even more specific + // 或者还可以再具体一点 if (typeof document.attachEvent !== "undefined") { document.attachEvent('onclick', console.log); } From 5d7c98d1e96e93b6827f3ddaef4d3bcc71a3360e Mon Sep 17 00:00:00 2001 From: TooBug Date: Sun, 12 May 2013 19:46:50 +0900 Subject: [PATCH 33/58] =?UTF-8?q?DOM=E7=BC=96=E7=A8=8B=20=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter8.markdown | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/chapter8.markdown b/chapter8.markdown index 5b224bb..69a9488 100644 --- a/chapter8.markdown +++ b/chapter8.markdown @@ -52,9 +52,9 @@ JavaScript(行为)层的地位不应该很显赫,也就是说它不应该 ## DOM编程 -操作页面的DOM树是在客户端JavaScript编程中最普遍的动作。这也是导致开发者头疼的最主要原因(这也导致了JavaScript名声不好),因为DOM方法在不同的浏览器中实现得有很多差异。这也是为什么使用一个抽象了浏览器差异的JavaScript库能显著提高开发速度的原因。 +操作页面的DOM树是在客户端JavaScript编程中最普遍的行为。这也是导致开发者头疼的最主要原因(这也导致了JavaScript名声不好),因为DOM方法在不同的浏览器中实现得有很多差异。这也是为什么使用一个抽象了浏览器差异的JavaScript库能显著提高开发速度的原因。 -我们来看一些在访问和修改DOM树时推荐的模式,主要考虑点是性能方面。 +我们来看一些在访问和修改DOM树时推荐的模式,主要考虑性能方面。 ### DOM访问 @@ -62,31 +62,32 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 一个原则就是DOM访问的次数应该被减少到最低,这意味者: -- 避免在环境中访问DOM +- 避免在循环中访问DOM - 将DOM引用赋给本地变量,然后操作本地变量 - 当可能的时候使用selectors API -- 遍历HTML collections时缓存length(见第2章) +- 遍历HTML collections时缓存`length`(见第二章) -看下面例子中的第二个(better)循环,尽管它看起来更长一些,但却要快上几十上百倍(取决于具体浏览器): +看下面例子中的第二个循环,尽管它看起来更长一些,但却要快上几十上百倍(取决于具体浏览器): - // antipattern + // 反模式 for (var i = 0; i < 100; i += 1) { document.getElementById("result").innerHTML += i + ", "; } - // better - update a local variable var i, content = ""; + // 更好的方式 - 更新本地变量 + var i, content = ""; for (i = 0; i < 100; i += 1) { content += i + ","; } document.getElementById("result").innerHTML += content; -在下一个代码片段中,第二个例子(使用了本地变量style)更好,尽管它需要多写一行代码,还需要多定义一个变量: +在下一个代码片段中,第二个例子(使用了本地变量`style`)更好,尽管它需要多写一行代码,还需要多定义一个变量: - // antipattern + // 反模式 var padding = document.getElementById("result").style.padding, margin = document.getElementById("result").style.margin; - // better + // 更好的方式 var style = document.getElementById("result").style, padding = style.padding, margin = style.margin; @@ -96,22 +97,22 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 document.querySelector("ul .selected"); document.querySelectorAll("#widget .class"); -这两个方法接受一个CSS选择器字符串,返回匹配这个选择器的DOM列表(译注:querySelector只返回第一个匹配的DOM)。selectors API在现代浏览器(以及IE8+)可用,它总是会比你使用其它DOM方法来做同样的选择要快。主流的JavaScript库的最近版本都已经使用了这个API,所以你有理由去检查你的项目,确保使用的是最新版本。 +这两个方法接受一个CSS选择器字符串,返回匹配这个选择器的DOM列表(译注:`querySelector`只返回第一个匹配的DOM)。selectors API在现代浏览器(以及IE8+)中可用,它总是会比你使用其它DOM方法来做同样的选择要快。主流的JavaScript库的最新版本都已经使用了这个API,所以你应该去检查你的项目,确保使用的是最新版本。 -给你经常访问的元素加上一个id属性也是有好处的,因为document.getElementById(myid)是找到一个DOM元素最容易也是最快的方法。 +给你经常访问的元素加上一个`id`属性也是有好处的,因为`document.getElementById(myid)`是找到一个DOM元素最容易也是最快的方法。 ### DOM操作 -除了访问DOM元素之外,你可能经常需要改变它们、删除其中的一些或者是添加新的元素。更新DOM会导致浏览器重绘(repaint)屏幕,也经常导致重排(reflow)(重新计算元素的位置),这些操作代价是很高的。 +除了访问DOM元素之外,你可能经常需要改变它们、删除其中的一些或者是添加新的元素。更新DOM会导致浏览器重绘(repaint)屏幕,也经常导致重排(reflow,重新计算元素的位置),这些操作代价是很高的。 -再说一次,通用的原则仍然是尽量少地更新DOM,这意味着我们可以将变化集中到一起,然后在“活动的”(live)文档树之外去执行这些变化。 +还是那句话,原则是尽量少地更新DOM,这意味着我们可以将变化集中到一起,然后在“活动的”(live)文档树之外去执行这些变化。 当你需要添加一棵相对较大的子树的时候,你应该在完成这棵树的构建之后再放到文档树中。为了达到这个目的,你可以使用文档碎片(document fragment)来包含你的节点。 不要这样添加节点: - // antipattern - // appending nodes as they are created + // 反模式 + // 在节点创建后就插入文档 var p, t; @@ -145,16 +146,16 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 document.body.appendChild(frag); -这个例子和前面例子中每段更新一次相比,文档树只被更新了一下,只导致一次重排/重绘。 +这个例子和前面例子中每段更新一次相比,文档树只被更新了一次,只导致一次重排/重绘。 当你添加新的节点到文档中时,文档碎片很有用。当你需要更新已有的节点时,你也可以将这些变化集中。你可以将你要修改的子树的父节点克隆一份,然后对克隆的这份做修改,完成之后再去替换原来的元素。 var oldnode = document.getElementById('result'), clone = oldnode.cloneNode(true); - // work with the clone... + // 修改克隆后的节点…… - // when you're done: + // 结束修改之后: oldnode.parentNode.replaceChild(clone, oldnode); ## 事件 From b0052791cab69d86a70ca276bdcd700642ab140d Mon Sep 17 00:00:00 2001 From: TooBug Date: Sun, 12 May 2013 19:00:54 +0800 Subject: [PATCH 34/58] =?UTF-8?q?=E4=BA=8B=E4=BB=B6=20=E6=A0=A1=E5=AF=B9?= =?UTF-8?q?=E4=B8=80=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter8.markdown | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/chapter8.markdown b/chapter8.markdown index 69a9488..154f414 100644 --- a/chapter8.markdown +++ b/chapter8.markdown @@ -160,13 +160,13 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 ## 事件 -在浏览器脚本编程中,另一块充满兼容性问题并且带来很多不愉快的区域就是浏览器事件,比如click,mouseover等等。同样的,一个JavaScript库可以解决支持IE(9以下)和W3C标准实现的双倍工作量。 +在浏览器脚本编程中,另一块充满兼容性问题并且带来很多不愉快的区域就是浏览器事件,比如`click`,`mouseover`等等。同样的,一个JavaScript库可以解决支持IE(9以下)和W3C标准实现带来的双倍工作量。 我们来看一下一些主要的点,因为你在做一些简单的页面或者快速开发的时候可能不会使用已有的库,当然,也有可能你正在写你自己的库。 ### 事件处理 -麻烦是从给元素绑定事件开始的。假设你有一个按钮,点击它的时候增加计数器的值。你可以添加一个内联的onclick属性,这在所有的浏览器中都能正常工作,但是会违反分离和渐进增强的思想。所以你应该尽力在JavaScript中来做绑定,而不是在标签中。 +麻烦是从给元素绑定事件开始的。假设你有一个按钮,点击它的时候增加计数器的值。你可以添加一个内联的`onclick`属性,这在所有的浏览器中都能正常工作,但是会违反分离和渐进增强的思想。所以你应该尽量在JavaScript中来做绑定,而不是在标签中。 假设你有下面的标签: @@ -174,7 +174,7 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 你可以将一个函数赋给节点的onclick属性,但你只能这样做一次: - // suboptimal solution + // 不好的解决方案 var b = document.getElementById('clickme'), count = 0; @@ -183,20 +183,20 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 b.innerHTML = "Click me: " + count; }; -如果你希望在按钮点击的时候执行好几个函数,那么在维持松耦合的情况下就不能用这种方法来做绑定。从技术上讲,你可以检测onclick是否已经包含一个函数,如果已经包含,就将它加到你自己的函数中,然后替换onclick的值为你的新函数。但是一个更干净的解决方案是使用addEventListener()方法。这个方法在IE8及以下版本中不存在,在这些浏览器需要使用attachEvent()。 +如果你希望在按钮点击的时候执行好几个函数,那么在保持松耦合的情况下就不能用这种方法来做绑定。从技术上讲,你可以检测`onclick`是否已经包含一个函数,如果已经包含,就将它加到你自己的函数中,然后替换`onclick`的值为你的新函数。但是一个更干净的解决方案是使用`addEventListener()`方法。这个方法在IE8及以下版本中不存在,在这些浏览器中需要使用`attachEvent()`。 -当我们回头看条件初始化模式(第4章)时,会发现一个示例实现是一个很好的解决跨浏览器事件监听的套件。现在我们不讨论细节,只看一下如何给我们的按钮绑定事件: +当我们回头看条件初始化模式(第四章)时,会发现其中的一个示例实现就是一个很好的解决跨浏览器事件监听的套件。现在我们不讨论细节,只看一下如何给我们的按钮绑定事件: var b = document.getElementById('clickme'); if (document.addEventListener) { // W3C b.addEventListener('click', myHandler, false); } else if (document.attachEvent) { // IE b.attachEvent('onclick', myHandler); - } else { // last resort + } else { // 为保险起见…… b.onclick = myHandler; } -现在当按钮被点击时,myHandler会被执行。让我们来让这个函数实现增加按钮文字“Click me: 0”中的数字的功能。为了更有趣一点,我们假设有好几个按钮,一个myHandler()函数来处理所有的按钮点击。如果我们可以从每次点击的事件对象中获取节点和节点对应的计数器值,那为每个按钮保持一个引用和计数器就显得不高效了。 +现在当按钮被点击时,`myHandler()`会被执行。我们来让这个函数实现增加按钮文字“Click me: 0”中的数字的功能。为了更有趣一点,我们假设有好几个按钮,一个`myHandler()`函数来处理所有的按钮点击。如果我们可以从每次点击的事件对象中获取节点和节点对应的计数器值,那为每个按钮保持一个引用和计数器就显得不高效了。 我们先看一下解决方案,稍后再来做些评论: @@ -204,16 +204,16 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 var src, parts; - // get event and source element + // 获取事件对象和事件来源 e = e || window.event; src = e.target || e.srcElement; - // actual work: update label + // 真正工作的部分:更新文字 parts = src.innerHTML.split(": "); parts[1] = parseInt(parts[1], 10) + 1; src.innerHTML = parts[0] + ": " + parts[1]; - // no bubble + // 阻止冒泡 if (typeof e.stopPropagation === "function") { e.stopPropagation(); } @@ -221,7 +221,7 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 e.cancelBubble = true; } - // prevent default action + // 阻止默认行为 if (typeof e.preventDefault === "function") { e.preventDefault(); } @@ -231,24 +231,26 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 } -一个在线的例子可以在找到。 +在线的例子可以在找到。 在这个事件处理函数中,有四个部分: -- 首先,我们需要访问事件对象,它包含事件的一些信息以及触发这个事件的页面元素。事件对象会被传到事件处理回调函数中,但是使用onclick属性时需要使用全局属性window.event来获取。 +- 首先,我们需要访问事件对象,它包含事件的一些信息以及触发这个事件的页面元素。事件对象会被传到事件处理回调函数中,但是使用`onclick`属性时需要使用全局属性`window.event`来获取 - 第二部分是真正用于更新文字的部分 -- 接下来是阻止事件冒泡。在这个例子中它不是必须的,但通常情况下,如果你不阻止的话,事件会一直冒泡到文档根元素甚至window对象。同样的,我们也需要用两种方法来阻止冒泡:W3C标准方式(stopPropagation())和IE的方式(使用cancelBubble) -- 最后,如果需要的话,阻止默认行为。有一些事件(点击链接、提交表单)有默认的行为,但你可以使用preventDefault()(IE是通过设置returnValue的值为false的方式)来阻止这些默认行为。 +- 接下来是阻止事件冒泡。在这个例子中它不是必须的,但通常情况下,如果你不阻止的话,事件会一直冒泡到文档根元素甚至`window`对象。同样的,我们也需要用两种方法来阻止冒泡:W3C标准方式(`stopPropagation()`)和IE的方式(使用`cancelBubble`) +- 最后,如果需要的话,阻止默认行为。有一些事件(点击链接、提交表单)有默认的行为,但你可以使用`preventDefault()`(IE是通过设置`returnValue`的值为`false`的方式)来阻止这些默认行为 -如你所见,这里涉及到了很多重复性的工作,所以使用第7章讨论过的外观模式创建自己的事件处理套件是很有意义的。 +如你所见,这里涉及到了很多重复性的工作,所以使用第七章讨论过的外观模式创建自己的事件处理套件是很有意义的。 ### 事件委托 -事件委托是通过事件冒泡来实现的,它可以减少分散到各个节点上的事件处理函数的数量。如果有10个按钮在一个div元素中,你可以给div绑定一个事件处理函数,而不是给每个按钮都绑定一个。 +事件委托是通过事件冒泡来实现的,它可以减少分散到各个节点上的事件处理函数的数量。如果有10个按钮在一个`div`元素中,你可以给`div`绑定一个事件处理函数,而不是给每个按钮都绑定一个。 -我们来的睦一个实例,三个按钮放在一个div元素中(图8-1)。你可以在看到这个事件委托的实例。 +我们来看一个实例,三个按钮放在一个`div`元素中(图8-1)。你可以在看到这个事件委托的实例。 -> (译注: 上面的URL中的例子在IE下单击会没有反应,问题在于使用document.attachEvernt时传递的第一个参数应该是'onclick',而不是'click'.) +> 译注: 上面的URL中的例子在IE下单击会没有反应,问题在于使用`document.attachEvernt()`时传递的第一个参数应该是`'onclick'`,而不是`'click'`。 + +------校对分隔线----- ![图8-1 事件委托示例:三个在点击时增加计数器值的按钮](./Figure/chapter8/8-1.jpg) From c18c2cd85035f5bc23413c78847eebd374826914 Mon Sep 17 00:00:00 2001 From: TooBug Date: Mon, 13 May 2013 13:26:21 +0800 Subject: [PATCH 35/58] =?UTF-8?q?=E4=BB=A3=E7=90=86=E6=A8=A1=E5=BC=8F=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 74 +++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index 55cb4e6..f6d5a03 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -661,25 +661,23 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 外观模式在做一些重新设计和重构工作时也很有用。当你想用一个不同的实现来替换某个对象的时候,你可能需要花相当长一段时间才能完成(一个复杂的对象),与此同时,一些使用这个新对象的代码也在被同步编写。你可以先想好新对象的API,然后在旧的对象前面使用新的API创建一个外观方法。使用这种方式,当你完全替换掉旧的对象的时候,你只需要修改少量的调用代码,因为新的代码已经是在使用新的API了。 - ## 代理模式 -在代理设计模式中,一个对象充当了另一个对象的接口的角色。它和外观模式不一样,外观模式带来的方便仅限于将几个方法调用联合起来。而代理对象位于某个对象和它的客户之间,可以保护对对象的访问。 +在代理模式中,一个对象充当了另一个对象的接口的角色。它和外观模式不一样,外观模式带来的方便仅限于将几个方法调用联合起来。而代理对象位于某个对象和它的使用者之间,可以保护对对象的访问。 -这个模式看起来开销有点大,但在出于性能考虑时非常有用。代理对象可以作为对象(也叫“真正的主体”)的保护者,让真正的主体对象做尽量少的工作。 +这个模式看起来开销有点大,但在出于性能考虑时非常有用。代理对象可以作为目标对象的保护者,让目标对象做尽量少的工作。 -一种示例用法是我们称之为“懒初始化”(延迟初始化)的东西。假设初始化真正的主体是开销很大的,并且正好客户代码将它初始化后并不真正使用它。在这种情况下,代理对象可以作为真正的主体的接口起到帮助作用。代理对象接收到初始化请求,但在真正的主体真正被使用之前都不会将它传递过去。 +一种示例用法是“懒初始化”(延迟初始化)。假设负责初始化的对象是开销很大的,并且正好使用者将它初始化后并不真正使用它。在这种情况下,代理对象可以作为目标对象的接口起到帮助作用。代理对象接收到初始化请求,但在目标对象真正被使用之前都不会将请求传递过去。 -图7-2展示了这个场景,当客户代码发出初始化请求时,代理对象回复一切就绪,但并没有将请求传递过去,只有在客户代码真正需要真正的主体做些工作的时候才将两个请求一起传递过去。 +图7-2展示了这个场景,当使用目标对象的代码发出初始化请求时,代理对象回复一切就绪,但并没有将请求传递过去,只有在真正需要目标对象做些工作的时候才将两个请求一起传递过去。 -![图7-2 通过代理对象时客户代码与真正的主体的关系](./Figure/chapter7/7-2.jpg) +![图7-2 通过代理对象时目标对象与使用者的关系](./Figure/chapter7/7-2.jpg) -图7-2 通过代理对象时客户代码与真正的主体的关系 +图7-2 通过代理对象时目标对象与使用者的关系 - ### 一个例子 -在真正的主体做某件工作开销很大时,代理模式很有用处。在web应用中,开销最大的操作之一就是网络请求,此时尽可能地合并HTTP请求是有意义的。我们来看一个这种场景下应用代理模式的实例。 +在目标对象做某件工作开销很大时,代理模式很有用处。在web应用中,开销最大的操作之一就是网络请求,此时尽可能地合并HTTP请求是有意义的。我们来看一个这种场景下应用代理模式的实例。 #### 一个视频列表(expando) @@ -697,14 +695,14 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 这个应用中最主要的角色是两个对象: -- videos +- `videos` - 负责对信息区域展开/收起(videos.getInfo()方法)和播放视频的响应(videos.getPlayer()方法) -- http + 负责对信息区域展开/收起(`videos.getInfo()`方法)和播放视频的响应(`videos.getPlayer()`方法) +- `http` - 负责通过http.makeRequest()方法与服务端通讯 + 负责通过`http.makeRequest()`方法与服务端通讯 -当没有代理对象的时候,videos.getInfo()会为每个视频调用一次http.makeRequest()方法。当我们添加代理对象proxy后,它将位于vidoes和http中间,接手对makeRequest()的调用,并在可能的时候合并请求。 +当没有代理对象的时候,`videos.getInfo()`会为每个视频调用一次`http.makeRequest()`方法。当我们添加代理对象`proxy`后,它将位于`vidoes`和`http`中间,接手对`makeRequest()`的调用,并在可能的时候合并请求。 我们首先看一下没有代理对象的代码,然后添加代理对象来提升应用的响应速度。 @@ -730,13 +728,13 @@ HTML代码仅仅是一个链接列表: #### 事件处理 -现在我们来看一下事件处理的逻辑。首先我们定义一个方便的快捷函数$: +现在我们来看一下事件处理的逻辑。首先我们定义一个方便的快捷函数`$`: var $ = function (id) { return document.getElementById(id); }; -使用事件代理(第8章有更多关于这个模式的内容),我们将所有id="vids"的条目上的点击事件统一放到一个函数中处理: +使用事件代理(第八章有更多关于这个模式的内容),我们将所有`id="vids"`的条目上的点击事件统一放到一个函数中处理: $('vids').onclick = function (e) { var src, id; @@ -764,19 +762,19 @@ HTML代码仅仅是一个链接列表: videos.getInfo(id); }; -#### videos对象 +#### `videos`对象 -videos对象有三个方法: +`videos`对象有三个方法: -- getPlayer() +- `getPlayer()` 返回播放视频需要的HTML代码(跟我们讨论的无关) -- updateList() +- `updateList()` - 网络请求的回调函数,接受从服务器返回的数据,然后生成用于视频详细信息的HTML代码。这一部分也没有什么太有趣的事情。 -- getInfo() + 网络请求的回调函数,接受从服务器返回的数据,然后生成用于视频详细信息的HTML代码。这一部分也没有什么需要关注的事情。 +- `getInfo()` - 这个方法切换视频信息的可视状态,同时也调用http对象的方法,并传递updaetList()作为回调函数。 + 这个方法切换视频信息的可视状态,同时也调用`http`对象的方法,并传递`updaetList()`作为回调函数。 下面是这个对象的代码片段: @@ -803,9 +801,9 @@ videos对象有三个方法: } }; -#### http对象 +#### `http`对象 -http对象只有一个方法,它向Yahoo!的YQL服务发起一个JSONP请求: +`http`对象只有一个方法,它向Yahoo!的YQL服务发起一个JSONP请求: var http = { makeRequest: function (ids, callback) { @@ -829,23 +827,23 @@ http对象只有一个方法,它向Yahoo!的YQL服务发起一个JSONP请求 当所有的六个视频都被选中后,将会向服务端发起六个独立的像这样的YQL请求: -select * from music.video.id where ids IN ("2158073") + select * from music.video.id where ids IN ("2158073") #### 代理对象 -前面的代码工作得很正常,但我们可以让它工作得更好。proxy对象就在这样的场景中出现,并接管了http和videos对象之间的通讯。它将使用一个简单的逻辑来尝试合并请求:50ms的延迟。videos对象并不直接调用后台接口,而是调用proxy对象的方法。proxy对象在转发这个请求前将会等待一段时间,如果在等待的50ms内有另一个来自videos的调用,则它们将被合并为同一个请求。50ms的延迟对用户来说几乎是无感知的,但是却可以用来合并请求以提升点击“toggle”时的体验,一次展开多个视频。它也可以显著降低服务器的负载,因为web服务器只需要处理更少量的请求。 +前面的代码工作得很好,但我们可以让它工作得更好。`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(),像这样: +在修改后的代码中,唯一的变化是`videos.getInfo()`现在调用的是`proxy.makeRequest()`而不是`http.makeRequest()`,像这样: proxy.makeRequest(id, videos.updateList, videos); -proxy对象创建了一个队列来收集50ms之内接受到的视频ID,然后将这个队列传递给http对象,并提供回调函数,因为videos.updateList()只能处理一次接收到的数据。(译注:指每次传入的回调函数只能处理当次接收到的数据。) +`proxy`对象创建了一个队列来收集50ms之内接受到的视频ID,然后将这个队列传递给`http`对象,并提供回调函数,因为`videos.updateList()`只能处理一个接收到的视频信息。 -下面是proxy对象的代码: +下面是`proxy`对象的代码: var proxy = { ids: [], @@ -854,13 +852,13 @@ proxy对象创建了一个队列来收集50ms之内接受到的视频ID,然后 callback: null, context: null, makeRequest: function (id, callback, context) { - // add to the queue + // 添加到队列 this.ids.push(id); this.callback = callback; this.context = context; - // set up timeout + // 设置延时 if (!this.timeout) { this.timeout = setTimeout(function () { proxy.flush(); @@ -871,7 +869,7 @@ proxy对象创建了一个队列来收集50ms之内接受到的视频ID,然后 http.makeRequest(this.ids, "proxy.handler"); - // clear timeout and queue + // 清除延时和队列 this.timeout = null; this.ids = []; @@ -879,20 +877,20 @@ proxy对象创建了一个队列来收集50ms之内接受到的视频ID,然后 handler: function (data) { var i, max; - // single video + // 单个视频 if (parseInt(data.query.count, 10) === 1) { proxy.callback.call(proxy.context, data.query.results.Video); return; } - // multiple videos + // 多个视频 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请求合并为一个。 +使用代理模式可以在只改动一处原来代码的情况下,将多个web service请求合并为一个。 图7-4和7-5展示了使用代理模式将与服务器三次数据交互(不用代理模式时)变为一次交互的过程。 @@ -905,9 +903,9 @@ proxy对象创建了一个队列来收集50ms之内接受到的视频ID,然后 图7-5 通过一个代理对象合并请求,减少与服务器数据交互 -#### 使用代理对象做缓存 +### 使用代理对象做缓存 -在这个例子中,客户对象(videos)已经可以做到不对同一个对象重复发出请求。但现实情况中并不总是这样。这个代理对象还可以通过缓存之前的请求结果到cache属性中来进一步保护真正的主体http对象(图7-6)。然后当videos对象需要对同一个ID的视频请求第二次时,proxy对象可以直接从缓存中取出,从而避免一次网络交互。 +在这个例子中,目标对象的使用者(`videos`)已经可以做到不对同一个对象重复发出请求,但现实情况中并不总是这样。其实这个代理对象还可以通过缓存之前的请求结果到`cache`属性中来进一步保护`http`对象(图7-6)。然后当`videos`对象需要对同一个ID的视频请求第二次时,`proxy`对象可以直接从缓存中取出,从而避免一次网络交互。 ![图7-6 代理缓存](./Figure/chapter7/7-6.jpg) From 7f971a4a05b9138db2b1b4398b82491b4bbc2161 Mon Sep 17 00:00:00 2001 From: TooBug Date: Mon, 13 May 2013 13:34:09 +0800 Subject: [PATCH 36/58] =?UTF-8?q?=E4=B8=AD=E4=BB=8B=E8=80=85=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=20=E6=A0=A1=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index f6d5a03..1d20dad 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -911,30 +911,28 @@ HTML代码仅仅是一个链接列表: 图7-6 代理缓存 - ## 中介者模式 -一个应用不论大小,都是由一些彼此独立的对象组成的。所有的对象都需要一个通讯的方式来保持可维护性,即你可以安全地修改应用的一部分而不破坏其它部分。随着应用的开发和维护,会有越来越多的对象。然后,在重构代码的时候,对象可能会被移除或者被重新安排。当对象知道其它对象的太多信息并且直接通讯(直接调用彼此的方法或者修改属性)时,会导致我们不愿意看到的紧耦合。当对象耦合很紧时,要修改一个对象而不影响其它的对象是很困难的。此时甚至连一个最简单的修改都变得不那么容易,甚至连一个修改需要用多长时间都难以评估。 +一个应用不论大小,都是由一些彼此独立的对象组成的。所有的对象都需要一个通讯方式来保持可维护性,即你可以安全地修改应用的一部分而不破坏其它部分。随着应用的开发和维护,会有越来越多的对象。然后,在重构代码的时候,对象可能会被移除或者被重新设计。当对象知道其它对象的太多信息并且直接通讯(直接调用彼此的方法或者修改属性)时,会导致我们不愿意看到的紧耦合。当对象耦合很紧时,要修改一个对象而不影响其它的对象是很困难的。此时甚至连一个最简单的修改都变得不那么容易,甚至连一个修改需要用多长时间都难以评估。 -中介者模式就是一个缓解此问题的办法,它通过解耦来提升代码的可维护性(见图7-7)。在这个模式中,各个彼此合作的对象并不直接通讯,而是通过一个mediator(中介者)对象通讯。当一个对象改变了状态后,它就通知中介者,然后中介者再将这个改变告知给其它应该知道这个变化的对象。 +中介者模式就是一个缓解此问题的办法,它通过解耦来提升代码的可维护性(见图7-7)。在这个模式中,各个彼此合作的对象并不直接通讯,而是通过一个`mediator`(中介者)对象通讯。当一个对象改变了状态后,它就通知中介者,然后中介者再将这个改变告知给其它应该知道这个变化的对象。 ![图7-7 中介者模式中的对象关系](./Figure/chapter7/7-7.jpg) 图7-7 中介者模式中的对象关系 - ### 中介者示例 我们来看一个使用中介者模式的实例。这个应用是一个游戏,它的玩法是比较两位游戏者在半分钟内按下按键的次数,次数多的获胜。玩家1需要按的是1,玩家2需要按的是0(这样他们的手指不会搅在一起)。当前分数会显示在一个计分板上。 对象列表如下: -- Player 1 -- Player 2 -- Scoreboard -- Mediator +- `Player1` +- `Player2` +- `Scoreboard` +- `Mediator` -中介者Mediator知道所有的对象。它与输入设备(键盘)打交道,处理keypress事件,决定现在是哪位玩家玩的,然后通知这个玩家(见图7-8)。玩家负责玩(即给自己的分数加一分),然后通知中介者他这一轮已经玩完。中介者再告知计分板最新的分数,计分板更新显示。 +中介者`Mediator`知道所有的对象,它与输入设备(键盘)打交道,处理`keypress`事件,决定现在是哪位玩家玩的,然后通知这个玩家(见图7-8)。玩家负责玩(即给自己的分数加一分),然后通知中介者他这一轮已经玩完。中介者再告知计分板最新的分数,计分板更新显示。 除了中介者之外,其它的对象都不知道有别的对象存在。这样就使得更新这个游戏变得很简单,比如要添加一位玩家或者是添加另外一个显示剩余时间的地方。 @@ -944,7 +942,7 @@ HTML代码仅仅是一个链接列表: 图7-8 游戏涉及的对象 -玩家对象是通过Player()构造函数来创建的,有自己的points和name属性。原型上的play()方法负责给自己加一分然后通知中介者: +玩家对象是通过`Player()`构造函数来创建的,有自己的`points`和`name`属性。原型上的`play()`方法负责给自己加一分然后通知中介者: function Player(name) { this.points = 0; @@ -955,14 +953,14 @@ HTML代码仅仅是一个链接列表: mediator.played(); }; -scoreboard对象(计分板)有一个update()方法,它会在每次玩家玩完后被中介者调用。计分板根本不知道玩家的任何信息,也不保存分数,它只负责显示中介者给过来的分数: +`scoreboard`对象(计分板)有一个`update()`方法,它会在每次玩家玩完后被中介者调用。计分板根本不知道玩家的任何信息,也不保存分数,它只负责显示中介者给过来的分数: var scoreboard = { - // HTML element to be updated + // 被更新的HTML元素 element: document.getElementById('results'), - // update the score display + // 更新分数显示 update: function (score) { var i, msg = ''; @@ -978,14 +976,14 @@ scoreboard对象(计分板)有一个update()方法,它会在每次玩家 } }; -现在我们来看一下mediator对象(中介者)。在游戏初始化的时候,在setup()方法中创建游戏者,然后放后players属性以便后续使用。played()方法会被游戏者在每轮玩完后调用,它更新score哈希然表然后将它传给scoreboard用于显示。最后一个方法是keypress(),负责处理键盘事件,决定是哪位玩家玩的,并且通知它: +现在我们来看一下`mediator`对象(中介者)。在游戏初始化的时候,在`setup()`方法中创建玩家,然后放入`players`属性以便后续使用。`played()`方法会被玩家在每轮玩完后调用,它更新`score`哈希然表然后将它传给`scoreboard`用于显示。最后一个方法是`keypress()`,负责处理键盘事件,决定是哪位玩家玩的,并且通知它: var mediator = { - // all the players + // 所有的玩家 players: {}, - // initialization + // 初始化 setup: function () { var players = this.players; players.home = new Player('Home'); @@ -993,7 +991,7 @@ scoreboard对象(计分板)有一个update()方法,它会在每次玩家 }, - // someone plays, update the score + // 玩家玩完后更新分数 played: function () { var players = this.players, score = { @@ -1004,14 +1002,14 @@ scoreboard对象(计分板)有一个update()方法,它会在每次玩家 scoreboard.update(score); }, - // handle user interactions + // 处理用户交互 keypress: function (e) { e = e || window.event; // IE - if (e.which === 49) { // key "1" + if (e.which === 49) { // 按键“1” mediator.players.home.play(); return; } - if (e.which === 48) { // key "0" + if (e.which === 48) { // 按键“0” mediator.players.guest.play(); return; } @@ -1020,11 +1018,11 @@ scoreboard对象(计分板)有一个update()方法,它会在每次玩家 最后一件事是初始化和结束游戏: - // go! + // 开始 mediator.setup(); window.onkeypress = mediator.keypress; - // game over in 30 seconds + // 游戏在30秒后结束 setTimeout(function () { window.onkeypress = null; alert('Game over!'); From ceae4a15eebff8e453d8203d48360d94ab054997 Mon Sep 17 00:00:00 2001 From: TooBug Date: Tue, 14 May 2013 20:13:00 +0800 Subject: [PATCH 37/58] =?UTF-8?q?=E7=AC=AC=E4=B8=83=E7=AB=A0=20=E6=A0=A1?= =?UTF-8?q?=E5=AF=B9=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter7.markdown | 104 ++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/chapter7.markdown b/chapter7.markdown index 1d20dad..4e8d4c0 100644 --- a/chapter7.markdown +++ b/chapter7.markdown @@ -304,11 +304,11 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 `Object()`也是一个工厂这一事实可能没有太多实际用处,仅仅是觉得值得作为一个例子提一下,告诉我们工厂模式是随处可见的。 -## 迭代器 +## 遍历模式 -在迭代器模式中,你有一些含有有序聚合数据的对象。这些数据可能在内部用一种复杂的结构存储着,但是你希望提供一种简单的方法来访问这种结构中的每个元素。数据的使用者不需要知道你是怎样组织你的数据的,他们只需要操作一个个独立的元素。 +在遍历模式中,你有一些含有有序聚合数据的对象。这些数据可能在内部用一种复杂的结构存储着,但是你希望提供一种简单的方法来访问这种结构中的每个元素。数据的使用者不需要知道你是怎样组织你的数据的,他们只需要操作一个个独立的元素。 -在迭代器模式中,你的对象需要提供一个`next()`方法。按顺序调用`next()`方法必须返回序列中的下一个元素,但是“下一个”在你的特定的数据结构中指什么是由你自己来决定的。 +在遍历模式中,你的对象需要提供一个`next()`方法。按顺序调用`next()`方法必须返回序列中的下一个元素,但是“下一个”在你的特定的数据结构中指什么是由你自己来决定的。 假设你的对象叫`agg`,你可以通过简单地在循环中调用`next()`来访问每个数据元素,像这样: @@ -318,22 +318,22 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 console.log(element); } -在迭代器模式中,聚合对象通常也会提供一个方便的方法`hasNext()`,这样对象的使用者就可以知道他们已经获取到你数据的最后一个元素。当使用`hasNext()`来按顺序访问所有元素时,是像这样的: +在遍历模式中,聚合对象通常也会提供一个方便的方法`hasNext()`,这样对象的使用者就可以知道他们已经获取到你数据的最后一个元素。当使用`hasNext()`来按顺序访问所有元素时,是像这样的: while (agg.hasNext()) { // 访问element…… console.log(agg.next()); } -## 装饰器 +## 装饰模式 -在装饰器模式中,一些额外的功能可以在运行时被动态地添加到一个对象中。在静态的基于类的语言中,处理这个问题可能是个挑战,但是在JavaScript中,对象本来就是可变的,所以给一个对象添加额外的功能本身并不是什么问题。 +在装饰模式中,一些额外的功能可以在运行时被动态地添加到一个对象中。在静态的基于类的语言中,处理这个问题可能是个挑战,但是在JavaScript中,对象本来就是可变的,所以给一个对象添加额外的功能本身并不是什么问题。 -装饰器模式的一个很方便的特性是可以对我们需要的特性进行定制和配置。刚开始时,我们有一个拥有基本功能的对象,然后可以从可用的装饰器中去挑选一些需要用到的去增强这个对象,如果有必要的话,还可以指定增强的顺序。 +装饰模式的一个很方便的特性是可以对我们需要的特性进行定制和配置。刚开始时,我们有一个拥有基本功能的对象,然后可以从可用的装饰中去挑选一些需要用到的去增强这个对象,如果有必要的话,还可以指定增强的顺序。 ### 用法 -我们来看一下这个模式的用法示例。假设你正在做一个卖东西的web应用,每个新交易是一个新的`sale`对象。这个对象“知道”交易的价格并且可以通过调用`sale.getPrice()`方法返回。根据环境的不同,你可以开始用一些额外的功能来装饰这个对象。假设一个场景是这笔交易是发生在加拿大的一个省Québec,在这种情况下,购买者需要付联邦税和Québec省税。根据装饰器模式的用法,你需要指明使用联邦税装饰器和Québec省税装饰器来装饰这个对象。然后你还可以给这个对象装饰一些价格格式的功能。这个场景的使用方式可能是像这样: +我们来看一下这个模式的用法示例。假设你正在做一个卖东西的web应用,每个新交易是一个新的`sale`对象。这个对象“知道”交易的价格并且可以通过调用`sale.getPrice()`方法返回。根据环境的不同,你可以开始用一些额外的功能来装饰这个对象。假设一个场景是这笔交易是发生在加拿大的一个省Québec,在这种情况下,购买者需要付联邦税和Québec省税。根据装饰模式的用法,你需要指明使用联邦税装饰器和Québec省税装饰器来装饰这个对象。然后你还可以给这个对象装饰一些价格格式的功能。这个场景的使用方式可能是像这样: var sale = new Sale(100); // 价格是100美元 sale = sale.decorate('fedtax'); // 加上联邦税 @@ -352,12 +352,12 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 ### 实现 -一种实现装饰器模式的方法是让每个装饰器成为一个拥有应该被重写的方法的对象。每个装饰器实际上是继承自已经被前一个装饰器增强过的对象。装饰器的每个方法都会调用父对象(继承自的对象)的同名方法并取得值,然后做一些额外的处理。 +一种实现装饰模式的方法是让每个装饰器成为一个拥有应该被重写的方法的对象。每个装饰器实际上是继承自已经被前一个装饰器增强过的对象。装饰器的每个方法都会调用父对象(继承自的对象)的同名方法并取得值,然后做一些额外的处理。 最终的效果就是当你在第一个例子中调用`sale.getPrice()`时,实际上是在调用`money`装饰器的方法(图7-1)。但是因为每个装饰器会先调用父对象的方法,`money`的`getPrice()`先调用`quebec`的`getPrice()`,而它又会去调用`fedtax`的`getPrice()`方法,依次类推。这个链会一直走到原始的未经装饰的由`Sale()`构造函数实现的`getPrice()`。 -![图7-1 装饰器模式的实现](./Figure/chapter7/7-1.jpg) -图7-1 装饰器模式的实现 +![图7-1 装饰模式的实现](./Figure/chapter7/7-1.jpg) +图7-1 装饰模式的实现 这个实现以一个构造函数和一个原型方法开始: @@ -486,7 +486,7 @@ JavaScript没有类,所以一字一句地说单例的定义并没有什么意 return price; }; -装饰器模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中,`getPrice()`是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,`decorators_list`属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。 +装饰模式的第二种实现方式更简单一些,并且没有引入继承。装饰的方法也会简单。所有的工作都由“同意”被装饰的方法来做。在这个示例实现中,`getPrice()`是唯一被允许装饰的方法。如果你想有更多可以被装饰的方法,那遍历装饰器列表的工作就需要由每个方法重复去做。但是,这可以很容易地被抽象到一个辅助方法中,给它传一个方法然后使这个方法“可被装饰”。如果这样实现的话,`decorators_list`属性就应该是一个对象,它的属性名字是方法名,值是装饰器对象的数组。 ## 策略模式 @@ -1029,44 +1029,42 @@ HTML代码仅仅是一个链接列表: }, 30000); - ## 观察者模式 -观察者模式被广泛地应用于JavaScript客户端编程中。所有的浏览器事件(mouseover,keypress等)都是使用观察者模式的例子。这种模式的另一个名字叫“自定义事件”,意思是这些事件是被编写出来的,和浏览器触发的事件相对。它还有另外一个名字叫“订阅者/发布者”模式。 +观察者模式被广泛地应用于JavaScript客户端编程中。所有的浏览器事件(`mouseover`,`keypress`等)都是使用观察者模式的例子。这种模式的另一个名字叫“自定义事件”,意思是这些事件是被编写出来的,和浏览器触发的事件相对。它还有另外一个名字叫“订阅者/发布者”模式(Pub/Sub)。 -使用这个模式的最主要目的就是促进代码触解耦。在观察者模式中,一个对象订阅另一个对象的指定活动并得到通知,而不是调用另一个对象的方法。订阅者也被叫作观察者,被观察的对象叫作发布者或者被观察者(译注:subject,不知道如何翻译,第一次的时候译为“主体”,第二次译时觉得不妥,还是直接叫被观察者好了)。当一个特定的事件发生的时候,发布者会通知(调用)所有的订阅者,同时还可能以事件对象的形式传递一些消息。 +使用这个模式的最主要目的就是促进代码解耦。在观察者模式中,一个对象订阅另一个对象的指定活动并得到通知,而不是调用另一个对象的方法。订阅者也被叫作观察者,被观察的对象叫作发布者或者被观察者。当一个特定的事件发生的时候,发布者会通知(调用)所有的订阅者,同时还可能以事件对象的形式传递一些消息。 - ### 例1:杂志订阅 -为了理解观察者模式的实现方式,我们来看一个具体的例子。我们假设有一个发布者paper,它发行一份日报和一份月刊。无论是日报还是月刊发行,有一个名叫joe的订阅者都会收到通知。 +为了理解观察者模式的实现方式,我们来看一个具体的例子。我们假设有一个发布者`paper`,它发行一份日报和一份月刊。无论是日报还是月刊发行,有一个名叫`joe`的订阅者都会收到通知。 -paper对象有一个subscribers属性,它是一个数组,用来保存所有的订阅者。订阅的过程就仅仅是将订阅者放到这个数组中而已。当一个事件发生时,paper遍历这个订阅者列表,然后通知它们。通知的意思也就是调用订阅者对象的一个方法。因此,在订阅过程中,订阅者需要提供一个方法给paper对象的subscribe()。 +`paper`对象有一个`subscribers`属性,它是一个数组,用来保存所有的订阅者。订阅的过程就仅仅是将订阅者放到这个数组中而已。当一个事件发生时,`paper`遍历这个订阅者列表,然后通知它们。通知的意思也就是调用订阅者对象的一个方法。因此,在订阅过程中,订阅者需要提供一个方法给`paper`对象的`subscribe()`。 -paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组中移除。paper对象的最后一个重要的方法是publish(),它负责调用订阅者的方法。总结一下,一个发布者对象需要有这些成员: +`paper`对象也可以提供`unsubscribe()`方法,它可以将订阅者从数组中移除。`paper`对象的最后一个重要的方法是`publish()`,它负责调用订阅者的方法。总结一下,一个发布者对象需要有这些成员: -- subscribers +- `subscribers` 一个数组 -- subscribe() +- `subscribe()` 将订阅者加入数组 -- unsubscribe() +- `unsubscribe()` 从数组中移除订阅者 -- publish() +- `publish()` 遍历订阅者并调用它们订阅时提供的方法 -所有三个方法都需要一个type参数,因为一个发布者可能触发好几种事件(比如同时发布杂志和报纸),而订阅者可以选择性地订阅其中的一种或几种。 +所有三个方法都需要一个`type`参数,因为一个发布者可能触发好几种事件(比如同时发布杂志和报纸),而订阅者可以选择性地订阅其中的一种或几种。 -因为这些成员对任何对象来说都是通用的,因此将它们作为独立对象的一部分提取出来是有意义的。然后,我们可以(通过混元模式)将它们复制到任何一个对象中,将这些对象转换为订阅者。 +因为这些成员对任何对象来说都是通用的,因此将它们作为一个单独的对象提取出来是有意义的。然后,我们可以(通过混元模式)将它们复制到任何一个对象中,将这些对象转换为订阅者。 -下面是这些发布者通用功能的一个示例实现,它定义了上面列出来的所有成员,还有一个辅助的visitSubscribers()方法: +下面是这些发布者通用功能的一个示例实现,它定义了上面列出来的所有成员,还有一个辅助的`visitSubscribers()`方法: var publisher = { subscribers: { - any: [] // event type: subscribers + any: [] // 对应事件类型的订阅者 }, subscribe: function (fn, type) { type = type || 'any'; @@ -1099,7 +1097,7 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 } }; -下面这个函数接受一个对象作为参数,并通过复制通用的发布者的方法将这个对象转变成发布者: +下面这个函数接受一个对象作为参数,并通过复制通用发布者的方法将这个对象转变成发布者: function makePublisher(o) { var i; @@ -1111,7 +1109,7 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 o.subscribers = {any: []}; } -现在我们来实现paper对象,它能做的事情就是发布日报和月刊: +现在我们来实现`paper`对象,它能做的事情就是发布日报和月刊: var paper = { daily: function () { @@ -1122,11 +1120,11 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 } }; -将paper对象变成发布者: +将`paper`对象变成发布者: makePublisher(paper); -现在我们有了一个发布者,让我们再来看一下订阅者对象joe,它有两个方法: +现在我们有了一个发布者,让我们再来看一下订阅者对象`joe`,它有两个方法: var joe = {
 drinkCoffee: function (paper) { @@ -1137,12 +1135,12 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 } }; -现在让joe来订阅paper: +现在让`joe`来订阅`paper`: paper.subscribe(joe.drinkCoffee); paper.subscribe(joe.sundayPreNap, 'monthly'); -如你所见,joe提供了一个当默认的any事件发生时被调用的方法,还提供了另一个当monthly事件发生时被调用的方法。现在让我们来触发一些事件: +如你所见,`joe`提供了一个当默认的`any`事件发生时被调用的方法,还提供了另一个当`monthly`事件发生时被调用的方法。现在让我们来触发一些事件: paper.daily(); paper.daily(); @@ -1156,23 +1154,23 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 Just read big news today About to fall asleep reading this interesting analysis -这里值得称道的地方就是paper对象并没有硬编码写上joe,而joe也同样没有硬编码写上paper。这里也没有知道所有事情的中介者对象。所有涉及到的对象都是松耦合的,而且在不修改代码的前提下,我们可以给paper添加更多的订阅者,同时joe也可以在任何时候取消订阅。 +这里值得称道的地方就是`paper`对象并没有硬编码写上`joe`,而`joe`也同样没有硬编码写上`paper`。这里也没有知道所有事情的中介者对象。所有涉及到的对象都是松耦合的,而且在不修改代码的前提下,我们可以给`paper`添加更多的订阅者,同时`joe`也可以在任何时候取消订阅。 -让我们更进一步,将joe也变成一个发布者。(毕竟,在博客和微博上,任何人都可以是发布者。)这样,joe变成发布者之后就可以在Twitter上更新状态: +让我们更进一步,将`joe`也变成一个发布者。(毕竟,在博客和微博上,任何人都可以是发布者。)这样,`joe`变成发布者之后就可以在Twitter上更新状态: makePublisher(joe); joe.tweet = function (msg) { this.publish(msg); }; -现在假设paper的公关部门准备通过Twitter收集读者反馈,于是它订阅了joe,提供了一个方法readTweets(): +现在假设`paper`的公关部门准备通过`Twitter`收集读者反馈,于是它订阅了`joe`,提供了一个方法`readTweets()`: paper.readTweets = function (tweet) { alert('Call big meeting! Someone ' + tweet); }; joe.subscribe(paper.readTweets); -这样每当joe发出消息时,paper就会弹出警告窗口: +这样每当`joe`发出消息时,`paper`就会弹出警告窗口: joe.tweet("hated the paper today"); @@ -1180,20 +1178,19 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 你可以在看到完整的源代码,并且在控制台中运行这个实例。 - ### 例2:按键游戏 -我们来看另一个例子。我们将实现一个和中介者模式的示例一样的按钮游戏,但这次使用观察者模式。为了让它看起来更高档,我们允许接受无限个玩家,而不限于2个。我们仍然保留用来产生玩家的Player()构造函数,也保留scoreboard对象。只有mediator会变成game对象。 +我们来看另一个例子。我们将实现一个和中介者模式的示例一样的按钮游戏,但这次使用观察者模式。为了让它看起来更高档,我们允许接受无限个玩家,而不限于2个。我们仍然保留用来产生玩家的`Player()`构造函数,也保留`scoreboard`对象,只有`mediator`会变成`game`对象。 -在中介者模式中,mediator对象知道所有涉及到的对象,并且调用它们的方法。而观察者模式中的game对象不是这样,它会让对象来订阅它们感兴趣的事件。比如,scoreboard会订阅game对象的scorechange事件。 +在中介者模式中,`mediator`对象知道所有涉及到的对象,并且调用它们的方法。而观察者模式中的`game`对象不是这样,它会让对象来订阅它们感兴趣的事件。比如,`scoreboard`会订阅`game`对象的`scorechange`事件。 -首先我们重新看一下通用的publisher对象,并且将它的接口做一点小修改以更贴近浏览器的情况: +首先我们重新看一下通用的`publisher`对象,并且将它的接口做一点小修改以更贴近浏览器的情况: -- 将publish(),subscribe(),unsubscribe()分别改为fire(),on(),remove() -- 事件的type每次都会被用到,所以把它变成三个方法的第一个参数 -- 可以给订阅者的方法额外加一个context参数,以便回调方法可以用this指向它自己所属的对象 +- 将`publish()`,`subscribe()`,`unsubscribe()`分别改为`fire()`,`on()`,`remove()` +- 事件的`type`每次都会被用到,所以把它变成三个方法的第一个参数 +- 可以给订阅者的方法额外加一个`context`参数,以便回调方法可以用`this`指向它自己所属的对象 -新的publisher对象是这样: +新的`publisher`对象是这样: var publisher = { subscribers: { @@ -1233,7 +1230,7 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 }; -新的Player()构造函数是这样: +新的`Player()`构造函数是这样: function Player(name, key) { this.points = 0; @@ -1247,11 +1244,11 @@ paper对象也可以提供unsubscribe()方法,它可以将订阅者从数组 this.fire('play', this); }; -变动的部分是这个构造函数接受key,代表这个玩家在键盘上用来按之后得分的按键。(这些键预先被硬编码过。)每次创建一个新玩家的时候,一个newplayer事件也会被触发。类似的,每次有一个玩家玩的时候,会触发play事件。 +变动的部分是这个构造函数接受`key`,代表这个玩家在键盘上用来按之后得分的按键。(这些键预先被硬编码过。)每次创建一个新玩家的时候,一个`newplayer`事件也会被触发。类似的,每次有一个玩家玩的时候,会触发`play`事件。 -scoreboard对象和原来一样,它只是简单地将当前分数显示出来。 +`scoreboard`对象和原来一样,它只是简单地将当前分数显示出来。 -game对象会关注所有的玩家,这样它就可以给出分数并且触发scorechange事件。它也会订阅浏览吕中所有的keypress事件,这样它就会知道按钮对应的玩家: +`game`对象会关注所有的玩家,这样它就可以给出分数并且触发`scorechange`事件。它也会订阅浏览器中所有的·keypress·事件,这样它就会知道按钮对应的玩家: var game = { @@ -1283,19 +1280,19 @@ game对象会关注所有的玩家,这样它就可以给出分数并且触发s } }; -用于将任意对象转变为订阅者的makePublisher()还是和之前一样。game对象会变成发布者(这样它才可以触发scorechange事件),Player.prototype也会变成发布者,以使得每个玩家对象可以触发play和newplayer事件: +用于将任意对象转变为订阅者的`makePublisher()`还是和之前一样。`game`对象会变成发布者(这样它才可以触发`scorechange`事件),`Player.prototype`也会变成发布者,以使得每个玩家对象可以触发`play`和`newplayer`事件: makePublisher(Player.prototype); makePublisher(game); -game对象订阅play和newplayer事件(以及浏览器的keypress事件),scoreboard订阅scorechange事件: +`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)时,才能通过字符串来指定回调函数。 +如你所见,`on()`方法允许订阅者通过函数(`scoreboard.update`)或者是字符串(`"addPlayer"`)来指定回调函数。当有提供`context`(如`game`)时,才能通过字符串来指定回调函数。 初始化的最后一点工作就是动态地创建玩家对象(以及它们对象的按键),用户想要多少个就可以创建多少个: @@ -1317,9 +1314,8 @@ game对象订阅play和newplayer事件(以及浏览器的keypress事件),s 这就是游戏的全部。你可以在看到完整的源代码并且试玩一下。 -值得注意的是,在中介者模式中,mediator对象必须知道所有的对象,然后在适当的时机去调用对应的方法。而这个例子中,game对象会显得笨一些(译注:指知道的信息少一些),游戏依赖于对象去观察特写的事件然后触发相应的动作:如scoreboard观察scorechange事件。这使得对象之间的耦合更松了(对象间知道彼此的信息越少越好),而代价则是弄清事件和订阅者之间的对应关系会更困难一些。在这个例子中,所有的订阅行为都发生在代码中的同一个地方,而随着应用规模的境长,on()可能会被在各个地方调用(如在每个对象的初始化代码中)。这使得调试更困难一些,因为没有一个集中的地方来看这些代码并理解正在发生什么事情。在观察者模式中,你将不再能看到那种从开头一直跟到结尾的顺序执行方式。 +值得注意的是,在中介者模式中,`mediator`对象必须知道所有的对象,然后在适当的时机去调用对应的方法。而这个例子中,`game`对象会显得笨一些(译注:指知道的信息少一些),游戏依赖于对象去观察特定的事件然后触发相应的动作:如`scoreboard`观察`scorechange`事件。这使得对象之间的耦合更松了(对象间知道彼此的信息越少越好),而代价则是弄清事件和订阅者之间的对应关系会更困难一些。在这个例子中,所有的订阅行为都发生在代码中的同一个地方,而随着应用规模的境长,`on()`可能会被在各个地方调用(如在每个对象的初始化代码中)。这使得调试更困难一些,因为没有一个集中的地方来看这些代码并理解正在发生什么事情。在观察者模式中,你将不再能看到那种从开头一直跟到结尾的顺序执行方式。 - ## 小结 在这章中你学习到了若干种流行的设计模式,并且也知道了如何在JavaScript中实现它们。我们讨论过的设计模式有: From 50bbc6a1a170445488ee9847cf6f0214f31abc09 Mon Sep 17 00:00:00 2001 From: TooBug Date: Tue, 14 May 2013 20:17:04 +0800 Subject: [PATCH 38/58] =?UTF-8?q?=E4=BA=8B=E4=BB=B6=20=E6=A0=A1=E5=AF=B9?= =?UTF-8?q?=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter8.markdown | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/chapter8.markdown b/chapter8.markdown index 154f414..352fa36 100644 --- a/chapter8.markdown +++ b/chapter8.markdown @@ -250,8 +250,6 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 > 译注: 上面的URL中的例子在IE下单击会没有反应,问题在于使用`document.attachEvernt()`时传递的第一个参数应该是`'onclick'`,而不是`'click'`。 -------校对分隔线----- - ![图8-1 事件委托示例:三个在点击时增加计数器值的按钮](./Figure/chapter8/8-1.jpg) 图8-1 事件委托示例:三个在点击时增加计数器值的按钮 @@ -264,12 +262,12 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 -你可以给包裹按钮的div绑定一个事件处理函数,而不是给每个按钮绑定一个。然后你可以使用和前面的示例中一样的myHandler()函数,但需要修改一个小地方:你需要将你不感兴趣的点击排除掉。在这个例子中,你只关注按钮上的点击,而在同一个div中产生的其它的点击应该被忽略掉。 +你可以给包裹按钮的`div`绑定一个事件处理函数,而不是给每个按钮绑定一个。然后你可以使用和前面的示例中一样的`myHandler()`函数,但需要修改一个小地方:你需要将你不感兴趣的点击排除掉。在这个例子中,你只关注按钮上的点击,而在同一个`div`中产生的其它的点击应该被忽略掉。 -myHandler()的改变就是检查事件来源的nodeName是不是“button”: +`myHandler()`的改变就是检查事件来源的`nodeName`是不是`"button"`: - // ... - // get event and source element + // …… + // 获取事件对象和事件来源 e = e || window.event; src = e.target || e.srcElement; @@ -280,7 +278,7 @@ myHandler()的改变就是检查事件来源的nodeName是不是“button”: 事件委托的坏处是筛选容器中感兴趣的事件使得代码看起来更多了,但好处是性能的提升和更干净的代码,这个好处明显大于坏处,因此这是一种强烈推荐的模式。 -主流的JavaScript库通过提供方便的API的方式使得使用事件委托变得很容易。比如YUI3中有Y.delegate()方法,它允许你指定一个用来匹配包裹容器的CSS选择器和一个用于匹配你感兴趣的节点的CSS选择器。这很方便,因为如果事件发生在你不关心的元素上时,你的事件处理回调函数不会被调用。在这种情况下,绑定一个事件处理函数很简单: +主流的JavaScript库通过提供方便的API的方式使得使用事件委托变得很容易。比如YUI3中有`Y.delegate()`方法,它允许你指定两个CSS选择器,一个用来匹配包裹容器,一个用来匹配你感兴趣的节点。这很方便,因为如果事件发生在你不关心的元素上时,你的事件处理回调函数不会被调用。在这种情况下,绑定一个事件处理函数很简单: Y.delegate('click', myHandler, "#click-wrap", "button"); From 6a9b70be0653dd0c2682813a1b19287d8214f19a Mon Sep 17 00:00:00 2001 From: TooBug Date: Tue, 14 May 2013 20:20:42 +0800 Subject: [PATCH 39/58] =?UTF-8?q?=E9=95=BF=E6=97=B6=E9=97=B4=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E7=9A=84=E8=84=9A=E6=9C=AC=20=20=E6=A0=A1=E5=AF=B9?= =?UTF-8?q?=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter8.markdown | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chapter8.markdown b/chapter8.markdown index 352fa36..626bcf9 100644 --- a/chapter8.markdown +++ b/chapter8.markdown @@ -300,11 +300,11 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 ## 长时间运行的脚本 -你可能注意到过,有时候浏览器会提示脚本运行时间过长,询问用户是否要停止执行。这种情况你当然不希望发生在自己的应用中,不管它有多复杂。 +你可能注意到过,有时候浏览器会提示脚本运行时间过长,询问用户是否要停止执行。不管应用有多复杂,你都不希望这种情况发生在自己的应用中。 同时,如果脚本运行时间太长的话,浏览器的UI将变得没有响应,用户不能点击任何东西。这是一种很差的用户体验,应该尽量避免。 -在JavaScript中没有线程,但你可以在浏览器中使用setTimeout来模拟,或者在现代浏览器中使用web workers。 +在JavaScript中没有线程,但你可以在浏览器中使用`setTimeout()`来模拟,或者在现代浏览器中使用web workers。 ### setTimeout() @@ -314,7 +314,7 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 ### Web Workers -现代浏览器为长时间运行的脚本提供了另一种解决方案:web workers。web workers在浏览器内部提供了后台线程支持,你可以将计算量很大的部分放到一个单独的文件中,比如my_web_worker.js,然后从主程序(页面)中这样调用它: +现代浏览器为长时间运行的脚本提供了另一种解决方案:web workers。web workers在浏览器内部提供了后台线程支持,你可以将计算量很大的部分放到一个单独的文件中,比如`my_web_worker.js`,然后从主程序(页面)中这样调用它: var ww = new Worker('my_web_worker.js'); ww.onmessage = function (event) { @@ -331,14 +331,14 @@ DOM操作性能不好,这是影响JavaScript性能的最主要原因。性能 while (end) { end -= 1; tmp += end; - if (end === 5e7) { // 5e7 is the half of 1e8 + 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事件处理函数来接受这些数据。 +web worker使用`postMessage()`来和调用它的程序通讯,调用者通过`onmessage`事件来接受更新。`onmessage`事件处理函数接受一个事件对象作为参数,这个对象含有一个由web worker传过来`data`属性。类似的,调用者(在这个例子中)也可以使用`ww.postMessage()`来给web worker传递数据,web worker可以通过一个`onmessage`事件处理函数来接受这些数据。 上面的例子会在浏览器中打印出: From 038808d37a83ef9cf0ce27e8c80b01e0f76fb76a Mon Sep 17 00:00:00 2001 From: TooBug Date: Tue, 14 May 2013 22:55:55 +0800 Subject: [PATCH 40/58] =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E8=84=9A=E6=9C=AC=20?= =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=AE=8C=E4=B8=80=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapter8.markdown | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/chapter8.markdown b/chapter8.markdown index 626bcf9..835e846 100644 --- a/chapter8.markdown +++ b/chapter8.markdown @@ -351,9 +351,9 @@ web worker使用`postMessage()`来和调用它的程序通讯,调用者通过` ### XMLHttpRequest -现在,XMLHttpRequest是一个特别的对象(构造函数),绝大多数浏览器都可以用,它使得我们可以从JavaScript来发送HTTP请求。发送一个请求有以下三步: +现在,`XMLHttpRequest`是一个特别的对象(构造函数),绝大多数浏览器都可以用,它使得我们可以从JavaScript来发送HTTP请求。发送一个请求有以下三步: -1. 初始化一个XMLHttpRequest对象(简称XHR) +1. 初始化一个`XMLHttpRequest`对象(简称XHR) 2. 提供一个回调函数,供请求对象状态改变时调用 3. 发送请求 @@ -363,11 +363,11 @@ web worker使用`postMessage()`来和调用它的程序通讯,调用者通过` 但是在IE7之前的版本中,XHR的功能是使用ActiveX对象实现的,所以需要做一下兼容处理。 -第二步是给readystatechange事件提供一个回调函数: +第二步是给`readystatechange`事件提供一个回调函数: xhr.onreadystatechange = handleResponse; -最后一步是使用open()和send()两个方法触发请求。open()方法用于初始化HTTP请求的方法(如GET,POST)和URL。send()方法用于传递POST的数据,如果是GET方法,则是一个空字符串。open()方法的最后一个参数用于指定这个请求是不是异步的。异步是指浏览器在等待响应的时候不会阻塞,这明显是更好的用户体验,因此除非必须要同步,否则异步参数应该使用true: +最后一步是使用`open()`和`send()`两个方法触发请求。`open()`方法用于初始化HTTP请求的方法(如GET,POST)和URL。`send()`方法用于传递POST的数据,如果是GET方法,则是一个空字符串。`open()`方法的最后一个参数用于指定这个请求是不是异步的。异步是指浏览器在等待响应的时候不会阻塞,这明显是更好的用户体验,因此除非必须要同步,否则异步参数应该使用true: xhr.open("GET", "page.html", true); xhr.send(); @@ -382,7 +382,7 @@ web worker使用`postMessage()`来和调用它的程序通讯,调用者通过` if (typeof XMLHttpRequest === "function") { // native XHR xhr = new XMLHttpRequest(); - } else { // IE before 7 + } else { // IE7以下 for (i = 0; i < activeXids.length; i += 1) { try { xhr = new ActiveXObject(activeXids[i]); @@ -406,14 +406,14 @@ web worker使用`postMessage()`来和调用它的程序通讯,调用者通过` 代码中的一些说明: -- 因为IE6及以下版本中,创建XHR对象有一点复杂,所以我们通过一个数组列出ActiveX的名字,然后遍历这个数组,使用try-catch块来尝试创建对象。 -- 回调函数会检查xhr对象的readyState属性。这个属性有0到4一共5个值,4代表“complete”(完成)。如果状态还没有完成,我们就继续等待下一次readystatechange事件。 -- 回调函数也会检查xhr对象的status属性。这个属性和HTTP状态码对应,比如200(OK)或者是404(Not found)。我们只对状态码200感兴趣,而将其它所有的都报为错误(为了简化示例,否则需要检查其它不代表出错的状态码)。 +- 因为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不同,它不受浏览器同源策略的限制,所以考虑到加载第三方站点的安全影响的问题,使用它时应该很谨慎。 +JSONP(JSON with padding)是另一种发起远程请求的方式。与XHR不同,它不受浏览器同源策略的限制,所以考虑到加载第三方站点内容的安全问题,使用它时应该很谨慎。 一个XHR请求的返回可以是任何类型的文档: @@ -422,26 +422,24 @@ JSONP(JSON with padding)是另一种发起远程请求的方式。与XHR不 - JSON数据(轻量、方便) - 简单的文本文件及其它 -使用JSONP的话,数据经常是被包裹在一个函数中的JSON,函数名称在请求的时候提供。 +而使用JSONP的话,返回的数据格式经常是被一个函数包裹的JSON,具体的函数名称在请求的时候提供。 JSONP的请求URL通常是像这样: http://example.org/getdata.php?callback=myHandler -getdata.php可以是任何类型的页面或者脚本。callback参数指定用来处理响应的JavaScript函数。 +`getdata.php`可以是任何类型的页面或者脚本。`callback`参数指定用来处理响应的JavaScript函数(译注:也就是前面提到的包裹JSON的函数)。 -这个URL会被放到一个动态生成的\元素中,像这样: +这个URL会被放到一个动态生成的` - // option 2 + // 第二种选择 但是,当你的目标是要构建一个高性能的web应用的时候,有些模式和考虑点还是应该知道的。 -作为题外话,来看一些比较常见的开发者会用在\元素上的属性: +作为题外话,来看一些比较常见的开发者会用在` - ... + …… @@ -666,7 +666,7 @@ script元素会阻塞页面的下载。浏览器会同时下载好几个组件 - ... + …… @@ -678,32 +678,32 @@ script元素会阻塞页面的下载。浏览器会同时下载好几个组件 My App - ... + …… ### HTTP分块 -HTTP协议支持“分块编码”。它允许将页面分成一块一块发送。所以如果你有一个很复杂的页面,你不需要将那些每个站都多多少少会有的(静态)头部信息也等到所有的服务端工作都完成后再开始发送。 +HTTP协议支持“分块编码”,它允许将页面分成一块一块发送。所以如果你有一个很复杂的页面,你不需要将那些(静态)头部信息也等到所有的服务端工作都完成后再开始发送。 -一个简单的策略是在组装页面其余部分的时候将页面\的内容作为第一块发送。也就是像这样子: +一个简单的策略是在组装页面其余部分的时候将页面``的内容作为第一块发送。也就是像这样子: My App - + - ... + …… - + -这种情况下可以做一个简单的发动,将JavaScript移回\,随着第一块一起发送。 +这种情况下可以做一个简单的改动,将JavaScript移回``,随着第一块一起发送。 -这样的话可以让服务器在拿到head区内容后就开始下载脚本文件,而此时页面的其它部分在服务端还尚未就绪: +这样的话可以让浏览器在拿到`head`区内容后就开始下载脚本文件,而此时页面的其它部分在服务端还尚未就绪: @@ -711,13 +711,13 @@ HTTP协议支持“分块编码”。它允许将页面分成一块一块发送 My App - + - ... + …… - + -一个更好的办法是使用第三块内容,让它在页面尾部,只包含脚本。如果有一些每个页面都用到的静态的头部,也可以将这部分随和一块一起发送: +一个更好的办法是使用第三块内容,让它在页面尾部,只包含脚本。如果有一些每个页面都用到的静态的头部,也可以将这部分随第一块一起发送: @@ -727,35 +727,35 @@ HTTP协议支持“分块编码”。它允许将页面分成一块一块发送 ... - + ... The full body of the page ... - + - + 这种方法很适合使用渐进增强思想的网站(关键业务不依赖JavaScript)。当HTML的第二块发送完毕的时候,浏览器已经有了一个加载、显示完毕并且可用的页面,就像禁用JavaScript时的情况。当JavaScript随着第三块到达时,它会进一步增强页面,为页面锦上添花。 -### 动态\元素实现非阻塞下载 +### 动态` - // becomes: + // 修改后的: - + 对很多应用来说,延迟加载的部分大部分情况下会比核心部分要大,因为我们关注的“行为”(比如拖放、XHR、动画)只在用户初始化之后才会发生。 @@ -841,44 +841,44 @@ frist_script是页面中一定存在的一个script标签,script是你创建 假设你页面的侧边栏上有一些tabs。点击tab会发出一个XHR请求获取内容,然后更新tab的内容,然后有一个更新的动画。如果这是页面上唯一需要XHR和动画库的地方,而用户又不点击tab的话会怎样? -下面介绍按需加载模式。你可以创建一个require()函数或者方法,它接受一个需要被加载的脚本文件的文件名,还有一个在脚本被加载完毕后执行的回调函数。 +下面介绍按需加载模式。你可以创建一个`require()`函数或者方法,它接受一个需要被加载的脚本文件的文件名,还有一个在脚本被加载完毕后执行的回调函数。 -require()函数可以被这样使用: +`require()`函数可以被这样使用: require("extra.js", function () { functionDefinedInExtraJS(); }); -我们来看一下如何实现这样一个函数。加载脚本很简单——你只需要按照动态\元素模式做就可以了。获知脚本已经加载需要一点点技巧,因为浏览器之间有差异: +我们来看一下如何实现这样一个函数。加载脚本很简单——你只需要按照动态`