【前端面试小册】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 属性,而 prototypeconstructor 属性,代表着它的构造函数
  • 当我们用 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 缺点

  1. 引用类型属性共享:所有实例共享父类的引用类型属性
  2. 无法向父类传参:创建子类实例时,无法向父类构造函数传参
  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 缺点

  1. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  2. 无法复用,每个子类都有父类实例函数的副本,影响性能

三、组合继承(最常用)

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.listc1.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 缺点

  1. 子类实例继承父类的引用属性是同一个,互相可篡改
  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 优点

  1. 结合了寄生和组合方式继承
  2. 只调用了一次父类构造函数,避免了在 Child.prototype 上创建多余的属性
  3. 原型链保持不变

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+ 现代项目推荐

十一、面试要点总结

核心知识点

  1. 继承的本质:通过原型链实现属性和方法的共享
  2. 组合继承:ES5 最常用的继承方式
  3. 寄生组合式继承:ES5 最优解
  4. ES6 extends:现代项目推荐使用

常见面试题

Q1: 请实现一个继承?

答:可以使用组合继承或寄生组合式继承。组合继承通过 call 继承实例属性,通过原型链继承原型方法。寄生组合式继承在此基础上优化,只调用一次父类构造函数。

Q2: 组合继承和寄生组合式继承的区别?

答:

  • 组合继承:调用两次父类构造函数,原型上会有冗余属性
  • 寄生组合式继承:只调用一次父类构造函数,原型上无冗余属性

Q3: ES6 的 extends 是如何实现的?

答:extends 底层实现类似于寄生组合式继承,通过 Object.create 创建父类原型的副本,通过 super 调用父类构造函数。

实战建议

  • ✅ 新项目优先使用 ES6 的 extends
  • ✅ 理解各种继承方式的优缺点
  • ✅ 掌握组合继承和寄生组合式继承的实现
  • ✅ 注意 constructor 的校正
#前端面试##面试题#
前端面试小册 文章被收录于专栏

每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!

全部评论

相关推荐

昨天 14:12
南京大学 golang
我的OC时间线
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务