Skip to content

JavaScript 继承面试题

1. 请解释 JavaScript 中的原型继承

Details

原型继承是 JavaScript 中实现继承的主要方式,它基于原型链机制。

原型继承的基本概念

在 JavaScript 中,每个对象都有一个原型(prototype),当访问对象的属性或方法时,如果对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到为止。

原型继承的实现

1. 使用构造函数和原型

javascript
// 父类构造函数
function Animal(name) {
  this.name = name;
  this.eat = function() {
    console.log(`${this.name} is eating`);
  };
}

// 父类原型方法
Animal.prototype.sleep = function() {
  console.log(`${this.name} is sleeping`);
};

// 子类构造函数
function Dog(name, breed) {
  // 调用父类构造函数
  Animal.call(this, name);
  this.breed = breed;
}

// 建立原型链
Dog.prototype = Object.create(Animal.prototype);
// 修复构造函数指向
Dog.prototype.constructor = Dog;

// 子类原型方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking`);
};

// 使用
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // Buddy is eating
dog.sleep(); // Buddy is sleeping
dog.bark(); // Buddy is barking

2. 使用 ES6 类语法

ES6 引入了类语法,使继承更加简洁明了。

javascript
// 父类
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating`);
  }
  
  sleep() {
    console.log(`${this.name} is sleeping`);
  }
}

// 子类
class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} is barking`);
  }
}

// 使用
const dog = new Dog("Buddy", "Golden Retriever");
dog.eat(); // Buddy is eating
dog.sleep(); // Buddy is sleeping
dog.bark(); // Buddy is barking

原型继承的特点

  1. 基于原型链:通过原型链实现属性和方法的继承。
  2. 共享原型方法:子类实例共享父类原型上的方法,节省内存。
  3. 动态性:可以在运行时修改原型,影响所有实例。

原型继承的优缺点

优点

  • 灵活性:可以动态修改原型,实现方法的共享。
  • 内存效率:原型上的方法被所有实例共享,节省内存。
  • 简洁性:通过原型链实现继承,代码简洁。

缺点

  • 复杂性:原型链的查找机制可能导致性能问题。
  • 容易混淆:原型继承的概念对于初学者来说可能难以理解。
  • 构造函数参数传递:在使用构造函数继承时,需要手动调用父类构造函数。

注意事项

  1. 原型链的长度:过长的原型链会影响属性查找性能。
  2. 原型方法的修改:修改原型会影响所有实例,需要谨慎操作。
  3. 构造函数的指向:使用 Object.create() 后需要修复构造函数的指向。

2. 请解释 JavaScript 中的 ES6 类继承

Details

ES6 引入了类语法,使继承更加简洁明了,它是原型继承的语法糖。

ES6 类继承的基本语法

javascript
// 父类
class Parent {
  constructor(name) {
    this.name = name;
  }
  
  method() {
    console.log(`Parent method called`);
  }
}

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

ES6 类继承的特点

  1. 使用 extends 关键字:明确指定父类。
  2. 使用 super 关键字:调用父类的构造函数和方法。
  3. 简洁的语法:相比传统的原型继承,语法更加清晰。
  4. 支持静态方法继承:静态方法会被自动继承。

ES6 类继承的实现原理

ES6 类继承本质上是原型继承的语法糖,它在内部仍然使用原型链来实现继承。

javascript
// ES6 类继承的底层实现
function Parent(name) {
  this.name = name;
}

Parent.prototype.method = function() {
  console.log(`Parent method called`);
};

function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数
  this.age = age;
}

// 建立原型链
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

Child.prototype.method = function() {
  Parent.prototype.method.call(this); // 调用父类方法
  console.log(`Child method called`);
};

ES6 类继承的注意事项

  1. 必须调用 super():在子类构造函数中,必须先调用 super(),然后才能使用 this。

    javascript
    class Child extends Parent {
      constructor(name, age) {
        // 错误:在调用 super() 之前使用 this
        // this.age = age;
        super(name);
        this.age = age; // 正确:在调用 super() 之后使用 this
      }
    }
  2. super 的使用

    • 在构造函数中,super() 调用父类构造函数。
    • 在方法中,super.method() 调用父类的方法。
  3. 静态方法继承

    javascript
    class Parent {
      static staticMethod() {
        console.log('Static method');
      }
    }
    
    class Child extends Parent {
    }
    
    Child.staticMethod(); // 输出: Static method
  4. getter 和 setter 继承

    javascript
    class Parent {
      get name() {
        return this._name;
      }
      
      set name(value) {
        this._name = value;
      }
    }
    
    class Child extends Parent {
    }
    
    const child = new Child();
    child.name = 'John';
    console.log(child.name); // 输出: John

ES6 类继承与传统原型继承的对比

特性ES6 类继承传统原型继承
语法简洁明了较为复杂
构造函数调用使用 super()使用 Parent.call(this)
方法调用使用 super.method()使用 Parent.prototype.method.call(this)
静态方法继承自动继承需要手动设置
可读性
浏览器支持现代浏览器所有浏览器

示例:ES6 类继承的完整实现

javascript
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating`);
  }
  
  sleep() {
    console.log(`${this.name} is sleeping`);
  }
  
  static create(name) {
    return new Animal(name);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
  
  bark() {
    console.log(`${this.name} is barking`);
  }
  
  // 重写父类方法
  sleep() {
    super.sleep();
    console.log(`${this.name} (${this.breed) is sleeping soundly`);
  }
  
  // 静态方法
  static create(name, breed) {
    return new Dog(name, breed);
  }
}

// 使用
const animal = Animal.create('Generic Animal');
animal.eat(); // Generic Animal is eating

const dog = Dog.create('Buddy', 'Golden Retriever');
dog.eat(); // Buddy is eating
dog.bark(); // Buddy is barking
dog.sleep(); // Buddy is sleeping
             // Buddy (Golden Retriever) is sleeping soundly

3. 请解释 JavaScript 中的组合继承

Details

组合继承是 JavaScript 中一种常用的继承模式,它结合了构造函数继承和原型继承的优点。

组合继承的基本概念

组合继承通过以下两个步骤实现:

  1. 使用构造函数继承(通过 call()apply())继承实例属性。
  2. 使用原型继承(通过设置原型链)继承原型方法。

组合继承的实现

javascript
// 父类构造函数
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

// 父类原型方法
Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类构造函数
function Child(name, age) {
  // 继承父类实例属性
  Parent.call(this, name);
  this.age = age;
}

// 继承父类原型方法
Child.prototype = new Parent();
// 修复构造函数指向
Child.prototype.constructor = Child;

// 子类原型方法
Child.prototype.sayAge = function() {
  console.log(this.age);
};

// 使用
const child1 = new Child('John', 10);
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
child1.sayName(); // John
child1.sayAge(); // 10

const child2 = new Child('Jane', 12);
console.log(child2.colors); // ['red', 'blue', 'green'](不受 child1 的影响)
child2.sayName(); // Jane
child2.sayAge(); // 12

组合继承的优缺点

优点

  1. 继承实例属性:通过构造函数继承,每个实例都有自己的实例属性副本,避免了引用类型属性的共享问题。
  2. 继承原型方法:通过原型继承,所有实例共享原型上的方法,节省内存。
  3. 方法重写:子类可以重写父类的方法,实现多态。

缺点

  1. 父类构造函数被调用两次:一次在创建子类原型时,一次在子类构造函数中。
  2. 原型上的实例属性:父类构造函数在创建子类原型时会在原型上创建实例属性,这些属性会被实例属性覆盖。

组合继承的优化

为了避免父类构造函数被调用两次,可以使用 Object.create() 来创建子类原型。

javascript
// 父类构造函数
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}

// 父类原型方法
Parent.prototype.sayName = function() {
  console.log(this.name);
};

// 子类构造函数
function Child(name, age) {
  // 继承父类实例属性
  Parent.call(this, name);
  this.age = age;
}

// 优化:使用 Object.create() 创建子类原型
Child.prototype = Object.create(Parent.prototype);
// 修复构造函数指向
Child.prototype.constructor = Child;

// 子类原型方法
Child.prototype.sayAge = function() {
  console.log(this.age);
};

组合继承与 ES6 类继承的关系

ES6 类继承本质上是组合继承的语法糖,它在内部使用了类似的机制:

  1. 使用 super() 调用父类构造函数,继承实例属性。
  2. 使用原型链继承父类的方法。
javascript
// ES6 类继承
class Parent {
  constructor(name) {
    this.name = name;
  }
  
  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 相当于 Parent.call(this, name)
    this.age = age;
  }
  
  sayAge() {
    console.log(this.age);
  }
}

组合继承的应用场景

组合继承适用于需要继承实例属性和原型方法的场景,特别是当父类有引用类型的实例属性时,组合继承可以确保每个实例都有自己的属性副本。

示例:组合继承的完整实现

javascript
// 父类:Person
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.friends = [];
}

Person.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name}`);
};

// 子类:Student
function Student(name, age, grade) {
  // 继承父类实例属性
  Person.call(this, name, age);
  this.grade = grade;
}

// 继承父类原型方法
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// 子类原型方法
Student.prototype.study = function() {
  console.log(`${this.name} is studying in grade ${this.grade}`);
};

// 重写父类方法
Student.prototype.greet = function() {
  console.log(`Hello, my name is ${this.name} and I'm in grade ${this.grade}`);
};

// 使用
const student1 = new Student('John', 15, 10);
student1.friends.push('Jane');
student1.greet(); // Hello, my name is John and I'm in grade 10
student1.study(); // John is studying in grade 10
console.log(student1.friends); // ['Jane']

const student2 = new Student('Bob', 16, 11);
student2.friends.push('Alice');
student2.greet(); // Hello, my name is Bob and I'm in grade 11
student2.study(); // Bob is studying in grade 11
console.log(student2.friends); // ['Alice'](不受 student1 的影响)