MST
星途 面试题库

面试题:JavaScript 原型链与继承的深入理解

描述 JavaScript 原型链的工作原理,以及如何通过原型链实现继承。请详细分析 ES5 和 ES6 中实现继承方式的优缺点,并举例说明。
39.4万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

JavaScript 原型链工作原理

  1. 原型对象:每个函数都有一个 prototype 属性,它指向一个对象,这个对象就是该函数所创建实例的原型对象。例如:
function Person() {}
console.log(Person.prototype); 
  1. 实例与原型关系:当使用 new 关键字调用构造函数创建实例时,实例内部会有一个 [[Prototype]](在现代 JavaScript 中可以通过 __proto__ 访问,但 __proto__ 已不推荐使用,应使用 Object.getPrototypeOf())属性,它指向构造函数的原型对象。
let person = new Person();
console.log(person.__proto__ === Person.prototype); 
  1. 属性查找:当访问实例的属性时,首先会在实例自身查找,如果找不到,则会沿着 [[Prototype]] 链向上查找,直到找到该属性或者到达原型链顶端(Object.prototype,其 [[Prototype]]null)。
Person.prototype.name = 'default';
console.log(person.name); 

通过原型链实现继承

  1. ES5 实现继承方式
    • 原型链继承
      • 原理:让子类型的原型指向超类型的实例。
      • 代码示例
function Animal() {
    this.species = 'animal';
}
function Dog() {}
Dog.prototype = new Animal();
let dog = new Dog();
console.log(dog.species); 
  - **优点**:简单,易于理解,子类型可以访问超类型原型上的属性和方法。
  - **缺点**:引用类型的属性被所有实例共享;在创建子类型实例时,无法向超类型构造函数传参。
- **构造函数继承**:
  - **原理**:在子类型构造函数内部调用超类型构造函数,通过 `call` 或 `apply` 方法改变 `this` 指向。
  - **代码示例**:
function Animal(name) {
    this.name = name;
}
function Dog(name) {
    Animal.call(this, name);
}
let dog = new Dog('Buddy');
console.log(dog.name); 
  - **优点**:可以解决原型链继承中引用类型属性共享问题,且可以向超类型构造函数传参。
  - **缺点**:只能继承实例属性和方法,不能继承原型属性和方法。
- **组合继承**:
  - **原理**:结合原型链继承和构造函数继承。先通过原型链继承超类型的原型属性和方法,再在子类型构造函数中调用超类型构造函数,继承实例属性。
  - **代码示例**:
function Animal(name) {
    this.name = name;
    this.friends = ['cat'];
}
Animal.prototype.speak = function() {
    console.log('I am an animal');
};
function Dog(name) {
    Animal.call(this, name);
}
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;
let dog1 = new Dog('Max');
let dog2 = new Dog('Sam');
dog1.friends.push('rabbit');
console.log(dog1.friends); 
console.log(dog2.friends); 
dog1.speak(); 
  - **优点**:融合了原型链继承和构造函数继承的优点,既可以继承实例属性,又可以继承原型属性和方法。
  - **缺点**:超类型构造函数会被调用两次,一次在 `new Animal()` 创建原型时,一次在 `Animal.call(this)` 时。
- **寄生组合式继承**:
  - **原理**:通过创建一个空函数 F,让其原型指向超类型的原型,然后创建 F 的实例作为子类型的原型,最后修正子类型原型的 `constructor` 属性。
  - **代码示例**:
function inheritPrototype(subType, superType) {
    let F = function() {};
    F.prototype = superType.prototype;
    subType.prototype = new F();
    subType.prototype.constructor = subType;
}
function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log('I am an animal');
};
function Dog(name) {
    Animal.call(this, name);
}
inheritPrototype(Dog, Animal);
let dog = new Dog('Tom');
dog.speak(); 
  - **优点**:解决了组合继承中构造函数被调用两次的问题,是最理想的继承方式。
  - **缺点**:实现相对复杂。

2. ES6 实现继承方式(类继承) - 原理:使用 classextends 关键字,内部实现依然基于原型链,但语法更加简洁和清晰。 - 代码示例

class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        console.log('I am an animal');
    }
}
class Dog extends Animal {
    constructor(name) {
        super(name);
    }
}
let dog = new Dog('Leo');
dog.speak(); 
- **优点**:语法糖,代码更简洁,易读性好;明确了类的概念,包括构造函数、方法等;通过 `super` 关键字方便地调用超类的构造函数和方法。
- **缺点**:本质上还是基于原型链,部分问题依然存在,如对于一些旧环境兼容性不如 ES5 继承方式(需转码工具如 Babel 辅助)。