共计 8438 个字符,预计需要花费 22 分钟才能阅读完成。
JavaScript 继承的六种形式
很多面向对象语言都反对 接口继承 和实现继承 两种继承形式,前者只继承办法签名,后者继承理论的办法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 惟一反对的继承形式,这次要是通过 原型链 实现的。
1. 原型链
1.1 思路
ECMAScript-262 把原型链定义为 ECMAScript 的次要继承形式。其根本思维是 通过原型继承多个援用类型的属性和办法。
构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回与之关联的构造函数,而实例有一个外部指针指向原型。
原型链:若原型是另一个类型的实例,则这个原型自身有一个外部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样在实例和原型之间就结构了一条原型链。
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;}
let instance = new SubType();
console.log(instance.getSuperValue()); // true
SubType
通过创立 SuperType
的实例并将其赋值给本人的原型 SubType.prototype
实现了对 SuperType
的继承,这一赋值重写了 Subtype
最后的原型,将其替换为 SuperType
的实例,即 SuperType
实例能够拜访的所有属性和办法也会存在于 SubType.prototype
,同时于是instance
(通过外部的[[Prototype]]
)指向SubType.prototype
,而SubType.prototype
(作为SuperType
的实例又通过外部的[[Prototype]]
)指向SuperType.prototype
。
留神:
getSuperValue
办法还在SuperType.prototype
对象上,而property
属性则在SubType.prototype
上。因为getSuperValue
是一个原型办法,而property
是一个实例属性。SubType.prototype
当初是SuperType
的一个实例,因而其上会存储property
属性。- 因为
SubType.prototype
的constructor
属性被重写为指向SuperType
,所以instance.constructor
也指向SuperType
。 - 在通过原型链实现继承时,搜寻过程会沿着原型链持续向上,调用
instance.getSuperValue
会顺次搜寻instance
、SubType.prototype
和SuperType.prototype
。在找不到属性或办法时,搜寻过程总是要到原型链末端才会停下。
1.2 扩大
-
默认原型
默认状况下,所有援用类型都继承自
Object
,这也是通过原型链实现的。任何函数的默认原型都是一个Object
的实例,即这个实例有一个外部指针指向Object.prototype
。因而自定义类型可能继承包含toString
、valueOf
在内的所有默认办法。 -
原型与继承的关系
原型与继承的关系能够通过两种形式确定:
-
instanceof
操作符:若一个实例的原型链中呈现过相应的构造函数,则instanceof
操作符返回true
。console.log(instance instanceof Object); // true console.log(instance instanceof SuperType); // true console.log(instance instanceof SubType); // true
-
isPrototypeOf
办法:原型链中的原型调用这个办法并传入实例时返回true。
console.log(Object.prototype.isPrototypeof(instance)); // true console.log(SuperType.prototype.isPrototypeof(instance)); // true console.log(SubType.prototype.isPrototypeof(instance)); // true
-
-
对于办法
-
若心愿笼罩父类的办法或减少父类没有的办法时,必须在原型赋值之后再增加到原型上。
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;} // 笼罩已有办法 SubType.prototype.getSuperValue = function () {return false;} let instance = new SubType(); console.log(instance.getSuperValue()); // false
-
以对象字面量形式创立原型办法会毁坏之前的原型链,相当于重写了原型链,将原型设置为一个
Object
的实例。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 () {return this.subproperty;} someOtherMethod () {return false;} } let instance = new SubType(); console.log(instance.getSuperValue()); // error
-
-
原型链的问题
-
次要问题是当原型中蕴含援用值时,该援用值会在实例间共享,这也是属性通常在构造函数中定义而不会在原型上定义的起因。
function SuperType () {this.colors = ['red', 'blue', 'green']; } function SubType () {} SubType.prototype = new SuperType(); let instance1 = new SubType(); instance1.colors.push('black'); let instance1 = new SubType(); console.log(instance1.colors); // 'red', 'blue', 'green', 'black' console.log(instance2.colors); // 'red', 'blue', 'green', 'black'
当
SubType
通过原型继承SuperType
之后,SubType.prototype
变成SuperType
的一个实例,因此取得了本人的colors
属性,相似于创立了SubType.prototype.colors
属性,故SubType
的所有实例都会共享这个colors
属性。 - 在实例化子类型时不能给父类型的构造函数传参。
-
2. 盗用构造函数
盗用构造函数(constructor stealing)技术也称为对象假装或经典继承。
2.1 思路
基本思路是在子类构造函数中调用父类构造函数。因为函数就是在特定上下文中执行代码的简略对象,所以能够应用 apply
和call
办法以新创建的对象为上下文执行构造函数。
function SuperType () {this.colors = ['red', 'blue', 'green'];
}
function SubType () {
// 继承 SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push('black');
let instance1 = new SubType();
console.log(instance1.colors); // 'red', 'blue', 'green', 'black'
console.log(instance2.colors); // 'red', 'blue', 'green'
SuperType
构造函数在为 SubType
的实例创立的新对象的上下文中执行,相当于新的 SubType
对象上运行了 SuperType
构造函数中的所有初始化代码,即每个实例都会有本人的 colors
属性。
2.2 扩大
-
传递参数
盗用构造函数的长处是能够在子类构造函数中向父类构造函数传参。
function SuperType (name) {this.name = name;} function SubType () {SuperType.call(this, 'Stan'); this.age = 24; } let instance = new SubType(); console.log(instance.name); // 'Stan' console.log(instance.age); // 24
在
SubType
构造函数中调用SuperType
构造函数时传入参数,实际上会在SubType
的实例上定义name
属性。为确保
SuperType
构造函数不会笼罩SubType
定义的属性,能够在调用父类构造函数之后再给子类实例增加额定的属性。 -
问题
盗用构造函数的次要毛病也是应用构造函数模式自定义类型的问题,即必须在构造函数中定义方法,因而函数不能重用。此外,子类也不能拜访父类原型上定义的办法,因而所有类型只能应用构造函数模式。
3. 组合继承
组合继承也称为伪经典继承,综合了原型链和盗用结构函树的长处。组合继承补救了原型链和盗用构造函数的有余,是 JavaScript 中应用最多的继承模式。同时,组合继承也保留了 instanceof
操作符和 isPrototypeOf
办法辨认合成对象的能力。
3.1 思路
组合继承的基本思路是应用原型链继承原型上的属性和办法,而通过盗用构造函数继承实例属性。
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);
}
let instance1 = new SubType('Stan', 24);
instance1.colors.push('black');
console.log(instance1.colors); // 'red', 'blue', 'green', 'black'
instance1.sayName(); // 'Stan'
instance1.sayAge(); // 24
let instance2 = new SubType('Greg', 20);
console.log(instance2.colors); // 'red', 'blue', 'green'
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 20
3.2 扩大
组合继承也存在效率问题,最次要的是父类构造函数会被调用两次,第一次是在创立子类原型时调用,第二次是在子类构造函数中调用。实质上,子类原型最终要蕴含超类对象的所有实例属性,只有在子类构造函数执行时重写其原型即可。
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);
}
以上代码执行后,SubType.prototype
上会有 name
和colors
两个属性,二者是 SuperType
的实例属性,当初成为了 SubType
的原型属性。在调用 SubType
构造函数时,也会调用 SuperType
构造函数,此时会在新对象上创立 name
和colors
两个实例属性,此时将遮蔽原型上同名的两个属性。
解决组合继承效率问题的计划是寄生组合继承。
4. 原型式继承
4.1 思路
最后,原型式继承(prototypal inheritance)是一种不波及严格意义上构造函数的继承办法,出发点是即便不自定义类型也能够通过原型实现对象之间的信息共享。
function object (o) {function F () {}
F.prototype = o;
return new F();}
object
函数通过创立一个长期构造函数,并将传入的对象赋值给这个构造函数的原型,最初返回这个长期类型的一个实例,实现继承。实质上 object
函数对传入的对象执行了一次浅拷贝。
let person = {
name: 'Stan',
friends: ['xiaoming', 'xiaohong']
};
let anotherPerson = object(person);
anotherPerson.name = 'xiaobai';
anotherPerson.friends.push('xiaohei');
let yetAnotherPerson = object(person);
yetAnotherPerson.name = 'xiaohei';
yetAnotherPerson.friends.push('xiaohei');
console.log(person.friends); // 'xiaoming', 'xiaohong', 'xiaobai', 'xiaohei'
原型式继承实用于有一个对象并心愿在其根底上再创立一个新对象的状况。把这个对象传给 object
函数,而后再对返回的对象进行适当批改。
4.2 扩大
-
Object.create
办法ECMAScript5 减少了
Object.create
办法对原型式继承的概念进行了标准。Object.create
办法接管两个参数,作为新对象原型的对象,以及给新对象定义额定属性的对象(可选)。在只有第一个参数时,Object.create
办法与之前的object
办法成果雷同。let person = { name: 'Stan', friends: ['xiaoming', 'xiaohong'] } let anotherPerson = Object.create(person); anotherPerson.name = 'xiaobai'; anotherPerson.friends.push('xiaohei'); let yetAnotherPerson = Object.create(person); yetAnotherPerson.name = 'xiaohei'; yetAnotherPerson.friends.push('xiaohei'); console.log(person.friends); // 'xiaoming', 'xiaohong', 'xiaobai', 'xiaohei'
Object.create
办法的第二个参数与Object.defineProperties
办法的第二个参数一样,每个新增属性都通过各自的描述符来形容。let person = { name: 'Stan', friends: ['xiaoming', 'xiaohong'] } let anotherPerson = Object.create(person, { name: {value: 'Greg'} }); console.log(anotherPerson.name); // 'Greg'
-
问题
原型式继承实用于不须要独自创立构造函数,但依然须要在对象间共享信息的场合。但与原型链一样,属性中蕴含的援用值始终会在相干对象间共享。
5. 寄生式继承
寄生式继承(parasitic inheritance)是一种与原型式继承比拟靠近的继承形式。
5.1 思路
寄生式继承的思路相似于寄生构造函数和工厂模式,即创立一个实现继承的函数,以某种形式加强对象,最初返回这个对象。
function createAnother (original) {
// 创立一个新对象
let clone = object(original);
// 以某种形式加强对象
clone.sayHi = function () {console.log('hi');
};
return clone;
}
let person = {name: 'Stan'}
let anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'
5.2 扩大
- 寄生式继承同样实用于次要关注对象,而不在乎类型和构造函数的场景。
object
函数不是寄生式继承所必须的,能够应用任何返回新对象的函数。- 通过寄生式继承给对象增加函数会导致函数难以重用,与构造函数模式相似。
6. 寄生组合继承
寄生组合继承通过盗用构造函数继承属性,但应用混合式原型链继承办法。
寄生组合继承的基本思路是不通过调用父类构造函数给子类原型赋值,而是获得父类原型的一个正本,即应用寄生式继承来继承父类原型,再将返回的新对象赋值给子类原型。
function inheritPrototype (SubType, SuperType) {let prototype = object(SuperType.prototype);
prototype.constructor = SubType;
SubType.prototype = prototype;
}
inheritPrototype
函数实现了寄生组合继承的外围逻辑。函数接管子类构造函数和父类构造函数两个参数。在函数外部,首先创立父类原型的正本;其次给返回的 prototype
对象设置 constructor
属性,解决因为重写原型导致默认 constructor
失落的问题;将新创建的对象赋值给子类型的原型。
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;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {console.log(this.age);
};
应用寄生组合继承时,原型链依然放弃不变,instanceof
操作符和 isPrototypeOf
办法失常无效。寄生组合继承是援用类型继承的最佳模式。