当被问到"解释一下 JavaScript 的原型链"时,你是否能背出标准答案:对象通过 __proto__ 连接到原型对象,形成原型链。
但你有没有想过:为什么 JavaScript 要设计得这么复杂?为什么有 prototype 又有 __proto__?直到深入研究了原型的设计思想,才开始理解这套看似混乱的机制背后的优雅逻辑。这篇文章是我的学习总结,希望能和你一起探索 JavaScript 对象系统的本质。
从一个困惑说起
先看一段让我困惑很久的代码:
// 环境: 浏览器 / Node.js 18+
// 场景: 理解 __proto__ 和 prototype 的关系
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(alice.constructor === Person); // true
这三个等式让我产生了很多疑问:
- 为什么
alice的__proto__等于Person.prototype? - 为什么需要两个看起来很像的属性?
constructor是从哪来的?
要理解这些问题,需要从 JavaScript 的设计初衷说起。
原型的本质:JavaScript 的对象系统
JavaScript 为什么选择原型而不是类?
JavaScript 诞生于 1995 年,Brendan Eich 在设计时面临一个选择:采用基于类的继承(如 Java)还是基于原型的继承?
他选择了原型,主要基于以下考虑:
1. 简单灵活
基于原型的系统更简单、更灵活。对象可以直接从其他对象继承,不需要预先定义类的层次结构。
2. 动态性
JavaScript 需要是一门动态语言。原型系统允许在运行时修改对象的行为,这在基于类的系统中很难做到。
3. 轻量级
作为浏览器脚本语言,JavaScript 需要轻量。原型系统的实现比类系统更简单。
原型解决了什么问题?
最核心的问题是:如何在多个对象之间共享属性和方法?
// 环境: 浏览器 / Node.js 18+
// 场景: 不使用原型的问题
// ❌ 方案 1: 每个对象都有自己的方法副本
function createPerson(name) {
return {
name: name,
sayHi: function() {
console.log(`Hi, I'm ${this.name}`);
}
};
}
const person1 = createPerson('Alice');
const person2 = createPerson('Bob');
console.log(person1.sayHi === person2.sayHi); // false
// 每个对象都有一个 sayHi 方法的副本,浪费内存
使用原型可以解决这个问题:
// 环境: 浏览器 / Node.js 18+
// 场景: 使用原型共享方法
function Person(name) {
this.name = name;
}
// 方法定义在原型上,所有实例共享
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const person1 = new Person('Alice');
const person2 = new Person('Bob');
console.log(person1.sayHi === person2.sayHi); // true
// 所有实例共享同一个方法,节省内存
对象、构造函数、原型的三角关系
理解原型的关键是理解这三者的关系:
graph LR
Instance[实例对象 alice] -->|__proto__| Prototype[Person.prototype]
Constructor[构造函数 Person] -->|prototype| Prototype
Prototype -->|constructor| Constructor
Instance -.->|constructor 继承自原型| Constructor
用代码表达:
// 环境: 浏览器 / Node.js 18+
// 场景: 三角关系的验证
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
// 1. 实例的 __proto__ 指向构造函数的 prototype
console.log(alice.__proto__ === Person.prototype); // true
// 2. 原型的 constructor 指向构造函数
console.log(Person.prototype.constructor === Person); // true
// 3. 实例可以通过原型链访问到 constructor
console.log(alice.constructor === Person); // true
console.log(alice.hasOwnProperty('constructor')); // false
// alice 本身没有 constructor,是从原型继承来的
关键理解:
prototype是构造函数的属性,指向原型对象__proto__是实例对象的属性,指向构造函数的原型constructor是原型对象的属性,指向构造函数
原型链:属性查找的秘密
原型链的完整查找过程
当访问对象的属性时,JavaScript 会沿着原型链向上查找:
// 环境: 浏览器 / Node.js 18+
// 场景: 原型链查找过程
function Person(name) {
this.name = name;
}
Person.prototype.species = 'Human';
const alice = new Person('Alice');
// 查找过程:
// 1. 先在 alice 自身查找 name -> 找到了
console.log(alice.name); // 'Alice'
// 2. 在 alice 自身查找 species -> 没找到
// 3. 沿着 __proto__ 到 Person.prototype 查找 -> 找到了
console.log(alice.species); // 'Human'
// 4. 在 alice 自身查找 toString -> 没找到
// 5. 在 Person.prototype 查找 toString -> 没找到
// 6. 在 Object.prototype 查找 toString -> 找到了
console.log(alice.toString()); // '[object Object]'
原型链的可视化
graph TB
alice[alice 实例] -->|__proto__| PersonProto[Person.prototype]
PersonProto -->|__proto__| ObjectProto[Object.prototype]
ObjectProto -->|__proto__| null[null]
Person[Person 构造函数] -->|prototype| PersonProto
Object[Object 构造函数] -->|prototype| ObjectProto
style null fill:#f9f,stroke:#333,stroke-width:2px
原型链的尽头
所有原型链的尽头都是 null:
// 环境: 浏览器 / Node.js 18+
// 场景: 探索原型链的尽头
function Person(name) {
this.name = name;
}
const alice = new Person('Alice');
console.log(alice.__proto__); // Person.prototype
console.log(alice.__proto__.__proto__); // Object.prototype
console.log(alice.__proto__.__proto__.__proto__); // null
// 验证原型链的尽头
console.log(Object.prototype.__proto__ === null); // true
为什么原型链要以 null 结尾?
这是一个有意的设计:
- 明确的终点:
null表示"这里什么都没有",是查找的明确终点 - 避免循环引用: 如果原型链是循环的,属性查找会陷入死循环
- 哲学意义 🐶:
null代表"虚无",所有对象最终都源于"虚无"
构造函数与 new 的魔法
new 操作符到底做了什么?
new 操作符看起来很神奇,但它的行为可以分解为几个步骤:
// 环境: 浏览器 / Node.js 18+
// 场景: new 操作符的行为
function Person(name) {
this.name = name;
this.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
}
const alice = new Person('Alice');
// new Person('Alice') 做了什么?
// 1. 创建一个新对象
// 2. 将新对象的 __proto__ 指向 Person.prototype
// 3. 将 Person 函数的 this 绑定到新对象
// 4. 执行 Person 函数
// 5. 如果 Person 返回对象,则返回该对象;否则返回新对象
手写 new 的实现
理解原理最好的方式是手写实现:
// 环境: 浏览器 / Node.js 18+
// 场景: 手写 new 操作符
function myNew(Constructor, ...args) {
// 1. 创建一个新对象,原型指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,this 绑定到新对象
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回对象,则返回该对象;否则返回新对象
return result instanceof Object ? result : obj;
}
// 测试
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const alice = myNew(Person, 'Alice');
console.log(alice.name); // 'Alice'
alice.sayHi(); // 'Hi, I'm Alice'
console.log(alice instanceof Person); // true
constructor 属性的作用与陷阱
constructor 属性指向创建该实例的构造函数,但它很容易丢失:
// 环境: 浏览器 / Node.js 18+
// 场景: constructor 丢失的问题
function Animal(name) {
this.name = name;
}
// ❌ 直接赋值会丢失 constructor
Animal.prototype = {
eat: function() {
console.log(`${this.name} is eating`);
}
};
const cat = new Animal('Cat');
console.log(cat.constructor === Animal); // false!
console.log(cat.constructor === Object); // true!
// ✅ 修复方法 1: 手动添加 constructor
Animal.prototype = {
constructor: Animal, // 手动指定
eat: function() {
console.log(`${this.name} is eating`);
}
};
// ✅ 修复方法 2: 使用 Object.defineProperty 让 constructor 不可枚举
Object.defineProperty(Animal.prototype, 'constructor', {
value: Animal,
writable: true,
enumerable: false, // 不可枚举,更接近原生行为
configurable: true
});
继承:从原型到 ES6 Class
原型链继承
最简单的继承方式是让子类的原型指向父类的实例:
// 环境: 浏览器 / Node.js 18+
// 场景: 原型链继承
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name) {
this.name = name;
}
// 原型链继承
Dog.prototype = new Animal();
const dog1 = new Dog('Dog1');
const dog2 = new Dog('Dog2');
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white', 'brown'] ❌ 共享了引用类型
问题:
- 引用类型的属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
构造函数继承
通过在子类中调用父类构造函数来继承属性:
// 环境: 浏览器 / Node.js 18+
// 场景: 构造函数继承
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, age) {
Animal.call(this, name); // 借用构造函数
this.age = age;
}
const dog1 = new Dog('Dog1', 3);
const dog2 = new Dog('Dog2', 5);
dog1.colors.push('brown');
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'] ✅ 互不影响
// ❌ 但是无法继承父类原型上的方法
dog1.eat(); // TypeError: dog1.eat is not a function
问题:
- 只能继承父类的实例属性,无法继承原型方法
- 每个实例都会执行一次父类构造函数
组合继承
结合原型链继承和构造函数继承:
// 环境: 浏览器 / Node.js 18+
// 场景: 组合继承
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, age) {
Animal.call(this, name); // 第二次调用 Animal
this.age = age;
}
Dog.prototype = new Animal(); // 第一次调用 Animal
Dog.prototype.constructor = Dog;
const dog1 = new Dog('Dog1', 3);
dog1.colors.push('brown');
const dog2 = new Dog('Dog2', 5);
console.log(dog1.colors); // ['black', 'white', 'brown']
console.log(dog2.colors); // ['black', 'white'] ✅
dog1.eat(); // 'Dog1 is eating' ✅
问题:
- 父类构造函数被调用了两次,浪费性能
寄生组合式继承(最优方案)
使用 Object.create() 避免多次调用父类构造函数:
// 环境: 浏览器 / Node.js 18+
// 场景: 寄生组合式继承(推荐)
function Animal(name) {
this.name = name;
this.colors = ['black', 'white'];
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, age) {
Animal.call(this, name); // 只调用一次 Animal
this.age = age;
}
// 关键: 使用 Object.create 创建父类原型的副本
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log(`${this.name} is barking`);
};
const dog = new Dog('Buddy', 3);
dog.eat(); // 'Buddy is eating'
dog.bark(); // 'Buddy is barking'
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true
为什么这是最优方案?
- 只调用一次父类构造函数
- 原型链保持完整
instanceof正常工作
ES6 Class 的本质
ES6 的 class 看起来像是全新的语法,但本质上还是基于原型:
// 环境: 浏览器 / Node.js 18+ (支持 ES6)
// 场景: ES6 Class 语法
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Dog extends Animal {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
bark() {
console.log(`${this.name} is barking`);
}
}
const dog = new Dog('Buddy', 3);
dog.eat(); // 'Buddy is eating'
dog.bark(); // 'Buddy is barking'
// 本质上还是原型继承
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(typeof Dog); // 'function'
ES6 Class 和传统原型的区别:
- 语法更清晰: class 语法更接近其他面向对象语言
- 严格模式: class 内部自动使用严格模式
- 不可枚举: class 中的方法不可枚举
- 必须用 new: class 不能像函数一样直接调用
- 没有提升: class 声明不会提升
// 环境: 浏览器 / Node.js 18+
// 场景: Class 的特殊行为
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
}
// ❌ class 不能直接调用
Person('Alice'); // TypeError: Class constructor Person cannot be invoked without 'new'
// ✅ 方法不可枚举
const p = new Person('Alice');
console.log(Object.keys(p)); // ['name']
console.log(Object.keys(Person.prototype)); // [] 方法不可枚举
实践应用
如何安全地检测属性
在实际开发中,我们经常需要判断属性来源:
// 环境: 浏览器 / Node.js 18+
// 场景: 属性检测的最佳实践
function Person(name) {
this.name = name;
}
Person.prototype.species = 'Human';
const alice = new Person('Alice');
// 1. hasOwnProperty: 检查自身属性(不包括原型)
console.log(alice.hasOwnProperty('name')); // true
console.log(alice.hasOwnProperty('species')); // false
// 2. in 操作符: 检查自身和原型链上的属性
console.log('name' in alice); // true
console.log('species' in alice); // true
console.log('toString' in alice); // true
// 3. 安全的 hasOwnProperty 调用(避免对象覆盖)
const obj = Object.create(null);
obj.hasOwnProperty = 'hacked';
// ❌ 直接调用会出错
// obj.hasOwnProperty('name'); // TypeError
// ✅ 安全的调用方式
console.log(Object.prototype.hasOwnProperty.call(obj, 'name'));
// ✅ ES2022 新方法
console.log(Object.hasOwn(obj, 'name'));
常见陷阱与最佳实践
陷阱 1: 修改内置对象原型
// 环境: 浏览器 / Node.js 18+
// 场景: 修改内置原型的危险
// ❌ 不要这样做!
Array.prototype.first = function() {
return this[0];
};
const arr = [1, 2, 3];
console.log(arr.first()); // 1
// 问题:
// 1. 污染全局命名空间
// 2. 可能与未来的标准冲突
// 3. 影响所有数组,包括第三方库的
// ✅ 替代方案: 使用工具函数
function first(arr) {
return arr[0];
}
console.log(first(arr)); // 1
陷阱 2: 原型链污染攻击
原型链污染是一个严重的安全问题:
// 环境: 浏览器 / Node.js 18+
// 场景: 原型链污染示例
// 危险的 merge 函数
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 恶意输入
const maliciousInput = JSON.parse('{"__proto__": {"isAdmin": true}}');
const user = {};
merge(user, maliciousInput);
// 所有对象都被污染了!
const newUser = {};
console.log(newUser.isAdmin); // true ❌
// ✅ 防御方案 1: 检查键名
function safeMerge(target, source) {
for (let key in source) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue; // 跳过危险键
}
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = safeMerge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// ✅ 防御方案 2: 使用 Object.create(null)
const safeObj = Object.create(null);
safeObj.__proto__ = 'safe'; // 只是普通属性
console.log(safeObj.__proto__); // 'safe'
// ✅ 防御方案 3: 使用 Map
const safeMap = new Map();
safeMap.set('__proto__', 'safe');
console.log(safeMap.get('__proto__')); // 'safe'
陷阱 3: instanceof 的局限性
// 环境: 浏览器 / Node.js 18+
// 场景: instanceof 的问题
function Person() {}
const alice = new Person();
console.log(alice instanceof Person); // true
// 问题 1: 可以被欺骗
Person.prototype = {};
console.log(alice instanceof Person); // false!
// 问题 2: 跨 iframe 失效
// const iframe = document.createElement('iframe');
// document.body.appendChild(iframe);
// const iframeArray = iframe.contentWindow.Array;
// const arr = new iframeArray();
// console.log(arr instanceof Array); // false!
// ✅ 替代方案 1: Object.prototype.toString
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call({})); // '[object Object]'
// ✅ 替代方案 2: Array.isArray (针对数组)
console.log(Array.isArray([])); // true
// ✅ 替代方案 3: constructor
console.log(alice.constructor === Person); // true (但也可能被修改)
最佳实践总结
// 环境: 浏览器 / Node.js 18+
// 场景: 原型使用的最佳实践
// ✅ 1. 使用 Object.create 实现继承
function Parent() {}
function Child() {}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// ✅ 2. 检查属性时使用 Object.hasOwn (ES2022)
const obj = { name: 'Alice' };
console.log(Object.hasOwn(obj, 'name')); // true
// ✅ 3. 创建纯数据对象时使用 Object.create(null)
const data = Object.create(null);
// ✅ 4. 不要直接修改内置对象原型
// ❌ Array.prototype.myMethod = ...
// ✅ function myMethod(arr) { ... }
// ✅ 5. 使用 class 语法(如果支持)
class MyClass {
constructor() {}
}
// ✅ 6. 防御原型污染
function safeAssign(target, source) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
if (!dangerousKeys.includes(key)) {
target[key] = source[key];
}
}
return target;
}
深入理解相关 API
instanceof 的实现原理
// 环境: 浏览器 / Node.js 18+
// 场景: 手写 instanceof
function myInstanceof(obj, Constructor) {
// 基本类型直接返回 false
if (obj === null || typeof obj !== 'object') {
return false;
}
// 获取构造函数的原型
let proto = Constructor.prototype;
// 获取对象的原型
let objProto = Object.getPrototypeOf(obj);
// 沿着原型链查找
while (objProto !== null) {
if (objProto === proto) {
return true;
}
objProto = Object.getPrototypeOf(objProto);
}
return false;
}
// 测试
function Person() {}
const alice = new Person();
console.log(myInstanceof(alice, Person)); // true
console.log(myInstanceof(alice, Object)); // true
console.log(myInstanceof(alice, Array)); // false
Object.setPrototypeOf vs proto
// 环境: 浏览器 / Node.js 18+
// 场景: 设置原型的不同方式
const parent = { greet: () => console.log('Hello') };
const child = { name: 'Alice' };
// 方式 1: __proto__ (不推荐,性能差)
child.__proto__ = parent;
// 方式 2: Object.setPrototypeOf (不推荐,性能差)
Object.setPrototypeOf(child, parent);
// 方式 3: Object.create (推荐,创建时指定)
const betterChild = Object.create(parent, {
name: { value: 'Bob', writable: true, enumerable: true, configurable: true }
});
// 为什么不推荐运行时修改原型?
// 1. 性能问题: 引擎无法优化
// 2. 影响所有访问该对象的代码
// 3. 可能破坏现有的继承链
// ✅ 最佳实践: 在创建对象时就确定原型
设计思想与演进
基于原型 vs 基于类
JavaScript 的原型系统和传统的类系统有本质区别:
基于类(如 Java):
// 类是模板,对象是实例
class Animal {
void eat() { }
}
class Dog extends Animal {
void bark() { }
}
// 类和实例是两种不同的东西
Dog dog = new Dog();
基于原型(JavaScript):
// 对象直接从其他对象继承
const animal = {
eat: function() { }
};
const dog = Object.create(animal);
dog.bark = function() { };
// 一切都是对象
核心区别:
- 灵活性: 原型系统更灵活,可以在运行时修改继承关系
- 简洁性: 不需要定义类,对象可以直接继承对象
- 动态性: 原型可以随时添加属性和方法
JavaScript 的设计哲学
Brendan Eich 在设计 JavaScript 时有几个核心理念:
- 简单性: 让非程序员也能使用
- 灵活性: 支持多种编程范式
- 动态性: 运行时可以修改对象
原型系统完美体现了这些理念:简单(不需要理解类的概念)、灵活(多种继承方式)、动态(运行时修改)。
从原型到 Class: 语法的进化
ES6 引入 class 关键字,但这只是"语法糖":
// 环境: 浏览器 / Node.js 18+
// 场景: class 只是语法糖
// ES6 Class
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
}
// 等价的 ES5 代码
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
// 本质上没有改变,只是语法更清晰
延伸思考
在 AI 辅助编程时代,理解原型还重要吗?
这个问题我在前面两篇文章中也思考过。对于原型,我的看法是:
理解原型让你更好地使用现代工具:
- 调试能力: 当出现
prototype相关的错误时,你能快速定位问题 - 性能优化: 知道什么时候该用实例属性,什么时候该用原型方法
- 框架理解: 很多框架(如 Vue 2)的响应式系统就是基于原型实现的
一个实际例子:
假设 AI 给你生成了这样的代码:
// AI 生成的代码
class TodoList {
constructor() {
this.items = [];
this.render = function() { // ❌ 每个实例都有一个 render 副本
console.log(this.items);
};
}
}
如果你理解原型,就会知道应该优化成:
// 优化后的代码
class TodoList {
constructor() {
this.items = [];
}
render() { // ✅ 所有实例共享同一个方法
console.log(this.items);
}
}
与 TypeScript 的关系
TypeScript 的类型系统和 JavaScript 的原型系统如何结合?
// TypeScript 中的类
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
eat(): void {
console.log(`${this.name} is eating`);
}
}
// 编译后仍然是基于原型的 JavaScript
// TypeScript 只是在编译时提供类型检查
TypeScript 为 JavaScript 添加了静态类型,但并没有改变底层的原型机制。
现代框架如何使用原型
React:
// React 类组件基于原型
class MyComponent extends React.Component {
// 方法定义在原型上
render() { }
}
现代趋势:
随着函数式编程的流行,框架越来越少直接操作原型,转而使用:
- 函数组件
- Hooks
- 组合式 API
但理解原型仍然有助于理解这些框架的底层实现。
小结
原型与原型链是 JavaScript 对象系统的核心,理解它们不仅仅是为了应付面试,更是为了:
- 理解 JavaScript 的本质: 知道对象是如何工作的
- 写出更好的代码: 知道何时用实例属性、何时用原型方法
- 调试更有效: 快速定位原型相关的问题
- 理解现代特性: class、私有字段等新特性都建立在原型之上
这篇文章是我的学习总结,而非权威教程。原型系统博大精深,我的理解可能也有偏差。如果你有不同的看法或补充,欢迎交流讨论。
参考资料
- MDN - Inheritance and the prototype chain - 原型链权威文档
- MDN - Object prototypes - 原型对象详解
- You Don't Know JS: this & Object Prototypes - Kyle Simpson 关于原型的深度讲解
- JavaScript: The Definitive Guide - David Flanagan 的经典著作
- Eloquent JavaScript - Objects and Prototypes - 优雅的原型讲解