什么是继承?
通过某种方式让一个对象可以访问到另一个对象中的属性和方法,我们把这种方式称之为继承。
通过定义,可以得到的结论:继承的目的就是对可以某些通用 属性和方法 的 共用,以达到减小开销的目的。
什么是好的继承?
由于继承的目的是为了减小开销,那么一个好的继承,标准就是:
- 在
继承实现的过程中创建的额外对象越少越好; - 创建出来的对象,副作用越小越好(每种创建方式的影响各不相同)。
1. 原型链继承
本质就是通过原型对象 prototype 对象来实现继承。
function Father() {
this.money = 10000;
this.houses = ['house1', 'house2', 'house3'];
}
function Son() {}
Son.prototype = new Father();
// Son.prototype.constructor = Son;
const son1 = new Son();
const son2 = new Son();
son1.houses.push('son1-house');
console.log(son2.houses); // ['house1', 'house2', 'house3', 'son1-house']
原型链继承虽然实现了 属性 和 方法 的共享,但是它是借用了 原型链,那么原型链存在的问题也被继承了:
- 原型对象中存在
复杂数据类型时,当其中一个实例修改了,其他实例也会收到影响(次要),比如son1在houses中添加了son1-house,son2 也共享到了修改后的数据; - 如果
Father需要传参,使用这种继承时,无法给父类传参(主要的问题)。
2. 借用构造函数
为了解决原型链继承的问题,通过构造函数的形式实现继承。
function Person(name, age) {
this.name = name;
this.age = age;
this.foodType = ['稻', '黍', '稷', '麦'];
this.eat = (food) => {
console.log(`吃了${food}`);
}
}
Person.prototype.move = () => {
console.log('go go go...');
}
function Male(name, age) {
/**
* 这里是解决原型链问题的关键:
* 1. 等于每个实例上都 复制 了一份父类的属性,所以自身改变了,不会影响其他实例;
* 2. 使用 call,父类可以传参
*/
Person.call(this, name, age);
}
const zs = new Male("张三", 20);
zs.foodType.push('豆');
console.log(zs.foodType); // ['稻', '黍', '稷', '麦', '豆']
const ls = new Male("李四", 18);
console.log(ls.foodType); // ['稻', '黍', '稷', '麦']
// 访问不到父类上的方法
ls.move(); // ls.move is not a function
但是它还是存在它的问题:
- 将父类上的属性给每个子类都复制了一份,虽然共享了数据和方法,但是影响性能;
- 不能继承父类原型链上的方法。
3. 组合式继承
组合继承就是将 原型继承 与 借用构造函数继承 结合起来。
function Person(name, age) {
this.name = name;
this.age = age;
this.foodType = ['稻', '黍', '稷', '麦'];
this.eat = (food) => {
console.log(`吃了${food}`);
}
}
Person.prototype.move = () => {
console.log('go go go...');
}
function Male(name, age) {
Person.call(this, name, age);
}
Male.prototype = new Person();
// 重写 constructor 属性,指向自己的构造函数
Male.prototype.constructor = Male;
const zs = new Male("张三", 20);
zs.foodType.push('豆');
console.log(zs.foodType); // ['稻', '黍', '稷', '麦', '豆']
const ls = new Male("李四", 18);
console.log(ls.foodType); // ['稻', '黍', '稷', '麦']
ls.move(); // go go go...
组合继承 相比于原型继承最大的区别就是一行代码 Male.prototype = new Person();。
缺点也同样明显:
- 父类实例化了两遍;
- 父类上的实例,在子类的原型上和子类上都存在,浪费内存。
注意:
组合式继承 算是最优 js 继承的主体,而剩下的继承方式,主要是优化父类的方法和属性,在子类的实例和原型上都存在的问题。
4.原型继承
这种方式,更像是 工厂设计模式 而不是继承方式,它通过空的一个构造函数,将传入的对象当这个构造函数的原型。
function generatorObj(obj) {
// 创建一个空的构造函数
function Fn() {}
Fn.prototype = obj;
return new Fn();
}
const person = {
vehicle: ['subway', 'car', 'bicycle'],
type: 'human'
}
const man = generatorObj(person);
man.name = 'zs';
console.log(man.type); // human
这样的方式实现了对方法和属性的共享,但是无法对新生成的对象添加方法和属性;而且是通过方法,返回一个空对象,也没有对象继承关系。
5.寄生式继承
这就是 原型继承 的加强版,解决了无法给新生成的对象添加属性的缺点。
function generatorObj(obj) {
// 创建一个空的构造函数
function Fn() {}
Fn.prototype = obj;
return new Fn();
}
function createObj(obj, property) {
const newObj = generatorObj(obj);
Object.assign(newObj, property);
return newObj
}
const person = {
vehicle: ['subway', 'car', 'bicycle'],
type: 'human'
}
const man = createObj(person, { name: "zs", age: 20 });
console.log(man.name); // zs
寄生式继承,其实就是 Object.create 函数的实质:
const person = {
vehicle: ['subway', 'car', 'bicycle'],
type: 'human'
}
const ls = Object.create(person, {
name: {
value: 'ls'
},
age: {
value: 20
},
})
console.log(ls.name); // ls
这种继承,还是更多的还是像 工厂模式,而不是语言的继承;同时对于继承的关联关系也不明确,一般不使用这种方式来实现继承,但是它可以与 组合继承 结合,实现最优的继承方式。
寄生组合继承
将寄生式继承与组合继承结合,更进一步的优化继承,减少父类实例化的次数,和存了两遍属性问题。
function Person(name) {
this.name = name;
this.pets = ['dog', 'cat'];
}
Person.prototype.move = () => {
console.log('go...');
}
function Man(name, age) {
// 这里通过 call,已经将父类中的属性共享
Person.call(this, name);
this.age = age;
}
// 切记,传入的式 父类的 prototype ,目的是共享父类的 prottotype
Man.prototype = Object.create(Person.prototype);
Man.prototype.constructor = Man;
const zs = new Man('张三', 20);
console.log(zs);
zs.pets.push('psg');
const ls = new Man('李四', 18);
console.log(ls.pets); // ['dog', 'cat']
ls.move(); // go...
这基本就是 js 继承最主流的方式。