JavaScript 原型与原型链:从困惑到完全理解
以前在看 JavaScript 代码的时候,经常会遇到一个问题:
const arr = [1, 2, 3];
arr.push(4); // 4
arr.join(','); // "1,2,3,4"
arr.toString(); // "1,2,3,4"
我明明只创建了一个数组,为什么它能调用 push、join、toString 这些方法?这些方法是从哪来的?
再看这段代码:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const person = new Person('张三');
person.sayHello(); // "Hello, I'm 张三"
person 对象本身没有 sayHello 方法,但却能调用它。这背后的机制就是原型链。
先搞清楚几个概念
在深入之前,先把几个容易混淆的概念理清楚:
[[Prototype]]、__proto__、prototype 的区别
| 概念 |
是什么 |
属于谁 |
作用 |
[[Prototype]] |
内部属性 |
所有对象 |
指向对象的原型,隐藏属性 |
__proto__ |
访问器属性 |
所有对象 |
暴露 [[Prototype]],非标准但广泛支持 |
prototype |
普通属性 |
函数 |
存放给实例共享的属性和方法 |
简单说:
-
prototype 是函数才有的属性,用来存放共享方法
-
__proto__ 是所有对象都有的属性,指向它的原型对象
-
[[Prototype]] 是 __proto__ 的内部实现
function Foo() {}
const foo = new Foo();
// prototype 只有函数才有
console.log(Foo.prototype); // {constructor: ƒ}
console.log(foo.prototype); // undefined
// __proto__ 所有对象都有
console.log(foo.__proto__ === Foo.prototype); // true
现代写法:Object.getPrototypeOf()
__proto__ 虽然好用,但它不是 ECMAScript 标准的一部分,只是各浏览器都实现了。推荐用标准方法:
// 获取原型
Object.getPrototypeOf(foo) === Foo.prototype // true
// 设置原型
Object.setPrototypeOf(obj, prototype)
// 创建时指定原型
Object.create(prototype)
原型是什么
JavaScript 里每个函数都有一个 prototype 属性,指向一个对象。这个对象叫做原型对象,它的作用是让该函数创建的所有实例共享属性和方法。
function Car(brand) {
this.brand = brand;
}
// 方法定义在原型上,所有实例共享
Car.prototype.start = function() {
console.log(`${this.brand} 启动了`);
};
const car1 = new Car('丰田');
const car2 = new Car('本田');
car1.start(); // 丰田 启动了
car2.start(); // 本田 启动了
// 两个实例用的是同一个方法
console.log(car1.start === car2.start); // true
这就是原型的核心价值:方法只需要定义一次,所有实例都能用。
如果把方法定义在构造函数里,每创建一个实例就会新建一个函数,浪费内存:
// 不推荐的写法
function BadCar(brand) {
this.brand = brand;
this.start = function() { // 每个实例都有一份
console.log(`${this.brand} 启动了`);
};
}
const bad1 = new BadCar('丰田');
const bad2 = new BadCar('本田');
console.log(bad1.start === bad2.start); // false,两个不同的函数
new 关键字到底做了什么
理解原型链之前,得先搞清楚 new 的工作原理。当你写 new Foo() 时,JavaScript 引擎会执行以下四个步骤:
flowchart LR
A['1. 创建空对象']:::step --> B['2. 设置原型链']:::step
B --> C['3. 执行构造函数']:::step
C --> D['4. 返回对象']:::success
classDef step fill:#cce5ff,stroke:#0d6efd,color:#004085
classDef success fill:#d4edda,stroke:#28a745,color:#155724
详细步骤
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
const john = new Person('John');
Step 1:创建一个空对象
// 内部创建:{}
Step 2:将空对象的 [[Prototype]] 指向构造函数的 prototype
// 内部操作:newObj.__proto__ = Person.prototype
Step 3:用这个空对象作为 this 执行构造函数
// 内部操作:Person.call(newObj, 'John')
// 执行后 newObj 变成 { name: 'John' }
Step 4:返回对象
- 如果构造函数返回一个对象,就用那个对象
- 否则返回 Step 1 创建的对象
手写一个 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;
}
// 测试
const p = myNew(Person, 'Alice');
p.greet(); // Hi, I'm Alice
console.log(p instanceof Person); // true
原型链的查找机制
当访问对象的属性或方法时,JavaScript 会按照这个顺序查找:
- 先在对象自身找
- 找不到,去对象的原型 (
__proto__) 上找
- 还找不到,继续往上一级原型找
- 直到
Object.prototype,再往上就是 null 了
这条查找链路就是原型链。
flowchart TB
A["dog 实例<br/>{ name: 'Buddy' }"]:::instance -->|__proto__| B["Dog.prototype<br/>{ bark: ƒ }"]:::proto
B -->|__proto__| C["Animal.prototype<br/>{ speak: ƒ }"]:::proto
C -->|__proto__| D["Object.prototype<br/>{ toString: ƒ, ... }"]:::rootProto
D -->|__proto__| E["null"]:::endNode
classDef instance fill:#cce5ff,stroke:#0d6efd,color:#004085
classDef proto fill:#d4edda,stroke:#28a745,color:#155724
classDef rootProto fill:#fff3cd,stroke:#ffc107,color:#856404
classDef endNode fill:#f8d7da,stroke:#dc3545,color:#721c24
代码示例
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
function Dog(name) {
Animal.call(this, name);
}
// 建立原型链:Dog.prototype -> Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('Woof!');
};
const dog = new Dog('Buddy');
// 查找过程:
dog.name; // 在 dog 自身找到
dog.bark(); // 在 Dog.prototype 找到
dog.speak(); // 在 Animal.prototype 找到
dog.toString(); // 在 Object.prototype 找到
用代码验证这条链:
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
这就解释了开头的问题。数组能调用 push、join,是因为这些方法定义在 Array.prototype 上。能调用 toString,是因为顺着原型链能找到 Object.prototype.toString(虽然 Array 重写了这个方法)。
完整的原型链图谱
JavaScript 的原型链比想象中更复杂,函数本身也是对象,也有自己的原型链:
flowchart TB
subgraph IL[实例层]
foo["foo 实例"]:::instance
end
subgraph PL[原型层]
FooP["Foo.prototype"]:::proto
ObjP["Object.prototype"]:::rootProto
end
subgraph FL[函数层]
Foo["Foo 函数"]:::func
Obj["Object 函数"]:::func
Func["Function 函数"]:::func
end
subgraph FPL[函数原型层]
FuncP["Function.prototype"]:::funcProto
end
foo -->|__proto__| FooP
FooP -->|__proto__| ObjP
ObjP -->|__proto__| NULL["null"]:::endNode
Foo -->|prototype| FooP
Foo -->|__proto__| FuncP
Obj -->|prototype| ObjP
Obj -->|__proto__| FuncP
Func -->|prototype| FuncP
Func -->|__proto__| FuncP
FuncP -->|__proto__| ObjP
classDef instance fill:#cce5ff,stroke:#0d6efd,color:#004085
classDef proto fill:#d4edda,stroke:#28a745,color:#155724
classDef rootProto fill:#fff3cd,stroke:#ffc107,color:#856404
classDef func fill:#e2d9f3,stroke:#6f42c1,color:#432874
classDef funcProto fill:#fce4ec,stroke:#e91e63,color:#880e4f
classDef endNode fill:#f8d7da,stroke:#dc3545,color:#721c24
style IL fill:#e8f4fc,stroke:#0d6efd
style PL fill:#e8f5e9,stroke:#28a745
style FL fill:#f3e5f5,stroke:#6f42c1
style FPL fill:#fce4ec,stroke:#e91e63
几个关键点
1. 所有函数都是 Function 的实例
console.log(Foo.__proto__ === Function.prototype); // true
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true(自己创建自己)
2. Function.prototype 也是对象,它的原型是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true
3. Object.prototype 是原型链的终点
console.log(Object.prototype.__proto__ === null); // true
4. 一个有趣的循环
// Object 是函数,所以它的 __proto__ 是 Function.prototype
console.log(Object.__proto__ === Function.prototype); // true
// Function.prototype 是对象,所以它的 __proto__ 是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true
// 这形成了一个有趣的"鸡生蛋蛋生鸡"的关系
属性遮蔽(Property Shadowing)
如果对象自身和原型上有同名属性,会发生什么?
function Person(name) {
this.name = name;
}
Person.prototype.name = 'Default';
Person.prototype.greet = function() {
console.log(`Hello, ${this.name}`);
};
const john = new Person('John');
// 自身属性遮蔽原型属性
console.log(john.name); // 'John',不是 'Default'
// 删除自身属性后,原型属性就露出来了
delete john.name;
console.log(john.name); // 'Default'
这就是属性遮蔽:自身属性会"遮住"原型链上的同名属性。
检查属性来源
const john = new Person('John');
// hasOwnProperty 只检查自身属性
console.log(john.hasOwnProperty('name')); // true
console.log(john.hasOwnProperty('greet')); // false
// in 操作符检查整个原型链
console.log('name' in john); // true
console.log('greet' in john); // true
实现继承
理解了原型链,继承就好办了。核心就两步:
- 调用父构造函数,继承实例属性
- 设置原型链,继承原型方法
function Vehicle(type) {
this.type = type;
this.speed = 0;
}
Vehicle.prototype.accelerate = function(amount) {
this.speed += amount;
console.log(`${this.type} 加速到 ${this.speed} km/h`);
};
function Car(brand) {
Vehicle.call(this, '汽车'); // 继承实例属性
this.brand = brand;
}
Car.prototype = Object.create(Vehicle.prototype); // 继承原型方法
Car.prototype.constructor = Car;
// 添加子类特有的方法
Car.prototype.honk = function() {
console.log(`${this.brand} 鸣笛`);
};
// 重写父类方法
Car.prototype.accelerate = function(amount) {
Vehicle.prototype.accelerate.call(this, amount);
if (this.speed > 120) {
console.log('超速警告');
}
};
const myCar = new Car('丰田');
myCar.accelerate(50); // 汽车 加速到 50 km/h
myCar.accelerate(80); // 汽车 加速到 130 km/h
// 超速警告
myCar.honk(); // 丰田 鸣笛
为什么用 Object.create() 而不是直接赋值
// 错误写法
Car.prototype = Vehicle.prototype;
// 问题:修改 Car.prototype 会影响 Vehicle.prototype
// 错误写法
Car.prototype = new Vehicle();
// 问题:会执行 Vehicle 构造函数,可能有副作用
// 正确写法
Car.prototype = Object.create(Vehicle.prototype);
// 创建一个新对象,原型指向 Vehicle.prototype
ES6 的 class 语法
ES6 引入了 class 关键字,写起来更清爽:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
const dog = new Dog('Buddy', 'Labrador');
dog.speak(); // Buddy makes a sound
dog.bark(); // Woof!
但要清楚,class 只是语法糖,底层还是原型链那套:
console.log(typeof Dog); // "function"
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
class 的一些特性
class Example {
// 实例属性(ES2022+)
instanceProp = 'instance';
// 私有属性(ES2022+)
#privateProp = 'private';
// 静态属性
static staticProp = 'static';
// 静态方法
static staticMethod() {
return 'static method';
}
// getter/setter
get value() {
return this.#privateProp;
}
}
几个容易踩的坑
1. 引用类型放原型上会共享
function Student(name) {
this.name = name;
}
Student.prototype.hobbies = []; // 所有实例共享这个数组
const s1 = new Student('张三');
const s2 = new Student('李四');
s1.hobbies.push('reading');
console.log(s2.hobbies); // ['reading'] // s2 也有了,出问题了
引用类型(数组、对象)应该放在构造函数里:
function Student(name) {
this.name = name;
this.hobbies = []; // 每个实例独立
}
2. 别直接替换 prototype 对象
function Foo() {}
// 直接替换 prototype 会丢失 constructor
Foo.prototype = {
method: function() {}
};
const foo = new Foo();
console.log(foo.constructor === Foo); // false,变成 Object 了
要么记得补上 constructor,要么用属性添加的方式:
// 方式一:补上 constructor
Foo.prototype = {
constructor: Foo,
method: function() {}
};
// 方式二:直接添加属性(推荐)
Foo.prototype.method = function() {};
3. 箭头函数不能用作构造函数
const Foo = () => {};
const foo = new Foo(); // TypeError: Foo is not a constructor
箭头函数没有 prototype 属性,也没有自己的 this,所以不能用 new。
4. instanceof 的局限性
// instanceof 检查的是原型链
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
// 跨 iframe/realm 时会失效
// iframe 里的 Array 和主页面的 Array 不是同一个
更可靠的类型检查:
Object.prototype.toString.call([]); // "[object Array]"
Array.isArray([]); // true
性能考虑
原型链查找有开销
属性查找会沿着原型链向上,链越长开销越大。虽然现代引擎有优化,但还是要注意:
// 如果频繁访问原型链上的属性,可以缓存
const method = obj.someMethod;
for (let i = 0; i < 1000000; i++) {
method.call(obj); // 比 obj.someMethod() 快
}
Object.create(null) 创建纯净对象
// 普通对象会继承 Object.prototype
const obj = {};
console.log(obj.toString); // ƒ toString() { [native code] }
// 纯净对象没有原型链
const pureObj = Object.create(null);
console.log(pureObj.toString); // undefined
// 适合用作字典/哈希表,不用担心键名冲突
const dict = Object.create(null);
dict['hasOwnProperty'] = 'safe'; // 不会覆盖原型方法
小结
原型链说穿了就是一条查找链:找属性时从对象自身开始,顺着 __proto__ 一路往上找,直到 null。
几个要点:
-
prototype 是函数的属性,用于存放共享的方法
-
__proto__(或 [[Prototype]])是对象的属性,指向它的原型
- 推荐用
Object.getPrototypeOf() 代替 __proto__
-
new 关键字做了四件事:创建对象、设置原型、执行构造函数、返回对象
- 方法定义在原型上,省内存
-
class 是语法糖,底层还是原型链
-
Object.prototype 是原型链的终点,它的 __proto__ 是 null
理解了这个机制,再看 JavaScript 的面向对象就清晰多了。框架源码里大量使用原型链,比如 Vue 2 的响应式系统、各种插件的 mixin 实现,都是基于这套机制。
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):
全栈项目(适合学习现代技术栈):
-
prompt-vault - Prompt 管理器,用的都是最新的技术栈,适合用来学习了解最新的前端全栈开发范式:Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
-
chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB
参考资料