JavaScript

JavaScript 原型(prototype)通俗讲解 推荐

前言🔖


原型就是 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 = 函数;

核心结论(先背下来)

  1. JS 所有普通函数,天生自带一个属性:prototype
  2. prototype 就是一个普通 JS 对象(键值对字典)
  3. 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(){}

因为:

  1. Student.prototype 就是一个普通字典对象
  2. sayHi 只是一个 key
  3. 右边函数只是一个

换成 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 件事,其中最关键的一步:

  1. 创建一个空新对象
  2. 把新对象的 __proto__,强行指向 Person.prototype
  3. 执行构造函数代码
  4. 返回这个对象

对象为什么要主动去找 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 是什么:它就是所有普通函数天生自带的一个属性,和你上面给函数加的 nameage 没区别,只是 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>