【前端面试知识点】JavaScript:解释原型链,如何实现继承(ES5和ES6方式)?

0 阅读2分钟

原型链与继承

一、原型链

在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](可通过 __proto__ 或 Object.getPrototypeOf() 访问),该属性指向另一个对象。当访问一个对象的属性时,如果自身没有该属性,就会沿着 [[Prototype]] 链向上查找,直到找到该属性或到达 null 为止。这种对象之间的关联关系称为原型链

  • 每个构造函数都有一个 prototype 属性,指向其原型对象。
  • 通过构造函数 new 创建的对象,其 [[Prototype]] 指向该构造函数的 prototype 对象。
  • 原型对象本身也是一个普通对象,其 [[Prototype]] 指向 Object.prototype,最终 Object.prototype 的 [[Prototype]] 为 null,形成链式结构。

javascript

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  console.log(`Hi, I'm ${this.name}`);
};

const p = new Person('Alice');
p.sayHi(); // 通过原型链找到 sayHi
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null

二、ES5 实现继承(推荐寄生组合继承)

ES5 中实现继承的常用方式是寄生组合继承,它解决了组合继承中会调用两次父类构造函数的问题,且避免了在父类原型上添加无用属性。

javascript

// 父类
function Animal(name) {
  this.name = name;
  this.colors = ['black', 'white'];
}
Animal.prototype.speak = function() {
  console.log(`${this.name} makes a sound.`);
};

// 子类
function Dog(name, breed) {
  // 借用构造函数继承属性
  Animal.call(this, name);
  this.breed = breed;
}

// 寄生组合继承:原型继承 + 修复构造函数指向
function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // 创建父类原型的副本
  prototype.constructor = child;                     // 修复 constructor 指向
  child.prototype = prototype;
}
inheritPrototype(Dog, Animal);

// 添加子类自己的方法
Dog.prototype.bark = function() {
  console.log('Woof!');
};

// 测试
const dog = new Dog('Buddy', 'Golden');
dog.speak(); // Buddy makes a sound.
dog.bark();  // Woof!
console.log(dog.colors); // ['black', 'white']
console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true

要点

  • 属性继承:在子类构造函数中用 Parent.call(this) 继承父类属性。
  • 方法继承:通过 Object.create(parent.prototype) 创建原型对象,并修复 constructor 指向,避免共享引用类型属性。
  • 这样既保证了父类构造器只调用一次,又使原型链正确。

三、ES6 实现继承(class 语法)

ES6 引入了 class 和 extends 关键字,底层仍基于原型链,但语法更清晰、更易理解。

javascript

// 父类
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ['black', 'white'];
  }
  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

// 子类
class Dog extends Animal {
  constructor(name, breed) {
    super(name);            // 调用父类构造函数,相当于 Animal.call(this, name)
    this.breed = breed;
  }
  bark() {
    console.log('Woof!');
  }
}

// 测试
const dog = new Dog('Buddy', 'Golden');
dog.speak(); // Buddy makes a sound.
dog.bark();  // Woof!
console.log(dog.colors); // ['black', 'white']
console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true

要点

  • extends 设置原型链:Dog.prototype.__proto__ === Animal.prototype
  • 子类构造函数中必须调用 super() 才能使用 this,且 super 调用前不能引用 this
  • 静态方法也可通过 extends 继承(Dog.__proto__ === Animal)。
  • 本质仍是原型链继承,只是语法糖。

四、继承的核心差异

方式属性继承方法继承特点
ES5 寄生组合Parent.call(this)Object.create(Parent.prototype)需手动处理原型链和构造函数指向
ES6 classsuper()extends + super语法简洁,自动处理原型链和构造函数

两种方式均能正确实现继承,ES6 更符合现代开发习惯,且保证了原型链的正确性。