The extend Function

1
2
3
4
5
6
7
/* Extend function. */
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
}

It sets the prototype and then resets the correct constructor. As a bonus, it adds the empty class F into the prototype chain in order to prevent a new (and possible large) instance of the superclass from having to be instantiated. This is also beneficial in situations where the superclass’s constructor has side effects or does something that is computationally intensive. Since the object that gets instantiated for the prototype is usually just a throwaway instance, you don’t want to create it unnecessarily

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Class Person. */
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
/* Class Author. */
function Author(name, books) {
Person.call(this, name);
this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function() {
return this.books;
};

Instead of setting the prototype and constructor attributes manually, simply call the extend function immediately after the class declaration (and before you add any methods to the prototype). The only problem with this is that the name of the superclass (Person) is hardcoded within the Author declaration. It would be better to refer to it in a more general way:

1
2
3
4
5
6
7
8
9
10
11
/* Extend function, improved. */
function extend(subClass, superClass) {
var F = function() {};
F.prototype = superClass.prototype;
subClass.prototype = new F();
subClass.prototype.constructor = subClass;
subClass.superclass = superClass.prototype;
if(superClass.prototype.constructor == Object.prototype.constructor) {
superClass.prototype.constructor = superClass;
}
}

This version is a little longer but provides the superclass attribute, which you can now use to make Author less tightly coupled to Person. The first four lines of the function are the same as before. The last three lines simply ensure that the constructor attribute is set correctly on the superclass (even if the superclass is the Object class itself). This will become important when you use this new superclass attribute to call the superclass’s constructor:

1
2
3
4
5
6
7
8
9
/* Class Author. */
function Author(name, books) {
Author.superclass.constructor.call(this, name);
this.books = books;
}
extend(Author, Person);
Author.prototype.getBooks = function() {
return this.books;
};

Adding the superclass attribute also allows you to call methods directly from the superclass.This is useful if you want to override a method while still having access to the superclass’s implementation of it. For instance, to override Person’s implementation of getName with a new version,you could use Author.superclass.getName to first get the original name and then add to it:

1
2
3
4
Author.prototype.getName = function() {
var name = Author.superclass.getName.call(this);
return name + ', Author of ' + this.getBooks().join(', ');
};

Static Methods and Attributes

Most methods and attributes interact with an instance of a class; static members interact with the class itself. Another way of putting it is to say that static members operate on the class-level instead of the instance-level; there is only one copy of each static member.

Here is the Book class with static attributes and methods:

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
45
46
47
48
49
50
51
52
var Book = (function() {
// Private static attributes.
var numOfBooks = 0;
// Private static method.
function checkIsbn(isbn) {
...
}
// Return the constructor.
return function(newIsbn, newTitle, newAuthor) { // implements Publication
// Private attributes.
var isbn, title, author;
// Privileged methods.
this.getIsbn = function() {
return isbn;
};
this.setIsbn = function(newIsbn) {
if(!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN.');
isbn = newIsbn;
};
this.getTitle = function() {
return title;
};
this.setTitle = function(newTitle) {
title = newTitle || 'No title specified';
};
this.getAuthor = function() {
return author;
};
this.setAuthor = function(newAuthor) {
author = newAuthor || 'No author specified';
};
// Constructor code.
numOfBooks++; // Keep track of how many Books have been instantiated
// with the private static attribute.
if(numOfBooks > 50) throw new Error('Book: Only 50 instances of Book can be '
+ 'created.');
this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
}
})();
// Public static method.
Book.convertToTitleCase = function(inputString) {
...
};
// Public, non-privileged methods.
Book.prototype = {
display: function() {
...
}
};

Private and privileged members are still declared within the constructor, using var and this respectively, but the constructor is changed from a normal function to a nested function that gets returned to the variable Book. This makes it possible to create a closure where you can declare private static members. The empty parentheses after the function declaration are extremely important. They serve to execute that function immediately, as soon as the code is loaded (not when the Book constructor is called).The result of that execution is another function, which is returned and set to be the Book constructor. When Book is instantiated, this inner function is what gets called; the outer function is used only to create a closure, within which you can put private static members.

In this example, the checkIsbn method is static because there is no point in creating a new
copy of it for each instance of Book. There is also a static attribute called numOfBooks, which allows you to keep track of how many times the Book constructor has been called. In this example, we use that attribute to limit the constructor to creating only 50 instances.

These private static members can be accessed from within the constructor, which means
that any private or privileged function has access to them. They have a distinct advantage over these other methods in that they are only stored in memory once. Since they are declared outside of the constructor, they do not have access to any of the private attributes, and as such, are not privileged; private methods can call private static methods, but not the other way around. A rule of thumb for deciding whether a private method should be static is to see whether it needs to access any of the instance data. If it doe not need access, making the method static is more efficient (in terms of memory use) because only a copy is ever created.

Public static members are much easier to create. They are simply created directly off of
the constructor, as with the previous method convertToTitleCase. This means you are essentially using the constructor as a namespace.

All public static methods could just as easily be declared as separate functions, but it is useful to bundle related behaviors together in one place. They are useful for tasks that are related to the class as a whole and not to any particular instance of it. They don’t directly depend on any of the data contained within the instances.

Constants

Constants are nothing more than variables that can’t be changed. In JavaScript, you can emulate constants by creating a private variable with an accessor but no mutator. Since constants are usually set at development time and don’t change with each instance that is created, it makes sense to create them as private static attributes. Here is how a call to get the constant UPPER_BOUND from Class would look:

1
Class.getUPPER_BOUND();

To implement this accessor, you would need a privileged static method, which we haven’t
covered yet. It is created just like a privileged instance method, with the this keyword:

1
2
3
4
5
6
7
8
9
10
11
12
13
var Class = (function() {
// Constants (created as private static attributes).
var UPPER_BOUND = 100;
// Privileged static method.
this.getUPPER_BOUND() {
return UPPER_BOUND;
}
...
// Return the constructor.
return function(constructorArgument) {
...
}
})();

If you have a lot of constants and don’t want to create an accessor method for each, you
can create a single generic accessor method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Class = (function() {
// Private static attributes.
var constants = {
UPPER_BOUND: 100,
LOWER_BOUND: -100
}
// Privileged static method.
this.getConstant(name) {
return constants[name];
}
...
// Return the constructor.
return function(constructorArgument) {
...
}
})();

Then you would get a constant by calling the single accessor:

1
Class.getConstant('UPPER_BOUND');

The Interface Class

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
// Constructor.
var Interface = function(name, methods) {
if(arguments.length != 2) {
throw new Error("Interface constructor called with " + arguments.length +
"arguments, but expected exactly 2.");
}
this.name = name;
this.methods = [];
for(var i = 0, len = methods.length; i < len; i++) {
if(typeof methods[i] !== 'string') {
throw new Error("Interface constructor expects method names to be "
+ "passed in as a string.");
}
this.methods.push(methods[i]);
}
};
// Static class method.
Interface.ensureImplements = function(object) {
if(arguments.length < 2) {
throw new Error("Function Interface.ensureImplements called with " +
arguments.length + "arguments, but expected at least 2.");
}
for(var i = 1, len = arguments.length; i < len; i++) {
var interface = arguments[i];
if(interface.constructor !== Interface) {
throw new Error("Function Interface.ensureImplements expects arguments"
+ "two and above to be instances of Interface.");
}
for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
var method = interface.methods[j];
if(!object[method] || typeof object[method] !== 'function') {
throw new Error("Function Interface.ensureImplements: object "
+ "does not implement the " + interface.name
+ " interface. Method " + method + " was not found.");
}
}
}
};

偏应用函数

“分部应用”一个函数是一项特别有趣的技术,在函数调用之前,我们可以预先传入一些函数。实际上,偏应用函数返回了一个含有预处理参数的新函数,以便后期可以调用。
这类代理函数--代理的是另外一个函数,并且在执行的时候会调用所代理的函数。
这种在一个函数中首先填充几个参数(然后再返回一个新函数)的技术称之为柯里化(curring)。

柯里化函数示例(在第一个特定参数中进行填充)

1
2
3
4
5
6
7
8
9
Function.prototype.curry = function() {
//该函数以及预填充的参数是保存在闭包中的
var fn = this,
args = Array.prototype.slice.call(arguments);
//创建一个匿名柯里化函数
return function(){
return fn.apply(this,args.concat(Array.prototype.slice.call(arguments)));
};
}

这种技术是另外一个利用闭包记住状态的很好例子。在本例中,我们要记住新增加的函数(这里的this参数不会存在于任何闭包中,因为每个函数调用的时候都有自己的this)以及预填充参数,并将它们转移到新创建的函数中。该新函数将有预填充的参数以及刚传入的新参数。其结果就是,这样的方法可以让我们预先传入一些参数,然后返回给我们一个新的简单函数供我们使用。

虽然这种风格的分部函数非常有用,但我们可以做的更好。如果我们给特定函数传递遗漏的参数,而不是从参数列表一开始就传。

一个更复杂的”分部”函数

1
2
3
4
5
6
7
8
9
10
11
12
Function.prototype.partial = function() {
var fn = this, args = Array.prototype.slice.call(arguments);
return function(){
var arg = 0;
for (var i=0; i < args.length && arg < arguments.length; i++) {
if (args[i] === undefined) {
args[i] = arguments[arg++];
}
}
return fn.apply(this, args);
};
}

该实现的本质类似于Prototype的curry()方法,但它有几个重要的差异。值得注意的是,用户可以在参数列表的任意位置指定参数,然后在后续的调用中,根据遗漏的参数值是否等于undefined来判断参数的遗漏,要实现这种功能,我们添加了参数合并功能。很有效果,遍历传入的所有参数参数,判断相应的参数是否遗漏了(是否是undefined),然后沿着顺序填充遗漏的参数。

1
2
3
4
5
var delay = setTimeout.partial(undefined, 10);
delay(function(){
assert(true, "A call to this function will be delayed 10 ms.");
});

这段代码创建了一个新的函数,名为delay(),通过该函数,我们可以传入另外一个10毫秒后进行调用的异步函数。
我们也可以创建一个简单的函数用于事件绑定:

1
2
3
4
5
var bindClick = document.body.addEventListener.partial("click", undefined, false);
bindClick(function(){
assert(true, "Click event bound via curried function");
});

面向对象(Object-Oriented,OO) 的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。ECMAScript中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
ECMAScript-262把对象定义为:”无序属性的集合,其属性可以包含基本值,对象或者函数。”严格地讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样,我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。

创建对象

工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装特定接口创建对象的细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
function createPerson(name,age,job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。例如,可以使用构造函数模式将前面的例子重写如下。

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.SayName = function() {
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

可以注意到, Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:

  • 没有显示地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句。

此外,还应该注意到函数名Person使用的是大写字母P。这个做法借鉴自其它OO语言,主要是为了区别于ECMAScript中的其他函数;因为构造函数也是函数,只不过可以用来创建对象而已。 以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

这个例子中创建的所有对象既是Object的实例,同时也是Person的实例,这一点通过instanceof操作符可以得到验证。

1
2
3
4
alert(person1 instanceof Object); //true
alert(person2 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Person); //true

创建自定义的构造函数意味着将来可以将它的实际标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义。

1
2
3
4
5
6
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); //与声明函数在逻辑上是等价的
}

从这个角度来看构造函数,更容易明白Person实例都包含一个不同的Function实例(以显示name属性)的本质。说明白些,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,一下代码可以证明这一点。

1
alert(person1.sayName == person2.sayName); //false

然而,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age ,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

在这个例子中,我们把sayName函数的定义转移到了构造函数外部。而在构造函数内部,我们将sayName属性设置成等于全局的sayName函数。这样一来,由于sayName包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。这样做确实解决了两个函数作用一件事的问题,可是新问题又来了:在全局作用中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

原型模式

创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来解释,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype = "Software Engineer";
Person.prototype = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true

原型与in操作符

同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。

1
2
3
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}

要取得对象上所有可枚举的实例属性,可以使用ECMAScript5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person() {
}
Person.prototype.name = "Nicholas";
Person.prototype.age = "29";
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
alert(this.name);
}
var keys = Object.keys(Person.prototype);
alert(keys) //"name,age,job,sayName"
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
alert(p1keys); //"name,age"

如果是通过Person的实例调用,则Object.keys()返回的数组只包含”name”和”age”这两个实例属性。
如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

1
2
var keys = Object.getOwnPropertyNames();
alert(keys); //"constructor,name,age,job,sayName"

注意结果中包含了不可枚举的constructor属性。Object.keys()和Object.getOwnPropertyNames()方法都可以用代替for-in循环。

更简单的原型语法

前面例子中美添加一个属性和方法都要敲一遍 Person.prototype。 为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是一个包含所有属性和方法的对象字面量来重写整个原型对象。

1
2
3
4
5
6
7
8
function.prototyype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};

上面的代码中将Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor 属性不再指向 person 了。因为没创建一个函数,就会同时创建它的prototype对象,因此 constructor 属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向 Person 函数。此时,尽管 instanceof 操作符还能返回正确的结果,但通过 constructor 已经无法确定对象的类型了。

1
2
3
4
5
6
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true

如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能立即从实例上反映出来----即使是先创建了实例后修改原型也照样如此。

1
2
3
4
5
6
7
var friend = new Person();
Person.prototype.sayHi = function() {
alert("hi");
};
friend.sayHi(); //"hi" (没有问题!)

其原因可以归结为实例与原型之间的松散连接关系。当我们调用 friend.sayHi() 时,首先会在实例中搜索名为 sayHi() 的属性,在没有找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存在那里的函数。
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function() {
}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function() {
alert(this.name);
}
};

原型对象的问题

原型模式省略了为构造函数传递初始化参数这一节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来不便,但还不是原型的问题。原型模式的最大问题是由其共享的本质所导致的。
原型中所有的属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,毕竟,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型的属性来说,问题就比较突出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person() {
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
firends : ["Sheby", "Court"],
sayName : function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,court,Van"
alert(person2.friends); //"Shelby,court,Van"
alert(person1.friends === person2.friends); //true

实例一般都是要有属于自己的全部属性的。这个问题正是我们很少看到有人单独使用原型模式的原因所在。

组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的脚本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

动态原型模式

有其它OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力解决这个问题的方案,它把所有的信息都封装在了构造函数中,而通过在狗仔函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName != "function") {
person.prototype.sayName = function(){
alert(this.name);
}
};
}
var friend = new Person("Nicholas", 29, "Software Enginner");
friend.sayName();

这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中, if 语句检查的可以是初始化之后应该存在的任何属性或方法---不必用一大堆 if 语句检查每个属性和方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型。

使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断实例与新原型之间的联系。

寄生构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
reuturn o;
}
var firend = new Person("Nicholas", 29, "Software Enginner");

除了使用new操作符并把使用的包装函数叫做构造函数外,这个模式跟工厂模式其实一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SpecialArray(){
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipdString = function(){
return this.join("|");
}
//返回数组
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red"|"blue"|"green"

需要说明的是,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与构造函数外部创建的函数没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,在可以使用其他模式的情况下,不要使用这种模式。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且方法也不引用this的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name);
};
//返回对象
return o;
}
var friend = Person("Nicholas", 29, "Software Enginner");
friend.sayName(); //"Nicholas"

稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境

与寄生构造函数模式类型,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof 操作符对这种对象也没有意义。

使用!!操作符转换布尔值

对于变量可以使用!!variable做检测,只要变量的值为:0、null、””、undefined或者NaN都将返回的是false,反之返回的是true。

使用+将字符串转换成数字

1
2
3
4
function toNumber(strNumber) {
return +strNumber;
}
console.log(toNumber("1234")); // 1234

并条件符

1
conected && login();

使用||运算符

1
2
3
4
function User(name, age) {
this.name = name || "Oliver Queen";
this.age = age || 27;
}

在循环中缓存array.length

1
2
3
for(var i = 0, length = array.length; i < length; i++) {
console.log(array[i]);
}

检测对象中属性

1
2
3
4
5
if ('querySelector' in document) {
document.querySelector("#id");
} else {
document.getElementById("id");
}

获取数组中最后一个元素

1
2
3
var array = [1,2,3,4,5,6];
console.log(array.slice(-1)); // [6]
console.log(array.slice(-2)); // [5,6]

数组截断

1
2
3
4
5
var array = [1,2,3,4,5,6];
console.log(array.length); // 6
array.length = 3;
console.log(array.length); // 3
console.log(array); // [1,2,3]

替换所有

1
2
3
var string = "john john";
console.log(string.replace(/hn/, "ana")); // "joana john"
console.log(string.replace(/hn/g, "ana")); // "joana joana"

合并数组

1
2
3
var array1 = [1,2,3];
var array2 = [4,5,6];
console.log(array1.push.apply(array1, array2)); // [1,2,3,4,5,6];

将NodeList转换成数组

1
2
3
var elements = document.querySelectorAll("p"); // NodeList
var arrayElements = [].slice.call(elements); // Now the NodeList is an array
var arrayElements = Array.from(elements); // This is another way of converting NodeList to Array

数组元素的洗牌

1
2
var list = [1,2,3];
console.log(list.sort(function() { Math.random() - 0.5 })); // [2,1,3]

编译器

当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声
明:var a;和a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执
行阶段。
我们的第一个代码片段会以如下形式进行处理:

1
2
3
var a;
a = 2;
console.log( a );

其中第一部分是编译,而第二部分是执行。

函数声明会被提升,但是函数表达式却不会被提升。

1
2
3
4
foo(); // 不是ReferenceError, 而是TypeError!
var foo = function bar() {
// ...
};

这段程序中的变量标识符foo()被提升并分配给所在作用域(在这里是全局作用域),因此foo()不
会导致ReferenceError。但是foo此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么
就会赋值)。foo()由于对undefined值进行函数调用而导致非法操作,因此抛出TypeError异常。
同时也要记住,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:

1
2
3
4
5
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};

这个代码片段经过提升后,实际上会被理解为以下形式:

1
2
3
4
5
6
7
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self...
// ...
}

函数优先

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声
明的代码中)是函数会首先被提升,然后才是变量。
考虑以下代码:

1
2
3
4
5
6
7
8
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};

会输出1而不是2!这个代码片段会被引擎理解为如下形式:

1
2
3
4
5
6
7
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};

注意,var foo尽管出现在function foo()…的声明之前,但它是重复的声明(因此被忽略了),因为
函数声明会被提升到普通变量之前。

尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

1
2
3
4
5
6
7
8
9
10
foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}

虽然这些听起来都是些无用的学院理论,但是它说明了在同一个作用域中进行重复定义是非常糟
糕的,而且经常会导致各种奇怪的问题。

一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示
的那样可以被条件判断所控制:

1
2
3
4
5
6
7
8
9
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
} else {
function foo() { console.log("b"); }
}
但是需要注意这个行为并不可靠,在JavaScript未来的版本中有可能发生改变,因此应该尽可能避
免在块内部声明函数。

现代的模块

各种模块依赖加载器/消息机制实质上都是将这种模块定义包装进一个友好的API。与其检视任意一个特定的库,不如让我 (仅)为了说明的目的 展示一个 非常简单 的概念证明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();

这段代码的关键部分是modules[name] = impl.apply(impl, deps)。这为一个模块调用了它的定义的包装函数(传入所有依赖),并将返回值,也就是模块的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
MyModules.define( "bar", [], function(){
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar){
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
bar.hello( "hippo" )
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

模块“foo”和“bar”都使用一个返回公有API的函数来定义。“foo”甚至接收一个“bar”的实例作为依赖参数,并且可以因此使用它。

花些时间检视这些代码段,来完全理解将闭包的力量付诸实践给我们带来的好处。关键之处在于,对于模块管理器来说真的没有什么特殊的“魔法”。它们只是满足了我在上面列出的模块模式的两个性质:调用一个函数定义包装器,并将它的返回值作为这个模块的API保存下来。

换句话说,模块就是模块,即便你在它们上面放了一个友好的包装工具。

es6的模块

ES6为模块的概念增加了头等的语法支持。当通过模块系统加载时,ES6将一个文件视为一个独立的模块。每个模块可以导入其他的模块或者特定的API成员,也可以导出它们自己的公有API成员。

注意: 基于函数的模块不是一个可以被静态识别的模式(编译器可以知道的东西),所以它们的API语义直到运行时才会被考虑。也就是,你实际上可以在运行时期间修改模块的API。

相比之下,ES6模块API是静态的(这些API不会在运行时改变)。因为编译器知道它,它可以(也确实在作!)在(文件加载和)编译期间检查一个指向被导入模块的成员的引用是否 实际存在。如果API引用不存在,编译器就会在编译时抛出一个“早期”错误,而不是等待传统的动态运行时解决方案(和错误,如果有的话)。

ES6模块 没有 “内联”格式,它们必须被定义在一个分离的文件中(每个模块一个)。浏览器/引擎拥有一个默认的“模块加载器”,它在模块被导入时同步地加载模块文件。

考虑这段代码:

bar.js

1
2
3
4
5
function hello(who) {
return "Let me introduce: " + who;
}
export hello;

foo.js

1
2
3
4
5
6
7
8
9
10
11
12
// 仅仅从“bar”模块中导入`hello()`
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello( hungry ).toUpperCase()
);
}
export awesome;

1
2
3
4
5
6
7
8
9
// 导入`foo`和`bar`整个模块
import foo from "foo";
import bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO

注意: 需要使用前两个代码片段中的内容分别创建两个分离的文件 “foo.js”“bar.js”。然后,你的程序将加载/导入这些模块来使用它们,就像第三个片段那样。

import在当前的作用域中导入一个模块的API的一个或多个成员,每个都绑定到一个变量(这个例子中是hello)。module将整个模块的API导入到一个被绑定的变量(这个例子中是foobar)。export为当前模块的公有API导出一个标识符(变量,函数)。在一个模块的定义中,这些操作符可以根据需要使用任意多次。

模块文件 内部的内容被视为像是包围在一个作用域闭包中,就像早先看到的使用函数闭包的模块那样。

断言

单元测试框架的核心是断言方法,通常叫assert()。该方法通常接收一个值--需要断言的值,以及一个表示该断言目的描述,如果该值执行结果为true,换句话说是”真值”,断言就会通过;否则,断言就会被认为是失败的。通常用一个相应的通过(pass) / 失败(fail)标记记录相关的信息。
从下面的代码清单中,可以看到一个简单的实现。

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
<html>
<head>
<title>Test Suite</title>
<script>
function assert(value, desc) {
var li = document.createElement("li");
li.className = value ? "pass" : "fail";
li.appendChild(document.createTextNode(desc));
document.getElementById('results').appendChild(li);
}
window.onload = function() {
assert(true, 'The test suite is running.');
assert(false, 'Fail!');
}
</script>
<style>
#results li.pass{ color:green; }
#results li.fail{ color:red; }
</style>
</head>
<body>
<ul id='results'></ul>
</body>
</html>

该测试套件包括两个微不足道的测试:一个总是成功,另一个总是失败。pass和fail的样式规则,则使用颜色在视觉上表示成功或失败。
该函数很简单,但对于未来开发,它将会是良好的构件块。