JavaScript 原型与原型链:从零到精通的深度解析

"JavaScript 的一切皆对象,而对象的背后,是原型。"

------ 本文将带你彻底、系统、深入地理解 JavaScript 中最核心、最独特、也最容易被误解的机制:原型(Prototype)原型链(Prototype Chain)。结合代码,逐行注解、层层递进,确保你不仅能"知道",更能"掌握"和"运用"。

引言:为什么原型如此重要?

在前端开发中,无论是面试大厂,还是阅读框架源码(如 React、Vue),亦或是编写高性能、可复用的组件,原型机制都是绕不开的核心知识。

很多开发者能写出 classextends,却说不清 prototype__proto__ 的区别;能调用 toString(),却不明白它到底从哪来。这种"黑盒式"编程,在遇到复杂问题时极易陷入困境。

本文的目标,就是撕开 JavaScript 面向对象的面纱,让你看清其底层运作逻辑。我们将从最基础的概念出发,通过真实代码、图示、类比和深度剖析,构建完整的认知体系。


第一章:JavaScript 的面向对象哲学 ------ 原型式 vs 类式

1.1 传统"类式"面向对象(Class-based OOP)

以 Java、C++、Python 为例:

  • 先定义一个 类(Class),作为模板。
  • 类包含属性和方法。
  • 通过 new Class() 创建 实例(Instance)
  • 实例"继承"类的结构,但拥有自己的属性值。
  • 类与实例之间存在"血缘关系"------实例是类的"后代"。
java 复制代码
// Java 示例
class Car {
  String color;
  Car(String c) { this.color = c; }
  void drive() { System.out.println("driving"); }
}

Car myCar = new Car("red");

这里,myCarCar 类的实例,二者有明确的类型归属。

1.2 JavaScript 的"原型式"面向对象(Prototypal OOP)

JavaScript 没有真正的类 (ES6 的 class 只是语法糖)。它的核心思想是:

对象直接从其他对象继承行为,而不是从抽象的"类"模板。

这被称为 "基于原型的继承"(Prototypal Inheritance)

  • 没有"类",只有对象
  • 一个对象可以作为另一个对象的原型(Prototype)
  • 当访问一个对象的属性时,如果自身没有,就去它的原型找;原型没有,再去原型的原型找......直到找到或到达终点。

这种机制更灵活、动态,但也更抽象。

"JS的面向对象是'原型式'而非'类式',实例与原型间无血缘关系,而是基于共享与委托的机制。"


第二章:构造函数 ------ 创造实例的"工厂"

要理解原型,必须先理解构造函数(Constructor Function)

2.1 什么是构造函数?

在 JavaScript 中,任何函数都可以作为构造函数使用 ,只要用 new 关键字调用。

javascript 复制代码
// 定义一个普通函数 Person
// 但它将被用作构造函数
function Person(name, age) { 
  // 当使用 new Person() 时,
  // JavaScript 引擎会创建一个新空对象 {}
  // 并将 this 绑定到这个新对象上
  this.name = name;   // 给新对象添加 name 属性
  this.age = age;     // 给新对象添加 age 属性
  // 函数执行完毕后,自动 return this(除非显式返回对象)
}

2.2 使用 new 创建实例

javascript 复制代码
const person1 = new Person('张三', 18);
const person2 = new Person('李四', 19);
  • person1person2 是两个独立的对象
  • 它们各自拥有 nameage 属性,互不影响。
  • 这些属性称为实例属性(Instance Properties)

✅ 关键点:

构造函数内部的 this 指向新创建的实例对象

实例属性是每个对象私有的。


第三章:原型(Prototype)------ 共享行为的"公共仓库"

如果每个实例都要有自己的方法(如 sayHello),那会浪费大量内存。于是,JavaScript 引入了 prototype

3.1 每个函数都有 prototype 属性

这是 JavaScript 引擎自动为函数添加的属性。

javascript 复制代码
function Car(color) {
  this.color = color;
}
console.log(Car.prototype); // { constructor: Car }
  • Car.prototype 是一个对象
  • 默认情况下,它只有一个属性:constructor,指向 Car 本身。
  • 这个对象就是未来所有 Car 实例的原型

3.2 在原型上定义共享属性和方法

继续看:

javascript 复制代码
// es5 没有类class
// JS 函数是一等对象 
function Car(color){
    // this 指向新创建的对象
    this.color = color;
    // this.drive
    // 车参数
    // this.name = '小米su7';
    // this.price = 300000;
}

Car.prototype = {
    drive(){
        console.log('drive');
    },
    name: '小米su7',
    price: 300000,
}
逐行深度解析:
  1. function Car(color){ ... }

    • 定义构造函数,接收 color 参数。
    • 每个实例将拥有自己的 color
  2. Car.prototype = { ... }

    • 重写整个原型对象
    • 原来的 { constructor: Car } 被替换了。
    • 新原型包含:
      • drive() 方法
      • name 属性(值为 '小米su7'
      • price 属性(值为 300000
  3. 创建实例并使用:

javascript 复制代码
const car1 = new Car('霞光紫');
console.log(car1, car1.name, car1.price); // 输出实例 + 共享属性
car1.drive(); // 调用原型上的方法

const car2 = new Car('海湾蓝');
console.log(car2, car2.name, car2.price);
  • car1.color'霞光紫'(实例属性)
  • car1.name'小米su7'(来自 Car.prototype
  • car1.drive() → 调用 Car.prototype.drive

✅ 核心优势:
namepricedrive 只在内存中存储一份,所有实例共享,极大节省内存!


第四章:__proto__ ------ 实例通往原型的"秘密通道"

现在问题来了:实例是如何访问到原型上的属性的?

答案是:通过 __proto__

4.1 什么是 __proto__

  • 每个对象 (包括数组、函数、普通对象)都有一个内部属性 [[Prototype]]
  • 在浏览器中,可通过非标准但广泛支持的 __proto__ 访问它。
  • obj.__proto__ 指向该对象的原型对象

4.2 实例的 __proto__ 指向构造函数的 prototype

例如:

javascript 复制代码
const person1 = new Person('张三', 18);
console.log(person1.__proto__, '////');

输出结果将是:

javascript 复制代码
{ speci: '人类' } '////'

因为:

javascript 复制代码
Person.prototype.speci = '人类'; // 在原型上添加属性

所以:

javascript 复制代码
person1.__proto__ === Person.prototype; // true

✅ 这是 JavaScript 原型机制的基石关系

4.3 图解:实例、构造函数、原型的关系

复制代码
          +------------------+
          |   Person (函数)  |
          +------------------+
          | prototype        |----+
          +------------------+    |
                                  ↓
+------------------+     +------------------+
|   person1 (对象) |     | Person.prototype |
+------------------+     +------------------+
| name: '张三'     |     | speci: '人类'    |
| age: 18          |     | constructor: Person
+------------------+     +------------------+
| __proto__        |-----^
+------------------+
  • person1 自身有 nameage
  • 当访问 person1.speci 时:
    • 先查 person1 自身 → 没有。
    • 再查 person1.__proto__(即 Person.prototype)→ 找到 '人类'

第五章:原型链(Prototype Chain)------ 属性查找的"寻宝地图"

原型不仅可以一层,还可以多层嵌套,形成链式结构 ,这就是原型链

5.1 原型链的形成过程

  1. 实例对象 → __proto__ → 构造函数的 prototype
  2. 构造函数的 prototype 本身也是对象 → 它也有 __proto__
  3. 默认情况下,Function.prototypeObject.prototype 成为上一级
  4. 最终,所有链都指向 Object.prototype
  5. Object.prototype.__proto__ === null链终止

5.2 完整原型链示例

car1 为例:

javascript 复制代码
const car1 = new Car('霞光紫');

其原型链如下:

复制代码
car1
  │
  └─ __proto__ → Car.prototype
                    │
                    ├─ drive()
                    ├─ name: '小米su7'
                    ├─ price: 300000
                    │
                    └─ __proto__ → Object.prototype
                                      │
                                      ├─ toString()
                                      ├─ hasOwnProperty()
                                      ├─ isPrototypeOf()
                                      │
                                      └─ __proto__ → null

5.3 属性查找规则(覆盖机制)

当你访问 obj.prop 时,JavaScript 引擎执行以下步骤:

  1. 检查 obj 自身是否有 prop → 有则返回。
  2. 没有?检查 obj.__proto__(即原型)是否有 prop → 有则返回。
  3. 还没有?继续沿 __proto__ 向上查找
  4. 直到 Object.prototype
  5. 若仍未找到,返回 undefined

"实例上有就用自己的,没有就去原型上找。"

5.4 覆盖 vs 修改

javascript 复制代码
person1.speci = '外星人';

console.log(person1.speci);       // '外星人'(实例属性)
console.log(person2.speci);       // '人类'(原型属性)
console.log(Person.prototype.speci); // '人类'
  • person1.speci = '外星人' 没有修改原型
  • 而是在 person1新建了一个同名属性覆盖了原型上的值。
  • 这体现了"实例优先"原则。

✅ 修改共享属性的正确方式:

javascript 复制代码
Person.prototype.speci = '智人';
// 所有未覆盖的实例都会看到新值

第六章:Object.getPrototypeOf() ------ 安全获取原型的标准方法

虽然 __proto__ 可用,但它是非标准属性,且可能被滥用。

ES5 引入了标准 API:

javascript 复制代码
Object.getPrototypeOf(obj)

它等价于 obj.__proto__,但更安全、规范。

javascript 复制代码
console.log(person1.__proto__, '////');
// 或写成:
console.log(Object.getPrototypeOf(person1), '////');

推荐使用 Object.getPrototypeOf(obj) 来安全获取对象的原型,优于直接访问 proto。大厂面试常考察此 API。


第七章:万物皆对象?连函数也是!

JavaScript 中,函数也是对象

javascript 复制代码
function fn() {}
console.log(fn instanceof Object); // true

因此,函数也有 __proto__

javascript 复制代码
function Car() {}
console.log(Car.__proto__ === Function.prototype); // true

Function.prototype 本身也是函数,它的 __proto__ 指向 Object.prototype

javascript 复制代码
console.log(Function.prototype.__proto__ === Object.prototype); // true

这解释了为什么函数也能调用 toString()hasOwnProperty() 等方法。


第八章:对象字面量也有原型!

即使不用构造函数,直接创建对象:

javascript 复制代码
const obj = {};

它依然有原型:

javascript 复制代码
console.log(obj.__proto__ === Object.prototype); // true

对象字面量也具有 proto 属性,说明即使非构造函数创建的对象依然遵循原型机制。

所以,{}[]/regex/ 等字面量,都自动链接到对应的内置原型。


第九章:constructor 属性 ------ 原型的"回指针"

每个原型对象都有一个 constructor 属性,指向其构造函数。

javascript 复制代码
Person.prototype.constructor === Person; // true
Car.prototype.constructor === Car;       // 在 1.js 中为 false!

为什么 Car 不成立?

因为在 1.js 中,我们重写了整个 prototype 对象

javascript 复制代码
Car.prototype = {
  drive() { ... },
  name: '小米su7',
  price: 300000,
};
// 此时 Car.prototype.constructor === Object!

修复方法:

javascript 复制代码
Car.prototype = {
  constructor: Car, // 手动指定
  drive() { ... },
  name: '小米su7',
  price: 300000,
};

或者使用 Object.defineProperty 设置不可枚举:

javascript 复制代码
Object.defineProperty(Car.prototype, 'constructor', {
  value: Car,
  writable: true,
  configurable: true,
  enumerable: false // 默认不遍历
});

第十章:原型链的终点 ------ Object.prototypenull

所有原型链最终都指向 Object.prototype,而它的 __proto__null

javascript 复制代码
console.log(Object.prototype.__proto__); // null

null 表示"没有原型",是查找的终止符

null表示没有原型对象,null在此处的作用是终止查找流程,防止内存溢出问题。

如果没有 null,引擎会无限循环查找,导致崩溃。


第十一章:高级应用 ------ 实现继承

利用原型链,我们可以实现"类继承"。

11.1 ES5 继承(组合寄生式)

javascript 复制代码
function Vehicle(type) {
  this.type = type;
}
Vehicle.prototype.move = function() {
  console.log('moving');
};

function Car(color) {
  Vehicle.call(this, 'car'); // 借用构造函数
  this.color = color;
}

// 设置原型链
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

Car.prototype.drive = function() {
  console.log('driving');
};

const myCar = new Car('red');
myCar.move(); // 'moving'(来自 Vehicle.prototype)
myCar.drive(); // 'driving'

这里,Car.prototype.__proto__ === Vehicle.prototype,形成两层原型链。

11.2 ES6 class 的本质

javascript 复制代码
class Parent {
  say() { console.log('parent'); }
}

class Child extends Parent {
  speak() { console.log('child'); }
}

等价于:

javascript 复制代码
function Parent() {}
Parent.prototype.say = function() { ... };

function Child() {
  Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.speak = function() { ... };

所以,class 只是语法糖,底层仍是原型链!


第十二章:常见误区与面试题

❌ 误区1:prototype 是所有对象都有的属性

错! 只有函数 才有 prototype

普通对象有 __proto__,但没有 prototype

复制代码
const obj = {};
console.log(obj.prototype); // undefined
console.log(obj.__proto__); // Object.prototype

❌ 误区2:修改实例属性会影响原型

错! 实例属性覆盖原型属性,但不会修改原型。

✅ 面试题:如何判断一个属性是实例自有还是继承的?

javascript 复制代码
obj.hasOwnProperty('prop'); // true 表示自有属性

✅ 面试题:instanceof 的原理?

javascript 复制代码
obj instanceof Constructor
// 等价于:检查 Constructor.prototype 是否在 obj 的原型链上

第十三章:总结 ------ 原型机制全景图

概念 说明 所属
prototype 函数的属性,指向原型对象 函数
__proto__ 对象的属性,指向其原型 所有对象
constructor 原型对象的属性,指回构造函数 原型对象
原型链 obj → __proto__ → prototype → __proto__ → ... → Object.prototype → null 查找路径
共享机制 方法和公共属性放在 prototype 上,节省内存 设计哲学
覆盖机制 实例属性优先于原型属性 查找规则

原型和原型链

  • prototype是函数的一个属性,指向一个对象
    示例:

    javascript 复制代码
    function Person() {
      this.name = '张三';
    }
    console.log(Person.prototype);
     // 表示Person这个函数的原型对象prototype是{name: '张三'}
  • __proto__是实例对象的一个属性,指向它的原型对象
    示例:

    javascript 复制代码
    const person = new Person();
    console.log(person.__proto__);
     // 即person这个实例对象的__proto__是{name: '张三'}
  • constructor是实例对象的一个属性,指向它的构造函数
    示例:

    javascript 复制代码
    console.log(person.__proto__.constructor);
     // 即{name: '张三'}.constructor 是 Person这个函数

结语:掌握原型,掌握 JavaScript 的灵魂

JavaScript 的原型机制,是其区别于其他语言的最大特色。它看似复杂,实则逻辑严密、设计精巧。

通过本文,你已经:

  • 理解了构造函数与实例的关系;
  • 掌握了 prototype__proto__ 的区别与联系;
  • 学会了原型链的查找规则;
  • 能解释 1.js2.js 中每一行代码的含义;
  • 知道了如何安全操作原型;
  • 能应对相关面试题。

记住:

不是"类"创造了对象,而是"对象"链接了对象。

这就是 JavaScript 的原型之美。

愿你在编程之路上,越走越远,越走越深!🚀


附:关键代码汇总(保留原始格式,逐行注解)

1.js 完整注解

javascript 复制代码
// es5 没有类class
// JS 函数是一等对象 
function Car(color){
    // this 指向新创建的对象(当使用 new 调用时)
    this.color = color; // 实例属性,每个 car 有自己的颜色
    // this.drive        // 此时 this 上还没有 drive 方法
    // 车参数
    // this.name = '小米su7';  // 如果放这里,每个实例都会复制一份,浪费内存
    // this.price = 300000;
}

// 重写 Car 的原型对象,用于共享属性和方法
Car.prototype = {
    // drive 是一个方法,所有实例共享
    drive(){
        console.log('drive');
    },
    // 共享属性:所有 Car 实例的 name 都是 '小米su7'
    name: '小米su7',
    // 共享属性:价格
    price: 300000,
    // 注意:此处丢失了 constructor 属性!
}

// 创建两个实例
const car1 = new Car('霞光紫');
// car1 自身有 color: '霞光紫'
// car1.name 来自 Car.prototype
console.log(car1, car1.name, car1.price);

// 调用原型上的方法
car1.drive();

const car2 = new Car('海湾蓝');
console.log(car2, car2.name, car2.price);

2.js 完整注解

javascript 复制代码
// 定义 Person 构造函数
function Person(name, age) {
  // 实例属性
  this.name = name; 
  this.age = age;
}

// 在 Person.prototype 上添加共享属性 speci(应为 species,但保留原拼写)
Person.prototype.speci = '人类';

// 创建实例
const person1 = new Person('张三', 18);
// person1 自身有 name、age;speci 来自原型
console.log(person1.name, person1.speci); // 张三 人类

const person2 = new Person('李四', 19);
console.log(person2.name, person2.speci); // 李四 人类

// 打印 person1 的原型(即 Person.prototype)
console.log(person1.__proto__, '////');
相关推荐
0***86331 小时前
SQL Server2019安装步骤+使用+解决部分报错+卸载(超详细 附下载链接)
javascript·数据库·ui
烛阴1 小时前
C#异常概念与try-catch入门
前端·c#
钮钴禄·爱因斯晨1 小时前
# 企业级前端智能化架构:DevUI与MateChat融合实践深度剖析
前端·架构
摆烂工程师1 小时前
2025年12月最新的 Google AI One Pro 1年会员教育认证通关指南
前端·后端·ai编程
Gavin在路上2 小时前
DDD之用事件风暴重构“电商订单履约”(11)
java·前端·重构
我命由我123452 小时前
VSCode - VSCode 颜色值快速转换
前端·ide·vscode·前端框架·编辑器·html·js
前端涂涂2 小时前
怎么设计一个加密货币 谁有权利发行数字货币 怎么防止double spending attack 怎么验证交易合法性 铸币交易..
前端
JuneTT2 小时前
【JS】使用内连配置强制引入图片为base64
前端·javascript
前端涂涂2 小时前
4.BTC-协议
前端