吃透 JS 原型:从构造函数到原型链,一篇讲透核心逻辑
作为前端开发者,你是否也曾被 JS 的 “原型” 搞得晕头转向😵?比如明明没给实例定义toString方法,却能直接调用;明明写的是普通函数,却能实现类似类的继承效果;ES6 的class看起来像传统面向对象语法,底层却还是原型在 “搞事情”。
其实,原型是 JS 面向对象的 “灵魂”——ES5 时代没有 “类” 的概念,我们熟知的 “类” 全靠构造函数 + 原型模拟;即便 ES6 引入class,它也只是原型的 “语法糖”,底层逻辑从未改变。今天,我们从小米 SU7 的实例出发🚗,从 “原型是什么” 到 “原型链如何工作”,再到 “实际开发怎么用”,把知识点揉碎了讲清楚,让你彻底吃透原型!
一、原型:JS 面向对象的核心基石✨
在拆解复杂逻辑前,先明确最核心的问题:原型到底是什么?
简单来说,原型(Prototype)是 JS 为实现 “属性共享” 和 “继承” 设计的底层机制,主要通过两类 “原型对象” 和一个 “关联规则” 实现:
- 显式原型(
prototype) :所有函数(除箭头函数)天生自带的属性,值是一个普通对象(称为 “原型对象”),是该函数所有实例的 “共享仓库”; - 隐式原型(
__proto__) :所有对象(除null/undefined)天生自带的私有属性,值严格指向创建该对象的构造函数的prototype,是实例与原型仓库的 “连接桥梁”; - 核心关联规则:实例访问属性 / 方法时,先查自身(自有属性),找不到就通过
__proto__查原型对象,再找不到就继续查原型对象的__proto__,直到null—— 这就是 “原型链查找”。
1:原型对象的默认属性 ——constructor
每个函数的prototype(原型对象)默认自带一个constructor属性,指向该原型对象对应的构造函数,形成 “构造函数→原型对象→构造函数” 的闭环:
javascript
运行
function Person() {}
// 原型对象默认的constructor指向构造函数
console.log(Person.prototype.constructor === Person); // true
// 实例通过原型链访问constructor
const person1 = new Person();
console.log(person1.constructor === Person); // true(person1自身没有constructor,查原型)
⚠️ 注意:如果手动重写prototype(如Person.prototype = { ... }),会覆盖默认的constructor,导致其指向Object(而非原构造函数),此时需要手动修复:
javascript
运行
Person.prototype = {
constructor: Person, // 手动绑定回原构造函数
sayHi() { console.log('Hi'); }
};
console.log(Person.prototype.constructor === Person); // true(修复后正确)
2:用类比理解原型体系
把原型体系比作 “工厂 - 工具箱 - 工人” 模型,更易理解🧰:
- 构造函数(如
Car)= 工厂:负责生产 “工人”(实例); - 显式原型(
Car.prototype)= 公共工具箱:存放所有工人都能用的工具(共享属性 / 方法); - 实例(如
car1)= 工人:自带 “个人口袋”(自有属性,如color),口袋里没有的工具,就用__proto__(钥匙)打开公共工具箱找; - 原型链 = 工具箱的层级:公共工具箱(
Car.prototype)也有自己的钥匙(__proto__),能打开更上层的 “通用工具箱”(Object.prototype)。
3:JS 原型式 vs 传统 class 面向对象 —— 核心差异是 “委托” 而非 “血缘”
很多开发者初次接触原型时,会下意识用传统面向对象(如 Java、C#)的 “class 血缘” 逻辑去套,但 JS 的原型式面向对象和传统模式有本质区别,核心差异在于 “关联方式” 不同:
(1)传统 class 面向对象:“血缘继承” 模式
传统语言的class是 “抽象模板”,子类通过extends继承父类,形成明确的 “父子血缘关系”,实例则是 “子类模板的具体实现”,整个体系是 “层级归属” 逻辑:
- 父类定义通用属性 / 方法(如
Animal类的species = '动物'); - 子类(如
Person)继承父类,自动拥有父类的属性 / 方法,还能新增自己的特性; - 实例(如
zhang)是子类的 “实例化产物”,天生属于子类,间接属于父类,血缘关系固定。
举个 Java 风格的伪代码示例:
java
运行
// 父类(抽象模板)
class Animal {
String species = "动物";
}
// 子类继承父类(血缘关系)
class Person extends Animal {}
// 实例属于Person类,间接属于Animal类
Person zhang = new Person();
System.out.println(zhang.species); // 动物(因血缘继承获得)
(2)JS 原型式面向对象:“原型委托” 模式
JS 没有传统意义上的 “class 模板” 和 “血缘关系”,它的核心是 “对象委托”—— 实例没有某个属性 / 方法时,不是 “天生继承自父类”,而是 “委托” 给原型对象去查找:
- 没有 “父类→子类” 的层级归属,只有 “实例→原型对象” 的委托关联;
- 实例能访问原型属性,不是因为 “血缘”,而是因为
__proto__指向原型对象,遵循 “原型链查找规则”; - 甚至可以动态修改原型对象(如
Car.prototype.drive = 新方法),所有未覆盖该方法的实例会立刻 “感知” 到变化(传统 class 继承中无法动态修改父类模板)。
回到之前的Person示例,更清晰体现 “委托” 逻辑:
javascript
运行
function Person(name) { this.name = name; }
Person.prototype.species = '人类';
const zhang = new Person('张三');
// zhang没有species,委托给__proto__指向的Person.prototype查找
console.log(zhang.species); // 人类(委托查找结果,非血缘继承)
// 动态修改原型,zhang会立刻感知
Person.prototype.species = '智人';
console.log(zhang.species); // 智人(委托的灵活性)
(3)核心差异对比表
| 维度 | 传统 class 面向对象(如 Java) | JS 原型式面向对象 |
|---|---|---|
| 核心逻辑 | 血缘继承(层级归属) | 原型委托(查找关联) |
| 类的角色 | 抽象模板(定义实例的 “应该有”) | 构造函数 + 原型(提供实例 “委托源”) |
| 实例与父类的关系 | 天生归属(血缘固定) | 无直接关系(通过原型间接关联) |
| 动态修改影响 | 父类模板修改不影响已创建实例 | 原型修改影响所有委托该原型的实例 |
| 继承本质 | 复制父类属性 / 方法到子类 | 委托原型对象查找属性 / 方法 |
原型体系逻辑示意图:
二、构造函数:JS 面向对象的 “入门钥匙”🔑
构造函数本质是普通函数,但有两个 “约定” 和三个 “自动行为”,这些细节直接影响使用效果:
1. 构造函数的 “约定”
- 函数名首字母大写(如
Person/Car):区分普通函数,避免误调用; - 必须用
new调用:若直接调用(如Person()),this会指向全局对象(浏览器中是window,Node 中是global),导致属性挂载错误。
2. new调用时的 “自动行为”
当用new调用构造函数(如new Person('张三', 18)),JS 会自动执行 3 步:
- 创建一个空对象(
const obj = {}); - 让空对象的
__proto__指向构造函数的prototype(obj.__proto__ = Person.prototype); - 执行构造函数,将
this绑定到空对象(Person.call(obj, '张三', 18)); - 若构造函数没有返回对象,则默认返回第一步创建的
obj(即实例)。
举个错误案例:忘记new调用的后果:
javascript
运行
function Person(name) {
this.name = name;
}
// 直接调用,this指向window
const person2 = Person('李四');
console.log(window.name); // 李四(属性错挂到全局)
console.log(person2); // undefined(构造函数无返回值,默认返回undefined)
三、prototype:所有实例的 “共享仓库”📦
prototype的核心价值是 “共享”—— 避免每个实例重复创建相同属性 / 方法,节省内存。但需要明确 “实例自有属性” 和 “原型属性” 的区别:
1. 自有属性 vs 原型属性对比
| 特性 | 实例自有属性(如this.name) | 原型属性(如Person.prototype.speci) |
|---|---|---|
| 存储位置 | 实例自身 | 构造函数的prototype对象 |
| 是否共享 | 否(每个实例单独存储) | 是(所有实例共享同一份) |
| 修改影响 | 只影响当前实例 | 影响所有未覆盖该属性的实例 |
| 访问优先级 | 高于原型属性(同名时覆盖) | 低于自有属性 |
2. 如何判断属性类型?——hasOwnProperty方法
JS 提供hasOwnProperty方法(继承自Object.prototype),用于判断属性是否为实例的 “自有属性”(不查原型链):
javascript
运行
function Person(name) {
this.name = name; // 自有属性
}
Person.prototype.speci = '人类'; // 原型属性
const person1 = new Person('张三');
console.log(person1.hasOwnProperty('name')); // true(自有属性)
console.log(person1.hasOwnProperty('speci')); // false(原型属性)
console.log(person1.hasOwnProperty('toString')); // false(继承自Object.prototype)
四、__proto__:实例与原型的 “连接桥梁”🌉
__proto__是实例的私有属性,虽然浏览器都支持,但并非 ES 标准推荐的操作方式(存在兼容性和性能问题)。ES6 提供了两个规范方法替代__proto__:
Object.getPrototypeOf(实例):获取实例的隐式原型(等价于实例.__proto__);Object.setPrototypeOf(实例, 新原型):设置实例的隐式原型(等价于实例.__proto__ = 新原型)。
示例:
javascript
运行
const person1 = new Person('张三');
// 获取原型(规范方式)
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
// 设置原型(谨慎使用,可能影响性能)
const newProto = { sayHi() { console.log('Hi'); } };
Object.setPrototypeOf(person1, newProto);
person1.sayHi(); // Hi(此时person1的原型已改为newProto)
五、原型链:JS 对象的 “继承脉络”🧬
原型链是 “实例→原型对象→原型对象的原型→…→null” 的层级结构,是 JS 实现继承的核心。
1. 原型链的完整层级(以person1为例)
plaintext
person1(实例)
↓ __proto__(Object.getPrototypeOf(person1))
Person.prototype(原型对象)
↓ __proto__
Object.prototype(所有普通对象的根原型)
↓ __proto__
null(原型链的终点,无原型)
2. 原型链查找的 3 个关键规则
- 优先查找自有属性:实例有该属性时,直接返回,不查原型;
- 逐层向上查找:实例没有则查
__proto__,原型对象没有则查其__proto__,直到Object.prototype; - 查找终止于
null:若Object.prototype也没有该属性,返回undefined(不会报错)。
示例:查找person1.toString()的过程:
person1自身没有toString→ 查Person.prototype;Person.prototype没有toString→ 查Object.prototype;Object.prototype有toString→ 执行该方法。
3. 开发禁忌:修改Object.prototype
Object.prototype是所有普通对象的根原型,若手动修改它(如添加属性 / 方法),会影响所有对象,可能导致不可预期的问题(如遍历对象时出现多余属性):
javascript
运行
// 禁忌:修改根原型
Object.prototype.myMethod = function() {};
const obj = { name: '王五' };
// 遍历obj时,会出现myMethod(来自Object.prototype)
for (const key in obj) {
console.log(key); // name、myMethod(不期望出现)
}
六、原型继承的经典实现方式
ES5 没有class,实现继承需结合 “构造函数” 和 “原型”,最常用的是组合继承(解决单独原型继承的缺陷)。
1. 组合继承:构造函数继承 + 原型继承
- 构造函数继承:用
Parent.call(this, ...args)继承 “自有属性”(解决原型继承无法传参的问题); - 原型继承:用
Child.prototype = new Parent()继承 “共享属性 / 方法”。
示例:实现 “Student 继承 Person”:
javascript
运行
// 父构造函数
function Person(name, age) {
this.name = name; // 自有属性
this.age = age;
}
// 父原型的共享方法
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
};
// 子构造函数(组合继承)
function Student(name, age, grade) {
// 1. 构造函数继承:继承父的自有属性
Person.call(this, name, age);
this.grade = grade; // 子的自有属性
}
// 2. 原型继承:继承父的共享方法
Student.prototype = new Person();
// 修复constructor(重写原型后丢失)
Student.prototype.constructor = Student;
// 子原型的共享方法
Student.prototype.study = function() {
console.log(`${this.name}在${this.grade}年级学习`);
};
// 测试
const student1 = new Student('张三', 18, '高三');
student1.sayHi(); // 你好,我是张三(继承自Person)
student1.study(); // 张三在高三年级学习(子自身的共享方法)
console.log(student1.grade); // 高三(子的自有属性)
七、ES6 class:原型的语法糖🍬
ES6 的class本质是原型的语法糖,但写法更规整,支持extends(继承)和super(调用父构造函数),底层逻辑与组合继承一致。需要注意的是:即便用了class,JS 依然是 “原型委托” 逻辑,不是传统的 “血缘继承” ——extends只是帮我们自动关联了原型链(Student.prototype.__proto__ = Person.prototype),没有改变 JS 面向对象的本质。
1. class继承的底层逻辑
javascript
运行
// ES6 class继承
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() { console.log(`你好,我是${this.name}`); }
}
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 等价于Person.call(this, name, age)
this.grade = grade;
}
study() { console.log(`${this.name}在学习`); }
}
// 底层逻辑:Student的原型链(委托关系未变)
console.log(Student.prototype.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(student1) === Student.prototype); // true
extends:让Student.prototype.__proto__指向Person.prototype,本质是建立 “Student 原型→Person 原型” 的委托关系,而非 “Student 类→Person 类” 的血缘;super:在子构造函数中调用父构造函数,仅用于初始化实例的自有属性,不影响原型委托逻辑。
2. 静态属性与静态方法
class中用static修饰的属性 / 方法,挂载到类本身(而非prototype),只能通过类调用,实例无法访问,常用于 “工具方法”:
javascript
运行
class Person {
static species = '人类'; // 静态属性(挂载到Person)
static create(name) { // 静态方法(挂载到Person)
return new Person(name);
}
constructor(name) {
this.name = name;
}
}
// 静态属性/方法通过类调用
console.log(Person.species); // 人类
const person1 = Person.create('张三'); // 调用静态方法创建实例
// 实例无法访问静态属性/方法
console.log(person1.species); // undefined
person1.create(); // 报错:person1.create is not a function
八、常见误区与避坑指南❌
- 误区 1:“
prototype是实例的属性”—— 错!prototype是函数的属性,实例的属性是__proto__; - 误区 2:“
class脱离原型”—— 错!class是语法糖,所有继承 / 共享逻辑仍依赖prototype和原型链,JS 从未变成 “血缘继承”; - 误区 3:“
Object.create(null)创建的对象有原型”—— 错!Object.create(null)创建的是 “无原型对象”,__proto__为null,无法访问toString等方法; - 误区 4:“原型属性修改会影响所有实例”—— 不完全对!若实例自身已覆盖该属性(如
li.species = 'LOL达人'),则原型属性修改不影响该实例; - 误区 5:“JS 原型继承 = 传统 class 继承”—— 错!前者是 “委托查找”,后者是 “血缘归属”,逻辑本质不同,只是
class语法让二者表现相似。
九、实际开发中的原型应用场景
原型不仅是理论,更是实际开发的 “工具”,常见场景包括:
- 扩展原生对象方法(谨慎使用):如给
Array.prototype加forEach(早期 ES5 前),但需避免污染原生原型; - 插件 / 库的封装:如 jQuery 的
$.fn(本质是jQuery.prototype),通过原型共享 DOM 操作方法; - 单例模式实现:用
Object.create基于原型创建单例,避免重复实例化; - React 组件继承(早期):React 16 前的
React.createClass底层用原型实现组件复用,现在虽用class,但底层逻辑不变。
总结:原型的核心逻辑图谱
JS 原型体系的核心可概括为 “三要素 + 一规则 + 一本质”:
- 三要素:构造函数(创建实例)、
prototype(共享仓库)、__proto__(连接桥梁); - 一规则:原型链查找(自有→原型→根原型→
null); - 一本质:委托而非血缘 —— 实例访问属性 / 方法时,是 “委托” 原型对象查找,不是 “继承” 父类的属性 / 方法。
无论用 ES5 的构造函数,还是 ES6 的class,理解原型的底层逻辑,才能真正掌握 JS 的面向对象 —— 毕竟面试中常问的 “原型链污染”“继承实现方式”“this指向”,本质都是原型的延伸知识点。