1 继承方式
1.1 原型链继承
需要两个构造函数来完成一个原型链继承
基本原理:使用类似作用域的原型链,进行继承查找
// SuperType 构造函数称为超类
function SuperType (){
this.name='super';
this.friend=[];
this.property = true;
}
SuperType.prototype.getName=function(){
return this.name;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
// SubType 构造函数称为子类
function SubType(name,age){
this.name=name;
this.age=age;
this.subproperty = false;
}
// 继承了superType
SubType.prototype=new SuperType();
SubType.prototype.constrcutor=SubType;
SubType.prototype.getAge=function(){
return this.age;
}
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var child = new SubType('shiny',12);
console.log(child.getName())//shiny
console.log(child.getAge())//12
定义两个构造函数,分别为父类(SuperType)、子类(SubType),为了实现子类能够使用父类的属性(本身和原型上面的属性)。重写子类的原型,让子类的原型指向父类实例,这样子类的构造函数就是父类的实例地址,实现子类可以使用父类的本身和原型上的属性
优点: 实现简单。
缺点:
- 不能向父类传递参数。
- 引用类型值的原型属性会被所有实例共享。
function Parent() {
this.likeFood = ['香蕉', '榴莲', '芒果']; // 引用属性
this.name="aaa" // 普通属性
}
Parent.prototype.getName = function () {
return this.name
}
function Child(name) {
this.name = name
}
Child.prototype = new Parent()
var nan = new Child('nan')
nan.name="bbbb"
var bei = new Child('bei')
nan.likeFood.push('木瓜')
console.log(nan.likeFood) // [ '香蕉', '榴莲', '芒果', '木瓜' ]
console.log(bei.likeFood) // [ '香蕉', '榴莲', '芒果', '木瓜' ]
console.log(nan.name) // bbbb
console.log(bei.name) // bei
修改nan对象的值,普通属性不会互相影响,引用类型的属性会互相影响。
1.2 借用构造函数继承
基本原理:子类构造函数内部调用父类构造函数,并传入 this指针。使用call apply方法,通过执行方法修改tihs (上下文),是的父级的this变成子类实例的this,这样每个实例都会得到父类的属性,实现引用属性备份
例子1
// 把父类当中一个函数使用
function SuperType(name){
this.name=name
this.friend=['a','b']
}
SuperType.prototype.getFriend=function(){
return this.firend
}
function SubType(name){
// 执行父类函数
SuperType.call(this,name);
}
var child = new SubType('shiny')
var childRed = new SubType('red')
console.log(child.name)//shiny
console.log(childRed.name)//red
// 修改引用类型的属性
child.friend.push('c')
console.log(child.friend)//a,b,c
console.log(childRed.friend)//a,b
// 子类不能调用父类原型的方法
console.log(childRed.getFriend)//undefined
例子2
function Parent(name) {
this.name=name // 普通属性
this.likeFood = ['香蕉', '榴莲', '芒果']; // 引用属性
}
function Child(name) {
Parent.call(this,name)
}
Parent.prototype.getName = function () {
return this.name
}
var nan = new Child('nan')
nan.likeFood.push('木瓜')
var bei = new Child('bei')
console.log(nan.likeFood) // [ '香蕉', '榴莲', '芒果', '木瓜' ]
console.log(bei.likeFood) // [ '香蕉', '榴莲', '芒果' ]
console.log(nan.name) // nan
console.log(bei.name) // bei
console.log(nan.getName()) // TypeError: nan.getName is not a function 超类的原型定义的方法对于子类是不可使用的,子类的实例只是得到了父类的this绑定的属性
优点
- 解决了原型链继承的 引用类型操作问题
- 解决了父类传递参数问题
缺点:
- 超类的原型定义的方法对于子类是不可使用的,子类的实例只是得到了父类的this绑定的属性
使用场景:
父类中需要一些子类使用共享的引用类型,并且子类可能会操作父类共享的引用类型 但是父类的非this绑定的属性和方法是不可以使用的(放在父类prototype的属性和方法)
1.3 组合继承
基本原理:组合继承(原型链继承+借用构造函数继承)
- 使用原型链的继承实现,通过原型查找功能来满足原型链共享方法
- 使用借用构造函数方法,使用实例备份父类共享引用类型备份
function Parent(name) {
this.name=name // 普通属性
this.likeFood = ['香蕉', '榴莲', '芒果']; // 引用属性
}
function Child(name,age) {
Parent.call(this,name) // 构造函数继承
this.age=age
}
Parent.prototype.getName = function () {
return this.name
}
// 原型链继承
Child.prototype=new Parent()
Child.prototype.constructor=Child
Child.prototype.getAge = function(){
return this.age;
}
var nan = new Child('nan',12)
nan.likeFood.push('木瓜')
var bei = new Child('bei',13)
console.log(nan.likeFood) // [ '香蕉', '榴莲', '芒果', '木瓜' ]
console.log(bei.likeFood) // [ '香蕉', '榴莲', '芒果' ]
console.log(nan.name) // nan
console.log(bei.name) // bei
console.log(nan.getName()) // nan
console.log(nan.getAge()) // 12
优点:
- 解决了原型链继承引用类型的实例操作导致引用改变,修改属性不互相影响
- 解决了借构造函数继承方式的,父类原型子类实例可以使用(子类可以调用父类原型的方法)
缺点:
- 父类的构造函数被实例换了两次 Child.prototype=new Parent() Parent.call(this,name)
- 实例会有父类的构造函数的一些this属性、子类的构造函数(prototype)上也有一份实例的上有的属性
使用场景
得到原型链继承和构造函数继承的优点,是被开发人员认可的一种继承方式,但是也有他的缺点
1.4 原型式继承
基本原理:借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
原型式继承,其实就是和原型链继承差别不大,只是省去了构造函数这一部,但是原型式继承也是有缺点的(不能够给备份的对象添加属性)
核心: 我们需要创建一个临时的构造函数,并将作为父类的对象作为构造函数的原型,并返回一个新对象。
// 在object()函数内部,先创建了一个临时性构造函数,然后将传入的对象作为这个构造函数的原型
// 最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象进行了一次浅复制
function object(o){
function F(){};
F.prototype=o;
return new F()
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var personShiny = object(person);
var personRed = object(person);
console.log(personShiny.name)//Nicholas
console.log(personRed.name)//Nicholas
personShiny.friends.push('red');
personRed.friends.push('shiny');
console.log(personShiny.friends)// [ 'Shelby', 'Court', 'Van', 'red', 'shiny' ]
console.log(personShiny.friends) // [ 'Shelby', 'Court', 'Van', 'red', 'shiny' ]
console.log(personRed.friends) // [ 'Shelby', 'Court', 'Van, 'red', 'shiny' ]
父类的属性对于子类来说都是共享的。所以,如果我们只是想一个对象和另一个对象保持一致,这将是不二之选。
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与 object()方法的行为相同。
function object(o){
function F(){};
F.prototype=o;
return new F()
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var personShiny = Object.create(person);
var personRed = Object.create(person);
console.log(personShiny.name)//Nicholas
console.log(personRed.name)//Nicholas
personShiny.friends.push('red');
personRed.friends.push('shiny');
console.log(personShiny.friends)// [ 'Shelby', 'Court', 'Van', 'red', 'shiny' ]
console.log(personShiny.friends) // [ 'Shelby', 'Court', 'Van', 'red', 'shiny' ]
console.log(personShiny.friends)//[ 'Shelby', 'Court', 'Van', 'red', 'shiny' ]
优点:
- 项目1再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分
缺点:
- 一些引用数据操作的时候会出问题,两个实例会公用继承实例的引用数据类
谨慎定义方法,以免定义方法也继承对象原型的方法重名 - 谨慎定义方法,以免定义方法也继承对象原型的方法重名
- 无法直接给父级构造函数使用参数
使用场景:
在不使用构造函数的情况下,只想让一个对象与另一个对象保持类似的情况下
1.5 寄生继承
基本原理:备份一个对象,然后给备份的对象进行属性添加,并返回
类似构造函数,通过一个执行方法,里面创建一个对象,为该对象添加属性和方法,然后返回
代码实现:
// 和工厂模式非常类似,创建一个对象,增强一些功能并返回该对象
function createAnother(o){
var clone = Object(o);
clone.sayHi=function(){
console.log('hi')
}
return clone
}
var person = {
name:'shiny',
friends:['a','b']
}
var personShiny = createAnother(person);
console.log(personShiny.sayHi())//Ho
优点:
- 再不用创建构造函数的情况下,实现了原型链继承,代码量减少一部分
- 可以给备份的对象添加一些属性
缺点:
- 父类被实例化两次、子类实例和子类的构造函数都有相同的属性
使用场景:
在考不使用构造函数的情况下实现继承,前面示 范继承模式时使用的 object()函数不是必需的;任何能够返回新对象的函数都适用于此模式
1.6 寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定子类型的原型而调用超类型的构造函数,我们需要的无非就是超类型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
基本原理: 子类构造函数内通过call、apply方法进行修改父类构造函数的this和执行父类构造函数,使的子类的实例拥有父类构造函数的一些属性,结合子类的原型修改成父类构造函数的原型,并把父类的原型的constructor指向子类构造函数
function inheritPrototype({SubType,SuperType}){
const prototype = Object(SuperType.prototype); //创建对象
prototype.constrcutor=SubType; //增强对象
SubType.prototype=prototype; //指定对象
}
这个示类的inheritPrototype()函数实现了寄生组合式继承的最简单形式
函数接收两个参数:子类型构造函数和超类型构造函数
在函数内部,第一步是创建超类型原型的一个副本
第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性
第三步将创建的对象(即副本)赋值给子类型的原型
这样我们调用inheritPrototype函数的语句,就可以替换前面例子中为子类型原型赋值的语句了
function inheritPrototype({SubType,SuperType}){
const prototype = Object(SuperType.prototype);
prototype.constrcutor=SubType;
SubType.prototype=prototype;
}
function SuperType(name){
this.name=name;
this.friends=["red","blue","green"]
}
SuperType.prototype.getName=function(){
return this.name;
}
function SubType(name,age){
this.age=age;
SuperType.call(this,name)
}
inheritPrototype({SubType,SuperType});
SubType.prototype.getAge=function(){
return this.age
}
var SubTypeShiny = new SubType('Shiny',23);
SubTypeShiny .friends.push('c')
var SubTypeRed = new SubType('Red',21);
SubTypeRed .friends.push('d')
console.log(SubTypeShiny.getName())//Shiny
console.log(SubTypeShiny.getAge())//22
console.log(SubTypeShiny.friends)//[ 'red', 'blue', 'green', 'c' ]
console.log( SubTypeRed.getName())//Red
console.log( SubTypeRed.getAge())//21
console.log( SubTypeRed.friends)//[ 'red', 'blue', 'green', 'd' ]
优点:
- 在少一次实例化父类的情况下,实现了原型链继承和借用构造函数
- 减少了原型链查找的次数(子类直接继承超类的prototype,而不是父类的实例)
组合继承和寄生组合继承的原型图对比
2 相关知识
2.1 Object.create()
参考链接
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
}
};
const me = Object.create(person); // me.__proto__ === person
me.name = "Matthew"; // name属性被设置在新对象me上,而不是现有对象person上
me.isHuman = true; // 继承的属性可以被重写
me.printIntroduction(); // My name is Matthew. Am I human? true
Object.create(proto[, propertiesObject])
proto必填参数,是新对象的原型对象,如上面代码里新对象me的__proto__指向person。注意,如果这个参数是null,那新对象就彻彻底底是个空对象,没有继承Object.prototype上的任何属性和方法,如hasOwnProperty()、toString()等。
var a = Object.create(null);
console.dir(a); // {}
console.log(a.__proto__); // undefined
console.log(a.__proto__ === Object.prototype); // false
console.log(a instanceof Object); // false 没有继承`Object.prototype`上的任何属性和方法,所以原型链上不会出现Object
propertiesObject是可选参数,指定要添加到新对象上的可枚举的属性(即其自定义的属性和方法,可用hasOwnProperty()获取的,而不是原型对象上的)的描述符及相应的属性名称。
var bb = Object.create(null, {
a: {
value: 2,
writable: true,
configurable: true
}
});
console.dir(bb); // {a: 2}
console.log(bb.__proto__); // undefined
console.log(bb.__proto__ === Object.prototype); // false
console.log(bb instanceof Object); // false 没有继承`Object.prototype`上的任何属性和方法,所以原型链上不会出现Object
// ----------------------------------------------------------
var cc = Object.create({b: 1}, {
a: {
value: 3,
writable: true,
configurable: true
}
});
console.log(cc); // {a: 3}
console.log(cc.hasOwnProperty('a'), cc.hasOwnProperty('b')); // true false 说明第二个参数设置的是新对象自身可枚举的属性
console.log(cc.__proto__); // {b: 1} 新对象cc的__proto__指向{b: 1}
console.log(cc.__proto__ === Object.protorype); // false
console.log(cc instanceof Object); // true cc是对象,原型链上肯定会出现Object
Object.create()创建的对象的原型指向传入的对象。跟字面量和new关键字创建有区别。
自己实现一个Object.create()
Object.mycreate = function(proto, properties) {
function F() {};
F.prototype = proto;
if(properties) {
Object.defineProperties(F, properties);
}
return new F();
}
var hh = Object.mycreate({a: 11}, {mm: {value: 10}});
console.dir(hh);
总结:Object.create(arg, pro)创建的对象的原型取决于arg,arg为null,新对象是空对象,没有原型,不继承任何对象;arg为指定对象,新对象的原型指向指定对象,继承指定对象