"JavaScript 的一切皆对象,而对象的背后,是原型。"
------ 本文将带你彻底、系统、深入地理解 JavaScript 中最核心、最独特、也最容易被误解的机制:原型(Prototype) 与 原型链(Prototype Chain)。结合代码,逐行注解、层层递进,确保你不仅能"知道",更能"掌握"和"运用"。
引言:为什么原型如此重要?
在前端开发中,无论是面试大厂,还是阅读框架源码(如 React、Vue),亦或是编写高性能、可复用的组件,原型机制都是绕不开的核心知识。
很多开发者能写出 class 和 extends,却说不清 prototype 和 __proto__ 的区别;能调用 toString(),却不明白它到底从哪来。这种"黑盒式"编程,在遇到复杂问题时极易陷入困境。
本文的目标,就是撕开 JavaScript 面向对象的面纱,让你看清其底层运作逻辑。我们将从最基础的概念出发,通过真实代码、图示、类比和深度剖析,构建完整的认知体系。
第一章:JavaScript 的面向对象哲学 ------ 原型式 vs 类式
1.1 传统"类式"面向对象(Class-based OOP)
以 Java、C++、Python 为例:
- 先定义一个 类(Class),作为模板。
- 类包含属性和方法。
- 通过
new Class()创建 实例(Instance)。 - 实例"继承"类的结构,但拥有自己的属性值。
- 类与实例之间存在"血缘关系"------实例是类的"后代"。
java
// Java 示例
class Car {
String color;
Car(String c) { this.color = c; }
void drive() { System.out.println("driving"); }
}
Car myCar = new Car("red");
这里,myCar 是 Car 类的实例,二者有明确的类型归属。
1.2 JavaScript 的"原型式"面向对象(Prototypal OOP)
JavaScript 没有真正的类 (ES6 的 class 只是语法糖)。它的核心思想是:
对象直接从其他对象继承行为,而不是从抽象的"类"模板。
这被称为 "基于原型的继承"(Prototypal Inheritance)。
- 没有"类",只有对象。
- 一个对象可以作为另一个对象的原型(Prototype)。
- 当访问一个对象的属性时,如果自身没有,就去它的原型找;原型没有,再去原型的原型找......直到找到或到达终点。
这种机制更灵活、动态,但也更抽象。
"JS的面向对象是'原型式'而非'类式',实例与原型间无血缘关系,而是基于共享与委托的机制。"
第二章:构造函数 ------ 创造实例的"工厂"
要理解原型,必须先理解构造函数(Constructor Function)。
2.1 什么是构造函数?
在 JavaScript 中,任何函数都可以作为构造函数使用 ,只要用 new 关键字调用。
javascript
// 定义一个普通函数 Person
// 但它将被用作构造函数
function Person(name, age) {
// 当使用 new Person() 时,
// JavaScript 引擎会创建一个新空对象 {}
// 并将 this 绑定到这个新对象上
this.name = name; // 给新对象添加 name 属性
this.age = age; // 给新对象添加 age 属性
// 函数执行完毕后,自动 return this(除非显式返回对象)
}
2.2 使用 new 创建实例
javascript
const person1 = new Person('张三', 18);
const person2 = new Person('李四', 19);
person1和person2是两个独立的对象。- 它们各自拥有
name和age属性,互不影响。 - 这些属性称为实例属性(Instance Properties)。
✅ 关键点:
构造函数内部的
this指向新创建的实例对象 。实例属性是每个对象私有的。
第三章:原型(Prototype)------ 共享行为的"公共仓库"
如果每个实例都要有自己的方法(如 sayHello),那会浪费大量内存。于是,JavaScript 引入了 prototype。
3.1 每个函数都有 prototype 属性
这是 JavaScript 引擎自动为函数添加的属性。
javascript
function Car(color) {
this.color = color;
}
console.log(Car.prototype); // { constructor: Car }
Car.prototype是一个对象。- 默认情况下,它只有一个属性:
constructor,指向Car本身。 - 这个对象就是未来所有
Car实例的原型。
3.2 在原型上定义共享属性和方法
继续看:
javascript
// es5 没有类class
// JS 函数是一等对象
function Car(color){
// this 指向新创建的对象
this.color = color;
// this.drive
// 车参数
// this.name = '小米su7';
// this.price = 300000;
}
Car.prototype = {
drive(){
console.log('drive');
},
name: '小米su7',
price: 300000,
}
逐行深度解析:
-
function Car(color){ ... }- 定义构造函数,接收
color参数。 - 每个实例将拥有自己的
color。
- 定义构造函数,接收
-
Car.prototype = { ... }- 重写整个原型对象!
- 原来的
{ constructor: Car }被替换了。 - 新原型包含:
drive()方法name属性(值为'小米su7')price属性(值为300000)
-
创建实例并使用:
javascript
const car1 = new Car('霞光紫');
console.log(car1, car1.name, car1.price); // 输出实例 + 共享属性
car1.drive(); // 调用原型上的方法
const car2 = new Car('海湾蓝');
console.log(car2, car2.name, car2.price);
car1.color→'霞光紫'(实例属性)car1.name→'小米su7'(来自Car.prototype)car1.drive()→ 调用Car.prototype.drive
✅ 核心优势:
name、price、drive只在内存中存储一份,所有实例共享,极大节省内存!
第四章:__proto__ ------ 实例通往原型的"秘密通道"
现在问题来了:实例是如何访问到原型上的属性的?
答案是:通过 __proto__。
4.1 什么是 __proto__?
- 每个对象 (包括数组、函数、普通对象)都有一个内部属性
[[Prototype]]。 - 在浏览器中,可通过非标准但广泛支持的
__proto__访问它。 obj.__proto__指向该对象的原型对象。
4.2 实例的 __proto__ 指向构造函数的 prototype
例如:
javascript
const person1 = new Person('张三', 18);
console.log(person1.__proto__, '////');
输出结果将是:
javascript
{ speci: '人类' } '////'
因为:
javascript
Person.prototype.speci = '人类'; // 在原型上添加属性
所以:
javascript
person1.__proto__ === Person.prototype; // true
✅ 这是 JavaScript 原型机制的基石关系!
4.3 图解:实例、构造函数、原型的关系
+------------------+
| Person (函数) |
+------------------+
| prototype |----+
+------------------+ |
↓
+------------------+ +------------------+
| person1 (对象) | | Person.prototype |
+------------------+ +------------------+
| name: '张三' | | speci: '人类' |
| age: 18 | | constructor: Person
+------------------+ +------------------+
| __proto__ |-----^
+------------------+
person1自身有name、age。- 当访问
person1.speci时:- 先查
person1自身 → 没有。 - 再查
person1.__proto__(即Person.prototype)→ 找到'人类'。
- 先查
第五章:原型链(Prototype Chain)------ 属性查找的"寻宝地图"
原型不仅可以一层,还可以多层嵌套,形成链式结构 ,这就是原型链。
5.1 原型链的形成过程
- 实例对象 →
__proto__→ 构造函数的prototype - 构造函数的
prototype本身也是对象 → 它也有__proto__ - 默认情况下,
Function.prototype或Object.prototype成为上一级 - 最终,所有链都指向
Object.prototype Object.prototype.__proto__ === null→ 链终止
5.2 完整原型链示例
以 car1 为例:
javascript
const car1 = new Car('霞光紫');
其原型链如下:
car1
│
└─ __proto__ → Car.prototype
│
├─ drive()
├─ name: '小米su7'
├─ price: 300000
│
└─ __proto__ → Object.prototype
│
├─ toString()
├─ hasOwnProperty()
├─ isPrototypeOf()
│
└─ __proto__ → null
5.3 属性查找规则(覆盖机制)
当你访问 obj.prop 时,JavaScript 引擎执行以下步骤:
- 检查
obj自身是否有prop→ 有则返回。 - 没有?检查
obj.__proto__(即原型)是否有prop→ 有则返回。 - 还没有?继续沿
__proto__向上查找。 - 直到
Object.prototype。 - 若仍未找到,返回
undefined。
"实例上有就用自己的,没有就去原型上找。"
5.4 覆盖 vs 修改
javascript
person1.speci = '外星人';
console.log(person1.speci); // '外星人'(实例属性)
console.log(person2.speci); // '人类'(原型属性)
console.log(Person.prototype.speci); // '人类'
person1.speci = '外星人'没有修改原型!- 而是在
person1上新建了一个同名属性 ,覆盖了原型上的值。 - 这体现了"实例优先"原则。
✅ 修改共享属性的正确方式:
javascriptPerson.prototype.speci = '智人'; // 所有未覆盖的实例都会看到新值
第六章:Object.getPrototypeOf() ------ 安全获取原型的标准方法
虽然 __proto__ 可用,但它是非标准属性,且可能被滥用。
ES5 引入了标准 API:
javascript
Object.getPrototypeOf(obj)
它等价于 obj.__proto__,但更安全、规范。
javascript
console.log(person1.__proto__, '////');
// 或写成:
console.log(Object.getPrototypeOf(person1), '////');
推荐使用 Object.getPrototypeOf(obj) 来安全获取对象的原型,优于直接访问 proto。大厂面试常考察此 API。
第七章:万物皆对象?连函数也是!
JavaScript 中,函数也是对象!
javascript
function fn() {}
console.log(fn instanceof Object); // true
因此,函数也有 __proto__:
javascript
function Car() {}
console.log(Car.__proto__ === Function.prototype); // true
而 Function.prototype 本身也是函数,它的 __proto__ 指向 Object.prototype:
javascript
console.log(Function.prototype.__proto__ === Object.prototype); // true
这解释了为什么函数也能调用 toString()、hasOwnProperty() 等方法。
第八章:对象字面量也有原型!
即使不用构造函数,直接创建对象:
javascript
const obj = {};
它依然有原型:
javascript
console.log(obj.__proto__ === Object.prototype); // true
对象字面量也具有 proto 属性,说明即使非构造函数创建的对象依然遵循原型机制。
所以,{}、[]、/regex/ 等字面量,都自动链接到对应的内置原型。
第九章:constructor 属性 ------ 原型的"回指针"
每个原型对象都有一个 constructor 属性,指向其构造函数。
javascript
Person.prototype.constructor === Person; // true
Car.prototype.constructor === Car; // 在 1.js 中为 false!
为什么 Car 不成立?
因为在 1.js 中,我们重写了整个 prototype 对象:
javascript
Car.prototype = {
drive() { ... },
name: '小米su7',
price: 300000,
};
// 此时 Car.prototype.constructor === Object!
修复方法:
javascript
Car.prototype = {
constructor: Car, // 手动指定
drive() { ... },
name: '小米su7',
price: 300000,
};
或者使用 Object.defineProperty 设置不可枚举:
javascript
Object.defineProperty(Car.prototype, 'constructor', {
value: Car,
writable: true,
configurable: true,
enumerable: false // 默认不遍历
});
第十章:原型链的终点 ------ Object.prototype 与 null
所有原型链最终都指向 Object.prototype,而它的 __proto__ 是 null。
javascript
console.log(Object.prototype.__proto__); // null
null 表示"没有原型",是查找的终止符。
null表示没有原型对象,null在此处的作用是终止查找流程,防止内存溢出问题。
如果没有 null,引擎会无限循环查找,导致崩溃。
第十一章:高级应用 ------ 实现继承
利用原型链,我们可以实现"类继承"。
11.1 ES5 继承(组合寄生式)
javascript
function Vehicle(type) {
this.type = type;
}
Vehicle.prototype.move = function() {
console.log('moving');
};
function Car(color) {
Vehicle.call(this, 'car'); // 借用构造函数
this.color = color;
}
// 设置原型链
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.drive = function() {
console.log('driving');
};
const myCar = new Car('red');
myCar.move(); // 'moving'(来自 Vehicle.prototype)
myCar.drive(); // 'driving'
这里,Car.prototype.__proto__ === Vehicle.prototype,形成两层原型链。
11.2 ES6 class 的本质
javascript
class Parent {
say() { console.log('parent'); }
}
class Child extends Parent {
speak() { console.log('child'); }
}
等价于:
javascript
function Parent() {}
Parent.prototype.say = function() { ... };
function Child() {
Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.speak = function() { ... };
所以,
class只是语法糖,底层仍是原型链!
第十二章:常见误区与面试题
❌ 误区1:prototype 是所有对象都有的属性
错! 只有函数 才有
prototype。普通对象有
__proto__,但没有prototype。
const obj = {};
console.log(obj.prototype); // undefined
console.log(obj.__proto__); // Object.prototype
❌ 误区2:修改实例属性会影响原型
错! 实例属性覆盖原型属性,但不会修改原型。
✅ 面试题:如何判断一个属性是实例自有还是继承的?
javascript
obj.hasOwnProperty('prop'); // true 表示自有属性
✅ 面试题:instanceof 的原理?
javascript
obj instanceof Constructor
// 等价于:检查 Constructor.prototype 是否在 obj 的原型链上
第十三章:总结 ------ 原型机制全景图
| 概念 | 说明 | 所属 |
|---|---|---|
prototype |
函数的属性,指向原型对象 | 函数 |
__proto__ |
对象的属性,指向其原型 | 所有对象 |
constructor |
原型对象的属性,指回构造函数 | 原型对象 |
| 原型链 | obj → __proto__ → prototype → __proto__ → ... → Object.prototype → null |
查找路径 |
| 共享机制 | 方法和公共属性放在 prototype 上,节省内存 |
设计哲学 |
| 覆盖机制 | 实例属性优先于原型属性 | 查找规则 |
原型和原型链
-
prototype是函数的一个属性,指向一个对象
示例:javascriptfunction Person() { this.name = '张三'; } console.log(Person.prototype); // 表示Person这个函数的原型对象prototype是{name: '张三'} -
__proto__是实例对象的一个属性,指向它的原型对象
示例:javascriptconst person = new Person(); console.log(person.__proto__); // 即person这个实例对象的__proto__是{name: '张三'} -
constructor是实例对象的一个属性,指向它的构造函数
示例:javascriptconsole.log(person.__proto__.constructor); // 即{name: '张三'}.constructor 是 Person这个函数
结语:掌握原型,掌握 JavaScript 的灵魂
JavaScript 的原型机制,是其区别于其他语言的最大特色。它看似复杂,实则逻辑严密、设计精巧。
通过本文,你已经:
- 理解了构造函数与实例的关系;
- 掌握了
prototype与__proto__的区别与联系; - 学会了原型链的查找规则;
- 能解释
1.js、2.js中每一行代码的含义; - 知道了如何安全操作原型;
- 能应对相关面试题。
记住:
不是"类"创造了对象,而是"对象"链接了对象。
这就是 JavaScript 的原型之美。
愿你在编程之路上,越走越远,越走越深!🚀
附:关键代码汇总(保留原始格式,逐行注解)
1.js 完整注解
javascript
// es5 没有类class
// JS 函数是一等对象
function Car(color){
// this 指向新创建的对象(当使用 new 调用时)
this.color = color; // 实例属性,每个 car 有自己的颜色
// this.drive // 此时 this 上还没有 drive 方法
// 车参数
// this.name = '小米su7'; // 如果放这里,每个实例都会复制一份,浪费内存
// this.price = 300000;
}
// 重写 Car 的原型对象,用于共享属性和方法
Car.prototype = {
// drive 是一个方法,所有实例共享
drive(){
console.log('drive');
},
// 共享属性:所有 Car 实例的 name 都是 '小米su7'
name: '小米su7',
// 共享属性:价格
price: 300000,
// 注意:此处丢失了 constructor 属性!
}
// 创建两个实例
const car1 = new Car('霞光紫');
// car1 自身有 color: '霞光紫'
// car1.name 来自 Car.prototype
console.log(car1, car1.name, car1.price);
// 调用原型上的方法
car1.drive();
const car2 = new Car('海湾蓝');
console.log(car2, car2.name, car2.price);
2.js 完整注解
javascript
// 定义 Person 构造函数
function Person(name, age) {
// 实例属性
this.name = name;
this.age = age;
}
// 在 Person.prototype 上添加共享属性 speci(应为 species,但保留原拼写)
Person.prototype.speci = '人类';
// 创建实例
const person1 = new Person('张三', 18);
// person1 自身有 name、age;speci 来自原型
console.log(person1.name, person1.speci); // 张三 人类
const person2 = new Person('李四', 19);
console.log(person2.name, person2.speci); // 李四 人类
// 打印 person1 的原型(即 Person.prototype)
console.log(person1.__proto__, '////');