对象继承
对象的继承是一个面试非常容易被问到的一个问题,但是却很少有人能够完整通透地将继承讲明白。
什么是继承
继承就是指2个类之间的一种关系;为了实现数据共享,可以通过继承,使子类也具有父类的相关特征和行为(即属性和方法)。
继承原理
在JavaScript中,对象的继承主要是依赖于原型链来实现的。
使用子类中的某个方法或者属性时,会首先在子类的属性和原型中进行寻找,如果没有找到则会按照原型链一级一级往上找。
6种继承方式
这里我们会详细讲述6种继承的方式。
1.原型链继承
实现方式: 原型链继承的本质就是复制,会把子构造函数的原型对象重写为父类的构造函数的原型。
function FatherFun(name, age) {
this.name = name;
this.age = age;
}
FatherFun.prototype.getType = () => {
console.log("我是父构造函数");
};
FatherFun.prototype.wealth = ["money", "house"];
function SonFun(name, age) {
this.name = name;
this.age = age;
}
SonFun.prototype = FatherFun.prototype;
const son1 = new SonFun("xm", 18);
const son2 = new SonFun("xh", 19);
son1.getType(); // 我是父构造函数
console.log(son1.wealth.push("knowledge"));
console.log(son2.wealth); // ['money', 'house', 'knowledge']
console.log(
son1.__proto__ === FatherFun.prototype,
son1.constructor === FatherFun
); // true true
如上图所示,我们发现在son1的实例上,我们能够调用父构造函数原型上的方法了,所以我们的son1继承了父构造函数原型上的方法。
我们也发现了son1的原型变成了父构造函数的原型,原型中的constructor都指向了FatherFun这个构造函数。
而且我们无法给超类型的构造函数传递参数。
同时,如果我们通过子构造函数的实例去修改原型上的引用属性时,子类的所有实例也会同步修改。
因此,原型链继承虽然实现了一个简单的继承,但是是存在以下一些问题的:
- 无法给父类构造函数传递参数
- 子类和父类使用的是同一个原型对象(即父类的原型对象),如果原型链中包含引用类型值的时候,这个引用类型会被所有实例共享。
- 通过constructor来判断子类类型时,不会等于子类的构造函数。
2.借用构造函数方式
实现方式: 在子类的构造函数内部通过call或者apply调用父类的构造函数,并将this指向为新创建的对象。
function FatherFun(name, age) {
this.name = name;
this.age = age;
this.wealth = ["money", "house"];
this.work = function () {
console.log("FatherFun构造函数内部方法");
};
}
FatherFun.prototype.sayHi = function () {
console.log("这是Father的原型里的sayHi");
};
function SonFun(name, age) {
FatherFun.call(this, name, age); //继承了Father,且向父类型传递参数
}
const son1 = new SonFun("小明", 18);
const son2 = new SonFun("弟弟", 3);
console.log(son1); // age:18 name:小明 wealth:[money,house] work:f()
console.log(son1.__proto__ === FatherFun.prototype); // false
// son1.sayHi(); // sayHi is not a function
son2.wealth.push("milk");
console.log(son1.constructor === SonFun); // true
console.log(son2.wealth, son1.wealth); // ['money', 'house', 'milk'] ['money', 'house']
console.log(son1 instanceof FatherFun); // false
如上图所示,我们可以看到通过该方式创建的2个子实例,继承了父构造函数内部的方法和属性。但是并没有继承父构造函数的原型上的方法和属性。
当然,相比于第一种原型链的继承,该方式不会有引用类型的问题,同时也能够通过constructor来确定实例的类型。但是通过instanceof来判断时,无法证明son1属于FatherFun的子实例。
因此会存在以下一些问题:
- 方法都在构造函数中定义
- 超类原型对象定义得到方法对子类的实例不可见,故实现不了方法的共享
3.组合继承方式
实现方式: 结合上述的两种继承方式,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
function FatherFun(name, age) {
this.name = name;
this.age = age;
this.wealth = ["money", "house"];
}
FatherFun.prototype.getSay = function () {
console.log("父构造函数原型的getSay方法");
};
function SonFun(sex, nickname, name, age) {
this.sex = sex;
this.nickname = nickname;
FatherFun.call(this, name, age);
}
SonFun.prototype = new FatherFun();
const son1 = new SonFun("男", "11", "张三", 16);
const son2 = new SonFun("女", "33", "lili", 3);
console.log(son1);
console.log(
son1.__proto__ === SonFun.prototype,
son1.__proto__ === FatherFun.prototype
); // true false
console.log(son1.constructor === SonFun); // false
console.log(son1 instanceof FatherFun); // true
son2.wealth.push("milk");
console.log(son1.wealth); // ['money', 'house']
son1.getSay(); // 父构造函数原型的getSay方法
如上图所示,我们发现通过该方式创建的子实例中,可以通过原型链访问到父构造函数的原型方法,也能够拥有父构造函数的属性。构造函数内的引用类型也不会影响到其他的子实例。
但是我们能够发现一个问题,那就是子构造函数的原型是父构造函数的一个实例,因此son1.constructor === SonFun会等于false。
而且,看上述代码,我们分别在子构造函数内部调用了一次父构造函数,还在外部设置子构造函数的原型时又调用了一次。
因此存在以下问题:
- 会调用父构造函数2次
- 子实例的原型是父构造函数的实例,原型上的constructor属性指向了父构造函数。
4.原型式继承
实现方式: 原型式继承本质是一个浅拷贝,在函数内部先创建一个临时的构造函数,然后将传入函数的对象作为这个函数的原型,最后返回这个函数的实例。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
const a = {
a: 1,
b: 2,
say: function () {
console.log("这是a的say方法");
},
};
const result = object(a);
console.log(result);
console.log(result.a); // 1
result.say(); // 这是a的say方法
如上图所示,得到的是一个F()的实例,而F构造函数的原型是指向传入object(o)的对象a。因此我们在后面使用
result.a和result.say时,实际上是通过原型链访问到了F构造函数的原型。
在es6中,Object.create()实现了这种继承方式。
5.寄生式继承
实现方式: 寄生式继承的实现方式就是在原型式继承的基础上,通过某种方式来增加对象。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function createAnother(original) {
var clone = object(original); //通过调用object函数创建一个新对象
clone.sayHi = function () {
//以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
const o = {
a: 1,
b: 2,
};
const result = createAnother(o);
console.log(result);
如上述代码所示,寄生式继承就是在原型式继承的基础上,将原型式继承产生的实例进行加强,然后返回这个对象。
6.寄生式组合继承
实现方式: 可以理解为寄生式继承和组合式继承的结合版本。
- 首先一个极限继承的核心函数。
- 在函数内部来修改子类的原型。
- 这个函数有2个参数,分别是子构造函数和父构造函数。
- 定义一个临时构造函数F,将这个构造函数的原型指向父构造函数的原型。
- 然后将子构造函数的原型指向临时构造函数F的实例。
- 注意记得将修改过后的子构造函数的原型的constructor属性指回自己。
- 子构造函数的内部需要使用call去调用父构造函数,将this指向新创建的对象(和上述构造函数方式实现继承一样)。
// superType:父构造函数 subType:子构造函数
function inheritPrototype(subType, superType) {
function F() {}
//F()的原型指向的是superType
F.prototype = superType.prototype;
//subType的原型指向的是F()
subType.prototype = new F();
// 重新将构造函数指向自己,修正构造函数
subType.prototype.constructor = subType;
}
// 设置父类
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);
};
var instance = new SubType("Taec", 18);
console.log(instance);
console.log(instance.__proto__);
console.log(instance.constructor);
如上述所示,这种方式是一个比较推荐的继承方式。
- 能够将父类的方法进行复用
- 也不会和组合式继承一样调用两次父类构造函数
- 子类构建实例的时候也能往父类传递参数