【JavaScript面试题-原型与继承】原型链是如何工作的?如何实现继承?ES6 `class` 的本质是什么?

0 阅读5分钟

一、原型链的工作原理

每个 JavaScript 对象都有一个内部私有属性 [[Prototype]](通常通过 __proto__ 或 Object.getPrototypeOf() 访问),它指向另一个对象,即该对象的原型。当我们访问一个对象的属性或方法时,引擎会执行以下步骤:

  1. 首先检查对象自身是否有该属性(自身属性,可通过 hasOwnProperty 判断)。
  2. 如果没有,则沿着 [[Prototype]] 链向上查找其原型对象,检查原型对象上是否有该属性。
  3. 如果仍然没有,继续向上查找原型的原型,以此类推。
  4. 直到找到该属性,或者到达原型链的末端(null),返回 undefined

这种通过 [[Prototype]] 层层连接的链条,就是原型链

一个形象比喻

假设你有一个家族,每个人都有自己的储物箱(自身属性)。当你需要找一件物品(访问属性)时:

  1. 你先翻自己的储物箱——如果有,直接拿出来用。
  2. 如果自己没有,你就去问你父亲(__proto__ 指向的原型对象),看看他那里有没有。
  3. 如果父亲也没有,父亲会继续问他的父亲(祖父),以此类推。
  4. 直到问到家族最年长的祖先(原型链末端的 null)。如果祖先也没有,就告诉你“没找到”(返回 undefined)。

在这个比喻中:

  •  → 当前对象
  • 父亲、祖父… → 原型链上的各级原型对象
  • 祖先 → Object.prototype(最终指向 null
  • 问的过程 → JavaScript 引擎沿着 [[Prototype]] 向上查找

这种一环扣一环的委托关系,就是原型链的本质——对象之间通过“原型”连接,形成一条查找属性的链条

示例

javascript

const parent = { name: 'parent' };
const child = Object.create(parent); // child 的原型指向 parent
child.age = 10;

console.log(child.age); // 10(自身属性)
console.log(child.name); // 'parent'(来自原型链)
console.log(child.toString); // 来自 Object.prototype(再往上一级)

二、如何实现继承

在 ES6 之前,主要通过构造函数和原型来模拟继承。常见的方式有:

1. 原型链继承

将子类的原型设置为父类的实例。

javascript

function Parent() {
  this.name = 'parent';
}
Parent.prototype.say = function() {
  console.log(this.name);
};

function Child() {}
Child.prototype = new Parent(); // 继承

const c = new Child();
c.say(); // 'parent'

缺点:所有实例共享父类实例的引用属性(如数组),且无法向父类构造函数传参。

2. 构造函数继承

在子类构造函数中调用父类构造函数(使用 call/apply)。

javascript

function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name); // 继承属性
}
const c = new Child('child');
console.log(c.name); // 'child'

缺点:无法继承父类原型上的方法,方法必须在构造函数中定义。

3. 组合继承

结合原型链继承和构造函数继承,既继承属性又继承方法。

javascript

function Parent(name) {
  this.name = name;
}
Parent.prototype.say = function() {
  console.log(this.name);
};

function Child(name) {
  Parent.call(this, name); // 第二次调用 Parent
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;

const c = new Child('child');
c.say(); // 'child'

缺点:父类构造函数被调用了两次,导致子类原型上存在多余的父类实例属性。

4. 寄生组合式继承(最理想的继承方式)

使用 Object.create() 直接让子类的原型继承父类的原型,避免了多余的父类实例属性。

javascript

function Parent(name) {
  this.name = name;
}
Parent.prototype.say = function() {
  console.log(this.name);
};

function Child(name) {
  Parent.call(this, name);
}
// 关键:将子类的原型设置为父类原型的副本
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const c = new Child('child');
c.say(); // 'child'

目前这种方式被认为是 ES5 环境下最理想的继承模式。

5. 使用 Object.create() 直接创建对象并指定原型

javascript

const parent = { name: 'parent' };
const child = Object.create(parent, {
  age: { value: 10, enumerable: true }
});
console.log(child.name); // 'parent'

三、ES6 class 的本质

ES6 引入了 class 关键字,但它并不是一种新的面向对象继承模型,而是基于原型的语法糖class 让对象的创建和继承变得更加清晰、接近传统面向对象语言,但底层依然是构造函数和原型链。

本质特性

  • class 定义的类本质上是一个函数,具有 prototype 属性,方法定义在 prototype 上。
  • 通过 new 调用时,会自动执行构造函数 constructor
  • 类内部定义的方法都是不可枚举的(这与 ES5 中将方法直接挂载在原型上不同)。
  • 类必须使用 new 调用,不能直接作为普通函数执行。
  • 类声明不会被提升(与函数声明不同),存在暂时性死区。
  • 类体默认采用严格模式

示例与转译

javascript

class Parent {
  constructor(name) {
    this.name = name;
  }
  say() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类构造函数
    this.age = age;
  }
  say() {
    super.say(); // 调用父类方法
    console.log(`age: ${this.age}`);
  }
}

上述代码经过 Babel 转译成 ES5 后,本质上就是寄生组合式继承的实现:

  • Child.prototype = Object.create(Parent.prototype) 建立原型链。
  • 在 Child 构造函数中调用 Parent.call(this, name) 继承实例属性。
  • super 关键字对应父类构造函数的调用或父类原型方法的调用。

class 与原型的关系验证

javascript

class A {}
typeof A // "function"
A.prototype.constructor === A // true

const a = new A();
a.__proto__ === A.prototype // true

静态方法

ES6 class 支持静态方法(使用 static 关键字),它们直接定义在构造函数上,而不是原型上。

javascript

class A {
  static hello() {
    console.log('static');
  }
}
A.hello(); // 'static'
typeof A.hello // "function"

总结

  • 原型链:通过对象的 [[Prototype]] 链实现属性查找的机制。
  • 继承实现:在 ES5 中可通过构造函数、原型、Object.create 等多种方式组合实现;最完善的是寄生组合式继承。
  • ES6 class 本质:语法糖,底层仍是构造函数 + 原型链,提供了更简洁的语法和更严格的语义,但并未改变 JavaScript 基于原型的本质。

#前端、#前端面试、#干货

如果这篇这篇文章对您有帮助?关注、点赞、收藏,三连支持一下。
有疑问或想法?评论区见