一文搞懂 JavaScript 原型链:从本质到实战应用

一文搞懂 JavaScript 原型链:从本质到实战应用

在 JavaScript 世界里,原型链是贯穿始终的核心概念。很多开发者对它又爱又恨------爱它支撑起了 JS 的面向对象特性,恨它抽象难懂、容易混淆。本文将从"是什么""为什么""怎么用"三个维度,用通俗的语言+直观的实例,帮你彻底搞懂原型链,从此不再被"原型""原型对象""构造函数"这些概念绕晕。

一、为什么需要原型链?------ JS 面向对象的底层逻辑

在传统面向对象语言(如 Java、C++)中,我们通过"类"来创建对象,类是对象的模板,规定了对象的属性和方法。但 JavaScript 在 ES6 之前并没有"类"的概念,那它是如何实现面向对象的呢?

答案就是 原型链(Prototype Chain) 。原型链的核心作用有两个:

  • 实现属性和方法的共享:避免每个对象都重复定义相同的方法,节省内存空间;
  • 实现继承:让一个对象可以复用另一个对象的属性和方法,无需重新编写代码。

举个简单的例子:如果我们要创建 100 个"人"对象,每个对象都有"姓名""年龄"属性和"说话"方法。如果没有原型链,我们需要给每个对象都定义一次"说话"方法,这会造成大量的内存浪费;而通过原型链,我们可以把"说话"方法定义在原型上,让所有"人"对象共享这个方法。

二、先理清三个核心概念:构造函数、原型对象、实例

要搞懂原型链,必须先分清三个紧密关联的概念:构造函数原型对象(Prototype)实例对象。它们三者的关系是:原型链的基础。

2.1 构造函数

构造函数是用来创建对象的函数,通常首字母大写(约定俗成)。通过 new 关键字调用构造函数,就能生成实例对象。

javascript 复制代码
// 构造函数:用来创建"人"对象
function Person(name, age) {
  this.name = name; // 实例属性(每个实例独有的属性)
  this.age = age;
}

// 通过 new 调用构造函数,生成实例对象
const person1 = new Person('张三', 20);
const person2 = new Person('李四', 22);

console.log(person1); // Person { name: '张三', age: 20 }
console.log(person2); // Person { name: '李四', age: 22 }

2.2 原型对象(Prototype)

每个构造函数都有一个 prototype 属性,这个属性指向一个对象,就是原型对象。原型对象的作用是存储所有实例共享的属性和方法。

我们可以把共享方法定义在构造函数的 prototype 上,让所有实例共享:

javascript 复制代码
// 给 Person 构造函数的原型对象添加共享方法
Person.prototype.sayHi = function() {
  console.log(`Hi, 我是 ${this.name},今年 ${this.age} 岁`);
};

// 所有实例都能调用原型上的方法
person1.sayHi(); // Hi, 我是 张三,今年 20 岁
person2.sayHi(); // Hi, 我是 李四,今年 22 岁

// 验证:两个实例的 sayHi 方法是同一个(共享的)
console.log(person1.sayHi === person2.sayHi); // true

2.3 实例与 proto

每个实例对象都有一个内置属性(非标准但几乎所有浏览器都支持)------ __proto__,这个属性指向创建它的构造函数的原型对象

也就是说,实例的 __proto__ === 构造函数的 prototype

javascript 复制代码
// 实例的 __proto__ 指向构造函数的 prototype
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

2.4 三者关系总结

用一张图就能理清三者的关系:

  • 构造函数(Person)通过 prototype 指向原型对象(Person.prototype);
  • 实例对象(person1、person2)通过 __proto__ 指向原型对象(Person.prototype);
  • 原型对象(Person.prototype)通过 constructor 指向构造函数(Person)(这个属性是默认存在的)。
javascript 复制代码
// 原型对象的 constructor 指向构造函数
console.log(Person.prototype.constructor === Person); // true

三、原型链的本质:层层向上的查找链条

原型对象本身也是一个对象,它也有自己的 __proto__ 属性,指向它的原型对象。这样一层一层向上追溯,就形成了一条链条------这就是原型链

原型链的终点是 Object.prototype,因为 Object 是 JS 中所有对象的"根对象",它的 __proto__ 指向 null(没有更上层的原型了)。

3.1 原型链的结构示例

以 person1 为例,它的原型链结构是:

person1 → person1.proto (Person.prototype)→ Person.prototype.proto (Object.prototype)→ Object.prototype.proto(null)

arduino 复制代码
// 验证原型链结构
console.log(person1.__proto__); // Person.prototype
console.log(person1.__proto__.__proto__); // Object.prototype
console.log(person1.__proto__.__proto__.__proto__); // null

3.2 原型链的核心作用:属性/方法查找规则

当我们访问一个实例对象的属性或方法时,JS 引擎会按照以下规则查找:

  1. 首先在实例对象本身查找,如果找到,直接使用;
  2. 如果没找到,就通过__proto__ 找到它的原型对象,在原型对象中查找;
  3. 如果原型对象中也没找到,就通过原型对象的 __proto__ 向上查找,直到找到 Object.prototype
  4. 如果在 Object.prototype 中还没找到,就返回 undefined(如果是方法,就会报错"xxx is not a function")。

举个例子理解查找规则:

scss 复制代码
// 1. 实例本身有 name 属性,直接访问
console.log(person1.name); // 张三(来自 person1 本身)

// 2. 实例本身没有 sayHi 方法,去原型对象(Person.prototype)查找
person1.sayHi(); // Hi, 我是 张三...(来自 Person.prototype)

// 3. 实例和 Person.prototype 都没有 toString 方法,去 Object.prototype 查找
console.log(person1.toString()); // [object Object](来自 Object.prototype)

// 4. 找不到的属性,返回 undefined
console.log(person1.gender); // undefined

四、原型链实现继承------JS 原生继承的核心方式

ES6 之前,JavaScript 没有 classextends 关键字,继承完全依赖原型链实现。核心思路是:让子类的原型对象指向父类的实例,从而让子类实例能通过原型链访问到父类的属性和方法。

4.1 基本原型链继承

javascript 复制代码
// 父类构造函数:Person
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 父类原型方法
Person.prototype.sayHi = function() {
  console.log(`Hi, 我是 ${this.name}`);
};

// 子类构造函数:Student(继承 Person)
function Student(name, age, studentId) {
  // 继承父类的实例属性(必须先调用,否则 this 会被覆盖)
  Person.call(this, name, age); 
  this.studentId = studentId; // 子类独有的属性
}

// 核心:让子类的原型对象指向父类的实例,建立原型链
Student.prototype = new Person();

// 修复子类原型的 constructor 指向(因为上面的赋值改变了 constructor)
Student.prototype.constructor = Student;

// 子类原型方法(重写父类方法,实现多态)
Student.prototype.sayHi = function() {
  console.log(`Hi, 我是学生 ${this.name},学号:${this.studentId}`);
};

// 子类独有的原型方法
Student.prototype.study = function() {
  console.log(`${this.name} 正在学习`);
};

// 实例化子类
const student1 = new Student('王五', 18, '2024001');

// 验证继承效果
console.log(student1.name); // 王五(继承自 Person)
console.log(student1.studentId); // 2024001(子类独有)
student1.sayHi(); // Hi, 我是学生 王五,学号:2024001(重写后的方法)
student1.study(); // 王五 正在学习(子类独有方法)
console.log(student1.toString()); // [object Object](继承自 Object.prototype)

4.2 原型链继承的注意点

  • 必须用 Person.call(this) :在子类构造函数中调用父类构造函数,才能继承父类的实例属性(否则子类实例不会有 name、age 等属性);
  • 修复 constructor 指向:将子类原型赋值为父类实例后,子类原型的 constructor 会指向父类,需要手动改回子类;
  • 父类原型的引用类型属性会被所有子类实例共享:这是原型链继承的缺陷,比如父类原型有一个数组属性,所有子类实例修改这个数组都会互相影响(解决方法:组合继承,即原型链+构造函数继承,上面的例子就是组合继承)。

五、原型链与 ES6 Class 的关系

很多开发者以为 ES6 的 class 是 JS 新增的继承模型,但实际上,class 只是原型链的语法糖------它的底层实现依然是原型链,只是写法更接近传统面向对象语言,更直观。

5.1 Class 本质是构造函数

javascript 复制代码
// ES6 Class
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHi() {
    console.log(`Hi, 我是 ${this.name}`);
  }
}

// Class 本质是构造函数
console.log(typeof Person); // function

// Class 的 prototype 属性依然存在
console.log(Person.prototype); // { constructor: ƒ Person(), sayHi: ƒ }

// 实例的 __proto__ 依然指向 Class 的 prototype
const person1 = new Person('张三', 20);
console.log(person1.__proto__ === Person.prototype); // true

5.2 extends 本质是原型链继承

ES6 的 extends 实现继承,底层依然是原型链。我们用 class 重写上面的 Student 继承案例:

javascript 复制代码
// 父类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHi() {
    console.log(`Hi, 我是 ${this.name}`);
  }
}

// 子类继承父类(extends 本质是原型链)
class Student extends Person {
  constructor(name, age, studentId) {
    super(name, age); // 相当于 Person.call(this, name, age)
    this.studentId = studentId;
  }

  sayHi() { // 重写父类方法
    console.log(`Hi, 我是学生 ${this.name},学号:${this.studentId}`);
  }

  study() { // 子类独有方法
    console.log(`${this.name} 正在学习`);
  }
}

const student1 = new Student('王五', 18, '2024001');

// 验证原型链关系
console.log(student1.__proto__ === Student.prototype); // true
console.log(Student.prototype.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true

可以看到,extends 本质上是帮我们自动完成了"子类原型指向父类原型"的操作(Student.prototype.proto = Person.prototype),省去了手动修改原型链的麻烦。

六、原型链的实战应用场景

原型链不是抽象的概念,在实际开发中有很多实用场景,掌握这些场景能让你写出更优雅、更高效的代码。

6.1 扩展内置对象的方法

我们可以通过修改内置对象的原型,给所有实例添加共享方法。比如给 Array 扩展一个去重方法:

javascript 复制代码
// 给 Array 原型添加去重方法
Array.prototype.unique = function() {
  return [...new Set(this)];
};

// 所有数组实例都能调用这个方法
const arr1 = [1, 2, 2, 3, 3, 3];
const arr2 = [4, 5, 5, 6];
console.log(arr1.unique()); // [1, 2, 3]
console.log(arr2.unique()); // [4, 5, 6]

注意:尽量不要修改内置对象的原型(如 Object.prototype、Array.prototype),可能会与其他代码冲突,或覆盖原生方法。

6.2 实现对象的属性复用

当多个对象需要共享一些属性/方法时,不用重复定义,而是让它们的 __proto__ 指向同一个原型对象:

ini 复制代码
// 共享原型对象
const animalProto = {
  eat() {
    console.log('吃食物');
  },
  sleep() {
    console.log('睡觉');
  }
};

// 创建两个对象,共享 animalProto 的方法
const cat = { name: '小猫' };
const dog = { name: '小狗' };

// 让两个对象的 __proto__ 指向共享原型
cat.__proto__ = animalProto;
dog.__proto__ = animalProto;

// 两个对象都能调用共享方法
cat.eat(); // 吃食物
dog.sleep(); // 睡觉
console.log(cat.eat === dog.eat); // true(共享同一个方法)

6.3 理解框架中的原型链应用

很多前端框架都用到了原型链。比如 React 的 Class Component,本质上就是基于原型链实现的;Vue 的实例方法(如 <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t 、 mount、 </math>mount、emit),也是定义在 Vue.prototype 上,让所有 Vue 实例共享。

七、常见误区

7.1 误区 1:proto 与 prototype 混淆

记住一句话:实例有 proto,构造函数有 prototype (原型对象也有 proto,因为它也是对象)。

javascript 复制代码
function Person() {}
const p = new Person();

console.log(p.__proto__); // 有(实例)
console.log(Person.prototype); // 有(构造函数)
console.log(Person.__proto__); // 有(函数也是对象,指向 Function.prototype)
console.log(Person.prototype.__proto__); // 有(原型对象是对象,指向 Object.prototype)

7.2 误区 2:原型链的终点是 Object.prototype

Object.prototype 的 proto 指向 null,所以原型链的终点是 null,不是 Object.prototype:

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

八、总结

原型链是 JavaScript 面向对象的底层基础,核心要点可以总结为:

  1. 三个核心概念:构造函数、原型对象(prototype)、实例(proto);
  2. 原型链是实例通过 proto 层层向上追溯原型形成的链条,终点是 null;
  3. 原型链的核心作用是实现属性/方法共享和继承;
  4. ES6 Class 是原型链的语法糖,底层依然依赖原型链;
  5. 判断实例属性用 hasOwnProperty(),避免混淆原型属性。

理解原型链的关键是"动手实践"------多写代码验证三者关系、跟踪属性查找过程,慢慢就会豁然开朗。如果本文对你有帮助,欢迎点赞、收藏、转发~ 如有疑问,欢迎在评论区交流!

相关推荐
Tzarevich2 小时前
从命令式到声明式:用 Vue 3 构建任务清单的开发哲学
javascript·vue.js·响应式编程
醉风塘2 小时前
NPM:从“模块之痛”到“生态之基”的演化史
前端·npm·node.js
Mapmost2 小时前
【高斯泼溅】大场景可视化的「速度与激情」:Mapmost 3DGS实时渲染技术拆解
前端
研☆香2 小时前
深入解析JavaScript的arguments对象
开发语言·前端·javascript
parksben2 小时前
告别 iframe 通信的 “飞鸽传书”:Webpage Tunnel 上手指南
前端·javascript·前端框架
全栈前端老曹2 小时前
【前端权限】 权限变更热更新
前端·javascript·vue·react·ui框架·权限系统·前端权限
写代码的皮筏艇2 小时前
react中的useCallback
前端·javascript
用户8168694747252 小时前
Fiber 双缓存架构与 Diff 算法
前端·react.js
AAA简单玩转程序设计2 小时前
Java集合“坑王”:ArrayList为啥越界还能浪?
java·前端