在 JavaScript 的世界里,原型链是一个贯穿始终的核心概念,它不仅支撑着 JavaScript 的继承机制,也决定了对象属性的查找规则。对于前端开发者而言,搞懂原型链,就相当于掌握了 JavaScript 面向对象编程的 “密码”。今天,我们就从关键要点出发,一步步揭开原型链的神秘面纱。
一、先搞懂两个 “原型”:prototype
与__proto__
要理解原型链,首先得区分两个容易混淆的概念 ——prototype
和__proto__
,这是原型链的 “基石”。
1. prototype
:函数的 “专属属性”
在 JavaScript 中,所有函数(除箭头函数外)都自带一个prototype
属性,它是一个对象,我们称之为 “原型对象”。这个原型对象有一个默认的constructor
属性,指向该函数本身。
举个例子:
// 定义一个构造函数
function Person(name) {
this.name = name;
}
// 访问函数的prototype属性
console.log(Person.prototype);
// 输出:{constructor: ƒ Person(name), __proto__: Object}
console.log(Person.prototype.constructor === Person);
// 输出:true(constructor指向原函数)
我们可以在prototype
上添加属性或方法,这些内容会被该函数创建的所有实例 “共享”—— 这也是原型链实现代码复用的核心逻辑。
2. __proto__
:对象的 “隐藏链接”
所有对象(除null
和undefined
外)都有一个__proto__
属性(ES6 后可通过Object.getPrototypeOf()
安全访问),它指向创建该对象的 “构造函数的prototype
”。简单来说,__proto__
是对象与它的原型对象之间的 “桥梁”。
还是用上面的Person
构造函数举例:
// 创建Person的实例
const zhangsan = new Person("张三");
// 实例的__proto__指向构造函数的prototype
console.log(zhangsan.__proto__ === Person.prototype);
// 输出:true
这里要注意:__proto__
是对象的属性,prototype
是函数的属性,二者的指向关系是 “对象.proto → 构造函数.prototype”,这是原型链形成的基础。
二、原型链的本质:“层层向上” 的查找链条
理解了prototype
和__proto__
的关系后,原型链就很好理解了 ——原型链是由__proto__
串联起来的、从对象指向其原型对象,再指向原型对象的原型对象,直到Object.prototype
的链条。
1. 原型链的 “顶端”:Object.prototype
在 JavaScript 中,所有对象的原型链最终都会指向Object.prototype
,而Object.prototype
的__proto__
指向null
(表示链条的终点)。我们用一张简单的关系图来展示:
用代码验证一下:
console.log(zhangsan.__proto__.__proto__ === Object.prototype);
// 输出:true(Person.prototype的原型是Object.prototype)
console.log(Object.prototype.__proto__);
// 输出:null(原型链顶端)
2. 原型链的核心作用:属性查找机制
当我们访问一个对象的属性时,JavaScript 会遵循以下规则:
首先在对象自身上查找该属性,如果找到则直接返回;
如果没找到,就通过
__proto__
去它的原型对象上查找;如果原型对象上也没有,就继续通过原型对象的
__proto__
向上查找,直到Object.prototype
;如果
Object.prototype
上仍未找到,就返回undefined
。
这就是原型链的 “查找机制”,也是 “继承” 的本质。比如我们常用的toString()
方法,其实就是Object.prototype
上的方法,所有对象都能通过原型链访问到它:
console.log(zhangsan.toString());
// 输出:[object Object](zhangsan自身没有toString,从Object.prototype继承)
三、原型链与继承:JavaScript 的 “继承实现方式”
JavaScript 没有传统面向对象语言中的 “类”(ES6 的class
本质也是原型链的语法糖),它的继承完全依赖原型链实现。这里我们通过一个实例,看看原型链如何实现 “子类继承父类”。
示例:用原型链实现继承
假设我们有一个Animal
构造函数(父类),再定义一个Dog
构造函数(子类),让Dog
继承Animal
的属性和方法:
// 父类:Animal
function Animal(type) {
this.type = type; // 动物类型(如“哺乳动物”)
}
// 父类原型上的方法
Animal.prototype.eat = function() {
console.log(`${this.type}需要吃东西`);
};
// 子类:Dog
function Dog(name) {
this.name = name; // 狗的名字
}
// 关键步骤:让Dog的原型指向Animal的实例
Dog.prototype = new Animal("哺乳动物");
// 修复constructor指向(否则Dog.prototype.constructor会指向Animal)
Dog.prototype.constructor = Dog;
// 子类原型上的方法
Dog.prototype.bark = function() {
console.log(`${this.name}在汪汪叫`);
};
// 创建Dog实例
const erha = new Dog("二哈");
// 测试继承的属性和方法
console.log(erha.type); // 输出:哺乳动物(从Animal继承)
erha.eat(); // 输出:哺乳动物需要吃东西(从Animal.prototype继承)
erha.bark(); // 输出:二哈在汪汪叫(自身原型上的方法)
这里的原型链关系是:
erha(Dog实例)
__proto__ → Dog.prototype(指向Animal实例)
__proto__ → Animal.prototype
__proto__ → Object.prototype
__proto__ → null
通过这种方式,Dog
实例不仅能访问自身和Dog.prototype
的属性,还能通过原型链访问Animal
和Object
的原型属性,实现了 “继承”。
四、原型链的常见 “坑” 与注意事项
理解原型链时,很容易因为概念混淆踩坑,这里总结几个关键注意事项:
1. 不要直接修改__proto__
__proto__
是对象的 “隐藏属性”(ES6 标准中仅作为访问器存在),直接修改它会破坏原型链的稳定性,还会影响性能(因为浏览器会优化原型链查找,修改后优化失效)。如果需要修改原型,应通过修改构造函数的prototype
实现。
2. null
没有原型链
null
是原型链的终点,它没有__proto__
属性。如果尝试访问null.__proto__
,会直接报错:
console.log(null.__proto__); // 报错:Cannot read property '__proto__' of null
3. 箭头函数没有prototype
箭头函数是 “简化版函数”,它没有prototype
属性,也不能作为构造函数使用(用new
调用箭头函数会报错)。因此,箭头函数无法参与原型链的构建。
4. 原型对象的修改会 “影响所有实例”
因为所有实例共享构造函数的prototype
,如果修改了prototype
,已创建的实例也会受到影响。比如:
// 先创建实例
const lisi = new Person("李四");
// 修改Person.prototype
Person.prototype.age = 18;
// 实例能访问到新添加的属性
console.log(lisi.age); // 输出:18
五、总结:原型链的核心要点
最后,我们用一句话总结原型链的核心:原型链是由__proto__
串联的对象原型链条,它决定了属性查找规则,是 JavaScript 继承的基础,最终指向Object.prototype
,终点为null
。
掌握原型链,不仅能帮我们理解class
、继承
等高级概念的本质,还能在遇到 “属性找不到”“继承失效” 等问题时,快速定位根源。希望这篇文章能让你对原型链有更清晰的认识,下次再面对原型链相关问题时,就能从容应对啦!