【前端面试小册】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天学完,上岸银行总行!

全部评论

相关推荐

11-11 14:49
牛客运营
1. 技术栈审计报告:🔸需至少连续三年以上在ACM/Kaggle竞赛履历,包含但不限于:ACM区域赛金牌、Kaggle Master、Codeforces红名(注:培训班批量产出项目除外,培训班out)🔸能清晰阐释Transformer与CNN的本质区别,并有过至少一次在实验室通宵调参后对导师说出“模型loss还在下降,我觉得还能抢救”的可验证经历(附服务器监控截图与终端对话记录)2. 开发能力证明:🔸精通Vim盲打,能在无图形界面的服务器上完成内核编译,且记得住所有Git指令的完整参数🔸可同时应对产品经理需求变更、线上服务告警、测试环境崩溃而不产生物理性砸键盘冲动🔸曾在凌晨四点的机房就着服务器轰鸣声,蹲在机柜旁用手机热点连VPN修复生产环境数据3. 生存状态白皮书:🔸每日咖啡因摄入≥400mg(手冲/冷萃优先,但更多时候是速溶粉干嚼)🔸平均睡眠时长≤5小时,其中包含在工位午休的15分钟浅眠🔸年均加班时长>800小时,并认为“关机重启”是解决人际关系的终极方案🔸能在分析家庭矛盾时自然画出系统架构图,用敏捷开发流程比喻亲子沟通模式🔸口头禅需包含“这个需求不符合第一性原理”、“容我先做个压力测试”、“你这段感情输出的日志不够结构化”4. 技术面经反思录:🔸一篇需说明:Why not 转管理?Why not 逃离互联网?🔸另一篇需阐述当产品说“借鉴下竞品功能”,你如何用左耳听需求,右手写违心代码5. 代码遗产集:🔸附某个深夜提交的commit记录(≥30次revert仍坚持push,含至少三次“Fix previous fix”的史诗级操作)🔸可选附加项:你为前任写的自动化分手脚本,集成情绪分析模型与财产分割算法6. 精神能耗采样:🔸需提供至少一条深夜技术论坛发言截图如《递归函数与代际创伤的相似性研究》、《当父母说“找个稳定工作”,我该如何进行多线程谎言维护》🔸服务器监控曲线中标注出:CPU使用率峰值与泪崩时间点高度重合的物理证据(注:所有材料需以JSON格式提交,附带SHA-256校验码,我们将通过AST抽象语法树解析你的情感模块是否仍存在技术债)参考文献:[1] 凌晨四点半的海《当文艺女开始谈论原生家庭》2025.[2]《文艺女和我讲原生家庭需要以下材料》2025.小红书[3] 豆包
点赞 评论 收藏
分享
评论
点赞
1
分享

创作者周榜

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