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

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

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

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

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

第一章:JavaScript 的面向对象哲学 —— 原型式 vs 类式1.1 传统“类式”面向对象(Class-based OOP)以 Java、C++、Python 为例:

先定义一个 类(Class),作为模板。类包含属性和方法。通过 new Class() 创建 实例(Instance)。实例“继承”类的结构,但拥有自己的属性值。类与实例之间存在“血缘关系”——实例是类的“后代”。

// Java 示例

class Car {

String color;

Car(String c) { this.color = c; }

void drive() { System.out.println("driving"); }

}

Car myCar = new Car("red");

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

1.2 JavaScript 的“原型式”面向对象(Prototypal OOP)JavaScript 没有真正的类(ES6 的 class 只是语法糖)。它的核心思想是:

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

这被称为 “基于原型的继承”(Prototypal Inheritance)。

没有“类”,只有对象。一个对象可以作为另一个对象的原型(Prototype)。当访问一个对象的属性时,如果自身没有,就去它的原型找;原型没有,再去原型的原型找……直到找到或到达终点。这种机制更灵活、动态,但也更抽象。

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

第二章:构造函数 —— 创造实例的“工厂”要理解原型,必须先理解构造函数(Constructor Function)。

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

// 定义一个普通函数 Person

// 但它将被用作构造函数

function Person(name, age) {

// 当使用 new Person() 时,

// JavaScript 引擎会创建一个新空对象 {}

// 并将 this 绑定到这个新对象上

this.name = name; // 给新对象添加 name 属性

this.age = age; // 给新对象添加 age 属性

// 函数执行完毕后,自动 return this(除非显式返回对象)

}

2.2 使用 new 创建实例

const person1 = new Person('张三', 18);

const person2 = new Person('李四', 19);

person1 和 person2 是两个独立的对象。它们各自拥有 name 和 age 属性,互不影响。这些属性称为实例属性(Instance Properties)。✅ 关键点:

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

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

第三章:原型(Prototype)—— 共享行为的“公共仓库”如果每个实例都要有自己的方法(如 sayHello),那会浪费大量内存。于是,JavaScript 引入了 prototype。

3.1 每个函数都有 prototype 属性这是 JavaScript 引擎自动为函数添加的属性。

function Car(color) {

this.color = color;

}

console.log(Car.prototype); // { constructor: Car }

Car.prototype 是一个对象。默认情况下,它只有一个属性:constructor,指向 Car 本身。这个对象就是未来所有 Car 实例的原型。3.2 在原型上定义共享属性和方法继续看:

// 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,

}

逐行深度解析:function Car(color){ ... }

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

重写整个原型对象!原来的 { constructor: Car } 被替换了。新原型包含:

drive() 方法name 属性(值为 '小米su7')price 属性(值为 300000)创建实例并使用:

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✅ 核心优势:name、price、drive只在内存中存储一份,所有实例共享,极大节省内存!

第四章:__proto__ —— 实例通往原型的“秘密通道”现在问题来了:实例是如何访问到原型上的属性的?

答案是:通过 __proto__。

4.1 什么是 __proto__?每个对象(包括数组、函数、普通对象)都有一个内部属性 [[Prototype]]。在浏览器中,可通过非标准但广泛支持的 __proto__ 访问它。obj.__proto__ 指向该对象的原型对象。4.2 实例的 __proto__ 指向构造函数的 prototype例如:

const person1 = new Person('张三', 18);

console.log(person1.__proto__, '////');

输出结果将是:

{ speci: '人类' } '////'

因为:

Person.prototype.speci = '人类'; // 在原型上添加属性

所以:

person1.__proto__ === Person.prototype; // true

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

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

+------------------+

| Person (函数) |

+------------------+

| prototype |----+

+------------------+ |

+------------------+ +------------------+

| person1 (对象) | | Person.prototype |

+------------------+ +------------------+

| name: '张三' | | speci: '人类' |

| age: 18 | | constructor: Person

+------------------+ +------------------+

| __proto__ |-----^

+------------------+

person1 自身有 name、age。当访问 person1.speci 时:

先查 person1 自身 → 没有。再查 person1.__proto__(即 Person.prototype)→ 找到 '人类'。第五章:原型链(Prototype Chain)—— 属性查找的“寻宝地图”原型不仅可以一层,还可以多层嵌套,形成链式结构,这就是原型链。

5.1 原型链的形成过程实例对象 → __proto__ → 构造函数的 prototype构造函数的 prototype 本身也是对象 → 它也有 __proto__默认情况下,Function.prototype 或 Object.prototype 成为上一级最终,所有链都指向 Object.prototypeObject.prototype.__proto__ === null → 链终止5.2 完整原型链示例以 car1 为例:

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 引擎执行以下步骤:

检查 obj 自身是否有 prop → 有则返回。没有?检查 obj.__proto__(即原型)是否有 prop → 有则返回。还没有?继续沿 __proto__ 向上查找。直到 Object.prototype。若仍未找到,返回 undefined。“实例上有就用自己的,没有就去原型上找。”

5.4 覆盖 vs 修改

person1.speci = '外星人';

console.log(person1.speci); // '外星人'(实例属性)

console.log(person2.speci); // '人类'(原型属性)

console.log(Person.prototype.speci); // '人类'

person1.speci = '外星人'没有修改原型!而是在 person1 上新建了一个同名属性,覆盖了原型上的值。这体现了“实例优先”原则。✅ 修改共享属性的正确方式:

Person.prototype.speci = '智人';

// 所有未覆盖的实例都会看到新值

第六章:Object.getPrototypeOf() —— 安全获取原型的标准方法虽然 __proto__ 可用,但它是非标准属性,且可能被滥用。

ES5 引入了标准 API:

Object.getPrototypeOf(obj)

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

console.log(person1.__proto__, '////');

// 或写成:

console.log(Object.getPrototypeOf(person1), '////');

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

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

function fn() {}

console.log(fn instanceof Object); // true

因此,函数也有 __proto__:

function Car() {}

console.log(Car.__proto__ === Function.prototype); // true

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

console.log(Function.prototype.__proto__ === Object.prototype); // true

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

第八章:对象字面量也有原型!即使不用构造函数,直接创建对象:

const obj = {};

它依然有原型:

console.log(obj.__proto__ === Object.prototype); // true

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

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

第九章:constructor 属性 —— 原型的“回指针”每个原型对象都有一个 constructor 属性,指向其构造函数。

Person.prototype.constructor === Person; // true

Car.prototype.constructor === Car; // 在 1.js 中为 false!

为什么 Car 不成立?

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

Car.prototype = {

drive() { ... },

name: '小米su7',

price: 300000,

};

// 此时 Car.prototype.constructor === Object!

修复方法:

Car.prototype = {

constructor: Car, // 手动指定

drive() { ... },

name: '小米su7',

price: 300000,

};

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

Object.defineProperty(Car.prototype, 'constructor', {

value: Car,

writable: true,

configurable: true,

enumerable: false // 默认不遍历

});

第十章:原型链的终点 —— Object.prototype 与 null所有原型链最终都指向 Object.prototype,而它的 __proto__ 是 null。

console.log(Object.prototype.__proto__); // null

null 表示“没有原型”,是查找的终止符。

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

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

第十一章:高级应用 —— 实现继承利用原型链,我们可以实现“类继承”。

11.1 ES5 继承(组合寄生式)

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 的本质

class Parent {

say() { console.log('parent'); }

}

class Child extends Parent {

speak() { console.log('child'); }

}

等价于:

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:修改实例属性会影响原型错! 实例属性覆盖原型属性,但不会修改原型。

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

obj.hasOwnProperty('prop'); // true 表示自有属性

✅ 面试题:instanceof 的原理?

obj instanceof Constructor

// 等价于:检查 Constructor.prototype 是否在 obj 的原型链上

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

prototype是函数的一个属性,指向一个对象

示例:

function Person() {

this.name = '张三';

}

console.log(Person.prototype);

// 表示Person这个函数的原型对象prototype是{name: '张三'}

__proto__是实例对象的一个属性,指向它的原型对象

示例:

const person = new Person();

console.log(person.__proto__);

// 即person这个实例对象的__proto__是{name: '张三'}

constructor是实例对象的一个属性,指向它的构造函数

示例:

console.log(person.__proto__.constructor);

// 即{name: '张三'}.constructor 是 Person这个函数

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

通过本文,你已经:

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

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

这就是 JavaScript 的原型之美。

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

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

1.js 完整注解

// 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 完整注解

// 定义 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__, '////');