真实面经题目 · 原创解析
说一下 JS 的继承机制
这道题考察 JS 继承背后的原型链查找、实例属性初始化、方法复用和 ES6 class 语义。高质量回答要把对象怎么找属性、子类实例怎么拿到父类初始化结果、不同继承写法各自的坑讲清楚。
真实面经题目 · 原创解析
这道题考察 JS 继承背后的原型链查找、实例属性初始化、方法复用和 ES6 class 语义。高质量回答要把对象怎么找属性、子类实例怎么拿到父类初始化结果、不同继承写法各自的坑讲清楚。
JS 的继承本质是两件事:属性查找沿原型链向上走,实例创建时把父构造函数里的实例属性初始化到当前对象上。原型链继承能复用父类原型方法,但如果用 new Parent() 作为 Child.prototype,父构造函数里的引用属性会被所有子实例共享;构造函数继承用 Parent.call(this) 能隔离实例属性,但拿不到父类原型方法。组合继承结合两者,但会执行两次父构造函数。更推荐寄生组合继承:Child.prototype = Object.create(Parent.prototype) 只建立原型连接,Parent.call(this) 只负责初始化实例属性,再把 constructor 指回 Child。ES6 class extends 是这套机制的语法封装,子类构造函数里必须先 super() 才能使用 this。
访问 child.name 时,JS 先查实例自身属性;没有再查 Child.prototype;还没有就继续查 Parent.prototype、Object.prototype,最终到 null。继承能复用方法,是因为子实例的原型链能连到父类原型,而不是复制了一份父类方法。
new Child() 会创建空对象、把对象原型连到 Child.prototype、用这个对象作为 this 执行 Child。父构造函数里的 this.items = [] 这类实例属性,只有在子构造函数中执行 Parent.call(this, ...) 才会落到每个子实例自己身上。
Child.prototype = new Parent() 能访问 Parent.prototype 上的方法,但 new Parent() 产生的 items、options 等引用属性会挂在 Child.prototype 上,多个子实例会共享。只写 Parent.call(this) 能让实例属性独立,但 Child.prototype 没有连到 Parent.prototype,父类原型方法无法复用。
组合继承通常写 Parent.call(this) 加 Child.prototype = new Parent(),实例属性隔离、方法也能复用,但父构造函数执行两次。寄生组合继承把连原型和初始化实例属性拆开:Object.create(Parent.prototype) 只连原型不执行父构造;Parent.call(this) 只在创建实例时执行一次;constructor 再修正回 Child。
class Child extends Parent 底层仍然建立 Child.prototype 到 Parent.prototype 的链,同时处理静态继承。super(...args) 调用父类构造逻辑来初始化当前实例;派生类构造函数里 this 必须等 super() 返回后才可用,super.method() 查的是父类原型方法,并以当前子实例作为 this 调用。
new 会创建新对象,把新对象的内部原型指向 Child.prototype,再用这个对象作为 this 执行 Child。继承关系来自 Child.prototype 后面接到哪里;实例属性来自构造函数执行时给 this 赋值。
这次 new Parent() 创建的是所有子实例共同使用的原型对象。如果 Parent 里有 this.items = [],这个数组会挂到 Child.prototype.items 上,多个子实例自身没有 items 时读到同一个数组。
第一次发生在 Child.prototype = new Parent(),目的是把子原型接到父原型,但顺带执行了父构造;第二次发生在 Child 构造函数里的 Parent.call(this),目的是给真实子实例初始化父类实例属性。
它把连原型和初始化实例属性拆成两个动作。Object.create(Parent.prototype) 创建以父原型为原型的新对象,不执行 Parent;Parent.call(this) 只在创建子实例时执行一次。
class 没有改变 JS 基于原型的继承模型,但统一封装了原型链、构造函数调用、静态继承和方法定义语义。派生类构造函数里必须先 super(),否则 this 不可用。