一文搞懂 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 引擎会按照以下规则查找:
- 首先在实例对象本身查找,如果找到,直接使用;
- 如果没找到,就通过
__proto__找到它的原型对象,在原型对象中查找; - 如果原型对象中也没找到,就通过原型对象的
__proto__向上查找,直到找到Object.prototype; - 如果在
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 没有 class 和 extends 关键字,继承完全依赖原型链实现。核心思路是:让子类的原型对象指向父类的实例,从而让子类实例能通过原型链访问到父类的属性和方法。
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 面向对象的底层基础,核心要点可以总结为:
- 三个核心概念:构造函数、原型对象(prototype)、实例(proto);
- 原型链是实例通过 proto 层层向上追溯原型形成的链条,终点是 null;
- 原型链的核心作用是实现属性/方法共享和继承;
- ES6 Class 是原型链的语法糖,底层依然依赖原型链;
- 判断实例属性用 hasOwnProperty(),避免混淆原型属性。
理解原型链的关键是"动手实践"------多写代码验证三者关系、跟踪属性查找过程,慢慢就会豁然开朗。如果本文对你有帮助,欢迎点赞、收藏、转发~ 如有疑问,欢迎在评论区交流!