【前端面试小册】JS-第12节:面试必会 - JavaScript 继承专题
一、原型链继承
1.1 实现方式
function Father() {
this.desc = '这里有:面试题、薪酬行情、谈薪技巧,升职经验、职场建议';
}
function Child(type, name) {
this.type = type;
this.name = name;
}
// 核心:子类的原型指向父类的实例
Child.prototype = new Father();
// 校正 Child 的 constructor
Child.prototype.constructor = Child;
const c = new Child("知识星球", "前端职场圈");
console.log(c.desc); // 这里有:面试题、薪酬行情、谈薪技巧,升职经验、职场建议
1.2 为什么需要校正 constructor
// 这一行代码的作用
Child.prototype.constructor = Child;
原因分析:
- 函数有
prototype属性,而prototype有constructor属性,代表着它的构造函数 - 当我们用
prototype实现继承Father后,Child.prototype.constructor也指向了Father - 但实例
c的构造函数是Child生成的,所以必须要有Child.prototype.constructor = Child来修正
// 原型继承导致 constructor 指向改变
console.log(Child.prototype.constructor === Father); // true(修正前)
console.log(c.constructor === Father); // true(修正前)
// 修正后
console.log(Child.prototype.constructor === Child); // true
console.log(c.constructor === Child); // true
1.3 缺点
- 引用类型属性共享:所有实例共享父类的引用类型属性
- 无法向父类传参:创建子类实例时,无法向父类构造函数传参
- 原型链过长:继承链过长会导致性能问题
二、借用构造函数继承(经典继承)
2.1 实现方式
function Father() {
this.desc = '这里有:面试题、薪酬行情、谈薪技巧,升职经验、职场建议';
}
function Child(type, name) {
// 核心:在子类构造函数中调用父类构造函数
Father.apply(this, arguments);
this.type = type;
this.name = name;
}
const c = new Child("知识星球", "前端职场圈");
console.log(c.desc); // 这里有:面试题、薪酬行情、谈薪技巧,升职经验、职场建议
2.2 特点
- 不需要
prototype,利用Father.apply(this, arguments)在创建子类实例时调用父类构造函数 - 每个子类实例都会将父类中的属性复制一份(不是共享)
2.3 缺点
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法复用,每个子类都有父类实例函数的副本,影响性能
三、组合继承(最常用)
3.1 实现方式
function Father(name) {
this.desc = '知识星球:前端职场圈;这里有:面试题、薪酬行情、谈薪技巧,升职经验、职场建议';
this.list = [1, 2, 3];
}
Father.prototype.sayHi = function() {
console.log('hi');
};
function Child(name, age) {
// 继承父类实例的属性
Father.call(this, name);
this.age = age;
}
// 通过原型链实现对原型属性和方法的继承
Child.prototype = new Father();
// 校正 Child.prototype 的 constructor 属性,指向自己的构造函数 Child
Child.prototype.constructor = Child;
let c = new Child("child", 18);
c.list.push('c'); // [ 1, 2, 3, 'c' ]
console.log(c.list);
c.sayHi(); // hi
let c1 = new Child("child1", 20);
// 注意这里的 list 并未因为上面实例 c 改变 list 而改变
console.log(c1.list); // [ 1, 2, 3 ]
c1.sayHi(); // hi
3.2 属性屏蔽原理
console.log(c1.list) 是 [ 1, 2, 3 ] 而不是 [ 1, 2, 3, 'c' ] 的原因:
属性屏蔽:
- 在
prototype上存在list可访问属性,并且没有被标记为只读(writable: false) - 会在实例
c上添加一个list的新属性 - 这样
c.list和c1.list就是不同的属性了
console.log(c);
// {
// desc: '知识星球:前端职场圈;这里有:面试题、薪酬行情、谈薪技巧,升职经验、职场建议',
// list: [ 1, 2, 3, 'c' ], // 实例属性
// age: 18
// }
console.log(c.__proto__);
// {
// list: [ 1, 2, 3 ], // 原型属性
// sayHi: function() { ... },
// constructor: Child
// }
3.3 优点
- 使用原型链实现对原型属性和方法的继承
- 通过借用构造函数来实现对实例属性的继承
- 既解决了引用类型属性共享的问题,又实现了方法复用
3.4 缺点
在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法(一份在实例上,一份在原型上)。
四、原型式继承
4.1 实现方式
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
let person = {
name: "成都巴菲特",
list: [1, 2, 3]
};
let person1 = object(person);
person1.name = "成都巴菲特1";
person1.list.push("person1");
let person2 = object(person);
person2.name = "成都巴菲特2";
person2.list.push("person2");
// 原因:object 对传入的对象浅复制
console.log(person.list); // [ 1, 2, 3, 'person1', 'person2' ]
4.2 缺点
- 子类实例继承父类的引用属性是同一个,互相可篡改
- 无法传递参数
五、寄生式继承
5.1 实现方式
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
function create(obj) {
var child = object(obj); // 通过调用 object() 函数创建一个新对象
child.sayHi = () => { // 以某种方式来增强对象
console.log('hi');
};
return child; // 返回这个对象
}
5.2 分析
- 在原型式继承的基础上,增强对象,返回构造函数
- 可用来为构造函数新增属性和方法,以增强函数
5.3 缺点
同原型式继承:
- 子类实例继承父类的引用属性是同一个,互相可篡改
- 无法传递参数
六、寄生组合式继承(最优解)
6.1 实现方式
function myExtends(child, father) {
let prototype = Object.create(father.prototype); // 创建对象,创建父类原型的一个副本
prototype.constructor = child; // 校正 constructor 属性
child.prototype = prototype; // 指定对象,将新创建的对象赋值给子类的原型
}
function father(name) {
this.name = name;
this.list = [1, 2, 3];
}
father.prototype.sayHi = function() {
console.log('hi');
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function child(name, age) {
father.call(this, name);
this.age = age;
}
// 将父类原型指向子类
myExtends(child, father);
// 新增子类原型属性
child.prototype.sayAge = function() {
console.log(this.age);
}
var c1 = new child("c1", 18);
var c2 = new child("c2", 20);
c1.list.push("child1");
c2.list.push("child2");
console.log(c1.list); // [ 1, 2, 3, 'child1' ]
console.log(c2.list); // [ 1, 2, 3, 'child2' ]
6.2 优点
- 结合了寄生和组合方式继承
- 只调用了一次父类构造函数,避免了在
Child.prototype上创建多余的属性 - 原型链保持不变
6.3 与组合继承的对比
寄生组合式继承:
- 实例上有
list属性 - 原型对象
prototype上没有list属性
组合继承:
- 实例上有
list属性 - 原型对象
prototype上也有list属性(冗余)
七、ES6 类继承(extends)
7.1 实现方式
class Father {
constructor(name) {
this.name = name;
}
static sayHello() {
console.log('hello');
}
sayName() {
console.log('my name is ' + this.name);
return this.name;
}
}
class Child extends Father {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
sayAge() {
console.log('my age is ' + this.age);
return this.age;
}
}
let father = new Father('Father');
let child = new Child('Child', 20);
console.log('father: ', father); // father: Father {name: "Father"}
father.sayHello(); // hello
father.sayName(); // my name is Father
console.log('child: ', child); // child: Child {name: "Child", age: 20}
// 子类没有 sayHello 方法,继承父类的
child.sayHello(); // hello
// 子类没有 sayName 方法,继承父类的
child.sayName(); // my name is Child
// 子类自己的方法
child.sayAge(); // my age is 20
7.2 extends 核心代码(ES5 实现)
function extends(child, father) {
// 以父类原型来创建副本
// 增强对象,弥补因重写原型而失去的默认的 constructor 属性
// 指定对象,将新创建的对象赋值给子类的原型
child.prototype = Object.create(father && father.prototype, {
constructor: {
value: child,
enumerable: false,
writable: true,
configurable: true
}
});
if (father) {
Object.setPrototypeOf
? Object.setPrototypeOf(child, father)
: child.__proto__ = father;
}
}
八、混入方式继承多个对象
8.1 实现方式
function Child() {
// 继承 Father 实例上属性
Father.call(this);
// 继承 Father1 实例上属性
Father1.call(this);
}
// 继承 Father 原型上属性
Child.prototype = Object.create(Father.prototype);
// 继承 Father1 原型上属性
Object.assign(Child.prototype, Father1.prototype);
// 校正 constructor
Child.prototype.constructor = Child;
九、ES5 实现 ES6 的 extends(简化版本)
9.1 实现方式
function Parent(name) {
this.name = name;
}
Parent.sayHi = function() {
console.log('hi');
}
Parent.prototype.sayName = function() {
console.log('my name is ' + this.name);
return this.name;
}
function Child(name, age) {
// 相当于 super
Parent.call(this, name);
this.age = age;
}
function myExtends(Child, Parent) {
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 让子类继承父类原型上的方法
Child.__proto__ = Parent;
}
myExtends(Child, Parent);
Child.prototype.sayAge = function() {
console.log('my age is ' + this.age);
return this.age;
}
let child = new Child('Child', 18);
console.log('child: ', child); // child: Child {name: "Child", age: 18}
// 注意:如果没有 Child.__proto__ = Parent,那么 Child.sayHi() 会报错
Child.sayHi(); // hi
child.sayName(); // my name is Child
child.sayAge(); // my age is 18
十、各种继承方式对比
| 继承方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 原型链继承 | 实现简单 | 引用类型共享、无法传参 | 不推荐使用 |
| 借用构造函数 | 可传参、实例独立 | 无法继承原型方法 | 不推荐单独使用 |
| 组合继承 | 可传参、方法复用 | 调用两次父类构造函数 | ES5 常用 |
| 原型式继承 | 实现简单 | 引用类型共享、无法传参 | 不推荐使用 |
| 寄生式继承 | 可增强对象 | 引用类型共享 | 不推荐使用 |
| 寄生组合式继承 | 最优解,只调用一次父类 | 实现稍复杂 | ES5 推荐 |
| ES6 extends | 语法简洁、功能强大 | 需要 ES6+ | 现代项目推荐 |
十一、面试要点总结
核心知识点
- 继承的本质:通过原型链实现属性和方法的共享
- 组合继承:ES5 最常用的继承方式
- 寄生组合式继承:ES5 最优解
- ES6 extends:现代项目推荐使用
常见面试题
Q1: 请实现一个继承?
答:可以使用组合继承或寄生组合式继承。组合继承通过 call 继承实例属性,通过原型链继承原型方法。寄生组合式继承在此基础上优化,只调用一次父类构造函数。
Q2: 组合继承和寄生组合式继承的区别?
答:
- 组合继承:调用两次父类构造函数,原型上会有冗余属性
- 寄生组合式继承:只调用一次父类构造函数,原型上无冗余属性
Q3: ES6 的 extends 是如何实现的?
答:extends 底层实现类似于寄生组合式继承,通过 Object.create 创建父类原型的副本,通过 super 调用父类构造函数。
实战建议
- ✅ 新项目优先使用 ES6 的
extends - ✅ 理解各种继承方式的优缺点
- ✅ 掌握组合继承和寄生组合式继承的实现
- ✅ 注意
constructor的校正
前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!
