当前位置: 首页 > news >正文

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(); // 18const 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; // 修复 constructorchild.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); // 调用父类的 constructorthis.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 {} // 自动正确处理 constructorconsole.log(Child.prototype.constructor === Child); // true

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

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

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

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

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

http://www.dtcms.com/a/310621.html

相关文章:

  • 2025牛客多校第六场D题解
  • Object对象中的常用方法
  • 当10米精度遇上64维AI大脑——Google全球卫星嵌入数据集(Satellite Embedding V1)全解析
  • 【华为机试】34. 在排序数组中查找元素的第一个和最后一个位置
  • 移动端 WebView 内存泄漏与性能退化问题如何排查 实战调试方法汇总
  • 文章发布Typecho网站技巧
  • Squid服务配置代理
  • SystemVerilog的系统函数和任务
  • Python 项目路径配置完全指南
  • C语言-字符串(定义)、字符串函数(strlen、strcat、strcpy、strcmp、strlwr、strupr)
  • 航天器VHF/UHF/L频段弱电磁信号兼容性设计
  • 【3】交互式图表制作及应用方法
  • Spring Cloud 和服务拆分:微服务落地的第一步
  • Java抽象类与接口深度解析:核心区别与应用场景全指南
  • C++ - 仿 RabbitMQ 实现消息队列--服务端核心模块实现(五)
  • 流式输出:概念、技巧与常见问题
  • c++详解(宏与内联函数,nullptr)
  • 每日面试题18:基本数据类型和引用数据类型的区别
  • 唐克的新游戏
  • 100道题通过CISSP,还剩70分钟
  • 体育数据API接入方式与数据类型详解
  • 连载【流程规划进阶 16/16】完结——35页16.流程的现状分析 【附全文阅读】
  • 达梦数据库权限体系详解:系统权限与对象权限
  • 大模型微调与部署课程笔记
  • FreeRTOS硬件中断发生时的现场
  • Spring AI 与 LangChain4j 对比及入门案例解析
  • Selenium:强大的 Web 自动化测试工具
  • VS Code中配置使用slint(Rust)的一个小例子
  • 亚马逊广告:如何借助AI玩转长尾词提升ROI
  • 伞状Meta分析重构癌症幸存者照护指南:从矛盾证据到精准决策