前言🔖
原型就是 JS 给对象 “共享属性和方法” 的机制,不用每个对象都重复写一遍代码,省空间、省代码。
你可以把原型理解成:一个公共的 “工具箱”,所有用同一个构造函数创建的对象,都能共用这个工具箱里的工具。
🔹先搞懂 3 个核心概念:
- ① prototype —— 构造函数的 “公共工具箱”
- 只有函数才有 prototype,它是一个对象,用来存放共享的方法和属性。
- ② proto —— 对象的 “工具箱钥匙”
- 所有对象都有 proto,它指向自己的构造函数的 prototype。
- ③ 原型链
- 对象找不到属性 / 方法 → 去 proto 找 → 还找不到 → 继续往上找 → 直到 null
这一条链就是原型链。
🔹用生活例子秒懂原型
场景:造 100 个学生
每个学生都有:名字、学号(自己独有)
每个学生都会:学习、吃饭、睡觉(大家共用)
如果不使用原型:
function Student(name, id) {
this.name = name
this.id = id
// 每个学生都创建一个 study 方法!100个学生就有100个方法,浪费内存
this.study = function() {
console.log('学习')
}
}
用原型优化(共享方法):
function Student(name, id) {
this.name = name // 自己的
this.id = id // 自己的
}
// 放在 prototype 里 = 公共工具箱
Student.prototype.study = function() {
console.log(this.name + '在学习')
}
Student.prototype.eat = function() {
console.log('吃饭')
}
现在:
- 100 个学生对象,只存 1 份 study、eat 方法
- 每个学生都能调用,因为它们的
__proto__指向这个公共工具箱
🔹对象怎么用原型?
// 1. 构造函数
function Student(name) {
this.name = name
}
// 2. 给原型加共享方法
Student.prototype.sayHi = function() {
console.log('我是 ' + this.name)
}
// 3. 创建对象
let s1 = new Student('小明')
let s2 = new Student('小红')
// 4. 调用方法(方法在原型上)
s1.sayHi() // 我是 小明
s2.sayHi() // 我是 小红
设计初衷:分离「存储」和「查找」
- prototype = 公共仓库(只读 / 挂载区)
- 每个构造函数只需要一份原型仓库
- 所有实例共用这一个仓库,极致节省内存
- 开发者只需要给「函数的 prototype」加方法,所有实例自动能用
如果没有prototype:每 new 一个对象,就要复制一遍所有方法,创建 1000 个实例就冗余 1000 份代码。
🔹原型链是什么?
一层一层往上找属性 / 方法,就是原型链。
例子:
s1.toString()
查找顺序:
s1 自己找 toString → 没有 去 s1.__proto__(Student.prototype)→ 没有 去 Student.prototype.__proto__(Object.prototype)→ 找到了! 执行
这就是原型链。
最终原型链顶端:
Object.prototype.__proto__ → null
🔹最重要的 3 条规则
- 函数才有 prototype,对象没有
- 对象都有 proto,指向创建它的构造函数的原型
- 对象调用方法 / 属性时:先找自己 → 找不到去原型找 → 再找不到沿原型链往上找
构造函数 Student
↓ (prototype)
Student.prototype 【公共工具箱】
↑ (__proto__)
对象 s1、s2
默认原型对象里面长什么样?🔖
prototype 就是一个普通 JS 对象(键值对字典)
function Person(){}
JS 会自动给 Person 绑定一个「天生自带的空原型对象」
也就是:
Person.prototype = 【浏览器自动创建的默认原型对象】
这个默认原型对象里面长什么样?
function Person() {}
console.log(Person.prototype);
{
constructor: Person, // 天生自带,关键标记
__proto__: Object.prototype
}
干干净净,没有任何多余方法。JS 引擎自动创建的、以当前函数为 constructor 的纯空原型对象
Person.prototype本质就是 Object 对象,只是引擎提前帮你建好、预装了 constructor。
在 JS 里:所有普通对象,本质就是一个 键值对字典
就跟 Java 的 Map<String, Object> 一模一样。
// JS 普通对象 = 纯字典
let obj = {
name: "张三",
age: 18
};
你可以随时:
- 加 key
- 删 key
- 修改 key
- key 的值可以是:数字、字符串、函数、数组、对象
function Student(){}
Student.prototype 不是特殊东西
它 = 引擎自动帮你创建的一个「普通字典对象」
// 引擎自动新建一个普通字典对象
Student.prototype = {
constructor: Student // 就这一个key
}
它就是一个纯纯的普通对象、纯字典,没有任何特权。
// 给字典加一个 key: sayHi,值是一个函数
Student.prototype.sayHi = function(){
console.log("你好")
}
等价于:
// 1. 先拿到这个字典
let 字典 = Student.prototype;
// 2. 给字典新增一个键值对
字典.sayHi = 函数;
核心结论(先背下来)
- JS 所有普通函数,天生自带一个属性:
prototype prototype就是一个普通 JS 对象(键值对字典)new 出来的实例,天生会挂靠到这个对象上,用来共享属性/方法
所以:Person.prototype = 一个提前准备好的「公共仓库对象」
Java可以动态给任何对象加方法吗?🔖
答案:完全不可以 ❌
这就是 Java 静态语言 和 JS 动态语言 最核心的分水岭。
🔹JavaScript 玩法
// 1. 普通对象
let obj = {}
// 2. 运行时随便加方法,随时随地
obj.sayHi = function() {
console.log("你好")
}
obj.sayHi()
✅ JS 任意对象,运行时随意增删属性、方法,天生支持。
🔹Java 绝对不行
class User {
// 只能在这里写死方法
void test(){}
}
public class Demo {
public static void main(String[] args) {
User u = new User();
// ❌ 语法报错!根本写不出来
u.sayHi = 方法;
}
}
Java 对象一旦 new 出来,结构完全固定:
- 不能新增变量
- 不能新增方法
- 不能删除原有属性
- 类长什么样,对象一辈子就长什么样
🔹底层本质区别
- Java:基于类 / 静态绑定
- 一切设计在 编译期 就定死
- 方法属于「类」,不属于单个对象
- 对象只是「数据载体」,只存成员变量
- 方法全部在 类模板 里,全局共享,不能临时加
Java 世界观: 先有 类(模板) → 再 new 对象 模板不改,对象永远不能加新功能
🔹JS:基于对象 / 动态绑定
- 一切都是对象
- 方法就是一个变量值,函数是一等公民
- obj.xxx = 函数 本质就是 给对象加一个键值对
- prototype 只是一个用来共享方法的普通对象
JS 世界观: 所有对象都是「字典」,key 随便加、随便改、随便删
为什么 JS 可以这样写?
Student.prototype.sayHi = function(){}
因为:
Student.prototype就是一个普通字典对象sayHi只是一个 key- 右边函数只是一个值
换成 Java 逻辑理解:
Java 没有「给对象临时加方法」的语法,
所有方法必须提前写在类里面,不能运行时临时挂载。
JS中,为什么对象会去找这个prototype?🔖
🔹先定 3 个核心结论
- JS 所有普通函数,天生自带一个属性:prototype
- prototype 就是一个普通 JS 对象(键值对字典)
- new 出来的实例,天生会挂靠到这个对象上,用来共享属性/方法
🔹函数的 prototype 是什么
你写一个普通函数
function Person() { }
JS 引擎自动偷偷做了一件事:
// 引擎自动创建一个 纯普通对象
Person.prototype = {
constructor: Person // 只存这一个属性
}
所以:Person.prototype = 一个提前准备好的「公共仓库对象」
它就是个普通对象,和 {} 没有本质区别,你可以随意加属性、加函数、改、删、覆盖。
// 往公共仓库里放方法
Person.prototype.say = function(){}
Person.prototype.age = 20
对比 Java:
Java 是「类里面写死方法」
JS 是「函数自带一个公共仓库,方法全部挂在仓库里」
🔹为什么 new 出来的对象,会去找这个 prototype?
当你执行:
const p = new Person()
new 做了 4 件事,其中最关键的一步:
- 创建一个空新对象
- 把新对象的
__proto__,强行指向Person.prototype - 执行构造函数代码
- 返回这个对象
对象为什么要主动去找 prototype?
核心:JS 的「属性查找规则」(语言底层死规则)
当你写:
p.say()
JS 会按固定顺序找,一步都不会乱:
- 先在自己身上找:p 自身有没有 say?
- 找不到 → 走隐藏引线 proto
- 找到 Person.prototype → 在这个仓库里找 say
- 找到就执行;找不到继续往上找(原型链)
🔹JS 为什么要这么设计
为什么不直接把方法写在实例上?
非要搞一个 prototype 让对象绕一圈去找?
- 为了省内存(最核心原因)
如果不用 prototype:
function Person(){
// 每 new 一个人,就新建一份函数
this.say = function(){}
}
const p1 = new Person()
const p2 = new Person()
p1 有一份 say
p2 有一份 say
1000 个实例 = 1000 份一模一样的函数,内存爆炸
用 prototype 之后:
// 全局就这 1 份
Person.prototype.say = function(){}
所有实例共用同一个函数,实例只存自己独有的数据(name/age),方法全共享。
- 为了动态扩展(Java 做不到)
后期想给所有实例统一加功能?直接改仓库就行:
// 一行代码,所有 已创建 / 未创建 的实例全部生效
Person.prototype.run = function(){}
Java 类一旦定义完,不能随便加方法,JS 靠 prototype 轻松实现。
__proto__ 为什么要加两个下划线呢?🔖
双下划线 __xx__,在编程语言里,有一个通用规矩:
前后双下划线 = 内部隐藏属性、引擎私有、官方内置、不建议开发者随便碰
__proto__ 意思就是:
这是 JS 引擎内部用的底层属性,不属于开发者正常使用的 API,只是被迫暴露出来给你看的。
为什么JS函数上能定义对象呢?🔖
核心答案一句话:因为在 JavaScript 里,函数本身就是对象。
为什么说 “函数就是对象”?
你可以把它理解成:
- 在 Java 里,函数和对象是完全分开的,函数不能当对象用;
- 但在 JS 里,函数是一种特殊的对象,天生就可以拥有属性和方法。
你写的这句代码:
Person.prototype = { /* ... */ }
本质就是:给函数这个 “对象”,添加 / 修改一个叫 prototype 的属性而已。
🔹怎么证明 “函数是对象”?
看这段代码就懂了:
function Person() {}
// 给函数本身加一个属性,完全合法!
Person.name = "人类构造器";
Person.age = 100;
Person.sayHi = function() {
console.log("我是一个函数,但我也能有自己的方法!");
};
console.log(Person.name); // "人类构造器"
console.log(Person.age); // 100
Person.sayHi(); // 正常执行
你会发现,函数可以像普通对象一样,被加属性、加方法,因为它本来就是对象。
prototype 是什么:它就是所有普通函数天生自带的一个属性,和你上面给函数加的 name、age 没区别,只是 JS 引擎默认帮你加上了而已。
- 这个属性的值,默认是一个空对象
- 你可以修改、覆盖它,就像修改任何对象属性一样
- 当你用
new Person()创建实例时,JS 会自动把实例的__proto__指向这个对象
这是 JS 的核心设计之一:一切皆对象
函数作为对象,才能被赋值、被传递、被修改属性 prototype 属性,就是为了实现原型继承而存在的特殊属性
写个html的代码,看到这个原型吗?🔖
可直接复制运行的 HTML,打开页面 → F12 控制台,就能肉眼看清:prototype、__proto__、constructor、原型链 全部一目了然。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JS 原型可视化</title>
</head>
<body>
<script>
// 1. 定义普通构造函数
function Person(name) {
this.name = name;
}
// 2. 往【函数的 prototype(原型对象)】挂载共享方法
Person.prototype.sayHello = function(){
console.log("我是原型上的方法,我的名字:", this.name);
};
// 3. new 生成实例
const p1 = new Person("张三");
const p2 = new Person("李四");
// ========== 控制台打印,清晰查看 ==========
console.log("---------- 1. 查看函数的 prototype ----------");
console.log("Person.prototype:", Person.prototype);
console.log("\n---------- 2. 查看实例的 __proto__ ----------");
console.log("p1.__proto__:", p1.__proto__);
console.log("\n---------- 3. 核心等式(关键) ----------");
console.log(p1.__proto__ === Person.prototype); // true
console.log("\n---------- 4. 查看 constructor ----------");
console.log(Person.prototype.constructor);
console.log("\n---------- 5. 调用原型方法 ----------");
p1.sayHello();
p2.sayHello();
console.log("\n---------- 6. 完整原型链 ----------");
console.log("p1 -->", p1.__proto__);
console.log("p1.__proto__ -->", p1.__proto__.__proto__); // Object.prototype
console.log("Object.prototype -->", p1.__proto__.__proto__.__proto__); // null
</script>
</body>
</html>
