Another RayJune

JavaScript Patterns 小记

偷懒是程序员的优良品质模式则是先人们总结的偷懒招式

Introduction

Patterns

In software development, a pattern is a solution to a common problem. A pattern is not necessarily a code solution ready for copy-and-paste but more of a best practice, a useful abstraction, and a template for solving categories of problems.
在软件开发领域,模式是指常见问题的通用解决方案。模式不是简单的代码复制和粘贴,而是一种最佳实践一种高级抽象是解决某一类问题的范本

This book discusses the following types of patterns:

  • Design patterns
  • Coding patterns (main part)
  • Antipatterns

四人帮所创造的设计模式们更适用于具有基于语言的强类型特性和类的继承的强类型语言(如 C++、Java 等 ),不适合于弱类型语言 JS,JS 要实现这些设计模式需要一些轻型的替代方案。并且:

The coding patterns are much more interesting; they are JavaScript-specific patterns and good practices related to the unique features of the language, such as the various uses of functions. JavaScript coding patterns are the main topic of the book.

One of the general rules in the Gang of Four book says, “Prefer object composition to class inheritance.”

Prototypes

JavaScript does have inheritance, although this is just one way to reuse code.

Inheritance can be accomplished in various ways, which usually make use of prototypes.

Essentials

Writing Maintainable Code

Maintainable code means code that:

  • readable
  • consistent 一致的
  • predictable
  • Looks as if it was written by the same person
  • documented 有文档的

Minimizing Globals

Single var Pattern (deprecated)

RayJune 注: 但这种单 var 模式现在并不推荐,因为容易将 ‘,’ 和 ‘;’ 打错,而不容易发现错误。

Hoisting: A Problem with Scattered vars

这里有必要对 “变量提前” 作进一步补充,实际上从 JavaScript 引擎的工作机制上看,这个过程稍微有点复杂。

代码处理经过了两个阶段:

  • 第一阶段是创建变量、函数和参数,这一步是预编译的过程,它会扫描整段代码的上下文。
  • 第二阶段是代码的运行,这一阶段将创建函数表达式和一些非法的标识符(未声明的变量)

But for practical purposes, we can adopt the concept of hoisting, which is actually not defined by ECMAScript standard but is commonly used to describe the behavior.
从实用性角度来讲,我们更愿意将这两个阶段归成一个概念 “变量提前”,尽管这个概念并没有在 ECMAScript 标准中定义,但我们常常用它来解释预编译的行为过程。

for Loops: for 循环

In for loops you iterate over arrays or array-like objects such as arguments and HTMLCollection objects. The usual for loop pattern looks like the following:

1
2
3
for (var i = 0; i < myarray.length; i++) {
// do something with myarray[i]
}

A problem with this pattern is that the length of the array is accessed on every loop iteration. This can slow down your code, especially when myarray is not an array but an HTMLCollection object.

The trouble with collections is that they are live queries against the underlying document (the HTML page). This means that every time you access any collection’s length, you’re querying the live DOM, and DOM operations are expensive in general.
这些对象的问题在于,它们均是指向文档(HTML 页面)中的活动对象。也就是说每次通过它们访问集合的 length 时,总是会去查询 DOM,而 DOM 操作则是很耗资源的。

That’s why a better pattern for for loops is to cache the length of the array (or collection) you’re iterating over, as shown in the following example:

1
2
3
for (var i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}

Following the single var pattern, you can also take the var out of the loop and make the loop like:

1
2
3
4
5
6
var i = 0,
max,
myarray = []; // ...
for (var i = 0, max = myarray.length; i < max; i++) {
// do something with myarray[i]
}

This pattern has the benefit of consistency because you stick to the single var pattern. A drawback is that it makes it a little harder to copy and paste whole loops while refactoring code. For example, if you’re copying the loop from one function to another,you have to make sure you also carry over i and max into the new function (and probably delete them from the original function if they are no longer needed there).
这种模式带来的好处就是提高了代码的一致性,因为你越来越依赖这种单 var 模式。缺点就是在重构代码的时候不能直接复制粘贴一个循环体,比如,你正在将某个循环从一个函数拷贝至另外一个函数中,必须确保 i 和 max 也拷贝至新函数里,并且需要从旧函数中将这些没用的变量删除掉

Two variations of the for pattern introduce some micro-optimizations because they:

  • Use one less variable (no max) 减少一个变量(没有 max)
  • Count down to 0, which is usually faster because it’s more efficient to compare to 0 than to the length of the array or to anything other than 0 减量循环至 0,这种方式速度更快,因为和零比较要比和非零数字或数组长度比较要高效的多

The first modified pattern is:

1
2
3
4
var i, myarray = [];
for (i = myarray.length; i--;) {
// do something with myarray[i]
}

And the second uses a while loop:

1
2
3
4
var myarray = [],
i = myarray.length;
while (i--) {
// do something with myarray[i] }

While, these are micro-optimizations and will only be noticed in performance-critical operations.

(Not) Augmenting Built-in Prototypes:(不)扩充内置原型

Augmenting the prototype property of constructor functions is a powerful way to add functionality, but it can be too powerful sometimes.

It’s tempting to augment prototypes of built-in constructors such as Object(), Array(), or Function(), but it can seriously hurt maintainability, because it will make your code less predictable.
这种做法严重降低了代码的可维护性,因为它让你的代码变得难以预测

Additionally, properties you add to the prototype may show up in loops that don’t use hasOwnProperty(), so they can create confusion.
如果将属性添加至原型中,很可能导致在那些不使用 hasOwnProperty() 做检测的循环中将原型上的属性遍历出来,这会造成混乱。

Therefore it’s best if you don’t augment built-in prototypes. You can make an exception of the rule only when all these conditions are met:

  1. It’s expected that future ECMAScript versions or JavaScript implementations will implement this functionality as a built-in method consistently. For example, you can add methods described in ECMAScript 5 while waiting for the browsers to catch up. In this case you’re just defining the useful methods ahead of time. 未来的 ECMAScript 版本的 JavaScirpt 会将你实现的方法添加为内置方法。比如,你可以实现 ECMAScript5 定义的一些方法,一直等到浏览器升级至支持 ES5。这样,你只是提前定义了这些有用的方法。
  2. You check if your custom property or method doesn’t exist already—maybe already implemented somewhere else in the code or already part of the JavaScript engine of one of the browsers you support. 如果你发现你自定义的方法已经不存在,要么已经在代码其他地方实现了,要么是浏览器的 JavaScript 引擎已经内置实现了。
  3. You clearly document and communicate the change with the team. 你所做的扩充附带充分的文档说明,且和团队其他成员做了沟通。

If these three conditions are met, you can proceed with the custom addition to the prototype, following this pattern:

1
2
3
4
5
if (typeof Object.protoype.myMethod !== "function") {
Object.protoype.myMethod = function () {
// implementation...
};
}

Avoiding Implied Typecasting: 避免隐式类型转换

Avoiding eval(): 避免使用 eval()

It’s also important to remember that passing strings to setInterval(), setTimeout(), and the Function() constructor is, for the most part, similar to using eval() and there- fore should be avoided. Behind the scenes, JavaScript still has to evaluate and execute the string you pass as programming code:
记住,多数情况下,给 setInterval()、setTimeout() 和 Function() 构造函数传入字符串的情形和 eval() 类似,这种用法也是应当避免的,这一点非常重要,因为这些情形中 JavaScript 最终还是会执行传入的字符串参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myFunc(x) {
console.log(x);
}

// antipatterns
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);

// preferred
setTimeout(myFunc, 1000);
setTimeout(myFunc(1), 1000);
setTimeout(function () {
myFunc(1, 2, 3);
}, 1000);

Number Conversions with parseInt(): 使用 parseInt() 进行数字转换

Using parseInt() you can get a numeric value from a string. The function accepts a second radix parameter, which is often omitted but shouldn’t be. The problems occur when the string to parse starts with 0: for example, a part of a date entered into a form field. Strings that start with 0 are treated as octal numbers (base 8) in ECMAScript 3; however, this has changed in ES5. To avoid inconsistency and unexpected results, al- ways specify the radix parameter:

RayJune 注:现在的浏览器全面升级 ES5,即不必担心这个 0 开头的数字会被转化为 8 进制的问题。

1
2
3
4
var month = "06",
year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);

In this example, if you omit the radix parameter like parseInt(year), the returned value will be 0, because “09” assumes octal number (as if you did parseInt(year, 8)) and 09 is not a valid digit in base 8.

Alternative ways to convert a string to a number include:

1
2
+"08" // result is 8 
Number("08") // 8

These are often faster than parseInt(), because parseInt(), as the name suggests, parses and doesn’t simply convert. But if you’re expecting input such as “08 hello”, parseInt() will return a number, whereas the others will fail with NaN.
这两种方法要比 parseInt() 更快一些,因为顾名思义 parseInt() 是一种 “解析” 而不是简单的 “转换”。但当你期望将 “08 hello” 这类字符串转换为数字,则必须使用 parseInt(),其他方法都会返回 NaN

Coding Conventions: 编码风格

It’s important to establish and follow coding conventions—they make your code consistent, predictable, and much easier to read and understand. A new developer joining the team can read through the conventions and be productive much sooner, understanding the code written by any other team member.
确立并遵守编码规范非常重要,这会让你的代码风格一致、可预测、可读性更强。团队新成员通过学习编码规范可以很快进入开发状态、并写出团队其他成员易于理解的代码。

Many flamewars have been fought in meetings and on mailing lists over specific aspects of certain coding conventions (for example, the code indentation—tabs or spaces?). So if you’re the one suggesting the adoption of conventions in your organization, be pre- pared to face resistance and hear different but equally strong opinions. Remember that it’s much more important to establish and consistently follow a convention, any convention, than what the exact details of that convention will be.
在开源社区和邮件组中关于编码风格的争论一直不断(比如关于代码缩进, 用 tab 还是空格?)。因此,如果你打算在团队内推行某种编码规范时,要做好应对各种反对意见的心理准备,而且要吸取各种意见,这对确立并一贯遵守某种编码规范是非常重要,而不是斤斤计较的纠结于编码规范的细节。

Opening Brace Location: 左花括号的位置

There are cases in which the program might behave differently depending on where the brace is. This is because of the semicolon insertion mechanism—JavaScript is not picky when you choose not to end your lines properly with a semicolon and adds it for you. This behavior can cause troubles when a function returns an object literal and the opening brace is on the next line:
有时候花括号位置的不同则会影响程序的执行。因为 JavaScript 会 “自动插入分号”。 JavaScript 对行结束时的分号并无要求,它会自动将分号补全。因此,当函数 return 语句返回了一个对象直接量,而对象的左花括号和 return 不在同一行时,程序的执行就和预想的不同了:

1
2
3
4
5
6
7
// warning: unexpected return value function 
func() {
return
{
name: "Batman"
};
}

The preceding code is equivalent to this one:

1
2
3
4
5
6
7
8
// warning: unexpected return value
function func() {
return undefined;
// unreachable code follows...
{
name: "Batman"
};
}

In conclusion, always use curly braces and always put the opening one on the same line as the previous statement.

1
2
3
4
5
function func() {
return {
name: "Batman"
};
}

White Space: 空格

Good places to use a white space include:

  • After the semicolons that separate the parts of a for loop: for example, for (var i = 0; i < 10; i += 1) {…} for 循环的分号之后
  • Initializing multiple variables (i and max) in a for loop: for (var i = 0, max = 10; i < max; i += 1) {…} for 循环中初始化多个变量
  • After the commas that delimit array items: var a = [1, 2, 3]; 分割数组项的逗号之后
  • After commas in object properties and after colons that divide property names and their values: var o = {a: 1, b: 2}; 对象属性后的逗号以及键值对之间的冒号之后
  • Delimiting function arguments: myFunc(a, b, c) 对象参数中
  • Before the curly braces in function declarations: function myFunc() {} 函数声明的花括号之前
  • After function in anonymous function expressions: var myFunc = function () {}; 匿名函数表达式 function 之后

Another good use for white space is to separate all operators and their operands with spaces, which basically means use a space before and after +, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=, and so on.

Naming Conventions: 命名规范

Capitalizing Constructors: 构造函数命名要首字母大写

1
var adam = new Person();

Other Naming Patterns: 其他命名风格

Sometimes developers use a naming convention to make up or substitute language features.
有时开发人员使用命名规范来弥补或代替语言特性的不足。

For example, there is no way to define constants in JavaScript (although there are some built-in such as Number.MAX_VALUE), so developers have adopted the convention of using all-caps for naming variables that shouldn’t change values during the life of the pro- gram, like:
比如,JavaScript 中无法定义常量(尽管有一些内置常量比如 Number.MAX_VALUE),所以开发者都采用了这种命名习惯,对于那些程序运行周期内不会更改的变量使用全大写字母来命名。比如:

1
2
3
// precious constants, please don't touch 
var PI = 3.14,
MAX_WIDTH = 800;

There’s another convention that competes for the use of all caps: using capital letters for names of global variables. Naming globals with all caps can reinforce the practice of minimizing their number and can make them easily distinguishable.
除了使用大写字母的命名方式之外,还有另一种命名规约:全局变量都大写。这种命名方式和 “减少全局变量” 的约定相辅相成,并让全局变量很容易辨认

Another case of using a convention to mimic functionality is the private members convention. Although you can implement true privacy in JavaScript, sometimes developers find it easier to just use an underscore prefix to denote a private method or property. Consider the following example:
除了常量和全局变量的命名惯例,这里讨论另外一种命名惯例,即私有变量的命名。尽管在 JavaScript 是可以实现真正的私有变量的,但开发人员更喜欢在私有成员或方法名之前加上下划线前缀,比如下面的例子:

1
2
3
4
5
6
7
8
9
10
var person = {
getName: function () {
return this._getFirst() + ' ' + this._getLast(); },
_getFirst: function () {
// ...
},
_getLast: function () {
// ...
}
};

Minify…In Production: 生产环境中的代码压缩(Minify)

The code you write will be read (by humans), so make it easy for the maintainer to understand it quickly and let the minifier (the machine) take care of reducing the file sizes.
你写的代码是需要被人阅读的,所以应当将注意力放在代码可读性和可维护性上,代码压缩的工作交给工具去完成。

Literals and Constructors: 字面量和构造函数

Object Literal: 对象字面量

When you think about objects in JavaScript, simply think about hash tables of key-value pairs (similar to what are called “associative arrays” in other languages).
我们可以将 JavaScript 中的对象简单的理解为键值对组成的散列表(hash table),在其他编程语言中被称作 “关联数组”。

The custom objects you create in JavaScript (in other words, the user-defined native objects) are mutable at any time. Many of the properties of the built-in native objects are also mutable. You can start with a blank object and add functionality to it as you go. The object literal notation is ideal for this type of on-demand object creation.
JavaScript 中自定义的对象(用户定义的本地对象)任何时候都是可变的。内置本地对象的属性也是可变的。你可以先创建一个空对象,然后在需要时给它添加功能。“对象字面量写法(object literal notation)” 是按需创建对象的一种理想方式。

1
2
3
4
5
6
7
8
9
10
// start with an empty object 
var dog = {};

// add one property
dog.name = "Benji";

// now add a method
dog.getName = function () {
return dog.name;
};

(*)Objects from a Constructor: 通过构造函数创建对象

1
2
3
4
5
6
7
// one way -- using a literal 
var car = {goes: "far"};

// another way -- using a built-in constructor
// warning: this is an antipattern
var car = new Object();
car.goes = "far";

As you can see from this example, an obvious benefit of the literal notation is that it’s shorter to type. Another reason why the literal is the preferred pattern for object creation is that it emphasizes that objects are simply mutable hashes and not something that needs to be baked from a “recipe” (from a class).
字面量写法的一个明显优势是,它的代码更少。“创建对象的最佳模式是使用字面量” 还有一个原因,它可以强调对象就是一个简单的可变的散列表而不必一定派生自某个类

Another reason for using literals as opposed to the Object constructor is that there is no scope resolution. Because it’s possible that you have created a local constructor with the same name, the interpreter needs to look up the scope chain from the place you are calling Object() all the way up until it finds the global Object constructor.
另外一个使用字面量而不是 Object 构造函数创建实例对象的原因是,对象字面量不需要 “作用域解析”(scope resolution)。因为新创建的实例有可能包含了一个本地的构造函数,当你调用 Object() 的时候,解析器需要顺着作用域链从当前作用域开始查找,直到找到全局 Object 构造函数为止

Object Constructor Catch: 获得对象的构造器

You have no reason to use the new Object() constructor when you can use an object literal, but you might be inheriting legacy code written by others, so you should be aware of one “feature” of this constructor (or yet another reason not to use it). The feature in question is that the Object() constructor accepts a parameter and, depending on the value passed, it may decide to delegate the object creation to another built-in constructor and return a different object than you expect.
创建实例对象时能用对象直接量就不要使用 new Object() 构造函数,但有时你希望能继承别人写的代码,这时就需要了解构造函数的一个 “特性”(也是不使用它的另一个原因),就是 Object() 构造函数可以接收参数,通过参数的设置可以把实例对象的创建委托给另一个内置构造函数,并返回另外一个实例对象,而这往往不是你所希望的。

Following are a few examples of passing a number, a string, and a boolean value to new Object(); the result is that you get objects created with a different constructor:
下面的示例代码中展示了给 new Object() 传入不同的参数:数字、字符串和布尔值,最终得到的对象都是由不同的构造函数生成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Warning: antipatterns ahead
// an empty object
var o = new Object();
console.log(o.constructor === Object); // true

// a number object
var o = new Object(1);
console.log(o.constructor === Number); // true

// a string object
var o = new Object("I am a string");
console.log(o.constructor === String); // true

// a boolean object
var o = new Object(true);
console.log(o.constructor === Boolean); // true

This behavior of the Object() constructor can lead to unexpected results when the value you pass to it is dynamic and not known until runtime. Again, in conclusion, don’t use new Object(); use the simpler and reliable object literal instead.
Object() 构造函数的这种特性会导致一些意想不到的结果,特别是当参数不确定的时候。最后再次提醒不要使用 new Object(),尽可能的使用对象字面量来创建实例对象

(*)Custom Constructor Functions

In addition to the object literal pattern and the built-in constructor functions, you can create objects using your own custom constructor functions, as the following example demonstrates:
除了对象直接量和内置构造函数之外,你也可以通过自定义的构造函数来创建实例对象

1
2
var adam = new Person("Adam"); 
adam.say(); // "I am Adam"

This new pattern looks very much like creating an object in Java using a class called Person. The syntax is similar, but actually in JavaScript there are no classes and Person is just a function.
这里用了 “类”Person 创建了实例,这种写法看起来很像 Java 中的实例创建。两者的语法的确非常接近,但实际上 JavaScript 中没有类的概念Person 是一个函数

1
2
3
4
5
6
var Person = function (name) {
this.name = name;
this.say = function () {
return 'I am ' + this.name
}
};

When you invoke the constructor function with new, the following happens inside the function:

  • An empty object is created and referenced by this variable, inheriting the prototype of the function. 创建一个空对象,将它的引用赋给 this,继承函数的原型(prototype)
  • Properties and methods are added to the object referenced by this. 通过 this 将属性和方法添加至这个对象
  • The newly created object referenced by this is returned at the end implicitly (if no other object was returned explicitly). 最后隐式返回 this 指向的新对象(如果没有手动返回其他的对象的话)

It’s as if something like this happens behind the scenes:

注:这个实例写的非常漂亮(尤其是注释)~!

1
2
3
4
5
6
7
8
9
10
11
var Person = function (name) {
// create a new object
// using the object literal
// var this = {};

// add properties and methods this.name = name;
this.say = function () {
return "I am " + this.name;
};
// return this;
};

For simplicity in this example, the say() method was added to this. The result is that any time you call new Person() a new function is created in memory. This is obviously inefficient, because the say() method doesn’t change from one instance to the next. The better option is to add the method to the prototype of Person:
正如这段代码所示,say() 方法添加至 this 中,结果是,不论何时调用 new Person(),在内存中都会创建一个新函数。显然这是效率很低的,因为所有实例的 say() 方法是一模一样的,因此没有必要“拷贝”多份。最好的办法是将方法添加至 Person 的原型中。

1
2
3
Person.prototype.say = function () {
return "I am " + this.name;
};

We’ll talk more about prototypes and inheritance in the chapters to come, but just remember that reusable members, such as methods, should go to the prototype.
我们将会在下一章里详细讨论原型和继承。现在只要记住将需要重用的成员和方法放在原型里即可。

There is one more thing that will become clear later in the book, but it is worth mentioning here for the sake of completeness. We said that inside of the constructor some- thing like this happens behind the scenes:
关于构造函数的内部工作机制也会在后续章节中有更细致的讨论。这里我们只做概要的介绍。刚才提到,构造函数执行的时候,首先创建一个新对象,并将它的引用赋给 this:

1
// var this = {};

That’s not the whole truth, because the “empty” object is not actually empty; it has inherited from the Person’s prototype. So it’s more like:
事实并不完全是这样,因为 “空” 对象并不是真的空,这个对象继承了 Person 的原型,看起来更像:

1
// var this = Object.create(Person.prototype);

We’ll discuss what Object.create() means later in the book.

(*)Constructor’s Return Values: 构造函数的返回值

When invoked with new, a constructor function always returns an object; by default it’s the object referred to by this. If you don’t add any properties to this inside of your constructor, an “empty” object is returned (“empty” aside from inheriting from the constructor’s prototype).
用 new 调用的构造函数总是会返回一个对象,默认返回 this 所指向的对象。 如果构造函数内没有给 this 赋任何属性,则返回一个 “空” 对象(除了继承构造函数的原型之外,没有 “自己的” 属性)。

Constructors implicitly return this, even when you don’t have a return statement in the function. But you can return any other object of your choosing. In the next example, a new object referenced by that is created and returned.
尽管我们不会在构造函数内写 return 语句,也会隐式返回 this。但我们是可以返回任意指定的对象的,在下面的例子中就返回了新创建的 that 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
var Objectmaker = function () {
// this *name* property will be ignored
// because the constructor
// decides to return another object instead
this.name = "This is it";
// creating and returning a new object
var that = {};
that.name = "And that's that";
return that;
};
// test
var o = new Objectmaker();
console.log(o.name); // "And that's that"

As can you see, you have the freedom to return any object in your constructors, as long as it’s an object. Attempting to return something that’s not an object (like a string or a boolean false, for example) will not cause an error but will simply be ignored, and the object referenced by this will be returned instead.
我们看到,构造函数中其实是可以返回任意对象的,只要你返回的东西是对象即可。如果返回值不是对象(字符串、数字或布尔值),程序不会报错,但这个返回值被忽略,最终还是返回 this 所指的对象

Patterns for Enforcing new: 强制使用 new 的模式

As mentioned already, constructors are still just functions but invoked with new. What happens if you forget new when you invoke a constructor? This is not going to cause syntax or runtime errors but might lead to logical errors and unexpected behavior. That’s because when you forget new, this inside the constructor will point to the global object. (In browsers this will point to window.)
当构造函数内包含 this.member 之类的代码,并直接调用这个函数(省略 new), 实际会创建一个全局对象的属性 member,可以通过 window.member 或 member 访问到它。这必然不是我们想要的结果,因为我们要努力确保全局命名空间 的整洁干净。

Naming Convention: 命名约定

The simplest alternative is to use a naming convention, as discussed in the previous chapter, where you uppercase the first letter in constructor names (MyConstructor) and lowercase it in “normal” functions and methods (myFunction).
最简单的选择是使用命名约定,前面的章节已经提到,构造函数名首字母大写(MyConstructor),普通函数和方法名首字母小写(myFunction)。

Using that: 使用 that

1
2
3
4
5
6
7
function Waffle() {
var that = {};

that.tastes = "yummy";

return that;
}

For simpler objects, you don’t even need a local variable such as that; you can simply return an object from a literal like so:
如果要创建简单的实例对象,甚至不需要定义一个局部变量 that,可以直接返回一个对象直接量,就像这样:

1
2
3
4
5
function Waffle() {
return {
tastes: "yummy"
};
}

Using any of the implementations above Waffle() always returns an object, regardless of how it’s called:
不管用什么方式调用它(使用 new 或直接调用),它同都会返回一个实例对象:

1
2
3
4
var first = new Waffle(),
second = Waffle();
console.log(first.tastes); // "yummy"
console.log(second.tastes); // "yummy"

The problem with this pattern is that the link to the prototype is lost, so any members you add to the Waffle() prototype will not be available to the objects.
这种模式的问题是丢失了原型,因此在 Waffle() 的原型上的成员不会继承到这些实例对象中

Note that the variable name that is just a convention; it’s not a part of the language. You can use any name, where other common variable names include self and me.
需要注意的是,这里用的 that 只是一种命名约定,that 不是语言的保留字,可以将它替换为任何你喜欢的名字,比如 self 或 me。

(*)Self-Invoking Constructor: 调用自身的构造函数

To address the drawback of the previous pattern and have prototype properties available to the instance objects, consider the following approach. In the constructor you check whether this is an instance of your constructor, and if not, the constructor invokes itself again, this time properly with new:
为了解决上述模式的问题,能够让实例对象继承原型属性,我们使用下面的方法。在构造函数中首先检查 this 是否是构造函数的实例,如果不是,再通过 new 调用构造函数,并将 new 的结果返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Wallfe() {
if (!(this instanceof Waffle)) { // instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性
return new Wallfe();
}
this.tastes = 'yummy';
}
Waffle.prototype.wantAnother = true;

// testing invocations
var first = new Waffle(),
second = Waffle();

console.log(first.tastes); // "yummy"
console.log(second.tastes); // "yummy"

console.log(first.wantAnother); // true
console.log(second.wantAnother); // true

Another general-purpose way to check the instance is arguments.callee instead of hard-coding the constructor name.

1
2
3
if (!(this instanceof arguments.callee)) {
return new arguments.callee();
}

This pattern uses the fact that inside every function, an object called arguments is created containing all parameters passed to the function when it was invoked. And arguments has a property called callee, which points back to the function that was called. Be aware that arguments.callee is not allowed in ES5’s strict mode, so it’s best if you limit its future use and also remove any instances should you find them in existing code.
这里需要说明的是,在任何函数内部都会自行创建一个 arguments 对象,它包含函数调用时传入的参数。同时arguments 包含一个 callee 属性,指向它所在的正在被调用的函数。需要注意,ES5 严格模式中是禁止使用 arguments.callee 的,因此最好对它的使用加以限制,并删除任何你能在代码中找到的实例 。

Array Constructor Curiousness: 有意思的数组构造器

One more reason to stay away from new Array() is to avoid a possible trap that this constructor has in store for you.

When you pass a single number to the Array() constructor, it doesn’t become the value of the first array element. It sets the length of the array instead. This means that new Array(3) creates an array with length of 3, but no actual elements. If you try to access any of the elements, you get the value undefined because the elements don’t exist. The following code example shows the different behavior when you use the literal and the constructor with a single value.
如果给 Array() 构造器传入一个数字,这个数字并不会成为数组的第一个元素,而是设置数组的长度。也就是说,new Array(3) 创建了一个长度为 3 的数组,而不是某个元素是 3。如果你访问数组的任意元素都会得到 undefined, 因为元素并不存在。下面示例代码展示了直接量和构造函数的区别:

1
2
3
4
5
6
7
8
9
// an array of one element 
var a = [3];
console.log(a.length); // 1
console.log(a[0]); // 3

// an array of three elements
var a = new Array(3);
console.log(a.length); // 3
console.log(typeof a[0]); // "undefined"

Although this behavior might be a little unexpected, it gets worse when you pass a floating point number to new Array() as opposed to an integer. This results in an error because the floating point is not a valid value for the array’s length:

1
2
3
4
5
6
// using array literal
var a = [3.14];
console.log(a[0]); // 3.14

var a = new Array(3.14); // RangeError: invalid array length
console.log(typeof a); // "undefined"

To avoid potential errors when creating dynamic arrays at runtime, it’s much safer to stick with the array literal notation.

There are some clever uses of the Array() constructor though, for example, for repeating strings. The following snippet returns a string with 255 white spaces:
有些人用 Array() 构造器来做一些有意思的事情,比如用来生成重复字符串。 下面这行代码返字符串包含 255 个空格:

1
var white = new Array(256).join(' ');

JSON

JSON stands for JavaScript Object Notation and is a data transfer format. It’s lightweight and convenient to work with in many languages, especially in JavaScript.
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。很多语言中都实现了 JSON,特别是在 JavaScript 中。

There’s actually nothing new to learn about JSON. It’s only a combination of the array and the object literal notation. Here’s an example of a JSON string:
JSON 格式及其简单,它只是数组和对象直接量的混合写法。

1
2
3
4
{
"name": "value",
"some": [1, 2, 3]
}

The only syntax difference between JSON and the object literal is that property names need to be wrapped in quotes to be valid JSON. In object literals the quotes are required only when the property names are not valid identifiers, for example, they have spaces {“first name”: “Dave”}.
JSON 和对象直接量在语法上的唯一区别是,合法的 JSON 属性名均用引号包含。而在对象直接量中,只有属性名是非法的标识符时采用引号包含,比如,属性名中包含空格 {"first name": "Dave"}

In JSON strings you cannot use functions or regular expression literals.
在 JSON 字符串中,不能使用函数和正则表达式字面量

JSON.parse(): 解析 JSON 数据为对象或数组
JSON.stringify(): 将对象或数组(或任 何原始值)转换为 JSON 字符串。

Regular Expression Literal: 正则表达式直接量

As you can see, the regular expression literal notation is shorter and doesn’t force you to think in terms of class-like constructors. Therefore it’s preferable to use the literal.
显然正则表达式直接量写法的代码更短,且不必强制按照类构造器的思路来写。因此更推荐使用字面量写法

另外,如果使用 RegExp() 构造函数写法,还需要考虑对引号和反斜杠进行转义

Primitive Wrappers: 原始值的包装对象

The wrapper objects have some useful properties and methods—for example, number objects have methods such as toFixed() and toExponential(). String objects have substring(), charAt(), and toLowerCase() methods (among others) and a length prop- erty. These methods are convenient and can be a good reason to decide to create an object, as opposed to using a primitive. But the methods work on primitives, too—as soon as you invoke a method, the primitive is temporarily converted to an object behind the scenes and behaves as if it were an object.
包装对象带有一些有用的属性和方法,比如,数字对象就带有 toFixed() 和 toExponential() 之类的方法。字符串对象带有 substring()、chatAt() 和
toLowerCase() 等方法以及 length 属性。这些方法非常方便,和原始值相比,这让包装对象具备了一定优势。其实原始值也可以调用这些方法,因为原始值会首先转换为一个临时对象,如果转换成功,则调用包装对象的方法

所以,不建议用原始值的包装对象方法来定义 Number(), String(), and Boolean()

Error Objects

字面量声明和构造函数声明没有区别。因为字面量声明更简单所以推荐。

Date

In general, with the exception of the Date() constructor, there’s rarely a need to use the other built-in constructors.
通常除了 Date() 构造函数之外,其他的内置构造函数并不常用

Functions

(*)Background

There are two main features of the functions in JavaScript that make them special:
JavaScript 的函数具有两个主要特性,正是这两个特性让它们与众不同。

  • 函数是一等对象(first-class object)
  • 函数提供作用域支持

Disambiguation of Terminology: 术语释义

Let’s take a moment to discuss the terminology surrounding the code used to define a function, because using accurate and agreed-upon names is just as important as the code when talking about patterns.
首先我们先简单讨论下创建函数相关的术语,因为精确无歧义的术语约定和我们所讨论的各种模式一样重要。

1
2
3
4
// named function expression 
var add = function add(a, b) {
return a + b;
};

If you skip the name (the second add in the example) in the function expression:

1
2
3
4
// function expression, a.k.a. anonymous function 
var add = function (a, b) {
return a + b;
};

The only difference is that the name property of the function object will be a blank string. The name property is an extension of the language (it’s not part of the ECMA standard) but widely available in many environments. If you keep the second add, then the property add.name will contain the string “add.” The name property is useful when using debuggers.
带名字和不带名字唯一的区别是函数对象的 name 属性是否是一个空字符串。name 属性属于语言的扩展(未在 ECMA 标准中定义),但很多环境都实现了。如果不省略第二个 add,那么属性 add.name 则 是 “add”.

1
2
3
4
// function declarations 函数声明
function foo() {
// function body goes here
}

There’s syntax difference between the two in the trailing semicolon. The semicolon is not needed in function declarations but is required in function expressions.
两种语法的一个区别是末尾的分号。函数声明末尾不需要分号,而函数表达式末尾是需要分号的。

Function Hoisting: 函数提前

From the previous discussion you may conclude that the behavior of function declarations is pretty much equivalent to a named function expression. That’s not exactly true, and a difference lies in the hoisting behavior.
通过前面的讲解,你可能以为函数声明和带名字的函数表达式是完全等价的。事实上不是这样,主要区别在于 “声明提前” 的行为

The term hoisting is not defined in ECMAScript, but it’s common and a good way to describe the behavior.
术语 “提前” 并未在 ECMAScript 中定义,但是并没有其他更好的方法来描述这种行为了。

函数声明定义的函数不仅能让声明提前,还能让定义提前但函数表达式则不能提前

一句话总结:函数声明提升了整个函数到函数内部的顶部(预编译时期),而函数表达式只是提升了声明,定义并没有提升。

Timeouts

Another example of the callback pattern in the wild is when you use the timeout methods provided by the browser’s window object: setTimeout() and setInterval(). These methods also accept and execute callbacks:
另外一个最常用的回调模式是在调用超时函数时,超时函数是浏览器 window 对象的方法,共有两个:setTimeout() 和 setInterval()。这两个方法的参数都是回调函数。

1
2
3
4
5
6
7
8
var thePlotThickens = function () {
console.log('500ms later...');
};

var a = setTimeout(thePlotThickens, 500);

// optional
clearTimeout(a);

Note again how the function thePlotThickens is passed as a variable, without paren- theses, because you don’t want it executed right away, but simply want to point to it for later use by setTimeout(). Passing the string “thePlotThickens()” instead of a function pointer is a common antipattern similar to eval().
再次需要注意,函数 thePlotThickens 是作为变量传入 setTimeout 的,它不带括号,如果带括号的话则立即执行了,这里只是用到这个函数的引用,以 便在 setTimeout 的逻辑中调用到它。也可以传入字符串 “thePlotThickens()”,但这是一种反模式,和 eval() 一样不推荐使用。

Closure & function

Because setup() wraps the returned function, it creates a closure, and you can use this closure to store some private data, which is accessible by the returned function but not to the outside code. An example would be a counter that gives you an incremented value every time you call it:
因为 setup() 把返回的函数作了包装,它创建了一个闭包,我们可以用这个闭包来存储一些私有数据(而且闭包保存了这个函数的状态),这些私有数据可以通过返回的函数进行操作,但在函数外部不能直接读取到这些私有数据。比如这个例子中提供了一个计数器,每次调用这个函数计数器都会加一:

1
2
3
4
5
6
7
8
9
10
11
12
var setup = function handleSetUp() {
var count = 0;
return function saveCount() {
return (count += 1);
};
};

// usage
var next = setup();
next(); // 1
next(); // 2
next(); // 3

()Lazy function definition

1
2
3
4
5
6
7
8
9
10
11
var scareMe = function handleScareMe() {
console.log('Boo~');
scareMe = function newSareMe() {
console.log('New Boo~');
};
};

// using the self-defining function
scareMe(); // Boo!
scareMe(); // Double boo!
scareMe(); // Double boo!

即第二次调用后就被彻底替换掉了引用

A drawback of the pattern is that any properties you’ve previously added to the original function will be lost when it redefines itself. Also if the function is used with a different name, for example, assigned to a different variable or used as a method of an object, then the redefinition part will never happen and the original function body will be executed.
函数中包含一些初始化操作,并希望这些初始化只执行一次,那么这种模式是非常适合这个场景的。因为能避免的重复执行则尽量避免,函数的一部分可能再也不会执行到。在这个场景中,函数执行一次后就被重写为另外一个函数了

Another name for this pattern is “lazy function definition,” because the function is not properly defined until the first time it’s used and it is being lazy afterwards, doing less work.
这种模式的另外一个名字是 “函数的懒惰定义”,因为直到函数执行一次后才重新定义,可以说它是 “某个时间点之后才存在”,简称 “懒惰定义”。

A drawback of the pattern is that any properties you’ve previously added to the original function will be lost when it redefines itself. Also if the function is used with a different name, for example, assigned to a different variable or used as a method of an object, then the redefinition part will never happen and the original function body will be executed.
这种模式有一种明显的缺陷,就是之前给原函数添加的功能在重定义之后都丢失了。如果将这个函数定义为不同的名字,函数赋值给了很多不同的变量,或作为对象的方法使用,那么新定义的函数有可能不会执行,原始的函数会照旧执行。

Let’s see an example where the scareMe() function is used in a way that a first-class object would be used:
让我们来看一个例子,scareMe() 函数在这里作为一等对象来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var scareMe = function handleScareMe() {
console.log('Boo~');
scareMe = function newSareMe() {
console.log('New Boo~');
};
};

// 1. adding a new property
scareMe.property = "properly";

// 2. assigning to a different name
var prank = scareMe;

// 3. using as a method
var spooky = {
boo: scareMe
};

// calling with a new name
prank(); // "Boo!"
prank(); // "Boo!"
console.log(prank.property); // "properly"

// calling as a method
spooky.boo(); // "Boo!"
spooky.boo(); // "Boo!"
console.log(spooky.boo.property); // "properly"

// using the self-defined function
scareMe(); // Double boo!
scareMe(); // Double boo!
console.log(scareMe.property); // undefined

Returned Values from Immediate Functions

Just like any other function, an immediate function can return values and these return values can be assigned to variables:

1
2
3
var result = (function () {
return 2 + 2;
}());

The same as:

1
2
3
var result = function () {
return 2 + 2;
}();

(*)Immediate Object Initialization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// here you can define setting values 
// a.k.a. configuration constants
({
maxwidth: 600,
maxheight: 400,

// you can also define utility methods
gimmeMax: function () {
return this.maxwidth + 'x' + this.maxheight;
},

// initialize
init: function () {
console.log(this.gimmeMax());
// more init tasks...
}
}.init());

You can also wrap the object and the init() invocation into grouping parentheses instead of wrapping the object only. In other words, both of these work:

1
2
({...}).init(); 
({...}.init());

The benefits of this pattern are the same as the immediate function pattern: you protect the global namespace while performing the one-off initialization tasks. It may look a little more involved in terms of syntax compared to just wrapping a bunch of code in an anonymous function, but if your initialization tasks are more complicated (as they often are) it adds structure to the whole initialization procedure. For example, private helper functions are clearly distinguishable because they are properties of the temporary object, whereas in an immediate function pattern, they are likely to be just functions scattered around.
这种模式的好处和自动执行的函数模式是一样的:在做一些一次性的初始化工作的时候保护全局作用域不被污染。从语法上看,这种模式似乎比只包含一段代码在一个匿名函数中要复杂一些,但是如果你的初始化工作比较复杂 (这种情况很常见),它会给整个初始化工作一个比较清晰的结构。比如,一些私有的辅助性函数可以被很轻易地看出来,因为它们是这个临时对象的属性,但是如果是在立即执行的函数模式中,它们很可能只是一些散落的函数。

This pattern is mainly suitable for one-off tasks, and there’s no access to the object after the init() has completed. If you want to keep a ref- erence to the object after it is done, you can easily achieve this by adding return this; at the end of init().
这种模式主要用于一些一次性的工作,并且在 init() 方法执行完后就无法再次访问到这个对象。如果希望在这些工作完成后保持对对象的引用,只需要简单地在 init() 的末尾加上 return this;即可。

1
2
3
4
5
6
7
8
9
10
11
var a = ({
maxwidth: 600,
maxheight: 400,
gimmeMax: function () {
return this.maxwidth + 'x' + this.maxheight;
},
init: function () {
console.log(this.gimmeMax());
return this;
}
}).init();

Function Properties—A Memoization Pattern

Functions are objects, so they can have properties. In fact, they do have properties and methods out-of-the-box. For example, every function, no matter what syntax you use to create it, automatically gets a length property containing the number of arguments the function expects:
函数也是对象,所以它们可以有属性。事实上,函数也确实本来就有一些属性。比如,对一个函数来说,不管是用什么语法创建的,它会自动拥有一个 length 属性来标识这个函数期待接受的参数个数:

1
2
function func(a, b, c) {} 
console.log(func.length); // 3

You can add custom properties to your functions at any time. One use case for custom properties is to cache the results (the return value) of a function, so the next time the function is called, it doesn’t have to redo potentially heavy computations. Caching the results of a function is also known as memoization.
任何时候都可以给函数添加自定义属性。添加自定义属性的一个有用场景是 缓存函数的执行结果(返回值),这样下次同样的函数被调用的时候就不需要再做一次那些可能很复杂的计算。缓存一个函数的运行结果也就是为大家所熟知的 Memoization。

In the following example, the function myFunc creates a property cache, accessible as usual via myFunc.cache. The cache property is an object (a hash) where the parameter param passed to the function is used as a key and the result of the computation is the value. The result can be any complicated data structure you might need:
在下面的例子中,myFunc 函数创建了一个 cache 属性,可以通过 myFunc.cache 访问到。这个 cache 属性是一个对象(hash 表),传给函数的参数会作为对象的 key,函数执行结果会作为对象的值。函数的执行结果可以是任何的复杂数据结构:

1
2
3
4
5
6
7
8
9
10
11
var myFunc = function (param) {
if (!myFunc.cache[param]) {
var result = {};
// ... expensive operation ...
myFunc.cache[param] = result;
}
return myFunc.cache[param];
};

// cache storage
myFunc.cache = {};

The preceding code assumes that the function takes only one argument param and it’s a primitive data type (such as a string). If you have more parameters and more complex ones, a generic solution would be to serialize them. For example, you can serialize the arguments object as a JSON string and use that string as a key in your cache object:
上面的代码假设函数只接受一个参数 param,并且这个参数是基本类型(比如字符串)。如果你有更多更复杂的参数,则通常需要对它们进行序列化, 比如,你需要将 arguments 对象序列化为 JSON 字符串,然后使用 JSON 字符串作为 cache 对象的 key:

1
2
3
4
5
6
7
8
9
10
11
12
13
var myFunc = function () {
var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)), // 转换 arguments 为数组,再转换 arguments 为 JSON 格式
result;
if (!myFunc.cache[cachekey]) {
result = {};
// ... expensive operation ...
myFunc.cache[cachekey] = result;
}
return myFunc.cache[cachekey];
};

// cache storage
myFunc.cache = {};

Be aware that in serialization, the “identify” of the objects is lost. If you have two different objects that happen to have the same properties, both will share the same cache entry.
需要注意的是,在序列化的过程中,对象的 “标识” 将会丢失。如果你有两个不同的对象,却碰巧有相同的属性,那么他们会共享同样的缓存内容。

Configuration Objects

The configuration object pattern is a way to provide cleaner APIs, especially if you’re building a library or any other code that will be consumed by other programs.
配置对象模式是一种提供更简洁的 API 的方法,尤其是当你正在写一个即将被其它程序调用的类库之类的代码的时候。

It’s a fact of life that software requirements change as the software is developed and maintained. It often happens that you start working with some requirements in mind, but more functionality gets added afterward.
软件在开发和维护过程中需要不断改变是一个不争的事实。这样的事情总是以一些有限的需求开始,但是随着开发的进行,越来越多的功能会不断被加进来。

consider about it:

1
2
3
4
5
function addPerson(first, last) {...}

function addPerson(first, last, dob, gender, address) {...}

addPerson("Bruce", "Wayne", new Date(), null, null, "batman");

Passing a large number of parameters is not convenient. A better approach is to substitute all the parameters with only one and make it an object; let’s call it conf, for “configuration”:
传一大串的参数真的很不方便。一个更好的办法就是将它们替换成一个参数,并且把这个参数弄成对象;我们叫它 conf,是 “configuration”(配置) 的缩写:

1
2
3
4
5
6
7
addPerson(conf);
var conf = {
username: "batman",
first: "Bruce",
last: "Wayne"
};
addPerson(conf);

Then the user of the function can do:

1
2
3
4
5
6
var conf = {
username: "batman",
first: "Bruce",
last: "Wayne"
};
addPerson(conf);

The pros of the configuration objects are:

  • No need to remember the parameters and their order 不需要记住参数的顺序
  • You can safely skip optional parameters 可以很安全地跳过可选参数
  • Easier to read and maintain 拥有更好的可读性和可维护性
  • Easier to add and remove parameters 更容易添加和移除参数

The cons of the configuration objects are:

  • You need to remember the names of the parameters 需要记住参数的名字
  • Property names cannot be minified 参数名字不能被压缩

This pattern could be useful when your function creates DOM elements, for example, or in setting the CSS styles of an element, because elements and styles can have a great number of mostly optional attributes and properties.
举些实例,这个模式对创建 DOM 元素的函数或者是给元素设定 CSS 样式的函数会非常实用,因为元素和 CSS 样式可能会有很多但是大部分可选的属性。

Currying 柯里化

The process of making a function understand and handle partial application is called currying.
让函数理解并且处理部分应用的过程,叫柯里化(Currying)。
柯里化是一个变换函数的过程。

来看一个柯里化的计算两个数字相加的函数:

1
2
3
4
5
6
7
8
9
10
11
// a curried add
// accepts partial list of arguments
function add(x, y) {
if (typeof y === "undefined") { // partial
return function (y) {
return x + y;
};
}
// full application
return x + y;
}

In these examples, the function add() itself took care of partial applications. But can we do the same in a more generic fashion? In other words, can we transform any function into a new one that accepts partial parameters? The next snippet shows an example of a general-purpose function, let’s call it schonfinkelize(), which does just that.
在这些例子中,add() 函数自己处理了部分应用。有没有可能用一种更为通用的方式来做同样的事情呢?换句话说,我们能不能对任意一个函数进行处理,得到一个新函数,使它可以处理部分参数?下面的代码片段展示了一个通用函数的例子,我们叫它 schonfinkelize(),正是用来做这个的。

1
2
3
4
5
6
7
8
9
function schonfinkelize(fn) {
var slice = Array.prototype.slice; // 存储了对 slice 方法的饮用
var stored_args = slice.call(arguments, 1); //arguments 不是一个数组,需要用 slice 将 arguments 转化为数组
return function () {
var new_args = slice.call(arguments),
args = stored_args.concat(new_args);
return fn.apply(null, args); // 应用到原来的函数,fn 指的是传入的函数名
};
}

The schonfinkelize() function is probably a little more complicated than it should be, but only because arguments is not a real array in JavaScript. Borrowing the slice() method from Array.prototype helps us turn arguments into an array and work more conveniently with it. When schonfinkelize() is called the first time, it stores a private reference to the slice() method (called slice) and also stores the arguments it was called with (into stored_args), only stripping the first, because the first argument is the function being curried. Then schonfinkelize() returns a new function. When the new function is called, it has access (via the closure) to the already privately stored arguments stored_args and the slice reference. The new function has to merge only the old partially applied arguments (stored_args) with the new ones (new_args) and then apply them to the original function fn (also privately available in the closure).
这个 schonfinkelize 可能显得比较复杂了,只是因为在 JavaScript 中 arguments 不是一个真的数组。从 Array.prototype 中借用 slice() 方法帮助我们将 arguments 转换成数组,以便能更好地对它进行操作。当 schonfinkelize() 第一次被调用的时候,它使用 slice 变量存储了对 slice() 方法的引用,同时也存储了调用时的除去第一个之外的参数(stored_args), 因为第一个参数是要被柯里化的函数。schonfinkelize() 返回了一个函数。 当这个返回的函数被调用的时候,它可以(通过闭包)访问到已经存储的参数 stored_args 和 slice。新的函数只需要合并老的部分应用的参数 (stored_args)和新的参数(new_args),然后将它们应用到原来的函数 fn(也可以在闭包中访问到)即可。

Now armed with a general-purpose way of making any function curried, let’s give it a try with a few tests:
现在有了通用的柯里化函数,就可以做一些测试了:

1
2
3
4
5
6
7
8
9
10
11
// a normal function 
function add(x, y) {
return x + y;
}

// curry a function to get a new function
var newadd = schonfinkelize(add, 5);
newadd(4); // 9

// another option -- call the new function directly
schonfinkelize(add, 6)(7); // 13

The transformation function schonfinkelize() is not limited to single parameters or to single-step currying. Here are some more usage examples:
用来做函数转换的 schonfinkelize() 并不局限于单个参数或者单步的柯里化。这里有些更多用法的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// a normal function
function add(a, b, c, d, e) {
return a + b + c + d + e;
}

// works with any number of arguments
schonfinkelize(add, 1, 2, 3)(5, 5); // 16

// two-step currying
var addOne = schonfinkelize(add, 1);
addOne(10, 10, 10, 10); // 41

var addSix = schonfinkelize(addOne, 2, 3);
addSix(5, 5); // 16

When to Use Currying: 什么时候使用柯里化

When you find yourself calling the same function and passing mostly the same parameters, then the function is probably a good candidate for currying. You can create a new function dynamically by partially applying a set of arguments to your function. The new function will keep the repeated parameters stored (so you don’t have to pass them every time) and will use them to pre-fill the full list of arguments that the original function expects.
当你发现自己在调用同样的函数并且传入的参数大部分都相同的时候,就是考虑柯里化的理想场景了。你可以通过传入一部分的参数动态地创建一个新的函数。这个新函数会存储那些重复的参数(所以你不需要再每次都传入), 然后再在调用原始函数的时候将整个参数列表补全,正如原始函数期待的那样。

Object Creation Patterns: 对象创建模式

Creating objects in JavaScript is easy. You either use the object literal or you use constructor functions. In this chapter we go beyond that and see some additional patterns for object creation.
在 JavaScript 中创建对象很容易——可以通过使用对象直接量或者构造函数。本章将在此基础上介绍一些常用的对象创建模式。

The JavaScript language is simple and straightforward and often there’s no special syntax for features you may be used to in other languages, such as namespaces, modules, packages, private properties, and static members. This chapter takes you through common patterns to implement, substitute, or just think differently about those features.
JavaScript 语言本身简单、直观,通常也没有其他语言那样的语法特性:命名空间、模块、包、私有属性以及静态成员。本章将介绍一些常用的模式,以此实现这些语法特性。

We take a look at namespacing, dependency declaration, module pattern, and sandbox patterns, they help you organize and structure your application code and mitigate the effect of the implied globals. Other topics of discussion include private and privileged members, static and private static members, object constants, chaining, and one class- inspired way to define constructors.
我们将对命名空间、依赖声明、模块模式以及沙箱模式进行初探——它们帮助更好地组织应用程序的代码,有效地减轻全局污染的问题。除此之外,还会对包括:私有和特权成员、静态和私有静态成员、对象常量、链以及类式函数定义方式在内的话题进行讨论。

Namespace Pattern: 命名空间模式

JavaScript doesn’t have namespaces built into the language syntax, but this is a feature that is quite easy to achieve. Instead of polluting the global scope with a lot of functions, objects, and other variables, you can create one (and ideally only one) global object for your application or library. Then you can add all the functionality to that object.
你可以为应用或者类库创建一个(通常就一个)全局对象,然后将所有的功能都添加到这个对象上,而不是到处申明大量的全局函数、全局对象以及其他全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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 = {};

For the name of the global namespace object, you can pick, for example, the name of your application or library, your domain name, or your company name. Often developers use the convention of making the global variable ALL CAPS, so it stands out to the readers of the code. (But keep in mind that all caps are also often used for constants.)
开发者经常约定全局变量都采用大写(所有字母都大写),这样可以显得比较突出(不过,要记住,一般大写的变量都用于表示常量)。

This pattern is a good way to namespace your code and to avoid naming collisions in your own code, and collisions between your code and third-party code on the same page, such as JavaScript libraries or widgets. This pattern is highly recommended and perfectly applicable for many tasks, but it does have some drawbacks:
这种模式是一种很好的提供命名空间的方式,避免了自身代码的命名冲突,同时还避免了同一个页面上自身代码和第三方代码(比如:JavaScript 类库或者小部件)的冲突。这种模式在大多数情况下非常适用,但也有它的缺点:

  • A bit more to type; prefixing every variable and function does add up in the total amount of code that needs to be downloaded 代码量稍有增加;在每个函数和变量前加上这个命名空间对象的前缀,会增加代码量,增大文件大小
  • Only one global instance means that any part of the code can modify the global instance and the rest of the functionality gets the updated state 该全局实例可以被随时修改
  • Long nested names mean longer (slower) property resolution lookups 命名的深度嵌套会减慢属性值的查询

The sandbox pattern discussed later in the chapter addresses these drawbacks.
本章后续要介绍的沙箱模式则可以避免这些缺点

General Purpose Namespace Function: 通用命名空间函数

As the complexity of a program grows and some parts of code get split into different files and included conditionally, it becomes unsafe to just assume that your code is the first to define a certain namespace or a property inside of it. Some of the properties you’re adding to the namespace may already exist, and you could be overwriting them. Therefore before adding a property or creating a namespace, it’s best to check first that it doesn’t already exist, as shown in this example:
随着程序复杂度的提高,代码会分置在不同的文件中以特定顺序来加载,这样一来,就不能保证你的代码一定是第一个申明命名空间或者改变量下的属性的。甚至还会发生属性覆盖的问题。所以,在创建命名空间或者添加属性的时候,最好先检查下是否存在,如下所示:

1
2
3
4
5
6
7
8
// unsafe
var MYAPP = {};
// better
if (typeof MYAPP === "undefined") {
var MYAPP = {};
}
// or shorter
var MYAPP = MYAPP || {};

You can see how these added checks can quickly result in a lot of repeating code. For example, if you want to define MYAPP.modules.module2, you’ll have to make three checks, one for each object or property you’re defining. That’s why it’s handy to have a reusable function that takes care of the namespacing details. Let’s call this function namespace() and use it like so:
如上所示,不难看出,如果每次做类似操作都要这样检查一下就会有很多重复性的代码。比方说,要申明 MYAPP.modules.module2,就要重复三次这样的检查。所以,我们需要一个重用的 namespace() 函数来专门处理这些检查工作,然后用它来创建命名空间,如下所示:

1
2
3
4
5
6
7
8
9
// using a namespace function 
MYAPP.namespace('MYAPP.modules.module2');

// equivalent to:
// var MYAPP = {
// modules: {
// module2: {} //
}
//};

Next is an example implementation of the namespacing function. This implementation is nondestructive, meaning that if a namespace exists, it won’t be re-created:
下面是上述 namespace 函数的实现案例。这种实现是无损的,意味着如果要创建的命名空间已经存在,则不会再重复创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var MYAPP = MYAPP || {};
MYAPP.namespace = function (ns_string) {
var parts = ns_string.split('.');
var parent = MYAPP;
var i;
var len;
// strip redundant leading global
if (parts[0] === 'MYAPP') {
parts = parts.slice(1);
}
len = parts.length;
for (i = 0; i < len; i++) {
if (typeof(parent[parts[i]]) === 'undefined') {
parent[parent[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
};

This implementation enables all of these uses:
上述实现支持如下使用:

1
2
3
4
5
6
7
8
// assign returned value to a local var
var module2 = MYAPP.namespace('MYAPP.modules.module2');
module2 === MYAPP.modules.module2; // true

// skip initial *MYAPP*
MYAPP.namespace('modules.module51');

// long namespace MYAPP.namespace('once.upon.a.time.there.was.this.long.nested.property');

Declaring Dependencies: 依赖声明

JavaScript libraries are often modular and namespaced, which enables you to include only the modules you require. For example, in YUI2 there’s a global variable YAHOO, which serves as a namespace, and then modules that are properties of the global variable, such as YAHOO.util.Dom (the DOM module) and YAHOO.util.Event (Events module).
JavaScript 库往往是模块化而且有用到命名空间的,这使用你可以只使用你需要的模块。比如在 YUI2 中,全局变量 YAHOO 就是一个命名空间,各个模块作为全局变量的属性,比如 YAHOO.util.Dom(DOM 模块)、YAHOO.util.Event(事件模块)。

It’s a good idea to declare the modules your code relies on at the top of your function or module. The declaration involves creating only a local variable and pointing to the desired module:
将你的代码依赖在函数或者模块的顶部进行声明是一个好主意。声明就是创建一个本地变量,指向你需要用到的模块

1
2
3
4
5
6
7
var myFunction = function () {
// dependencies
var event = YAHOO.util.Event;
var dom = YAHOO.util.Dom;
// use event and dom variables
// for the rest of the function...
};

This is an extremely simple pattern, but at the same time it has numerous benefits:

  • Explicit declaration of dependencies signals to the users of your code the specific script files that they need to make sure are included in the page. 明确的声明依赖是告知你代码的用户,需要保证指定的脚本文件被包含在页面中。
  • Upfront declaration at the top of the function makes it easy to find and resolve dependencies. 将声明放在函数顶部使得依赖很容易被查找和解析。
  • Working with a local variable (such as dom) is always faster than working with a global (such as YAHOO) and even faster than working with nested properties of a global variable (such as YAHOO.util.Dom), resulting in better performance. When following this dependency declaration pattern, the global symbol resolution is performed only once in the function. After that the local variable is used, which is much faster. 本地变量(如 dom)永远会比全局变量(如 YAHOO)要快,甚至比全局变量的属性(如 YAHOO.util.Dom)还要快,这样会有更好的性能。使用了依赖声明模式之后,全局变量的解析在函数中只会进行一次,在此之后将会使用更快的本地变量
  • Advanced minification tools such as YUICompressor and Google Closure compiler will rename local variables (so event will likely become just one character such as A), resulting in smaller code, but never global variables, because it’s not safe to do so. 一些高级的代码压缩工具比如 YUI Compressor 和 Google Closure compiler 会重命名本地变量(比如 event 可能会被压缩成一个字母,如 A),这会使代码更精简,但这个操作不会对全局变量进行,因为这样做不安全。

Private Properties and Methods: 私有属性和方法

JavaScript has no special syntax to denote private, protected, or public properties and methods, unlike Java or other languages. All object members are public.
JavaScript 不像 Java 或者其它语言,它没有专门的提供私有、保护、公有属性和方法的语法。所有的对象成员都是公有的。

1
2
3
4
5
6
7
var myobj = {
myprop: 1,
getProp: function () {
return this.myprop;
}
};
console.log(myobj.myprop); // *myprop* is publicly accessible console.log(myobj.getProp()); // getProp() is public too

The same is true when you use constructor functions to create objects; all members are still public:

1
2
3
4
5
6
7
8
function Gadget() {
this.name = 'MacBook Air';
this.stretch = function () {
return 'iPod';
};
}
var toy = new Gadget();
console.log(toy.name); // *name* is public console.log(toy.stretch()); MacBook Air// stretch() is public

Private Members: 私有成员

Although the language doesn’t have special syntax for private members, you can implement them using a closure. Your constructor functions create a closure and any variables that are part of the closure scope are not exposed outside the constructor. However, these private variables are available to the public methods: methods defined inside the constructor and exposed as part of the returned objects. Let’s see an example where name is a private member, not accessible outside the constructor:
尽管语言并没有用于私有成员的专门语法,但你可以通过闭包来实现。在构造函数中创建一个闭包,任何在这个闭包中的部分都不会暴露到构造函数之外。但是,这些私有变量却可以被公有方法访问,也就是在构造函数中定义的并且作为返回对象一部分的那些方法。我们来看一个例子,name 是一个私有成员,在构造函数之外不能被访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Gadget() {
var name = 'MBA';
this.getName = function () {
return name;
};
}
var toy = new Gadget();

// *name* is undefined, it's private
console.log(toy.name); // undefined

// public method has access to *name*
console.log(toy.getName()); // "MBA"

As you can see, it’s easy to achieve privacy in JavaScript. All you need to do is wrap the data you want to keep private in a function and make sure it’s local to the function, which means not making it available outside the function.
如你所见,在 JavaScript 创建私有成员很容易。你需要做的只是将私有成员放在一个函数中,保证它是函数的本地变量,也就是说让它在函数之外不可以被访问。

Privileged Methods: 特权方法

The notion of privileged methods doesn’t involve any specific syntax; it’s just a name given to the public methods that have access to the private members (and hence have more privileges).
特权方法的概念不涉及到任何语法,它只是一个给可以访问到私有成员的公有方法的名字(就像它们有更多权限一样)。

In the previous example, getName() is a privileged method because it has “special” access to the private property name.
在前面的例子中,getName() 就是一个特权方法,因为它有访问 name 属性的特殊权限。

Privacy Failures: 私有成员失效

When you’re directly returning a private variable from a privileged method and this variable happens to be an object or array, then outside code can modify the private variable because it’s passed by reference.
当你直接通过特权方法返回一个私有变量,而这个私有变量恰好是一个对象或者数组时,外部的代码可以修改这个私有变量,因为它是按引用传递的。

Let’s examine the second case a little more closely. The following implementation of Gadget looks innocent:

1
2
3
4
5
6
7
8
9
10
11
function Gad() {
var specs = {
screen_width: 320,
screen_height: 480,
color: 'white'
};
//public function
this.getSpecs = function () {
return spaces;
};
}

The problem here is that getSpecs() returns a reference to the specs object. This enables the user of Gadget to modify the seemingly hidden and private specs:
这里的问题是 getSpecs() 返回了一个 specs 对象的引用。这使得 Gadget 的使用者可以修改貌似隐藏起来的私有成员 specs:

1
2
3
4
5
var toy = new Gadget(),
specs = toy.getSpecs();
specs.color = "black";
specs.price = "free";
console.dir(toy.getSpecs());

The solution to this unexpected behavior is to be careful not to pass references to objects and arrays you want to keep private. One way to achieve this is to have getSpecs() return a new object containing only some of the data that could be interesting to the consumer of the object. This is also known as Principle of Least Authority (POLA), which states that you should never give more than needed. In this case, if the consumer of Gadget is interested whether the gadget fits a certain box, it needs only the dimensions. So instead of giving out everything, you can create getDimensions(), which re- turns a new object containing only width and height. You may not need to implement getSpecs() at all.
这个意外的问题的解决方法就是不要将你想保持私有的对象或者数组的引用传递出去。达到这个目标的一种方法是让 getSpecs() 返回一个新对象,这个新对象只包含对象的使用者感兴趣的数据。这也是众所周知的 “最低授权原则”(Principle of Least Authority,简称 POLA),指永远不要给出比需求更多的东西。在这个例子中,如果 Gadget 的使用者关注它是否适应一个特定的盒子,它只需要知道尺寸即可。所以你应该创建一个 getDimensions(),用它返回一个只包含 width 和 height 的新对象,而不是把什么都给出去。也就是说,也许你根本不需要实现 getSpecs() 方法。

Another approach, when you need to pass all the data, is to create a copy of the specs object, using a general-purpose object-cloning function. The next chapter offers two such functions—one is called extend() and does a shallow copy of the given object (copies only the top-level parameters). The other one is called extendDeep(), which does a deep copy, recursively copying all properties and their nested properties.
当你需要传递所有的数据时,有另外一种方法,就是使用通用的对象复制函数创建 specs 对象的一个副本。下一章提供了两个这样的函数——一个叫 extend(),它会浅复制一个给定的对象(只复制顶层的成员)。另一个叫 extendDeep(),它会做深复制,遍历所有的属性和嵌套的属性

Object Literals and Privacy: 对象字面量和私有成员

So far we’ve looked only at examples of using constructors to achieve privacy. But what about the cases when your objects are created with object literals? Is it still possible to have private members?
到目前为止,我们只看了使用构建函数创建私有成员的示例。如果使用对象字面量创建对象时会是什么情况呢?是否有可能含有私有成员?

As you saw before, all you need is a function to wrap the private data. So in the case of object literals, you can use the closure created by an additional anonymous immediate function. Here’s an example:
如你前面所看到的那样,私有数据使用一个函数来包裹。所以在使用对象字面量时,你也可以使用一个立即执行的匿名函数创建的闭包。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var myobj = {};
(function() {
// private members
var name = "RayJune";
// immpelment the public part
// note -- no 'var'
myobj = { // 相当于为 myobj 这个对象增加一个方法
getName: function () {
return name;
}
};
})();
myobj.getName(); //RayJune

The same idea but with slightly different implementation is given in the following example:

1
2
3
4
5
6
7
8
9
10
11
12
var myobj = (function (){
// private members
var name = 'RayJune';
// implement the public part
return {
getName: function() {
return name;
}
}
}
)();
myobj.getName(); //RayJune

This example is also the bare bones of what is known as “module pattern,” which we examine in just a bit.
这个例子也是所谓的“模块模式” 的基础,我们稍后将讲到它。

Prototypes and Privacy: 原型和私有成员(完美版)

One drawback of the private members when used with constructors is that they are recreated every time the constructor is invoked to create a new object.
使用构造函数创建私有成员的一个弊端是,每一次调用构造函数创建对象时,这些私有成员都会被创建一次

This is actually a problem with any members you add to this inside of constructors. To avoid the duplication of effort and save memory, you can add common properties and methods to the prototype property of the constructor. This way the common parts are shared among all the instances created with the same constructor. You can also share the hidden private members among the instances.
这对在构建函数中添加到 this 的成员来说是一个问题。为了避免重复劳动,节省内存,你可以将共用的属性和方法添加到构造函数的 prototype(原型)属性中。这样的话这些公共的部分会在使用同一个构造函数创建的所有实例中共享。你也同样可以在这些实例中共享私有成员。

To do so you can use a combination of two patterns: private properties inside constructors and private properties in object literals. Because the prototype property is just an object, it can be created with the object literals.
你可以将两种模式联合起来达到这个目的:构造函数中的私有属性和对象字面量中的私有属性。因为 prototype 属性也只是一个对象,可以使用对象字面量创建(这点说的很赞)。

Here’s an example of how you can achieve this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
};
})();

var toy = new Gadget();
console.log(toy.getName()); // privileged "own" method
console.log(toy.getBrowser()); // privileged prototype method

Revealing Private Functions As Public Methods: 将私有函数暴露为公有方法

Let’s take an example, building on top of one of the privacy patterns—the private members in object literals:
我们来看一个例子,它建立在对象字面量的私有成员模式之上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var myarray;
(function () {
var astr = "[object Array]";
var toString = Object.prototype.toString;
function isArray(a) {
return toString.call(a) === astr;
}
function indexOf(haystack, needle) {
var i;
var max = haystack.length;
for (i = 0; i < max; i += 1) {
if (haystack[i] === needle) {
return i;
}
}
return 0;
}
myarray = {
isArray: isArray,
indexOf: indexOf,
inArray: indexOf
};
}());

myarray.isArray([1,2]); // true
myarray.isArray({0: 1}); // false
myarray.indexOf(["a", "b", "z"], "z"); // 2
myarray.inArray(["a", "b", "z"], "z"); // 2

Now if something unexpected happens, for example, to the public indexOf(), the private indexOf() is still safe and therefore inArray() will continue to work:
现在假如有一些意外的情况发生在暴露的 indexOf() 方法上,私有的 indexOf() 方法仍然是安全的,因此 inArray() 仍然可以正常工作:

1
2
myarray.indexOf = null;
myarray.inArray(["a", "b", "z"], "z"); // 2

Module Pattern 模块模式

The module pattern is widely used because it provides structure and helps organize your code as it grows. Unlike other languages, JavaScript doesn’t have special syntax for packages, but the module pattern provides the tools to create self-contained de-coupled pieces of code, which can be treated as black boxes of functionality and added, replaced, or removed according to the (ever-changing) requirements of the software you’re writing.
模块模式使用得很广泛,因为它可以为代码提供特定的结构,帮助组织日益增长的代码。不像其它语言,JavaScript 没有专门的 “包”(package)的语法,但模块模式提供了用于创建独立解耦的代码片段的工具,这些代码可以被当成黑盒,当你正在写的软件需求发生变化时,这些代码可以被添加、替换、移除。

The module pattern is a combination of several patterns described so far in the book, namely:

  • Namespaces 命名空间模式
  • Immediate functions 立即执行的函数模式
  • Private and privileged members 私有和特权成员模式
  • Declaring dependencies 依赖声明模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// The first step is setting up a namespace.
MYAPP.namespace('MYAPP.utilities.array');

// The next step is defining the module.
MYAPP.utilities.array = (function () {
return {
// todo...
};
}());

// Next, let’s add some methods to the public interface:
MYAPP.utilities.array = (function () {
return {
inArray: function (needle, haystack) {
// ...
},
isArray: function (a) {
// ...
}
};
}());

Using the private scope provided by the immediate function, you can declare some private properties and methods as needed. Right at the top of the immediate function will also be the place to declare any dependencies your module might have. Following the variable declarations, you can optionally place any one-off initialization code that helps set up the module. The final result is an object returned by the immediate function that contains the public API of your module:
如果需要的话,你可以在立即执行的函数提供的闭包中声明私有属性和私有方法。函数顶部也是声明依赖的地方。在变量声明的下方,你可以选择性地放置辅助初始化模块的一次性代码。函数最终返回的是一个包含模块公共 API 的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
MYAPP.namespace('MYAPP.utilities.array');
MYAPP.utilities.array = (function () {
// dependencies
var uobj = MYAPP.utilities.object;
var ulang = MYAPP.utilities.lang;
// private properties
var array_string = "[object Array]";
var ops = Object.prototype.toString;

// private methods // ...

// end var

// optionally one-time init procedures // ...

// public API
return {
inArray: function (needle, haystack) {
for (var i = 0, max = haystack.length; i < max; i += 1) {
if (haystack[i] === needle) {
return true;
}
}
},
isArray: function (a) {
return ops.call(a) === array_string;
}
// ... more methods and properties
};
}());

The module pattern is a widely used and highly recommended way to organize your code, especially as it grows.
模块模式被广泛使用,这是一种值得强烈推荐的模式,它可以帮助组织代码,尤其是代码量在不断增长的时候。

Revealing Module Pattern: 暴露模块模式

The above can become:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
}
return 0;
},
isArray = function (a) {
return ops.call(a) === array_string;
};
// end var
// revealing public API
return {
isArray: isArray,
indexOf: inArray
};
}());

Modules That Create Constructors: 创建构造函数的模块

The preceding example produced an object MYAPP.utilities.array, but sometimes it’s more convenient to create your objects using constructor functions. You can still do that using the module pattern. The only difference is that the immediate function that wraps the module will return a function at the end, and not an object.
前面的例子创建了一个对象 MYAPP.utilities.array,但有时候使用构造函数来创建对象会更方便。你也可以同样使用模块模式来做。唯一的区别是包裹模块的立即执行的函数会在最后返回一个函数,而不是一个对象

Consider the following example of the module pattern that creates a constructor function MYAPP.utilities.Array:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
MYAPP.namespace('MYAPP.utilities.Array');
MYAPP.utilities.array = (function () {
// dependencies
var uobj = MYAPP.utilities.object;
var ulang = MYAPP.utilities.lang;
// private properties and methods...
var Constr;
// end var

// optionally one-time init procedures // ...

// public API -- constructor
Constr = function (o) {
this.elements = this.toArray(o);
};
// public API -- prototype
Constr.prototype = {
constructor: MYAPP.utilities.Array,
version: "2.0",
toArray: function (obj) {
for (var i = 0, a = [], len = obj.length; i < len; i += 1) {
a[i] = obj[i];
}
return a;
}
};
// return the constructor
// to be assigned to the new namespace return Constr;
}());

//The way to use this new constructor will be like so:
var arr = new MYAPP.utilities.Array(obj);

Importing Globals into a Module: 在模块中引入全局上下文

In a common variation of the pattern, you can pass arguments to the immediate function that wraps the module. You can pass any values, but usually these are references to global variables and even the global object itself. Importing globals helps speed up the global symbol resolution inside the immediate function, because the imported variables become locals for the function:
作为这种模式的一个常见的变种,你可以给包裹模块的立即执行的函数传递参数。你可以传递任何值,但通常会传递全局变量甚至是全局对象本身。引入全局上下文可以加快函数内部的全局变量的解析因为引入之后会作为函数的本地变量

1
2
3
4
5
MYAPP.utilities.module = (function (app, global) {
// references to the global object
// and to the global app namespace object
// are now localized
}(MYAPP, this));

Sandbox Pattern

The sandbox pattern addresses the drawbacks of the namespacing pattern, namely:

  • Reliance on a single global variable to be the application’s global. In the name-spacing pattern, there is no way to have two versions of the same application or library run on the same page, because they both need the same global symbol name, for example, MYAPP. 依赖一个全局变量成为应用的全局命名空间。在命名空间模式中,没有办法在同一个页面中运行同一个应用或者类库的不同版本,在为它们都会需要同一个全局变量名,比如 MYAPP。
  • Long, dotted names to type and resolve at runtime, for example, MYAPP.utilities.array. 代码中以点分隔的名字比较长,无论写代码还是解析都需要处理这个很长的名字,比如 MYAPP.utilities.array。

As the name suggests, the sandbox pattern provides an environment for the modules to “play” without affecting other modules and their personal sandboxes.
顾名思义,沙箱模式为模块提供了一个环境,模块在这个环境中的任何行为都不会影响其它的模块和其它模块的沙箱。

A Global Constructor: 全局构造函数

In the namespacing pattern you have one global object; in the sandbox pattern the single global is a constructor: let’s call it Sandbox(). You create objects using this constructor, and you also pass a callback function, which becomes the isolated sandboxed environment for your code.
在命名空间模式中 ,有一个全局对象,而在沙箱模式中,唯一的全局变量是一个构造函数,我们把它命名为 Sandbox()。我们使用这个构造函数来创建对象同时也要传入一个回调函数,这个函数会成为代码运行的独立空间

  • 使用这个构造函数来创建对象
  • 回调函数成为代码运行的独立空间

Using the sandbox will look like this:

1
2
3
new Sandbox(function (box) { 
// your code here...
});

The object box will be like MYAPP in the namespacing example—it will have all the library functionality you need to make your code work.

Let’s add two more things to the pattern:

  • With some magic (enforcing new pattern from Chapter 3), you can assume new and not require it when creating the object. 构造时不一定需要 new
  • The Sandbox() constructor can accept an additional configuration argument (or arguments) specifying names of modules required for this object instance. We want the code to be modular, so most of the functionality Sandbox() provides will be contained in modules. 让 Sandbox() 构造函数可以接受一个(或多个)额外的配置参数,用于指定这个对象需要用到的模块名字。我们希望代码是模块化的,因此绝大部分 Sandbox() 提供的功能都会被包含在模块中。

You can omitra new and create an object that uses some fictional “ajax” and “event” modules like so:

1
2
3
Sandbox(['ajax', 'event'], function (box) {
// console.log(box);
});

This example is similar to the preceding one, but this time module names are passed as individual arguments:

1
2
3
Sandbox('ajax', 'dom', function (box) {
// console.log(box);
});

And how about using a wildcard argument to mean “use all available modules”? For convenience, let’s also say that when no modules are passed, the sandbox will assume . So two ways to use all available modules will be like:
使用通配符 “*” 来表示 “使用所有可用的模块” 如何?为了方便,我们也假设没有任何模块传入时,沙箱使用 “*”。所以有两种使用所有可用模块的方法:

1
2
3
4
5
6
7
Sandbox('x', function(box) {
//console.log(box)
});

Sandbox(function(box) {
//console.log(box)
});

And one more example of using the pattern illustrates how you can instantiate sandbox objects multiple times—and you can even nest them one within the other without the two interfering:
下面的例子展示了如何实例化多个消息箱对象,你甚至可以将它们嵌套起来而互不影响:

1
2
3
4
5
6
7
8
9
10
11
Sandbox('dom', 'event', function (box) {
// work with dom and event
Sandbox('ajax', function (box) {
// another sandboxed "box" object
// this "box" is not the same as
// the "box" outside this function
//...
// done with Ajax
});
// no trace of Ajax module here
});

As you can see from these examples, when using the sandbox pattern, you can protect the global namespace by having your code wrapped into callback functions.
从这些例子中看到,使用沙箱模式可以通过将代码包裹在回调函数中的方式来保护全局命名空间。

Now let’s see how you can approach implementing the Sandbox() constructor and its modules to support all this functionality.
现在我们来看一下如何实现 Sandbox() 构造函数和它的模块来支持上面讲到的所有功能。

Adding Modules: 添加模块

Before implementing the actual constructor, let’s see how we can approach adding modules.

The Sandbox() constructor function is also an object, so you can add a static property called modules to it. This property will be another object containing key-value pairs where the keys are the names of the modules and the values are the functions that implement each module:
Sandbox() 构造函数也是一个对象,所以可以给它添加一个 modules 静态属性。这个属性也是一个包含名值(key-value)对的对象,其中 key 是模块的名字, value 是模块的功能实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Sandbox.modules = {}; 
Sandbox.modules.dom = function (box) {
box.getElement = function(){};
box.getStyle = function() {};
box.foo = "bar";
};
Sandbox.modules.event = function (box) {
// access to the Sandbox prototype if needed:
// box.constructor.prototype.m = "mmm";
box.attachEvent = function () {};
box.dettachEvent = function () {};
};
Sandbox.modules.ajax = function (box) {
box.makeRequest = function () {};
box.getResponse = function () {};
};

In this example we added modules dom, event, and ajax, which are common pieces of functionality in every library or complex web application.

The functions that implement each module accept the current instance box as a parameter and may add additional properties and methods to that instance.
实现每个模块功能的函数接受一个实例 box 作为参数,并给这个实例添加属性和方法。

Implementing the Constructor: 实现构造函数(代码写的非常赞)

Finally, let’s implement the Sandbox() constructor (naturally, you would want to rename this type of constructor to something that makes sense for your library or application):
最后,我们来实现 Sandbox() 构造函数(你可能会很自然地想将这类构造函数命名为对你的类库或者应用有意义的名字):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function Sandbox() {
var args = Array.prototype.slice.call(arguments); // 转换 arguments 为数组
var callback = args.pop(); // 弹出最后一个元素,因为最后一个元素是 callback
// modules can be passed as an array or as individual parameters
var modules = (args[0] && typeof args[0] === "string") ? args : args[0];
var 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.a = 1;
this.b = 2;

// now add modules to the core *this* object
// no modules or "*" both mean "use all modules"
if (!modules || modules === '*') {
modules = [];
for (i in Sandbox.modules) {
if (Sandbox.modules.hasOwnProperty(i)) {
modules.push(i);
}
}
}

// 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",
getName: function () {
return this.name;
}
};

构造函数的最后一个参数是回调函数。这个回调函数会在最后使用新创建的实
例来调用。事实上这个回调函数就是用户的沙箱,它被传入一个 box 对象,这
个对象包含了所有依赖的功能。

Static Members: 静态成员

Static properties and methods are those that don’t change from one instance to another. In class-based languages, static members are created using special syntax and then used as if they were members of the class itself. For example, a static method max() of some MathUtils class would be invoked like MathUtils.max(3, 5). This is an example of a public static member, which can be used without having to create an instance of the class. There can also be private static members—not visible to the consumer of the class but still shared among all the instances of the class. Let’s see how to implement both private and public static members in JavaScript.
静态属性和方法是指那些在所有的实例中保持一致的成员。在基于类的语言中,表态成员是用专门的语法来创建,使用时就像是类自己的成员一样。比如 MathUtils 类的 max() 方法会被像这样调用:MathUtils.max(3, 5)。这是一个公有静态成员的示例,即可以在不实例化类的情况下使用。同样也可以有私有的静态方法,即对类的使用者不可见,而在类的所有实例间是共享的。 我们来看一下如何在 JavaScript 中实现公有和私有静态成员。

Public Static Members: 公有静态成员

The following example defines a constructor Gadget with a static method isShiny() and a regular instance method setPrice(). The method isShiny() is a static method because it doesn’t need a specific gadget object to work (just like you don’t need a particular gadget to figure out that all gadgets are shiny). setPrice(), on the other hand, needs an object, because gadgets can be priced differently:
下面的例子定义了一个构造函数 Gadget,它有一个静态方法 isShiny() 和一个实例方法 setPrice()。isShiny() 是一个静态方法,因为它不需要指定一个对象才能工作(就像你不需要先指定一个工具(gadget)才知道所有的工具是不是有光泽的(shiny))。但 setPrice() 却需要一个对象,因为工具可能有不同的定价:

1
2
3
4
5
6
7
8
9
10
11
12
// 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;
};

Now let’s call these methods. The static isShiny() is invoked directly on the constructor, whereas the regular method needs an instance:
现在我们来调用这些方法。静态方法 isShiny() 可以直接在构造函数上调用,但其它的常规方法需要一个实例:

1
2
3
4
5
6
// calling a static method 
Gadget.isShiny(); // "you bet"

// creating an instance and calling a method
var iphone = new Gadget();
iphone.setPrice(500);

Attempting to call an instance method statically won’t work; same for calling a static method using the instance iphone object:
使用静态方法的调用方式去调用实例方法并不能正常工作,同样,用调用实例方法的方式来调用静态方法也不能正常工作:

1
2
typeof Gadget.setPrice; // "undefined"
typeof iphone.isShiny; // "undefined"

Sometimes it could be convenient to have the static methods working with an instance too. This is easy to achieve by simply adding a new method to the prototype, which serves as a façade pointing to the original static method:
有时候让静态方法也能用在实例上会很方便。我们可以通过在原型上加一个新方法来很容易地做到这点,这个新方法作为原来的静态方法的一个包装:

1
2
Gadget.prototype.isShiny = Gadget.isShiny;
iphone.isShiny(); // "you bet"

In such cases you need to be careful if you use this inside the static method. When you do Gadget.isShiny() then this inside isShiny() will refer to the Gadget constructor function. If you do iphone.isShiny() then this will point to iphone.
在这种情况下,你需要很小心地处理静态方法内的 this。当你运行 Gadget.isShiny() 时,在 isShiny() 内部的 this 指向 Gadget 构造函数。而如果你运行 iphone.isShiny(),那么 this 会指向 iphone。

One last example shows how you can have the same method being called statically and nonstatically and behave slightly different, depending on the invocation pattern. Here instanceof helps determine how the method was called:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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);
};

//Testing a static method call:
Gadget.isShiny(); // "you bet"
//Testing an instance, nonstatic call:
var a = new Gadget('499.99');
a.isShiny(); // "you bet, it costs $499.99!"

Private Static Members: 私有静态成员

By private static members, we mean members that are:
到目前为止,我们都只讨论了公有的静态方法,现在我们来看一下如何实现私有静态成员。所谓私有静态成员是指:

  • Shared by all the objects created with the same constructor function 被所有由同一构造函数创建的对象共享
  • Not accessible outside the constructor 不允许在构造函数外部访问

Let’s look at an example where counter is a private static property in the constructor Gadget. In this chapter there was already a discussion on private properties, so this part is still the same—you need a function to act as a closure and wrap around the private members. Then let’s have the same wrapper function execute immediately and return a new function. The returned function value is assigned to the variable Gadget and becomes the new constructor:
我们来看一个例子,counter 是 Gadget 构造函数的一个私有静态属性。在本章中我们已经讨论过私有属性,这里的做法也是一样,需要一个函数提供的闭包来包裹私有成员。然后让这个包裹函数立即执行并返回一个新的函数。 将这个返回的函数赋值给 Gadget 作为构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
var Gadget = (function () {
// static variable/property
var counter = 0;

// returning the new implementation
// of the constructor
return function () {
console.log(counter += 1);
};
}());
var g1 = new Gadget(); // logs 1
var g2 = new Gadget(); // logs 2
var g3 = new Gadget(); // logs 3

The unique identifier could be useful, so why not expose it via a privileged method? Below is an example that builds upon the previous and adds a privileged method getLastId() to access the static private property:
这个唯一标识可能会很有用,那为什么不把它通用一个特权方法暴露出去呢?下面的例子是基于前面的例子,增加了用于访问私有静态属性的 getLastId() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// constructor
var Gadget = (function () {
// static variable/property
var counter = 0;
var NewGadget;
// this will become the new constructor implementation
NewGadget = function () {
counter += 1;
};

// a privileged method
NewGadget.prototype.getLastId = function () {
return counter;
};

// overwrite the constructor
return NewGadget;
}()); // execute immediately

var iphone = new Gadget();
iphone.getLastId(); // 1
var ipod = new Gadget();
ipod.getLastId(); // 2
var ipad = new Gadget();
ipad.getLastId(); // 3

Static properties (both private and public) can be quite handy. They can contain methods and data that are not instance-specific and don’t get re-created with every instance. In Chapter 7, when we discuss the singleton pattern, you can see an example implementation that uses static properties to implement class-like singleton constructors.
静态属性(包括私有和公有)有时候会非常方便,它们可以包含和具体实例无关的方法和数据,而不用在每次实例中再创建一次。当我们在第七章中讨论单例模式时,你可以看到使用静态属性实现类式单例构造函数的例子。

Object Constants: 对象常量

如果环境提供 const 的话,可以用 const 来声明常量。

As a workaround, a common approach is to use a naming convention and make variables that shouldn’t be changed stand out using all caps. This convention is actually used in the built-in JavaScript objects:
一种常用的解决办法是通过命名规范,让不应该变化的变量使用全大写。这个规范实际上也用在 JavaScript 原生对象中:

1
2
3
Math.PI; // 3.141592653589793
Math.SQRT2; // 1.4142135623730951
Number.MAX_VALUE; // 1.7976931348623157e+308

For your own constants you can adopt the same naming convention and add them as static properties to the constructor function:
你自己的常量也可以用这种规范,然后将它们作为静态属性加到构造函数中:

1
2
3
4
5
6
7
8
// constructor
var Widget = function () {
// implementation...
};

// constants
Widget.MAX_HEIGHT = 320;
Widget.MAX_WIDTH = 480;

If you really want to have an immutable value, you can create a private property and provide a getter method, but no setter. This is probably overkill in many cases when you can get by with a simple convention, but it is still an option.
如果你真的希望有一个不能被改变的值,那么可以创建一个私有属性,然后提供一个取值的方法(getter),但不给赋值的方法(setter)。这种方法在很多可以用命名规范解决的情况下可能有些矫枉过正,但不失为一种选择。

Pros and Cons of the Chaining Pattern: 链式调用模式的好与坏

Just like this:

1
document.getElementsByTagName('head')[0].appendChild(new node);

A benefit of using the chaining pattern is that you can save some typing and create more concise code that almost reads like a sentence.
使用链式调用模式的一个好处就是可以节省代码量,使得代码更加简洁和易读,读起来就像在读句子一样。

Another benefit is that it helps you think about splitting your functions and creating smaller, more specialized functions, as opposed to functions that try to do too much. This improves the maintainability in the long run.
另外一个好处就是帮助你思考如何拆分你的函数,创建更小、更有针对性的函数,而不是一个什么都做的函数。长时间来看,这会提升代码的可维护性。

A drawback is that it gets harder to debug code written this way. You may know that an error occurs on a specific line, but there’s just too much going on in this line. When one of the several methods you’ve chained fails silently, you have no idea which one. Robert Martin, author of the book Clean Code, goes so far as to call this a “train wreck” pattern.
一个弊端是调用这样写的代码会更困难。你可能知道一个错误出现在某一行,但这一行要做很多的事情。当链式调用的方法中的某一个出现问题而又没报错时,你无法知晓到底是哪一个出问题了。《代码整洁之道》的作者 Robert Martion 甚至叫这种模式为 “train wreck” 模式。

In any event, it’s good to recognize this pattern, and when a method you write has no obvious and meaningful return value, you can always return this. The pattern is widely used, for example, in the jQuery library. And if you look at the DOM API, you can notice that it’s also prone to chaining with constructs such as:
不管怎样,认识这种模式总是好的,当你写的方法没有明显的有意义的返回值时,你就可以返回 this。这个模式应用得很广泛,比如 jQuery 库。如果你去看 DOM 的 API 的话,你会发现它也会以这样的形式倾向于链式调用:

Code Reuse Patterns: 代码复用模式

Code reuse is an important and interesting topic simply because it’s natural to strive for writing as little and reusing as much as possible from existing code, which you or someone else has already written. Especially if it’s good, tested, maintainable, extensible, and documented code.
代码复用是一个既重要又有趣的话题,因为努力在自己或者别人写的代码上写尽量少且可以复用的代码是件很自然的事情,尤其当这些代码是经过测试的、可维护的、可扩展的、有文档的时候。

When talking about code reuse, the first thing that comes to mind is inheritance, and a great deal of the chapter is dedicated to this topic. You see several ways to do “classical” and nonclassical inheritance. But it’s important to keep the end goal in mind— we want to reuse code;. Inheritance is one way (means) for us to reach that goal. And it’s not the only way. You see how you can compose objects from other objects, how to use object mix-ins, and how you can borrow and reuse only the functionality you need without technically inheriting anything permanently.
当我们说到代码复用的时候,想到的第一件事就是继承,本章会有很大篇幅讲述这个话题。你将看到好多种方法来实现 “类式(classical)” 和一些其它方式的继承。但是,最最重要的事情,是你需要记住终极目标——代码复用。继承是达到这个目标的一种方法,但是不是唯一的。在本章,你将看到怎样基于其它对象来构建新对象,怎样使用掺元,以及怎样在不使用继承的情况下只复用你需要的功能

When approaching a code reuse task, keep in mind the advice the Gang of Four book has to offer on object creation: “Prefer object composition to class inheritance.”
在做代码复用的工作的时候,谨记 Gang of Four 在书中给出的关于对象创建的建议:“优先使用对象创建而不是类继承”。

Classical Versus Modern Inheritance Patterns: 类式继承 vs 现代继承模式

经典的类式是类似 Java 的 class 继承。在 JavaScript 中,因为没有类,所以类的实例的概念没什么意义。JavaScript 的对象仅仅是简单的键值对,这些键值对都可以动态创建或者是改变。

But JavaScript has constructor functions, and the syntax of the new operator resembles a lot the syntax of using classes.
但是 JavaScript 拥有构造函数(constructor functions),并且有语法和使用类非常相似的 new 运算符。

In Java you could do something like: Person adam = new Person();
In JavaScript you would do: var adam = new Person();

Other than the fact that Java is strongly typed and you need to declare that adam is of type Person, the syntax looks the same. JavaScript’s constructor invocation looks as if Person were a class, but it’s important to keep in mind that Person is still just a function. The similarity in syntax has led many developers to think about JavaScript in terms of classes and to develop ideas and inheritance patterns that assume classes. Such implementations we can call “classical.” Let’s also say that “modern” are any other patterns that do not require you to think about classes.
除了 Java 是强类型语言需要给 adam 添加类型 Person 外,其它的语法看起来是一样的。JavaScript 的创建函数调用看起来感觉 Person 是一个类,但事实上,Person 仅仅是一个函数。语法上的相似使得非常多的开发者陷入对 JavaScript 类的思考,并且给出了很多模拟 类的继承方案。这样的实现方式,我们叫它 “类式继承”。顺便也提一下,所谓 “现代” 继承模式是指那些不需要你去想类这个概念的模式。

Classical Inheritance: 类式继承

Expected Outcome When Using Classical Inheritance: 类式继承的期望结果

Although the discussion is about classical patterns, let’s avoid using the word “class.” Saying “constructor function” or “constructor” is longer, but it’s accurate and not ambiguous. In general, strive for eliminating the word “class” when communicating within your team, because when it comes to JavaScript, the word may mean different things to different people.
尽管我们是在讨论类式继承,但还是尽量避免使用 “类” 这个词。“构造函数” 或者 “constructor” 虽然更长,但是更准确,不会让人迷惑。通常情况下,应该努力避免在跟团队沟通的时候使用 “类” 这个词,因为在 JavaScript 中,很可能每个人都会有不同的理解。

Here’s an example of defining the two constructors Parent() and Child():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// the parent constructor 
function Parent(name) {
this.name = name || 'Adam';
}

// adding functionality to the prototype
Parent.prototype.say = function () {
return this.name;
};

// empty child constructor
function Child(name) {}

// inheritance magic happens here
inherit(Child, Parent);

Here you have the parent and child constructors, a method say() added to the parent constructor’s prototype, and a call to a function called inherit() that takes care of the inheritance. The inherit() function is not provided by the language, so you have to implement it yourself. Let’s see several approaches to implementing it in a generic way.
上面的代码定义了两个构造函数 Parent() 和 Child(),say() 方法被添加到了 Parent() 构建函数的原型(prototype) 中,inherit() 函数完成了继承的工作。inherit() 函数并不是原生提供的,需要自己实现。让我们来看一看比较大众的实现它的几种方法。

Classical Pattern #1—The Default Pattern: 默认模式

1
2
3
4
5
6
function inherit(C, P) {
C.prototype = new P();
}

var kid = new Child();
kid.say(); // "Adam"

It’s important to remember that the prototype property should point to an object, not a function, so it has to point to an instance (an object) created with the parent con- structor, not to the constructor itself. In other words, pay attention to the new operator, because you need it for this pattern to work.
需要强调的是原型(prototype 属性)应该指向一个对象而不是函数,所以它需要指向由父构造函数创建的实例(对象),而不是构造函数自己。换 句话说,请注意 new 运算符,有了它这种模式才可以正常工作。

Following the Prototype Chain: 跟踪原型链

Using this pattern you inherit both own properties (instance-specific properties added to this, such as name) and prototype properties and methods (such as say()).
在这种模式中,子对象既继承了(父对象)“自己的属性”(添加给 this 的实例属性,比如 name),也继承了原型中的属性和方法(比如 say())。

Drawbacks When Using Pattern #1: 默认模式的缺点

One drawback of this pattern is that you inherit both own properties added to this and prototype properties. Most of the time you don’t want the own properties, because they are likely to be specific to one instance and not reusable.
这种模式的一个缺点是既继承了(父对象)“自己的属性”,也继承了原型中的属性。大部分情况下你可能并不需要 “自己的属性”,因为它们更可能是为实例对象添加的,并不用于复用。

Another thing about using a generic inherit() function is that it doesn’t enable you to pass parameters to the child constructor, which the child then passes to the parent. Consider this example:
在使用这个 inherit() 函数时另外一个不便是它不能够让你传参数给子构造函数,这些参数有可能是想再传给父构造函数的。考虑下面的例子:

1
2
var s = new Child('Seth');
s.say(); // "Adam"

This is not what you’d expect. It’s possible for the child to pass parameters to the parent’s constructor, but then you have to do the inheritance every time you need a new child, which is inefficient, because you end up re-creating parent objects over and over.
这并不是我们期望的结果。事实上传递参数给父构造函数是可能的,但这样需要在每次需要一个子对象时再做一次继承,很不方便,因为需要不断地创建父对象。

Classical Pattern #2—Rent-a-Constructor: 借用构造函数

1
2
3
function Child(a, c, b, d) {
Parent.apply(this, arguments); // 因为 arguments 是类似数组的元素,所以可以这样直接传递
}

This way you can only inherit properties added to this inside the parent constructor. You don’t inherit members that were added to the prototype.
使用这种模式时,只能继承在父对象的构造函数中添加到 this 的属性不能继承原型上的成员

Using the borrowed constructor pattern, the children objects get copies of the inherited members, unlike the classical #1 pattern where they only get references. The following example illustrates the difference:
使用借用构造函数的模式,子对象通过复制的方式继承父对象的成员,而不是像类式继承 1 中那样获得引用。下面的例子展示了这两者的不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// a paren constructor
function Article() {
this.tags = ['js', 'css'];
}

var article = new Article();

// 默认模式
function BlogPost(){}

BlogPost.prototype = article;

var blog = new BlogPost();

// 借用构造函数
function StaticPage() {
Article.call(this);
}

var page = new StaticPage();

console.log(article.hasOwnProperty('tags')); //true
console.log(blog.hasOwnProperty('tags')); //false
console.log(page.hasOwnProperty('tags')); //true

Note the difference when modifying the inherited tags property:

1
2
3
blog.tags.push('html');
page.tags.push('php');
console.log(article.tags.join(', ')); // "js, css, html"

In this example the child blog object modifies the tags property, and this way it also modifies the parent because essentially both blog.tags and article.tags point to the same array. Changes to page.tags don’t affect the parent article because page.tags is a separate copy created during inheritance.
在这个例子中,blog 对象修改了 tags 属性,同时,它也修改了父对象,因为实际上 blog.tags 和 article.tags 是引向同一个数组。而对 pages.tags 的修改并不影响父对象 article,因为 pages.tags 在继承的时候是一份独立的拷贝

Multiple Inheritance by Borrowing Constructors: 利用借用构造函数模式实现多继承

Using the borrowing constructors patterns, it’s possible to implement multiple inheritance simply by borrowing from more than one constructor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Cat() {
this.legs = 6;
this.say = function () {
return 'www';
}
}

function Bird() {
this.wings = 2;
this.fly = true;
}

function CatWings() {
Cat.apply(this);
Bird.apply(this);
}

var jane = new CatWings();
console.dir(jane);

The result is shown in Figure 6-5. Any duplicate properties will be resolved by having the last one win.
任何重复的属性都会以最后的一个值为准。

Pros and Cons of the Borrowing Constructor Pattern: 借用构造函数的利与弊

The drawback of this pattern is obviously that nothing from the prototype gets inherited and, as mentioned before, the prototype is the place to add reusable methods and properties, which will not be re-created for every instance.
这种模式的一个明显的弊端就是无法继承原型。如前面所说,原型往往是添加可复用的方法和属性的地方,这样就不用在每个实例中再创建一遍。

A benefit is that you get true copies of the parent’s own members, and there’s no risk that a child can accidentally overwrite a parent’s property.
这种模式的一个好处是获得了父对象自己成员的拷贝,不存在子对象意外改写父对象属性的风
险。

So how can the children inherit prototype properties too, in the previous case, and how can kid get access to the say() method? The next pattern addresses this question.
那么,在上一个例子中,怎样使一个子对象也能够继承原型属性呢?怎样能 使 kid 可以访问到 say() 方法呢?下一种继承模式解决了这个问题。

Classical Pattern #3—Rent and Set Prototype: 类式继承 3——借用并设置原型

Combining the previous two patterns, you first borrow the constructor and then also set the child’s prototype to point to a new instance of the constructor:
综合以上两种模式,首先借用父对象的构造函数,然后将子对象的原型设置为父对象的一个新实例:

1
2
3
4
5
function Child(a, c, b, d) {
Parent.apply(this, arguments);
}

Child.prototype = new Parent();

The benefit is that the result objects get copies of the parent’s own members and references to the parent’s reusable functionality (implemented as members of the prototype). The child can also pass any arguments to the parent constructor. This behavior is probably the closest to what you’d expect in Java; you inherit everything there is in the parent, and at the same time it’s safe to modify own properties without the risk of modifying the parent.
这样做的好处是子对象获得了父对象自己的成员,也获得了父对象中可复用的(在原型中实现的)方法。子对象也可以传递任何参数给父构造函数。这种行为可能是最接近 Java 的,子对象继承了父对象的所有东西,同时可以安全地修改自己的属性而不用担心修改到父对象

A drawback is that the parent constructor is called twice, so it could be inefficient. At the end, the own properties (such as name in our case) get inherited twice.
一个弊端是父构造函数被调用了两次,所以不是很高效。最后,(父对象) 自己的属性(比如这个例子中的 name)也被继承了两次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent(name) {
this.name = name || 'rayjune';
}

Parent.prototype.say = function() {
return this.name;
};

// child constructor
function Child(name) {
Parent.apply(this, arguments);
}

Child.prototype = new Parent();

var kid = new Child('liga');
kid.name;
delete kid.name;
kid.say(); //liga

Classical Pattern #4—Share the Prototype: 类式继承 4——共享原型

The rule of thumb was that reusable members should go to the prototype and not this. Therefore for inheritance purposes, anything worth inheriting should be in the prototype. So you can just set the child’s prototype to be the same as the parent’s prototype:
一般的经验是将可复用的成员放入原型中而不是 this。从继承的角度来看,则是任何应该被继承的成员都应该放入原型中。这样你只需要设定子对象的原型和父对象的原型一样即可:

1
2
3
function inherit(C, P) {
C.prototype = P.prototype;
}

This gives you short and fast prototype chain lookups because all objects actually share the same prototype. But that’s also a drawback because if one child or grandchild somewhere down the inheritance chain modifies the prototype, it affects all parents and grandparents.
这种模式的原型链很短并且查找很快,因为所有的对象实际上共享着同一个原型。但是这样也有弊端,那就是如果子对象或者在继承关系中的某个地方的任何一个子对象修改这个原型,将影响所有的继承关系中的父对象。

Classical Pattern #5—A Temporary Constructor: 类式继承 5——临时构造函数

1
2
3
4
5
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
}
Storing the Superclass: 存储父类(Superclass)

Building on top of the previous pattern, you can add a reference to the original parent. This is like having access to the superclass in other languages and could be handy on occasion.
在上一种模式的基础上,还可以添加一个指向原始父对象的引用。这很像其它语言中访问超类(superclass)的情况,有时候很方便。

The property is called uber because “super” is a reserved word and “superclass” may lead the unsuspecting developer down the path of thinking that JavaScript has classes. Here’s an improved implementation of this classical pattern:
我们将这个属性命名为 “uber”,因为 “super” 是一个保留字,而 “superclass” 则可能误导别人认为 JavaScript 拥有类。下面是这种类式继 承模式的一个改进版实现:

1
2
3
4
5
6
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
}
Resetting the Constructor Pointer: 重置构造函数引用

One last thing to add to this almost perfect classical inheritance function is to reset the pointer to the constructor function in case you need it down the road.
这个近乎完美的模式上还需要做的最后一件事情就是重置构造函数 (constructor)的指向,以便未来在某个时刻能被正确地使用。

If you don’t reset the pointer to the constructor, then all children objects will report that Parent() was their constructor, which is not useful. So using the previous imple- mentation of inherit(), you can observe this behavior:
如果不重置构造函数的指向,那所有的子对象都会认为 Parent() 是它们的构 造函数,而这个结果完全没有用。使用前面的 inherit() 的实现,你可以观察到这种行为:

1
2
3
4
5
6
7
8
9
// parent, child, inheritance 
function Parent() {}
function Child() {}
inherit(Child, Parent);

// testing the waters
var kid = new Child();
kid.constructor.name; // "Parent"
kid.constructor === Parent; // true

The constructor property is rarely used but could be convenient for runtime introspection of objects. You can reset it to point to the expected constructor function with- out affecting the functionality because this property is mostly informational.
constructor 属性很少用,但是在运行时检查对象很方便。你可以重新将它指 向期望的构造函数而不影响功能,因为这个属性更多是 “信息性” 的。

The final Holy Grail version of this classical inheritance pattern will look like so:

1
2
3
4
5
6
7
function inherit(C, P) {
var F = function () {};
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
C.prototype.constructor = C;
}

If you decide that this is the best approach for your project.

A common optimization of the Holy Grail pattern is to avoid creating the temporary (proxy) constructor every time you need inheritance. It’s sufficient to create it once and only change its prototype. You can use an immediate function and store the proxy function in its closure:
一种常见的对 Holy Grail 模式的优化是避免每次需要继承的时候都创建一个临时(代理)构造函数。事实上创建一次就足够了,以后只需要修改它的原型即可。你可以用一个立即执行的函数来将代理函数存储到闭包中:

1
2
3
4
5
6
7
8
9
var inherit = (function () { 
var F = function () {};
return function (C, P) {
F.prototype = P.prototype;
C.prototype = new F();
C.uber = P.prototype;
C.prototype.constructor = C;
}
}());

Prototypal Inheritance: 原型继承(现代继承)

Let’s start the discussion of “modern” classless patterns with a pattern called prototypal inheritance. In this pattern there are no classes involved; here objects inherit from other objects. You can think about it this way: you have an object that you would like to reuse and you want to create a second object that gets its functionality from the first one.
现在,让我们从一个叫作 “原型继承” 的模式来讨论没有类的现代继承模式。这种模式中,没有任何类进来,在这里,一个对象继承自另外一个对象。你可以这样理解它:你有一个想复用的对象,然后你想创建第二个对象,并且获得第一个对象的功能。下面是这种模式的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 需要继承的对象
var parent = {
name: "Papa"
};

function object(o) {
function F() {}
F.prototype = o;
return new F();
}

// the new object
var child = object(parent);

// test console.logg
console.log(child.name); // "Papa"

Similarly to the classical Holy Grail, you would use an empty temporary constructor function F(). You then set the prototype of F() to be the parent object. Finally, you return a new instance of the temporary constructor:
与 Holy Grail 类式继承相似,可以使用一个空的临时构造函数 F(),然后设 定 F() 的原型为 parent 对象。最后,返回一个临时构造函数的新实例。

1
2
3
4
5
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

Here child always starts as an empty object, which has no properties of its own but at the same time has all the functionality of its parent by benefiting from the __proto__ link.
child 总是以一个空对象开始,它没有自己的属性但通过原型链(__proto__)拥有父对象的所有功能

In the prototypal inheritance pattern, your parent doesn’t need to be created with the literal notation (although that is probably the more common way). You can have con- structor functions create the parent. Note that if you do so, both “own” properties and properties of the constructor’s prototype will be inherited:
在原型继承模式中,parent 不需要使用对象字面量来创建。(尽管这是一种更直觉的方式。)可以使用构造函数来创建 parent 。注意,如果你这样做,那么自己的属性和原型上的属性都将被继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// parent constructor 
function Person() {
// an "own" property
this.name = "Adam";
}
// a property added to the prototype
Person.prototype.getName = function () {
return this.name;
};

// create a new person
var papa = new Person(); // inherit
var kid = object(papa);

// test that both the own property
// and the prototype property were inherited
kid.getName(); // "Adam"

In another variation of this pattern you have the option to inherit just the prototype object of an existing constructor. Remember, objects inherit from objects, regardless of how the parent objects were created. Here’s an illustration using the previous example, slightly modified:
在这种模式的另一个变种中,你可以选择只继承已存在的构造函数的原型对象。记住,对象继承自对象,不管父对象是怎么创建的。这是前面例子的一个修改版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// parent constructor 
function Person() {
// an "own" property
this.name = "Adam";
}
// a property added to the prototype
Person.prototype.getName = function () {
return this.name;
};

// 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

Addition to ECMAScript 5: 例外的 ECMAScript 5

In ECMAScript 5, the prototypal inheritance pattern becomes officially a part of the language. This pattern is implemented through the method Object.create(). In other words, you won’t need to roll your own function similar to object(); it will be built into the language:
在 ECMAScript 5 中,原型继承已经正式成为语言的一部分。这种模式使用 Object.create 方法来实现。换句话说,你不再需要自己去写类似 object() 的函数,它是语言原生的了:

1
var child = Object.create(parent);

Object.create() accepts an additional parameter, an object. The properties of the extra object will be added as own properties of the new child object being returned. This is a convenience that enables you to inherit and build upon the child object with one method call. For example:
Object.create() 接收一个额外的参数——一个对象。这个额外对象中的属性将被作为自己的属性添加到返回的子对象中。这让我们可以很方便地将继承和创建子对象在一个方法调用中实现。例如:

1
2
3
4
var child = Object.create(parent, {
age: {value: 2}
});
child.hasOwnProperty('age'); //true

Inheritance by Copying Properties: 通过复制属性继承

Let’s take a look at another inheritance pattern—inheritance by copying properties. In this pattern, an object gets functionality from another object, simply by copying it. Here’s an example implementation of a sample function extend() that does that:
让我们来看一下另外一种继承模式——通过复制属性继承。在这种模式中,一个对象通过简单地复制另一个对象来获得功能。下面是一个简单的实现这种功能的 extend() 函数:

1
2
3
4
5
6
7
8
9
10
function extend(parent, child) {
var i;
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}

It’s a simple implementation, just looping through the parent’s members and copying them over. In this implementation child is optional; if you don’t pass an existing object to be augmented, then a brand new object is created and returned:
这是一个简单的实现(浅拷贝),仅仅是遍历了父对象的成员然后复制它们。在这个实现中,child 是可选参数,如果它没有被传入一个已有的对象,那么一个全新的对象将被创建并被返回:

1
2
3
var dad = {name: "Adam"}; 
var kid = extend(dad);
kid.name; // "Adam"

The implementation given is a so-called “shallow copy” of the object. A deep copy on the other hand would mean checking if the property you’re about to copy is an object or an array, and if so, recursively iterating through its properties and copying them as well. With the shallow copy (because objects are passed by reference in JavaScript), if you change a property of the child, and this property happens to be an object, then you’ll be modifying the parent as well. This is actually preferable for methods (as functions are also objects and are passed by reference) but could lead to surprises when working with other objects and arrays. Consider this:
上面给出的实现叫作对象的 “浅拷贝”(shallow copy)。另一方面,“深拷贝” 是指检查准备复制的属性本身是否是对象或者数组,如果是也遍历它们的属性并复制。如果使用浅拷贝的话(因为在 JavaScript 中对象是按引用传递),如果你改变子对象的一个属性,而这个属性恰好是一个对象, 那么你也会改变父对象。实际上这对方法来说可能很好(因为函数也是对象,也是按引用传递),但是当遇到其它的对象和数组的时候可能会有些意外情况:

1
2
3
4
5
6
7
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};
var kid = extend(dad);
kid.counts.push(4);
dad.counts.toString(); // "1,2,3,4" dad.reads === kid.reads; // true

Now let’s modify the extend() function to make deep copies. All you need is to check if a property’s type is an object, and if so, recursively copy its properties. Another check you need is if the object is a true object or if it’s an array. Let’s use the check for array-ness discussed in Chapter 3. So the deep copy version of extend() would look like so:
现在让我们来修改一下 extend() 函数以便做深拷贝。所有你需要做的事情只是检查一个属性的类型是否是对象,如果是 , 则递归遍历它的属性。另外一个需要做的检查是这个对象是真的对象还是数组。我们可以使用第 3 章讨论过的数组检查方式。最终深拷贝版的 extend() 是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function extendDeep(parent, child) {
var i;
var toStr = Object.prototype.toString;
var astr = '[object Array]';
child = child || {};

for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === 'object') {
child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
extendDeep(parent[i], child[i]);
} else {
child[i] = parent[i];
}
}
}
return child;
}

Now testing the new implementation gives us true copies of objects, so child objects don’t modify their parents:
现在测试时这个新的实现给了我们对象的真实拷贝,所以子对象不会修改父对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var dad = {
counts: [1, 2, 3],
reads: {paper: true}
};
var kid = extendDeep(dad);

kid.counts.push(4);
kid.counts.toString(); // "1,2,3,4"
dad.counts.toString(); // "1,2,3"

dad.reads === kid.reads; // false
kid.reads.paper = false;

kid.reads.web = true;
dad.reads.paper; // true

It’s worth noting that there are no prototypes involved in this pattern at all; it’s only about objects and their own properties.
这种模式并不高深,因为根本没有原型牵涉进来,而只跟对象和它们的属性有关。

Mix-ins: 掺元(Mix-ins)

Taking the idea of inheritance by property copying a step further, let’s consider a “mix-in” pattern. Instead of copying from one object, you can copy from any number of objects and mix them all into a new object.
既然谈到了通过复制属性来继承,就让我们顺便多说一点,来讨论一下 “掺元” 模式。除了前面说的从一个对象复制,你还可以从任意多数量的对象中复制属性,然后将它们混在一起组成一个新对象。

The implementation is simple; just loop through arguments and copy every property of every object passed to the function:
实现很简单,只需要遍历传入的每个参数然后复制它们的每个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function mix() {
var arg;
var prop;
var child = {};
for (arg = 0; arg < arguments.length; arg += 1) {
for (prop in arguments[arg]) {
if (arguments[arg].hasOwnProperty(prop)) {
child[prop] = arguments[arg][prop];
}
}
}
return child;
}

var cake = mix(
{eggs: 2, large: true},
{butter: 1, salted: true},
{flour: "3 cups"},
{sugar: "sure!"}
);
console.dir(cake);

Here we simply loop, copy own properties, and break the link with the parent(s).
这里我们只是简单地遍历、复制自己的属性,并没有与父对象的链接。

Borrowing Methods: 借用方法

Sometimes it may happen that you only like one or two methods of an existing object. You want to reuse them, but you don’t really want to form a parent-child relationship with that object. You want to use just the methods you like, without inheriting all the other methods that you’ll never need. This is possible with the borrowing methods pattern, which benefits from the function methods call() and apply(). You’ve seen this pattern already in the book and even in this chapter in the implementation of extendDeep(), for example.
有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。受益于函数方法 call() 和 apply(),通过借用方法模式,这是可行的。在本书中,你其实已经见过这种模式了,甚至在本章 extendDeep() 的实现中也有用到。

1
2
3
// call() example
notmyobj.doStuff.call(myobj, param1, p2, p3); // apply() example notmyobj.doStuff.apply(myobj, [param1, p2, p3]);
Array.prototype.slice.call(arguments);

Borrow and Bind: 借用并绑定

When borrowing methods either through call()/apply() or through simple assignment, the object that this points to inside of the borrowed method is determined based on the call expression. But sometimes it’s best to have the value of this “locked” or bound to a specific object and predetermined in advance.
当借用方法的时候,不管是通过 call()/apply() 还是通过简单的赋值,方法中的 this 指向的对象都是基于调用的表达式来决定的。但是有时候最好的使用方式是将 this 的值锁定或者提前绑定到一个指定的对象上。

Just like this:

1
2
3
4
5
function bind(o, m) {
return function () {
return m.apply(o, [].slice.call(arguments));
};
}

This bind() function accepts an object o and a method m, binds the two together, and then returns another function. The returned function has access to o and m via a closure. Therefore even after bind() returns, the inner function will have access to o and m, which will always point to the original object and method. Let’s create a new function using bind():
这个 bind() 函数接受一个对象 o 和一个方法 m,然后把它们绑定在一起,再返回另一个函数。返回的函数通过闭包可以访问到 o 和 m。也就是说,即使在 bind() 返回之后,内层的函数仍然可以访问到 o 和 m,而 o 和 m 会始终指向 原始的对象和方法。

The price you pay for the luxury of having a bind is the additional closure.
绑定是奢侈的,你需要付出的代价是一个额外的闭包。

Function.prototype.bind()

ECMAScript 5 adds a method bind() to Function.prototype, making it just as easy to use as apply() and call(). So you can do expressions like:

1
var newFunc = obj.someFunc.bind(myobj, 1, 2, 3);

Let’s see how you can implement Function.prototype.bind() when your program runs in pre-ES5 environments:

1
2
3
4
5
6
7
8
9
10
if (typeof Function.prototype.bind === 'undefined') {
Function.prototype.bind = function (thisArg) {
var fn = this;
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
return function () {
return fn.apply(thisArg, args.concat(slice.call(arguments)));
};
};
}

Summary

There are many options available when it comes to inheritance in JavaScript. It’s beneficial to study and understand the different patterns because they help improve your grasp of the language. In this chapter you learned about several classical and several modern patterns to approach inheritance.
在 JavaScript 中,继承有很多种方案可以选择。学习和理解不同的模式是有好处的,因为这可以增强你对这门语言的掌握能力。在本章中你看到了很多类式继承和现代继承的方案。

However, inheritance is probably not a problem you face often during development. This is partially due to the fact that this problem is probably already solved in some way or another by the library you use—and partially because it’s rare that you need to establish long and complicated inheritance chains in JavaScript. In static strongly typed languages, inheritance may be the only way to reuse code. In JavaScript you may often have much simpler and elegant ways, including borrowing methods, binding them, copying properties, and mixing-in properties from several objects.
但是,也许在开发过程中继承并不是你经常面对的一个问题。这一部分是因为这个问题已经被使用某种方式或者某个你使用的类库解决了,另一部分是因为你不需要在 JavaScript 中建立很长很复杂的继承链。在静态强类型语言中,继承可能是唯一可以利用代码的方法,但在 JavaScript 中你可能有多更简单更优化的方法,包括借用方法、绑定、复制属性、掺元等

Remember that code reuse is the goal, and inheritance is just one of the ways to accomplish that goal.
记住,代码复用才是目标,继承只是达成这个目标的一种手段

DOM and Browser Patterns: DOM 和浏览器中的模式

Separation of Concerns: 分离

In practice, the separation of concerns means:

  • Testing the page with CSS turned off to see if the page is still usable and the content is present and readable
  • Testing with JavaScript turned off and making sure the page can still perform its main purpose, all links work (no instances of href=”#”), and any forms still work and submit properly
  • Not using inline event handlers (such as onclick) or inline style attributes, because these do not belong to the content layer
  • Using semantically meaningful HTML elements such as headings and lists

DOM Scripting: DOM 编程

Working with the DOM tree of a page is one of the most common tasks in client-side JavaScript. This is also a major cause of headaches (and gives JavaScript a bad name) because the DOM methods are inconsistently implemented across browsers. That’s why using a good JavaScript library, which abstracts the browser differences, can significantly speed up development.
操作页面的 DOM 树是在客户端 JavaScript 编程中最普遍的动作。这也是导致开发者头疼的最主要原因(这也导致了 JavaScript 名声不 好),因为 DOM 方法在不同的浏览器中实现得有很多差异。这也是为什么使用一个抽象了浏览器差异的 JavaScript 库能显著提高开发速度的原因。

DOM Access: DOM 访问

DOM access is expensive; it’s the most common bottleneck when it comes to JavaScript performance. This is because the DOM is usually implemented separately from the JavaScript engine. From a browser’s perspective, it makes sense to take this approach, because a JavaScript application may not need DOM at all. And also languages other than JavaScript (for example, VBScript in IE) can be used to work with the page’s DOM.
DOM 操作性能不好,这是影响 JavaScript 性能的最主要原因。性能不好是因为浏览器的 DOM 实现通常是和 JavaScript 引擎分离的。 从浏览器的角度来讲,这样做是很有意义的,因为有可能一个 JavaScript 应用根本不需要 DOM,而除了 JavaScript 之外的其它语言(如 IE 的 VBScript)也可以用来操作页面中的 DOM。

The bottom line is that DOM access should be reduced to minimum. This means:
一个原则就是 DOM 访问的次数应该被减少到最低,这意味者:

  • Avoiding DOM access in loops 避免在循环中访问 DOM
  • Assigning DOM references to local variables and working with the locals 将 DOM 引用赋给本地变量,然后操作本地变量
  • Using selectors API where available 尽可能使用 selectors API
  • Caching the length when iterating over HTML collections 遍历 DOM 节点时缓存长度

Consider the following example where the second (better) loop, despite being longer, will be tens to hundreds of times faster, depending on the browser:

1
2
3
4
5
6
7
8
9
10
11
// antipattern
for (var i = 0; i < 100; i += 1) {
document.getElementById("result").innerHTML += i + ", ";
}

// better - update a local variable
var i, content = "";
for (i = 0; i < 100; i += 1) {
content += i + ",";
}
document.getElementById("result").innerHTML += content;

In the next snippet, the second example (using a local variable style) is better, although it requires one more line of code and one more variable:

1
2
3
4
5
6
7
8
// antipattern
var padding = document.getElementById("result").style.padding;
var margin = document.getElementById("result").style.margin;

// better
var style = document.getElementById("result").style;
var padding = style.padding;
var margin = style.margin;

Using selector APIs means using the methods:

1
2
document.querySelector("ul .selected"); 
document.querySelectorAll("#widget .class");

It will also help if you add id=”” attributes to elements you’ll be accessing often, because document.getElementById(myid) is the easiest and fastest way to find a node.
给你经常访问的元素加上一个 id 属性也是有好处的,因为 document.getElementById(myid) 是找到一个 DOM 元素最容易也是最快的方法。

DOM Manipulation: DOM 操作

In addition to accessing the DOM elements, you often need to change them, remove some of them, or add new ones. Updating the DOM can cause the browser to repaint the screen and also often reflow (recalculate elements’ geometry), which can be expensive.
除了访问 DOM 元素之外,你可能经常需要改变它们、删除其中的一些或者是添加新的元素。更新 DOM 会导致浏览器重绘(repaint)屏幕,也经常导致重排 (reflow)(重新计算元素的位置),这些操作代价是很高的。

Again, the general rule of thumb is to have fewer DOM updates, which means batching changes and performing them outside of the “live” document tree.
再说一次,通用的原则仍然是尽量少地更新 DOM,这意味着我们可以将变化集中到一起,然后在 “活动的”(live)文档树之外去执行这些变化

When you need to create a relatively big subtree, you should do so without adding to the live document until the end. For this purpose you can use a document fragment to contain all your nodes.
当你需要添加一棵相对较大的子树的时候,你应该在完成这棵树的构建之后再放到文档树中。为了达到这个目的,你可以使用文档碎片(document fragment)来包含你的节点。

Here’s how not to append nodes:

1
2
3
4
5
6
7
8
9
10
11
12
// antipattern
// appending nodes as they are created
var p, t;
p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
document.body.appendChild(p);

p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
document.body.appendChild(p);

A better version will be to create a document fragment, update it “offline,” and add it to the live DOM when it’s ready. When you add a document fragment to the DOM tree, the content of the fragment gets added, not the fragment itself. And this is really convenient. So the document fragment is a good way to wrap a number of nodes even when you’re not containing them in a suitable parent (for example, your paragraphs are not in a div element).
一个更好的版本是创建一个文档碎片,然后“离线地” 更新它,当它准备好之后再将它加入文档树中。当你将文档碎片添加到 DOM 树中时,碎片的内容将会被添加进去,而不是碎片本身。这个特性非常好用。所以当有好几个没有被包裹在同一个父元素的节点时,文档碎片(document.createDocumentFragment();)是一个很好的包裹方式。

Here’s an example of using a document fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var p, t, frag;

frag = document.createDocumentFragment();
p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
frag.appendChild(p);

p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
frag.appendChild(p);

document.body.appendChild(frag);

In this example the live document is updated only once, causing a single reflow/repaint, as opposed to one for every paragraph, as was the case in the previous antipattern snippet.
这个例子和前面例子中每段更新一次相比,文档树只被更新了一下,只导致一次重排 / 重绘。

The document fragment is useful when you add new nodes to the tree. But when you update an existing part of the tree, you can still batch changes. You can make a clone of the root of the subtree you’re about to change, make all the changes to the clone, and then when you’re done, swap the original with the clone:
当你添加新的节点到文档中时,文档碎片很有用。当你需要更新已有的节点时,你也可以将这些变化集中。你可以将你要修改的子树的父节点克隆一份,然后对克隆的这份做修改,完成之后再去替换原来的元素

1
2
3
4
5
6
var oldnode = document.getElementById('result'),
clone = oldnode.cloneNode(true);
// work with the clone...

// when you're done:
oldnode.parentNode.replaceChild(clone, oldnode);

Events: 事件

Another area of browser scripting that is full of inconsistencies and a source of frustration is working with the browser events, such as click, mouseover, and so on. Again, a JavaScript library can take away much of the double work that needs to be done to support IE (before version 9) and the W3C-conforming implementations.
在浏览器脚本编程中,另一块充满兼容性问题并且带来很多不愉快的区域就是浏览器事件,比如 click,mouseover 等等。同样的,一个 JavaScript 库可以解决支持 IE(9 以下)和 W3C 标准实现的双倍工作量。

Let’s go over the main points, because you may not always be using an existing library for simple pages and quick hacks, or you may be creating your own library.
我们来看一下一些主要的点,因为你在做一些简单的页面或者快速开发的时候可能不会使用已有的库,当然,也有可能你正在写你自己的库。

Event Handling: 事件处理

需要对一个元素添加多个处理函数时用 addEventListener 即可。

Event Delegation: 事件委托(强烈推荐)

The event delegation pattern benefits from the event bubbling and reduces the number of event listeners attached to separate nodes. If there are 10 buttons inside a div element, you can have one event listener attached to the div as opposed to 10 listeners attached to each button.
事件委托是通过事件冒泡来实现的,它可以减少分散到各个节点上的事件处理函数的数量。如果有 10 个按钮在一个 div 元素中,你可以给 div 绑定一个事件处理函数,而不是给每个按钮都绑定一个。

1
2
3
4
5
<div id="click-wrap">
<button>Click me: 0</button>
<button>Click me too: 0</button>
<button>Click me three: 0</button>
</div>

Instead of attaching listeners to each button, you attach one to the “click-wrap” div. Then you can use the same myHandler() function from the previous example with only one little change: you have to filter out clicks you’re not interested in. In this case you look only for clicks on any button, so all other clicks in the same div could be ignored.
你可以给包裹按钮的 div 绑定一个事件处理函数,而不是给每个按钮绑定一个。然后你可以使用和前面的示例中一样的 myHandler() 函数,但需 要修改 一个小地方:你需要将你不感兴趣的点击排除掉。在这个例子中,你只关注按钮上的点击,而在同一个 div 中产生的其它的点击应该被忽略掉。

The change to myHandler() would be to check if the nodeName of the source of the event is a “button”:
myHandler() 的改变就是检查事件来源的 nodeName 是不是 “button”:

1
2
3
4
5
6
7
8
// ...
// get event and source element e = e || window.event;
src = e.target || e.srcElement;

if (src.nodeName.toLowerCase() !== "button") {
return;
}
// ...

The drawback of the event delegation is the slightly more code to filter out the events that happen in the container that are not interesting for you. But the benefits— performance and cleaner code—outweigh the drawbacks significantly, so it’s a highly recommended pattern.
事件委托的坏处是筛选容器中感兴趣的事件使得代码看起来更多了,但好处是性能的提升和更干净的代码,这个好处明显大于坏处,因此这是一种强烈推荐的模式

Modern JavaScript libraries make it easy to use event delegation by providing convenient APIs.
主流的 JavaScript 库通过提供方便的 API 的方式使得使用事件委托变得很容易。

Long-Running Scripts: 长时间运行的脚本

You have noticed that sometimes the browser complains that a script has been running for too long and asks the user if the script should be stopped. You don’t want this to happen in your application, no matter how complicated your task is.

Also, if the script works too hard, the browser’s UI becomes unresponsive to the point where the user cannot click anything. This is bad for the experience and should be avoided.

In JavaScript there are no threads, but you can simulate them in the browser by using setTimeout() or, in more modern browsers, web workers.

setTimeout()

The idea is to split a big amount of work into smaller chunks and perform each chunk with a timeout of 1ms. Using 1ms timeout chunks can cause the task to be completed slower overall, but the user interface will remain responsive, and the user is more comfortable and in control of the browser.
它的思想是将一大堆工作分解成为一小段一小段,然后每隔 1 毫秒运行一段。 使用 1 毫秒的延迟会导致整个任务完成得更慢,但是用户界面会保持可响应状态,用户会觉得浏览器没有失控,觉得更舒服。

Timeout of 1 (or even 0 milliseconds) will actually become more than that, depending on the browser and the operating system. Setting a 0 timeout doesn’t mean right away, but rather “as soon as possible.” In Internet Explorer, for example, the shortest clock “tick” is 15 milliseconds.
1 毫秒(甚至 0 毫秒)的延迟执行命令在实际运行的时候会延迟更多,这取 决于浏览器和操作系统。设定 0 毫秒的延迟并不意味着马上执行,而是指 “尽 快执行”。比如,在 IE 中,最短的延迟是 15 毫秒。

Web Workers

Recent browsers offer another solution to long-running scripts: web workers. Web workers provide background thread support in the browser. You put your heavy computations in a separate file, for example, my_web_worker.js, and then call it from the main program (page) like so:
现代浏览器为长时间运行的脚本提供了另一种解决方案:web workers。web workers 在浏览器内部提供了后台线程支持,你可以将计算量很大的部分放 到一个单独的文件中,比如 my_web_worker.js,然后从主程序(页面)中这 样调用它:

Remote Scripting: 远程脚本编程

Today’s web applications often use remote scripting to communicate with the server without reloading the current page. This enables for much more responsive and desktop-like web applications. Let’s consider a few ways to communicate with the server from JavaScript.
现代 web 应用经常会使用远程脚本编程和服务器通讯,而不刷新当前页面。 这使得 web 应用更灵活,更像桌面程序。我们来看一下几种用 JavaScript 和服务器通讯的方法。

XMLHttpRequest

XMLHttpRequest is a special object (a constructor function) available in most browsers today, which lets you make an HTTP request from JavaScript. There are three steps to making a request:

  1. Set up an XMLHttpRequest object (called XHR for short).
  2. Provide a callback function to be notified when the request object changes state.
  3. Send the request.
1
var x = new XMLHttpRequest();

然后给 readystatechange 事件提供一个回调函数::

1
xhr.onreadystatechange = handleResponse;

The last step is to fire off the request—using two methods open() and send(). The open() method sets up the HTTP request method (for example, GET, POST) and the URL. The send() method passes any POST data or just a blank string in the case of GET. The last parameter to open() specifies whether the request is asynchronous. Asynchronous means that the browser will not block waiting for the response. This is definitely the better user experience, so unless there’s a strong reason against it, the asynchronous parameter should always be true:
最后一步是使用 open() 和 send() 两个方法触发请求。open() 方法用于初始化 HTTP 请求的方法(如 GET,POSThe response to an XHR request can be any type of document:T)和 URL。 send() 方法用于传递 POST 的数据,如果是 GET 方法,则是一个空字符串。open() 方法的最后一个参数用于 指定这个请求是不是异步的。异步是指 浏览器在等待响应的时候不会阻塞,这明显是更好的用户体验,因此除非必须要同步,否则异步参数应该使用 true

1
2
xhr.open("GET", "page.html", true);
xhr.send();

JSONP

JSONP (JSON with padding) is another way to make remote requests. Unlike XHR, it’s not restricted by the same-domain browser policy, so it should be used carefully because of the security implications of loading data from third-party sites.
JSONP(JSON with padding)是另一种发起远程请求的方式。与 XHR 不同,它不受浏览器同源策略的限制,所以考虑到加载第三方站点的安全影响的问 题,使用它时应该很谨慎。

Deploying JavaScript: 部署 JavaScript

There are a few performance considerations when it comes to serving JavaScript. Let’s discuss the most important ones at a high level. For all the details, check High Performance Web Sites and Even Faster Web Sites, both published by O’Reilly.
在生产环境中使用 JavaScript 时,有不少性能方面的考虑。我们来讨论一下 最重要的一些。如果需要了解所有的细节,可以参见 O’Reilly 出社的《高性 能网站建设指南》和《高性能网站建设进阶指南》。

Combining Scripts: 合并脚本

The first rule when building fast-loading pages is to have as few external components as possible, because HTTP requests are expensive. When it comes to JavaScript, that means you can speed up the page loading times significantly by combining external script files together.
创建高性能网站的第一个原则就是尽量减少外部引用的组件 ,因为 HTTP 请求的代价是比较大的。具体就 JavaScript 而言,可以通过合并外部脚本来显著提高页面加载速度

Minifying and Compressing: 压缩代码

The benefit of minification can be different depending on how generously you use comments and white space, and also on the specific minification tools you use. But on average you would be looking at around 50% file size reduction.
压缩代码带来的好处多少取决于代码中注释和空白的数量,也取决于你使用 的压缩工具。平均来说,压缩可以减少 50% 左右的体积。

Expires Header: 缓存头

Contrary to the popular belief, files do not get to stay for too long in the browser cache. You can do your due diligence and increase the chances of having your files in the cache for repeat visits by using an Expires header.
与流行的观点相反,文件在浏览器缓存中的时间并没有那么久。你可以尽你 自己的努力,通过使用 Expires 头来增加非首次访问时命中缓存的概率:

Again, this is a one-off server configuration you can do in .htaccess:
这也是一个在 .htaccess 中做的一次性配置工作:

1
2


The drawback is that if you want to change the file, you also need to rename it, but you’ll probably be doing this already if you’ve established a naming convention for your bundles of combined files.
它的弊端是当你想更改这个文件时,你需要给它重命名,如果你已经处理好了合并的文件命名规则,那你就已经处理好这里的命名问题了。

Using a CDN: 使用 CDN

CDN stands for Content Delivery Network. This is a paid (sometimes quite pricey) hosting service that lets you distribute copies of your files in different data centers around the world and have them served quicker to your users, while still keeping the same URL in your code.
CDN 是指 “文件分发网络”(Content Delivery Network)。这是一项收费(有时候还相当昂贵)的托管服务,它将你的文件分发到世界上各个不同的数据中心,但代码中的 URL 却都是一样的,这样可以使用户更快地访问。

Loading Strategies: 加载策略

The Place of the

文章标题:JavaScript Patterns 小记

文章作者:RayJune

时间地点:下午 9:30,于知名 2-201 自习教室

原始链接:https://www.rayjune.me/2017/08/30/JavaScript-Patterns-note/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。