JavaScript 中的对象继承:从浅入深

JavaScript 实现继承的方式有多种,我将从最简单的方式开始,逐步深入讲解更复杂的继承方式。

1. 原型链继承(最简单的继承方式)

这是 JavaScript 最基本的继承方式,通过原型链实现。

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

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

function Child() {
  this.name = 'Child';
}

// 关键步骤:将 Child 的原型指向 Parent 的实例
Child.prototype = new Parent();

const child1 = new Child();
child1.sayName(); // 输出 "Child"
console.log(child1.colors); // ["red", "blue", "green"]

const child2 = new Child();
child1.colors.push('black');
console.log(child2.colors); // ["red", "blue", "green", "black"] (共享引用属性)

问题:

引用类型的属性会被所有实例共享

创建子类实例时,不能向父类构造函数传参

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

为了解决原型链继承的问题,可以使用构造函数继承。

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

function Child(name, age) {
  // 关键步骤:在子类构造函数中调用父类构造函数
  Parent.call(this, name); // 相当于把父类的实例属性复制一份到子类
  this.age = age;
}

const child1 = new Child('Tom', 18);
console.log(child1.name); // "Tom"
console.log(child1.age); // 18
child1.colors.push('black');

const child2 = new Child('Jerry', 20);
console.log(child2.colors); // ["red", "blue", "green"] (不共享引用属性)

优点:

避免了引用类型属性被共享

可以在子类中向父类传参

缺点:

方法都在构造函数中定义,每次创建实例都会创建一遍方法

不能继承父类原型上的方法

3. 组合继承(最常用的继承方式)

结合原型链继承和构造函数继承的优点。

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); // 第二次调用 Parent()
  this.age = age;
}

// 继承方法
Child.prototype = new Parent(); // 第一次调用 Parent()
Child.prototype.constructor = Child; // 修复构造函数指向
Child.prototype.sayAge = function() {
  console.log(this.age);
};

const child1 = new Child('Tom', 18);
child1.colors.push('black');
console.log(child1.colors); // ["red", "blue", "green", "black"]
child1.sayName(); // "Tom"
child1.sayAge(); // 18

const child2 = new Child('Jerry', 20);
console.log(child2.colors); // ["red", "blue", "green"]
child2.sayName(); // "Jerry"
child2.sayAge(); // 20

优点:

实例属性私有,引用属性不共享

父类方法可以复用

可以传参

缺点:

调用了两次父类构造函数

4. 原型式继承

类似于 Object.create() 的实现方式。

function createObj(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

const person = {
  name: 'Default',
  friends: ['Tom', 'Jerry']
};

const person1 = createObj(person);
person1.name = 'Person1';
person1.friends.push('Bob');

const person2 = createObj(person);
person2.name = 'Person2';
person2.friends.push('Alice');

console.log(person.friends); // ["Tom", "Jerry", "Bob", "Alice"] (共享引用属性)

适用场景:

不需要单独创建构造函数,只想让一个对象与另一个对象保持类似

5. 寄生式继承

在原型式继承的基础上增强对象。

function createAnother(original) {
  const clone = Object.create(original); // 通过调用函数创建一个新对象
  clone.sayHi = function() { // 以某种方式增强这个对象
    console.log('Hi');
  };
  return clone;
}

const person = {
  name: 'Default',
  friends: ['Tom', 'Jerry']
};

const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "Hi"

缺点:

方法不能复用,每次创建对象都会创建一遍方法

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

解决组合继承调用两次父类构造函数的问题。

function inheritPrototype(child, parent) {
  const prototype = Object.create(parent.prototype); // 创建父类原型的副本
  prototype.constructor = child; // 修复 constructor
  child.prototype = prototype; // 将副本赋值给子类原型
}

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); // 只调用一次 Parent 构造函数
  this.age = age;
}

// 关键步骤:替换原来的 Child.prototype = new Parent()
inheritPrototype(Child, Parent);

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

const child = new Child('Tom', 18);
child.sayName(); // "Tom"
child.sayAge(); // 18

优点:

只调用一次父类构造函数

避免在子类原型上创建不必要的属性

原型链保持不变

7. ES6 的 class 继承

ES6 引入了 class 语法糖,使继承更加清晰。

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

class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类的 constructor
    this.age = age;
  }
  
  sayAge() {
    console.log(this.age);
  }
}

const child = new Child('Tom', 18);
child.sayName(); // "Tom"
child.sayAge(); // 18

特点:

语法更接近传统面向对象语言

底层仍然是基于原型链实现的

使用 extends 和 super 关键字

总结

原型链继承:简单但有问题(引用共享)

构造函数继承:解决引用共享但方法不能复用

组合继承:常用但调用两次构造函数

原型式继承:类似 Object.create()

寄生式继承:增强对象但方法不能复用

寄生组合式继承:最理想的继承方式

ES6 class:语法糖,底层仍是原型继承

在实际开发中,ES6 的 class 继承是最推荐的方式,因为它语法简洁清晰。如果需要在旧环境中使用,寄生组合式继承是最佳选择。

Child.prototype = new Parent() 的详细解析

这个操作到底做了什么?

Child.prototype = new Parent() 实际上做了两件事:

创建了一个 Parent 的实例对象(包含 Parent 构造函数中定义的实例成员)

将这个实例对象设置为 Child 的原型(从而也继承了 Parent 原型上的成员)

具体继承的内容

1. 继承 Parent 的实例成员(构造函数中定义的属性/方法)

function Parent() {
  this.parentProperty = '我是Parent的实例属性'; // 实例成员
  this.colors = ['red', 'blue'];
}

const childProto = new Parent();
console.log(childProto.parentProperty); // "我是Parent的实例属性"
console.log(childProto.colors); // ["red", "blue"]

这些实例属性会成为 Child.prototype 的属性,也就是会被所有 Child 实例共享。

2. 继承 Parent 的原型成员(prototype 上的方法)

Parent.prototype.parentMethod = function() {
  return '我是Parent原型上的方法';
};

console.log(childProto.parentMethod()); // "我是Parent原型上的方法"

与仅继承原型链的区别

如果你只想继承 Parent 原型链上的方法(而不继承实例成员),应该使用:

Child.prototype = Object.create(Parent.prototype);
// 而不是 new Parent()

实际影响示例

function Parent() {
  this.instanceProp = '实例属性'; // 会被所有Child实例共享
}
Parent.prototype.protoMethod = function() {
  return '原型方法';
};

// 方式1:使用 new Parent()
function Child1() {}
Child1.prototype = new Parent();

const c1a = new Child1();
const c1b = new Child1();
console.log(c1a.instanceProp); // "实例属性"(来自原型)
c1a.instanceProp = '修改后的值';
console.log(c1b.instanceProp); // "实例属性"(共享问题!)

// 方式2:使用 Object.create()
function Child2() {
  Parent.call(this); // 显式调用父类构造函数
}
Child2.prototype = Object.create(Parent.prototype);

const c2a = new Child2();
const c2b = new Child2();
console.log(c2a.instanceProp); // "实例属性"(来自实例)
c2a.instanceProp = '修改后的值';
console.log(c2b.instanceProp); // "实例属性"(不共享)

为什么组合继承是更好的选择

function Child() {
  Parent.call(this); // 1. 继承实例成员(不共享)
}

Child.prototype = Object.create(Parent.prototype); // 2. 继承原型成员
Child.prototype.constructor = Child;

这样:

实例属性通过 Parent.call(this) 正确初始化(每个实例独立)

原型方法通过原型链正确继承

总结
Child.prototype = new Parent() 会:

继承 Parent 的实例成员(成为 Child.prototype 的属性,被所有实例共享)

继承 Parent 的原型成员(通过原型链)

在大多数情况下,这不是最佳实践,因为:

会不必要地将实例成员放到原型上

可能导致引用类型属性被所有实例共享的问题

更好的做法是使用组合继承或 ES6 的 class 语法。

关于 Child.prototype.constructor = Child 的作用

为什么需要手动修复 constructor 指向?
在 JavaScript 中,每个函数都有一个 prototype 属性,而这个 prototype 对象默认会有一个 constructor 属性指向函数本身。这是一个自动建立的循环引用。
默认情况下:

function Child() {}
console.log(Child.prototype.constructor === Child); // true - 这是默认情况

当我们修改原型链时:

function Parent() {}
function Child() {}

Child.prototype = new Parent(); // 重写 Child 的原型

console.log(Child.prototype.constructor === Child); // false
console.log(Child.prototype.constructor === Parent); // true

发生了什么?
当我们执行 Child.prototype = new Parent() 时,完全重写了 Child 的原型对象

新的原型对象(new Parent() 实例)的 constructor 属性来自于 Parent.prototype.constructor

因此 Child.prototype.constructor 现在指向了 Parent 而不是 Child

为什么需要修复 constructor 指向?
保持一致性:按照 JavaScript 的设计,构造函数的 prototype.constructor 应该指向自身

某些场景依赖 constructor 属性:

实例的 constructor 属性是通过原型链查找得到的

一些库或框架可能会使用 constructor 属性来检测对象类型

开发者可能依赖 instance.constructor 来获取构造函数

function Parent() {}
function Child() {}

Child.prototype = new Parent();
const child = new Child();

console.log(child.constructor); // 输出 Parent 函数(如果不修复)

符合预期行为:

直觉上,Child 实例的构造函数应该是 Child 而不是 Parent

修复后可以保持这种直觉一致性

底层机制
原型链查找规则:

当访问 child.constructor 时,JavaScript 会:

先在 child 对象自身查找

找不到则去 child.proto(即 Child.prototype)查找

如果 Child.prototype 也没有,则继续向上查找

constructor 的来源:

默认情况下,Child.prototype 是一个对象,它的 constructor 属性自动指向 Child

但当我们完全替换了 Child.prototype 后,新的原型对象(new Parent())的 constructor 来自于 Parent.prototype

实际影响示例

function Parent() {}
function Child() {}

// 情况1:不修复 constructor
Child.prototype = new Parent();
const child1 = new Child();
console.log(child1.constructor === Parent); // true(不符合预期)

// 情况2:修复 constructor
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 修复
const child2 = new Child();
console.log(child2.constructor === Child); // true(符合预期)

最佳实践

在 ES5 风格的继承中,总是应该修复 constructor 指向:

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

在 ES6 的 class 语法中,这个修复是自动完成的:

class Parent {}
class Child extends Parent {} // 自动正确处理 constructor

console.log(Child.prototype.constructor === Child); // true

总结
Child.prototype.constructor = Child 的作用是:

修复因重写原型链而断裂的 constructor 引用

确保实例的 constructor 属性正确指向创建它的构造函数

保持 JavaScript 原型系统的自洽性和一致性

虽然在某些简单场景中不修复可能不会立即出现问题,但在大型应用或复杂继承关系中,保持正确的 constructor 指向可以避免很多潜在的问题。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值