个人空间: https://blog.csdn.net/m0_73589512
https://blog.csdn.net/m0_73589512大家好,我是 叁佰万**~~**
如果本文对您有帮助,请献上你的小❥(^_-)哟~~~
接续上文:
目录
[深入理解 JavaScript 原型与继承:从基础到进阶](#深入理解 JavaScript 原型与继承:从基础到进阶)
[1. 面向过程编程](#1. 面向过程编程)
[2. 面向对象编程(OOP)](#2. 面向对象编程(OOP))
[3. 函数式编程](#3. 函数式编程)
[二、JS 的对象系统:简单对象、函数对象与原型](#二、JS 的对象系统:简单对象、函数对象与原型)
[1. 简单对象:最基础的数据容器](#1. 简单对象:最基础的数据容器)
[2. 函数对象:构造函数的核心载体](#2. 函数对象:构造函数的核心载体)
[3. 原型对象:继承的底层逻辑](#3. 原型对象:继承的底层逻辑)
[三、new 的底层:构造函数创建实例的过程](#三、new 的底层:构造函数创建实例的过程)
[四、JS 继承方式的演进:从 "有缺陷" 到 "最优解"](#四、JS 继承方式的演进:从 “有缺陷” 到 “最优解”)
[1. 构造函数继承:解决 "引用类型共享" 问题](#1. 构造函数继承:解决 “引用类型共享” 问题)
[2. 原型链继承:解决 "方法复用" 问题](#2. 原型链继承:解决 “方法复用” 问题)
[3. 组合继承:融合前两者的优点](#3. 组合继承:融合前两者的优点)
[4. 寄生组合继承:最优解](#4. 寄生组合继承:最优解)
[5. ES6 class:语法糖简化继承](#5. ES6 class:语法糖简化继承)
深入理解 JavaScript 原型与继承:从基础到进阶
在 JavaScript 的世界里,"面向对象" 始终是绕不开的核心话题,但与 Java、C++ 等传统面向对象语言不同,JS 并非基于 "类",而是靠构造函数 + 原型搭建起独特的对象系统。本文将从编程范式对比入手,拆解原型、原型链的本质,梳理 JS 继承方式的演进脉络,帮你彻底搞懂这一核心知识点。
一、编程范式:面向过程、面向对象与函数式
在聊 JS 的对象系统前,先明确编程范式的差异 ------ 这能帮我们理解 "为什么 JS 要这么设计"。
1. 面向过程编程
核心是 "以步骤为中心",按时间顺序拆解任务,数据与操作分离。比如实现 "用户下单",会拆成 "校验参数→查询库存→扣减库存→生成订单" 等步骤,步骤间依赖极强,容易产生冗余代码和复杂的调用链。
2. 面向对象编程(OOP)
核心是 "以对象为中心",将数据和操作封装成 "模板(类 / 构造函数)",通过实例化创建独立对象。它的核心特性是:
- 封装:数据和方法包裹在对象内,隐藏内部细节;
- 继承:子类复用父类的属性和方法;
- 多态:同一方法在不同对象上有不同实现;
- 抽象:只暴露必要接口,屏蔽复杂逻辑。
3. 函数式编程
核心是 "以函数为中心",强调纯函数、不可变性,避免共享状态和副作用,像 "管道" 一样串联函数处理数据。
JS 并非单一范式语言,而是支持多范式,但它的 OOP 体系最具特色 ------ 基于原型,而非类。
二、JS 的对象系统:简单对象、函数对象与原型
JS 里 "万物皆对象",但对象也分类型,不同类型的对象承担着不同的角色。
1. 简单对象:最基础的数据容器
简单对象就是键值对的集合,没有类模板,直接定义,适合做数据载体:
// 字面量创建
const person = {
name: 'Alice',
age: 21,
greet: function() {
return `hello,I'am ${this.name}`;
}
};
// Object构造函数创建
const car = new Object();
car.brand = 'Toyota';
它的特点是:属性可以是原始值、对象或函数,但无法实例化、复用性低。
2. 函数对象:构造函数的核心载体
在 JS 中,函数也是对象 ------ 这是理解构造函数的关键。函数对象不仅能执行逻辑,还拥有特殊属性:
function Person(name) {
this.name = name;
}
// 函数对象的特殊属性
console.log(Person.name); // "Person"(函数名)
console.log(Person.length); // 1(形参个数)
console.log(Person.prototype); // 原型对象(构造函数专属)
其中,prototype是核心 ------ 只有函数对象拥有这个属性,它是后续实现 "继承" 的基础。
3. 原型对象:继承的底层逻辑
每个实例对象都有__proto__属性(指向其原型),每个构造函数都有prototype属性(指向原型对象),这两者共同构成了 "原型链":
function Animal(name) {
this.name = name;
}
// 原型对象上定义共享方法
Animal.prototype.eat = function() {
return `${this.name} is eating`;
};
const dog = new Animal('Dog');
// 原型链:dog -> Animal.prototype -> Object.prototype -> null
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
__proto__就像 "血缘关系",实例访问属性 / 方法时,会沿着原型链向上查找,直到null------ 这就是 JS 继承的本质。
三、new 的底层:构造函数创建实例的过程
用new调用构造函数时,JS 会自动完成 4 步操作:
- 创建空对象:生成一个新的空对象作为实例;
- 绑定原型 :空对象的
__proto__指向构造函数的prototype; - 绑定 this :将构造函数的
this指向这个空对象; - 执行构造函数:运行构造函数的初始化代码,给实例添加属性。
这一步的关键是:实例之间彼此独立,但能通过原型链共享方法,既保证了独立性,又实现了代码复用。
四、JS 继承方式的演进:从 "有缺陷" 到 "最优解"
继承的核心需求是 "复用父类的属性和方法",但早期 JS 没有class语法,开发者摸索出了多种继承方式,一步步从 "有缺陷" 走向 "最优解"。
1. 构造函数继承:解决 "引用类型共享" 问题
核心是通过Parent.call(this)调用父类构造函数,让子类实例拥有独立的属性:
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function Child(name, age) {
Parent.call(this, name); // 盗用父类构造函数
this.age = age;
}
const child1 = new Child('Alice', 10);
child1.colors.push('green');
console.log(child2.colors); // ['red', 'blue'](实例属性独立)
优点 :实例属性不共享、可向父类传参;缺点:方法无法复用(每个实例都会创建新方法)、无法访问父类原型的方法。
2. 原型链继承:解决 "方法复用" 问题
核心是将子类原型指向父类实例,让子类能访问父类原型的方法:
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayName = function() {
return this.name;
};
function Child(age) {
this.age = age;
}
Child.prototype = new Parent(); // 绑定原型链
Child.prototype.constructor = Child; // 修复constructor指向
优点 :方法可复用;缺点:引用类型属性被所有实例共享、无法向父类传参。
3. 组合继承:融合前两者的优点
结合 "构造函数继承"(继承实例属性)和 "原型链继承"(继承原型方法):
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 继承实例属性(第二次调用父类)
this.age = age;
}
Child.prototype = new Parent(); // 继承原型方法(第一次调用父类)
Child.prototype.constructor = Child;
优点 :实例属性独立、方法可复用、可传参;缺点:父类构造函数被调用两次,子类原型存在冗余属性。
4. 寄生组合继承:最优解
核心是通过Object.create()创建父类原型的副本,替代 "子类原型 = 父类实例",避免父类构造函数被重复调用:
// 封装继承逻辑
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'];
}
Parent.prototype.sayName = function() {
return this.name;
};
function Child(name, age) {
Parent.call(this, name); // 仅调用一次父类
this.age = age;
}
inheritPrototype(Child, Parent); // 绑定原型链
这是 JS 继承的 "最终方案"------ES6 的class extends底层就是基于这种方式实现的。
5. ES6 class:语法糖简化继承
ES6 的**class本质** 是构造函数的语法糖,让继承更直观:
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
return this.name;
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 相当于Parent.call(this, name)
this.age = age;
}
sayAge() {
return this.age;
}
}
写法更接近传统面向对象语言,但底层逻辑依然是 "原型 + 构造函数"。
五、核心总结:原型与继承的关键认知
-
prototype vs proto:
prototype是构造函数的属性,用于 "定义 / 设计" 继承关系(蓝图);__proto__是实例的属性,用于 "实现 / 查找" 继承关系(血缘);- 业务代码中优先用
Object.getPrototypeOf()/Object.setPrototypeOf(),避免直接操作__proto__。
-
继承方式选择:
- 简单需求:用 ES6
class(底层是寄生组合继承); - 多继承需求:用 "混入(Mixin)" 实现;
- 避免过度设计:组合优于继承,优先用对象组合替代复杂继承链。
- 简单需求:用 ES6
-
本质:JS 的面向对象核心是 "原型复用",所有继承方式的最终目的都是 ------ 在保证实例独立的前提下,最大化代码复用。
理解原型和继承,不仅能写出更优雅的 JS 代码,更能看懂框架源码中的设计思路(比如 Vue 的原型链、React 的类组件)。从构造函数到原型链,从组合继承到寄生组合继承,每一步演进都是为了弥补前一种方式的缺陷 ------ 这也是编程的核心:发现问题,解决问题,优化方案。