要理解 JavaScript 中的「原型」,首先要跳出其他面向对象语言(如 Java、C#)的「类」思维------JS 是通过 原型(Prototype) 实现继承和属性共享的,核心是「对象基于原型关联,而非类的实例化」。
一、先搞懂:为什么需要原型?
原型的本质是 "资源复用工具" 。它解决了一个核心问题:避免重复创建相同属性/方法,节省内存。
举个例子:如果创建 100 个「人」对象,每个对象都需要 sayHello 方法。如果直接在每个对象里定义 sayHello,会生成 100 个完全相同的函数,造成内存浪费。
而原型的思路是:把 sayHello 放在一个"公共模板"(原型对象)里,所有「人」对象都"关联"这个模板,需要时直接从模板里拿方法 ------这样只需要 1 个 sayHello 函数,实现资源复用。
二、核心概念:3 个关键角色
理解原型,必须先分清 3 个紧密关联的概念:原型对象(Prototype) 、prototype 属性 、__proto__ 属性 (或 Object.getPrototypeOf() 方法)。
| 概念 | 作用 | 归属 |
|---|---|---|
| 原型对象(Prototype) | 存储公共属性/方法的"模板对象",被其他对象共享 | 每个对象都有一个原型对象(除了 Object.prototype) |
prototype 属性 |
函数特有,指向该函数创建的「实例对象」的原型对象 | 函数(如构造函数、普通函数) |
__proto__ 属性 |
对象特有,指向该对象的原型对象(非标准,但浏览器普遍支持) | 所有对象(除了 null) |
三、直观理解:原型链的"关联关系"
所有对象的原型会形成一条「原型链」:对象 → 它的原型对象 → 原型的原型对象 → ... → 最终指向 Object.prototype(原型链的顶端),Object.prototype 的原型是 null(没有更上层的原型)。
我们通过一个具体例子,拆解原型链的关联逻辑:
1. 用构造函数创建对象(经典场景)
js
// 1. 定义一个构造函数(首字母大写,约定用于创建对象)
function Person(name) {
this.name = name; // 实例属性:每个 Person 实例都有独立的 name
}
// 2. 在构造函数的 prototype 上定义"公共方法"
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
// 3. 用 new 关键字创建 Person 实例
const zhangsan = new Person('张三');
2. 拆解原型关联(关键!)
上面代码中,zhangsan、Person、Person.prototype 三者的原型关系如下:
zhangsan(实例对象)的原型 :zhangsan.__proto__ === Person.prototype(或Object.getPrototypeOf(zhangsan) === Person.prototype)
→ 实例的原型,指向构造函数的prototype属性。Person.prototype(原型对象)的原型 :Person.prototype.__proto__ === Object.prototype
→ 所有自定义原型对象,最终都关联到Object.prototype(JS 内置的顶层原型)。Object.prototype的原型 :Object.prototype.__proto__ === null
→ 原型链的终点,没有更上层的原型。Person(构造函数)的原型 :Person.__proto__ === Function.prototype
→ 函数也是对象,所有函数的原型都指向Function.prototype(Function.prototype.__proto__最终也指向Object.prototype)。
3. 原型链的作用:属性查找规则
当你访问一个对象的属性(如 zhangsan.sayHello)时,JS 会按以下规则查找:
- 先在对象自身查找:如果
zhangsan自己有sayHello方法,直接使用; - 自身没有,就去「对象的原型对象」查找:即
Person.prototype,找到sayHello,使用; - 原型对象没有,就去「原型的原型」查找:即
Object.prototype; - 一直查到
null还没找到,就返回undefined(不会报错)。
比如:zhangsan.toString()(toString 是 Object.prototype 的方法,zhangsan 自身没有,但能通过原型链找到)。
四、原型的核心特性
1. 共享性:原型上的属性/方法被所有实例共享
js
const lisi = new Person('李四');
zhangsan.sayHello(); // Hello, I'm 张三
lisi.sayHello(); // Hello, I'm 李四
// 两个实例调用的是同一个 sayHello 方法(来自 Person.prototype)
console.log(zhangsan.sayHello === lisi.sayHello); // true
2. 动态性:原型的修改会影响所有已创建的实例
即使实例已经创建,修改原型对象的属性/方法,实例也能立即访问到新的内容:
js
// 给 Person.prototype 新增一个方法
Person.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
// 已创建的 zhangsan 能直接调用新方法
zhangsan.eat(); // 张三 is eating
3. 实例可"覆盖"原型属性(不修改原型本身)
如果实例自身定义了和原型同名的属性/方法,会优先使用实例自身的(原型链查找时会先找到自身属性,停止向上查找):
js
// 给 zhangsan 自身定义 sayHello 方法
zhangsan.sayHello = function() {
console.log(`Hi, I'm ${this.name}`);
};
zhangsan.sayHello(); // Hi, I'm 张三(用自身的方法)
lisi.sayHello(); // Hello, I'm 李四(用原型的方法,不受影响)
五、常见误区澄清
- "原型是类"?错!
JS 直到 ES6 才引入class语法,但class只是「原型的语法糖」------本质还是基于原型实现继承,并非像 Java 那样的"类"。 prototype和__proto__分不清?
-
- 记住:
prototype是函数的属性 ,指向实例的原型;__proto__是对象的属性,指向自己的原型。 - 简单公式:
实例.__proto__ === 构造函数.prototype。
- 记住:
- "所有对象都有
__proto__"?错!
null没有__proto__(Object.getPrototypeOf(null)会返回null),它是原型链的终点。
六、总结:原型的本质
原型的核心是「对象之间的关联关系 」------每个对象都通过 __proto__ 关联到一个原型对象,从而共享原型上的资源。这种关联形成的「原型链」,决定了 JS 中属性查找、继承的逻辑。
理解原型,就能理解 JS 面向对象的底层逻辑(比如 new 关键字的作用、继承的实现方式),也是后续学习 class、extends 等语法的基础。