返回首页

说说JavaScript中的继承?

问题解析

继承是面向对象编程的核心概念之一。JavaScript 作为一门基于原型的语言,其继承机制与 Java、C++ 等基于类的语言有很大不同。理解 JavaScript 的继承方式,对于编写可维护、可复用的代码至关重要。

核心概念

继承的本质

继承使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在子类继承父类的同时,可以重新定义某些属性,并重写某些方法,使其获得与父类不同的功能。

详细解答

1. 原型链继承

function Parent() {
    this.name = 'Parent';
    this.colors = ['red', 'blue'];
}

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

function Child() {
    this.age = 18;
}

// 继承 Parent
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const child1 = new Child();
const child2 = new Child();

child1.sayName();  // "Parent"

// 问题:引用类型属性被共享
child1.colors.push('green');
console.log(child1.colors);  // ["red", "blue", "green"]
console.log(child2.colors);  // ["red", "blue", "green"] —— 也被修改了!

优点

  • 可以继承父类原型上的方法

缺点

  • 引用类型属性被所有实例共享
  • 创建子类实例时不能向父类传参

2. 构造函数继承(借用构造函数)

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

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

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

const child1 = new Child('Alice', 18);
const child2 = new Child('Bob', 20);

// 引用类型属性独立
child1.colors.push('green');
console.log(child1.colors);  // ["red", "blue", "green"]
console.log(child2.colors);  // ["red", "blue"]

// 问题:无法继承父类原型上的方法
child1.sayName();  // TypeError: child1.sayName is not a function

优点

  • 引用类型属性独立,不被共享
  • 可以向父类传参

缺点

  • 无法继承父类原型上的方法
  • 每次创建实例都会调用一次父类构造函数

3. 组合继承

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

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

function Child(name, age) {
    // 继承实例属性(第二次调用 Parent)
    Parent.call(this, name);
    this.age = age;
}

// 继承原型方法(第一次调用 Parent)
Child.prototype = new Parent();
Child.prototype.constructor = Child;

Child.prototype.sayAge = function() {
    console.log(this.age);
};

const child1 = new Child('Alice', 18);
const child2 = new Child('Bob', 20);

child1.sayName();  // "Alice"
child1.sayAge();   // 18

// 引用类型属性独立
child1.colors.push('green');
console.log(child1.colors);  // ["red", "blue", "green"]
console.log(child2.colors);  // ["red", "blue"]

优点

  • 结合了原型链继承和构造函数继承的优点
  • 可以继承实例属性和原型方法
  • 引用类型属性独立

缺点

  • 父类构造函数被调用两次
  • 子类原型上有多余的父类实例属性

4. 原型式继承

// 基于现有对象创建新对象
function createObject(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

// ES5 Object.create 的实现
const person = {
    name: 'Person',
    colors: ['red', 'blue'],
    sayName() {
        console.log(this.name);
    }
};

const person1 = Object.create(person);
person1.name = 'Alice';
person1.colors.push('green');

const person2 = Object.create(person);
console.log(person2.colors);  // ["red", "blue", "green"] —— 共享引用类型

// 可以指定第二个参数,定义额外属性
const person3 = Object.create(person, {
    name: { value: 'Bob' },
    age: { value: 25, writable: true, enumerable: true }
});

优点

  • 简单,不需要构造函数
  • 适合对象之间的浅复制

缺点

  • 引用类型属性共享
  • 无法实现代码复用

5. 寄生式继承

function createAnother(original) {
    // 通过某种方式创建对象(如 Object.create)
    const clone = Object.create(original);

    // 增强对象,添加方法
    clone.sayHi = function() {
        console.log('Hi!');
    };

    return clone;
}

const person = {
    name: 'Person',
    sayName() {
        console.log(this.name);
    }
};

const person1 = createAnother(person);
person1.sayName();  // "Person"
person1.sayHi();    // "Hi!"

优点

  • 在原型式继承基础上增强了对象

缺点

  • 引用类型属性共享
  • 方法无法复用(每次创建新函数)

6. 寄生组合式继承(推荐)

function inheritPrototype(Child, Parent) {
    // 创建父类原型的副本
    const prototype = Object.create(Parent.prototype);
    // 修正 constructor 指向
    prototype.constructor = Child;
    // 赋值给子类原型
    Child.prototype = prototype;
}

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

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

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 使用寄生组合式继承
inheritPrototype(Child, Parent);

Child.prototype.sayAge = function() {
    console.log(this.age);
};

const child = new Child('Alice', 18);
child.sayName();  // "Alice"
child.sayAge();   // 18
console.log(child instanceof Child);   // true
console.log(child instanceof Parent);  // true

优点

  • 只调用一次父类构造函数
  • 避免了在子类原型上创建不必要的属性
  • 原型链保持不变
  • 效率最高

7. ES6 Class 继承

class Parent {
    constructor(name) {
        this.name = name;
        this.colors = ['red', 'blue'];
    }

    sayName() {
        console.log(this.name);
    }

    // 静态方法
    static isParent(obj) {
        return obj instanceof Parent;
    }
}

class Child extends Parent {
    constructor(name, age) {
        super(name);  // 调用父类构造函数,必须在使用 this 之前调用
        this.age = age;
    }

    sayAge() {
        console.log(this.age);
    }

    // 重写父类方法
    sayName() {
        super.sayName();  // 调用父类方法
        console.log(`I am ${this.age} years old`);
    }
}

const child = new Child('Alice', 18);
child.sayName();  // "Alice" "I am 18 years old"
child.sayAge();   // 18

console.log(Child.isParent(child));  // true

本质:ES6 Class 是寄生组合式继承的语法糖

// Class 本质上仍然是原型链
console.log(typeof Child);           // "function"
console.log(Child.prototype.__proto__ === Parent.prototype);  // true
console.log(child.__proto__ === Child.prototype);             // true
console.log(child.__proto__.__proto__ === Parent.prototype);  // true

深入理解

继承方式对比

继承方式 优点 缺点
原型链继承 可以继承原型方法 引用类型共享,不能传参
构造函数继承 引用类型独立,可以传参 不能继承原型方法
组合继承 结合前两者优点 调用两次父类构造函数
原型式继承 简单,不需要构造函数 引用类型共享
寄生式继承 增强对象 引用类型共享,方法无法复用
寄生组合式继承 效率高,只调用一次父类构造函数 实现稍复杂
ES6 Class 语法清晰,易于理解 需要转译(旧浏览器)

多重继承(Mixin)

// JavaScript 不支持多重继承,但可以通过 Mixin 实现
const Mixin1 = {
    method1() {
        console.log('method1');
    }
};

const Mixin2 = {
    method2() {
        console.log('method2');
    }
};

function mix(...mixins) {
    return function(target) {
        mixins.forEach(mixin => {
            Object.assign(target.prototype, mixin);
        });
    };
}

@mix(Mixin1, Mixin2)
class MyClass {}

// 或使用函数方式
function applyMixins(targetClass, ...mixins) {
    mixins.forEach(mixin => {
        Object.getOwnPropertyNames(mixin).forEach(name => {
            targetClass.prototype[name] = mixin[name];
        });
    });
}

class MyClass {}
applyMixins(MyClass, Mixin1, Mixin2);

const obj = new MyClass();
obj.method1();  // "method1"
obj.method2();  // "method2"

最佳实践

1. 现代项目推荐使用 ES6 Class

class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        console.log(`${this.name} makes a sound`);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }

    speak() {
        super.speak();
        console.log(`${this.name} barks`);
    }
}

2. 需要兼容旧浏览器时使用寄生组合式继承

function inherit(Child, Parent) {
    Child.prototype = Object.create(Parent.prototype);
    Child.prototype.constructor = Child;
}

function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

inherit(Child, Parent);

3. 避免使用 __proto__

// ❌ 不推荐
obj.__proto__ = otherObj;

// ✅ 推荐
Object.setPrototypeOf(obj, otherObj);
// 或创建时指定
const obj = Object.create(otherObj);

面试要点

  1. JavaScript 有哪些继承方式?

    • 原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承、ES6 Class 继承
  2. 组合继承的缺点是什么?

    • 父类构造函数被调用两次
    • 子类原型上有多余的父类实例属性
  3. 为什么寄生组合式继承是最佳方案?

    • 只调用一次父类构造函数
    • 避免了不必要的属性创建
    • 原型链结构清晰
  4. ES6 Class 继承的本质是什么?

    • 是寄生组合式继承的语法糖
    • 使用 extends 关键字建立原型链
    • 使用 super 调用父类构造函数
  5. super 的作用?

    • 在构造函数中:调用父类构造函数
    • 在方法中:调用父类原型上的方法
    • 必须在子类构造函数中使用 this 之前调用 super()
  6. 如何判断一个对象是否是某个类的实例?

    • instanceof 操作符
    • Object.prototype.isPrototypeOf()
    • constructor 属性(不推荐,可能被修改)