面向对象程序设计
ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。
理解对象
创建自定义对象的最简单方式就是创建一个Object实例,然后再为它添加属性和方法。
js
var person = new Object();
person.name = 'Nicholas';
person.age = 20;
person.job = 'Software Engineer';
person.sayName = function () {
console.log(this.name);
}
属性类型
ECMAScript中有两种属性:数据属性和访问器属性。
数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性
Configurable
:表示能否通过delete
删除属性从而重新定义属性,或者能否把属性修改为访问器属性。Enumerable
:表示属性是否可枚举的(通过for-in
可否返回属性)。Writable
:表示能否修改属性的值。Value
:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。
对于前面的例子中,直接在对象上定义的属性,它们的Configurable
、Enumerable
和Writable
特性都被设置为true
。
要修改属性默认的特性,必须使用ES5的Object.defineProperty()
方法。这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。其中描述符对象的属性必须是:configurable
、enumerable
、writable
和value
。设置其中的一个或多个值,可以修改对应的特性值。
js
var person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: 'Nicholas'
});
console.log(person.name); // 'Nicholas'
person.name = 'Greg'; // 在严格模式下会报错
console.log(person.name); // 'Nicholas'
把configurable
设置为false
表示不能从对象中删除属性,如果对这个属性调用delete
,在非严格模式下,什么也不会发生,但在严格模式下会导致错误。而且,一旦把属性定义为不可配置的,就不能再把它变回可配置的了。
在调用Object.defineProperty()
创建一个新属性时,如果不指定,configurable
、writable
和enumerable
特性的默认值都是false
。
访问器属性
访问器属性不包含数据值:它们包含一对getter
和setter
函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数
,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数
并传入新值,这个函数负责决定如何处理数据。访问器属性有以下4个特性:
Configurable
。Enumerable
。Get
:在读取属性时调用的函数。默认值为undefined。Set
:在写入属性时调用的函数。默认值为undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()
来定义。
js
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, 'year', {
get: function () {
return this._year;
},
set: function (val) {
if (val > 2004) {
this._year = val;
this.edition += val - 2004;
}
}
});
book.year = 2005;
console.log(book.edition); // 2
定义多个属性
由于对象定义多个属性的可能性很大,ES5又定义了一个Object.defineProperties()
方法。利用这个方法可以通过描述符对象一次定义多个属性。
js
book = {};
Object.defineProperties(book, {
_year: {
writable: true,
value: 2004
},
edition: {
writable: true,
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (val) {
if (val > 2004) {
this._year = val;
this.edition += val - 2004;
}
}
}
});
读取属性的特性
使用ES5的Object.getOwnPropertyDescriptor()
方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取的属性名称。返回值是一个对象。
js
// 接上面的例子
var descriptor = Object.getOwnPropertyDescriptor(book, '_year');
console.log(descriptor)
// { value: 2004,
// writable: true,
// enumerable: false,
// configurable: false }
创建对象
工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到在ECMAScript中无法创建类(在ES6已经可以,这里不作讨论),开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。
js
function createPerson (name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
var person1 = createPerson('Nicholas', 29, 'SE');
var person2 = createPerson('Greg', 27, 'Doctor');
构造函数模式
js
function Person (name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
console.log(this.name);
}
}
var person1 = new Person('Nicholas', 29, 'SE');
var person2 = new Person('Greg', 27, 'Doctor');
在这个例子中,Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()中相同的部分之外,还存在以下不同之处:
- 没有显式地创建对象;
- 将属性和方法赋给了this对象;
- 没有return语句
此外,还应该注意到函数名Person
使用的是大写字母P
。按照惯例,构造函数始终都应该以一下大写字母开头,而非构造函数则应该以一个小写字母开头。
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。
将构造函数当作函数
构造函数与其他函数的唯一区别,就是调用它们的方式不同。不过,构造函数毕竟也是函数,存在定义构造函数的特殊语法。任何函数,只要通过new命令
来调用,那它就可以作为构造函数,不通过new命令
调用,那它跟普通函数不会有什么两样。
js
// 接上例
// 当作构造函数使用
var person = new Person('Nicholas', 29, 'SE');
person.sayName(); // 'Nicholas'
// 作为普通函数调用
Person('greg', 27, 'Doctor');
window.sayName(); // 'Greg'
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'Kristen', 25, 'Nurse');
o.sayName(); // 'Kristen'
构造函数的问题
构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。
原型模式
我们创建的每个函数都有一个prototype(原型)
属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特写类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以让这些信息直接添加到原型对象中。
js
function Person () {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person();
person1.sayName(); // 'Nicholas'
var person2 = new Person();
person1.sayName(); // 'Nicholas'
console.log(person1.sayName === person2.sayName) // true
理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特写的规则为该函数创建一个prototype属性
,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性
,这个属性是一个指向prototype属性
所在的函数的指针。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性
;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMS-262第5版中管这个指针叫Prototype
。
isPrototypeOf()
虽然在所有实现中都无法访问到Prototype
,但可以通过isPrototypeOf()
方法来确定对象之间是否存在这种关系。从本质上讲,如果Prototype
指向调用isPrototype()
方法的对象,那么这个方法就返回true。
js
function Person () {}
var person1 = new Person();
var person2 = new Person();
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
Object.getPrototypeOf()
ES5新增了一个方法——Object.getPrototypeOf()
,在所有支持的实现中,这个方法返回Prototype
的值。
js
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写。如果我们在实例中添加一个属性,而该属性与实例原型中的某一个属性同名,那么就会屏蔽掉原型中的那个属性。
js
function Person () {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = 'Greg';
console.log(person1.name); // 'Greg'
console.log(person2.name); // 'Nicholas'
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加的这个属性只会阻止而不会去修改原型上的那个属性。即使将这个属性值设置为null,也只会在实例中设置这个属性,而不会恢复原型的连接。不过,使用delete命令
则可以完全移除这个实例属性,从而让我们能够重新访问原型中的属性。
js
// 接上例
delete person1.name;
console.log(person1.name); // 'Nicholas'
console.log(person2.name); // 'Nicholas'
hasOwnProperty()
使用hasOwnProperty()
方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回true。
js
function Person () {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty('name')); // false
person1.name = 'Greg';
console.log(person1.hasOwnProperty('name')); // true
delete person1.name;
console.log(person1.hasOwnProperty('name')); // false
原型与in命令
有两种方式使用in命令
;单独使用和在for-in循环
中使用。在单独使用时,in命令
会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。
js
console.log(person1.hasOwnProperty('name')); // false
console.log('name' in person1); // true
person1.name = 'Greg';
console.log(person1.hasOwnProperty('name')); // true
console.log('name' in person1); // true
delete person1.name;
console.log(person1.hasOwnProperty('name')); // false
console.log('name' in person1); // true
由于in命令
只要通过对象能够访问到属性就返回true,hasOwnProperty()
只在属性存在于实例时才返回true,因此只要in命令
返回true而hasOwnProperty()
返回false,就可以确定属性是原型中的属性。
在使用for-in循环
时,返回的是所有能够通过对象访问的、可枚举的属性。根据规定,所有开发人员定义的属性都是可枚举的(IE8及更早的版本中例外),所以for-in
循环也可以返回原型中Enumerable
为false
,但被实例同名属性屏蔽的属性。
js
var o = {
toString: function () {
return 'My Object';
}
};
for (var prop in o) {
if (prop === 'toString') {
console.log('Found toString');
}
}
// Found toString
要取得对象上所有可枚举的实例属性,可以使用ES5的Object.keys()
方法,这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
js
function Person () {}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'SE';
Person.prototype.sayName = function () {
console.log(this.name);
}
console.log(Object.keys(Person.prototype)); // [ 'name', 'age', 'job', 'sayName' ]
var p1 = new Person();
p1.name = 'Rob';
p1.age = 41;
console.log(Object.keys(p1)); // [ 'name', 'age' ]
如果你想要得到所有实例属性,无论它是否可枚举,可以使用Object.getOwnPropertyNames()
;
js
console.log(Object.getOwnPropertyNames(Person.prototype)); // [ 'constructor', 'name', 'age', 'job', 'sayName' ]
更简单的原型语法
为减少不必要的输入,也为了从视觉上更好地封装原型的功能,最常见的做是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
js
function Person() {};
Person.prototype = {
name: 'Nicholas',
age: 29,
job: 'SE',
sayName: function () {
console.log(this.name);
}
}
在上面的代码中,我们将Person.prototype
设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor属性
不再指向Person了。之前我们介绍过,每创建一个函数,就会同时创建它的prototype对象
,这个对象也会自动获得constructor属性
。而我们这里使用的语法,本质上完全重写了默认的prototype对象
,因此constructor属性
也就变成了新对象的constructor属性
(指向Object构造函数),不再指向Person函数。此时,尽管instanceof命令
还能返回正确的结果,但通过constructor
已经无法确定对象的类型了。
js
var friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor === Object); // true
console.log(friend.constructor === Person); // false
如果,constructor
的值真的很重要,可以像下面这样特意将它设置回适当的值。
js
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Nicholas'
// ...
}
注意,以这种方式重设constructor属性
会导致它的Enumerable
特性被设置为true。默认情况下,原生的constructor属性
是不可枚举的,因此,我们可以使用Object.defineProperty()
加以改进。
js
function Person () {}
Person.prototype = {
name: 'Nicholas'
// ...
}
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
js
var friend = new Person();
Person.prototype.sayHi = function () {
console.log('hi')
}
friend.sayHi(); // 'hi'
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的Prototype指针
,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。
js
function Person() {}
var friend = new Person();
Person.prototype = {
constructor: Person,
name: 'Nicholas',
sayName: function () {
console.log(this.name);
}
}
friend.sayName(); // Error
原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String等)都在其构造函数的原型上定义了方法。
js
console.log(typeof Array.prototype.sort); // 'function'
console.log(typeof String.prototype.substring); // 'function'
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随便添加方法。
js
String.prototype.startsWith = function (str) {
return this.indexOf(str) === 0;
}
var msg = 'Hello world!';
console.log(msg.startsWith(msg)); // true
原型对象的问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值的属性倒也说得过去,然而,对于包含引用类型值的属性来说,问题就比较突出了。
js
function Person () {}
Person.prototype = {
constructor: Person,
name: 'Nicholas',
age: 29,
job: 'SE',
friends: ['Shelby', 'Court'],
sayName: function () {
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Van');
console.log(person1.friends); // [ 'Shelby', 'Court', 'Van' ]
console.log(person2.friends); // [ 'Shelby', 'Court', 'Van' ]
console.log(person1.friends === person2.friends); // true
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。
js
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 Person1('Nicholas', 29, 'SE');
var person2 = new Person2('Greg', 27, 'Doctor');
person1.friends.push('Van');
console.log(person1.friends); // [ 'Shelby', 'Court', 'Van' ]
console.log(person2.friends); // [ 'Shelby', 'Court' ]
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true
动态原型模式
通过检测某个应该存在的方法是否有效,来决定是否初始化原型。
js
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, 'SE');
friend.sayName();
寄生构造函数模式
通常,在前述几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封闭创建对象的代码,然后再返回新创建的对象。从表面上看,这个函数又很像是典型的构造函数。
js
function Person (name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
}
return o;
}
这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。
js
function SpecialArray () {
var arr = new Array();
arr.push.apply(arr, arguments);
arr.toPipedString = function () {
return this.join('|');
}
return arr;
}
关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象在与构造函数外部创建的对象没有什么不同。为此不能依赖instanceof命令
来确定对象类型。
稳妥构造函数模式
稳定构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用this;二是不使用new命令
调用构造函数。
js
function Person (name, age, job) {
var o = new Object();
// 可以在这里定义私有属性和方法
// 添加方法
o.sayName = function () {
console.log(name);
}
return o;
}
继承
ECMAScript只支持实现继承,不支持接口继承。而且其实现继承主要是依靠原型链来实现的。
原型链
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
实现原型链有一种基本模式,其代码大致如下:
js
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
在上面的代码中,我们没有使用SubType默认提供的原型,而是给它换了一个新原型;这个新原型就是SuperType的实例。于是,新原型不仅具有作为一个SuperType的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType的原型。最终的结果是这样的:instance指向SubType的原型,SubType的原型又指向了SuperType的原型。getSuperValue()方法仍然还在SuperType.prototype中,但property则位于SubType.prototype中。
默认的原型
我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype
。这也正是所有自定义类型都会继承toString()
、valueOf()
等默认方法的根本原因。所以,我们说上面的例子展示的原型链中还应该包括另外一个继承层次。
确定原型和实例的关系
instanceof()
只要用instanceof命令
来测试实例与原型链中出现过的构造函数,结果就会返回true。
js
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
isPrototypeOf()
只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()
也会返回true。
js
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
谨慎地定义方法
子类型有时候需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
js
function SuperType () {
this.property = true;
}
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SubType () {
this.subproperty = false;
}
SubType.prototype = new SuperType();
// 添加新方法
SubType.prototype.getSubValue = function () {
return this.subproperty;
}
// 重写超类型中的方法
SubType.prototype.getSuperValue = function () {
return false;
}
var instance = new SubType();
console.log(instance.getSuperValue); // false
还有一点需要注意,在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。
原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。
js
function SuperType () {
this.colors = ['red', 'green', 'blue'];
}
function SubType () {}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue', 'black']
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
借用构造函数
在解决原型中包含引用类型值所带问题的过程中,开发人员开使使用一种叫做借用构造函数的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。
js
function SuperType () {
this.colors = ['red', 'green', 'blue'];
}
function SubType () {
SuperType.call(this); // 使用apply也行
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'green', 'blue', 'black']
var instance2 = new SubType();
console.log(instance2.colors); // ['red', 'green', 'blue']
传递参数
js
function SuperType (name) {
this.name = name;
}
function SubType () {
SuperType.call(this, 'Nicholas');
this.age = 29;
}
var instance = new SubType();
console.log(instance.name); // 'Nicholas'
console.log(instance.age); // 29
借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用也就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
组合继承
组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
js
function SuperType (name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
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', 'green', 'blue', 'black']
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // ['red', 'green', 'blue']
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27
原型式继承
js
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
在object()函数的内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
克罗克福德主张的这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。
ES5通过新增Object.create()
方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()
和object()
方法的行为相同。
js
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); // [ 'Shelby', 'Court', 'Van', 'Rob', 'Barbie' ]
Object.create()
方法的第二个参数与Object.defineProperties()
方法的第二个参数相同。
js
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = Object.create(person, {
name: {
value: 'Greg'
}
});
console.log(anotherPerson.name); // 'Greg'
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推广的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
js
function createAnother (original) {
var clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function () { // 以某种方式来增强这个对象
console.log('hi');
};
return clone; // 返回这个对象
}
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()
函数不是必需的;任何能够返回新对象的函数都适用此模式。
寄生组合式继承
组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
js
function SuperType (name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
SuperType.prototype.sayName = function () {
// ...
}
function SubType (name, age) {
SuperType.call(this, name); // 第二次调用SuperType()
}
SubType.prototype = new SuperType(); // 第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
// ...
}
为了解决这个问题,我们需要使用寄生组合式继承。所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
js
function inheritPrototype (subType, superType) {
var prototype = object(superTYpe.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 指定对象
}
function SuperType (name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
SuperType.prototype.sayName = function () {
// ...
}
function SubType (name, age) {
SuperType.call(this, name);
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {
// ...
}