原型链

在 ES2015/ES6中引入了class关键字,但只是语法糖,JavaScript 仍然是基于原型的

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object )都有一个私有属性(称之为__proto__)指向它的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的Object的实例。

尽管这种原型继承通常被认为是JavaScript的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

从 ECMAScript 6 开始,[[Prototype]] 可以通过Object.getPrototypeOf()和Object.setPrototypeOf()访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

性能

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(prototype),而实例都包含一个指向原型对象的内部指针(_proto_)。

创建对象


工厂模式

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)
1
2
3
4
5
6
7
8
9
10
11
12
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");



构造函数模式

1
2
3
4
5
6
7
8
9
10
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");


与createPerson()不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句。

要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

原型模式

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

person2.sayName(); //"Nicholas"
console.log(person1.sayName == person2.sayName); //true

// person1 和 person2。因为它们内部都有一个指向 Person.prototype 的指针,因此都返回了 true
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true


ECMAScript 5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个
方法返回[[Prototype]]的值。例如:
1
2
alert(Object.getPrototypeOf(person1) == Person.prototype); //true 
alert(Object.getPrototypeOf(person1).name); //"Nicholas"


1
2
Object.keys()  
Object.getOwnPropertyNames()


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

//重设构造函数,只适用于ECMAScript 5兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});


原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。
原型模式的最大问题是由其共享的本性所导致的


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(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


构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性

动态原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age, job) {
// 属性
this.name = name;
this.age = age;
this.job = job;
// 方法
if (typeof this.sayName != "function") {
Person.prototype.sayName = function () {
console.log(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();


通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型

寄生构造函数模式

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);
};
return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"



稳妥构造函数模式

1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age, job) {
// 创建要返回的对象
var o = new Object();
// 可以在这里定义私有变量和函数

// 添加方法
o.sayName = function () {
console.log(name);
};
// 返回对象
return o;
}


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

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
- 新创建对象的实例方法不引用this
- 不使用 new操作符调用构造函数。

继承


原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.property = true;
}

SuperType.prototype.getSuperValue = function () {
return this.property;
};

function SubType() {
this.subproperty = false;
}

//继承了 SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue()); //true



借用构造函数


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType() {
this.colors = ["red", "blue", "green"];
}

function SubType() {
//继承了 SuperType
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"



组合继承

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
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function () {
console.log(this.name);
};

function SubType(name, age) {

//继承属性
SuperType.call(this, name);

this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27



原型式继承

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
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}

// 父类的方法
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};

// Rectangle - 子类(subclass)
function Rectangle() {
Shape.call(this); // call super constructor.
}

// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?',
rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'


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
// Principles of Object-Oriented JavaScript p75
function Rectangle(length, width) {
this.length = length;
this.width = width;
}
Rectangle.prototype.getArea = function () {
return this.length * this.width;
};
Rectangle.prototype.toString = function () {
return "[Rectangle " + this.length + "x" + this.height + "]";
};
// inherits from Rectangle
function Square(size) {
Rectangle.call(this, size, size);
}
Square.prototype = Object.create(Rectangle.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Square,
writable: true
}
});
// call the supertype method
Square.prototype.toString = function () {
var text = Rectangle.prototype.toString.call(this);
return text.replace("Rectangle", "Square");
};

var square = new Square(6);
console.log(square.length);
console.log(square.width);
console.log(square.getArea());



寄生式继承



寄生组合式继承