文章目录
  1. 1. 创建单个对象
  2. 2. 工厂模式
  3. 3. 构造函数模式
  4. 4. 原型模式
  5. 5. 组合构造函数模式 + 原型模式
  6. 6. 动态原型模式
  7. 7. 寄生构造函数模式
  8. 8. 稳妥构造函数模式

想记录这节笔记的原因有三:

  • 后知后觉:早在 2011年10月左右,实验室的周神就曾组织大家去学习 js 的这些,并进行了两次组内分享。那时是我第一次接触到 js 中的面向对象机制。但也仅仅是参与了分享上的讨论(说是讨论,更确切的讲是他讲我们听,他提问我们互动回答-记得他当时还夸我反应挺快来着),后来在项目的编码中也没有实际的用到。第二次是在 2012年10月毕业找工作时,看了些,但之后也没用起来。第三次就是现在了,工作一年后,发现自己真正用 js 的这些特性去好好的面向对象,次数超级无敌少。才发觉自己的山寨和业余,于是便决定好好做人,理论与实践相结合去全身心体会 js 的关键特性们。
  • 感慨与惊叹:虽不是第一次接触这几种听起来白富美的xxx模式,但每次看到的时候,都被它们的 “a 解决了xx问题,但又存在xx问题。于是b就诞生了” 的这种环环相扣的递进魅力所吸引。所以,这次我要把它们都记录下来,以提醒自己要以解决问题为驱动去理解知识,以及工具从没好坏它只有自己的适用范围。
  • 我那该死的倔强:之前我就知道这是 js 很重要的一个特性,但从没真正碰到过要做一个东西去用它,所以也就从没认认真真地去学习它消化它。而这次,用到了,所以决定好好学下。告诫自己:不要因为没实际用到就不静下心来去学重要的东西,尤其是工作以后。因为工作一年了,我发现,其实有些东西你不自己深入学下去的话,平时的工作也是可以完成的,合格的。如果我还把自己的这份倔强当做是性格的话,那n年之后,我一定还是在原地踏步踏,不进则使劲退。要学会并适应这种有备无患地处理重要但不紧急的事情。

创建单个对象

1、Object 构造函数

1
2
3
4
5
6
var person = new Object();
person.name = "anjia";
person.age = 26;
person.sayName = function(){
alert(this.name);
};

2、字面量

1
2
3
4
5
6
7
var person = {
name: "anjia",
age: 26,
sayName: function(){
alert(this.name);
}
};

问题 1:若要用这同一个接口去创建很多对象,会产生大量的重复代码。
解决:为解决这一“创建多个相似对象”问题,可使用工厂模式的变体。

工厂模式

由于 ECMAScript 无法创建类,所以就用函数去封装用特定接口去创建对象。

1
2
3
4
5
6
7
8
9
10
11
12
function createPerson(name, age){
var o = new Object();
o.name = "anjia";
o.age = 26;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Lily", 20);
var person2 = createPerson("David", 29);

函数 createPerson() 据形参构建一个包含必要信息的对象,我们可以无数次调用该函数,它每次都会返回包含两个属性一个方法的对象。

问题 2:工厂模式没办法知道一个对象的类型
解决:随着 js 的发展,出现了构造函数模式

构造函数模式

1
2
3
4
5
6
7
8
9
10
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Lily", 20);
var person2 = new Person("David", 29);

与工厂模式相比,除了用 Person() 函数取代了 createPerson() 之外,在 Person() 内部还有以下不同:
(1)没有显示地创建对象;
(2)直接将属性和方法赋给了 this 对象;
(3)没有 return 语句;
此外,函数名 Person 首字母大写(按惯例,构造函数与非构造函数的命名规则)。

用构造函数去创建一个 Person 实例,必须使用 new 操作符。用 new 去调用构造函数会经历以下4个步骤:
(1)创建一个新对象
(2)让渡作用域:构造函数的作用域赋给新的对象,此时 this 就指向了这个新对象 【这个是关键,用 new 操作符】
(3)给新对象添加属性和方法
(4)返回新对象

此处,person1 和 person2 分别保存着 Person 的一个不同实例。这两对象中都有一个属性 constructor,指向了 Person。即:
person1.constructor == Person // true
person2.constructor == Person // true

对象的 constructor 属性最初是用来标识对象的类型的。但是提到检测对象类型,还是用 instanceof 操作符更可靠些。即
alert(person1 instanceof Object); // true
alert(person1 instanceof Person); // true

构造函数与其他函数的唯一区别,在于调用它们的方式不同。不过呢,构造函数本质上就是个好函数,所以不存在定义构造函数的特殊语法。任何函数只要通过 new 操作符来调用,那它就可以作为构造函数;相对应,任何构造函数如果不通过 new 去调用,那它和普通的函数也没啥不同。

构造函数胜过工厂模式的地方,即可以将它的实例标识为一种特定的类型。构造函数虽然好用,但它也是有问题的,就是每个方法都要在每个实例上重新创建一遍。但是嘞,创建两个完成同样任务的 function 实例确实是没有必要。来看:
alert(person1.sayName == person2.sayName); // false

问题3:每个方法都要在每个实例上重新创建一遍。
解决:我们完全可以把函数定义转移到构造函数外部来解决重复的问题,即:

1
2
3
4
5
6
7
8
9
10
11
12
funtion Person(name, age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Lily", 20);
var person2 = new Person("David", 29);

这样呢,person1 和 person2 就共享了全局作用域中定义的一个 sayName() 函数了。如此,虽然解决了两个函数做同一件事情的问题,但是引入了新问题。

问题Q4:用全局函数的方式去定义函数就没有封装性可言了。
解决:原型模式

原型模式

我们创建的每一个函数都有一个 prototye 属性,该属性是一个指针,它指向了一个对象。这个对象包含可以由特定类型的实例共享的方法和属性。prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。来看看将信息直接添加到原型对象中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(){
}
Person.prototype.name = "anjia";
Person.prototype.age = 26;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.sayName == person2.sayName); // true

与构造函数模式不同的是,这些对象的属性和方法是由所有实例共享的。

也可以用一种更简单的原型语法

1
2
3
4
5
6
7
8
9
10
11
function Person(){
}
Person.prototype = {
constructor: Person, //重新指回
name: "anjia",
age: 26,
sayName: function(){
alert(this.name);
}
};

由于重写了 prototype 对象,将它设置成了一个用字面量对象创建的新对象,这会导致 constructor 属性不再指向 Person 了(指向 Object 构造函数了)。因为每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。此时,尽管 instanceof 还是 Person,但是通过 constructor 已经无法确定对象的类型了。
点击了解更多有关 原型对象 的性质。

原型对象的问题 5:
1、由于它省略了为构造函数传参这一过程,结果导致所有实例在默认情况下都是取得相同的属性值
2、逻辑上不该共享的属性也被多个实例共享了。当然可以通过在实例上加个同名属性,进而屏蔽原型链的向上查找。所以还能忍。
3、最大的问题是:由于其共享的本质,当包含的属性是个引用类型值时,就会有 bug。
鉴于原因3,所以很少会单独使用原型模式。

组合构造函数模式 + 原型模式

构造函数模式去定义实例属性;原型函数模式去定义方法和共享属性。这样,每个实例都会有自己的一份实例属性,且同时共享对方法的引用,最大限度地节省了内存。
且还支持向构造函数传参,可谓集两种模式之长。

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){
this.name = name;
this.age = age;
this.friends = ["Hen", "Berg"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
};
var person1 = new Person("Lily", 20);
var person2 = new Person("David", 29);
person1.friends.push("Lily2");
alert(person1.friends); // Hen,Berg,Lily2
alert(person2.friends); // Hen,Berg
alert(person1.friends == person2.friends); // false
alert(person1.sayName == person2.sayName); // true

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

动态原型模式

寄生构造函数模式

稳妥构造函数模式

文章目录
  1. 1. 创建单个对象
  2. 2. 工厂模式
  3. 3. 构造函数模式
  4. 4. 原型模式
  5. 5. 组合构造函数模式 + 原型模式
  6. 6. 动态原型模式
  7. 7. 寄生构造函数模式
  8. 8. 稳妥构造函数模式