一文搞懂 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 的实例方法(如 m o u n t 、 mount、 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(),避免混淆原型属性。

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

相关推荐
To_OC41 分钟前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic1 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy1 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志1 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble3 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长3 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯3 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜4 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司4 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
心中有国也有家5 小时前
AtomGit Flutter 鸿蒙客户端:ModalBottomSheet 实战
android·javascript·学习·flutter·华为·harmonyos