引言 💭
在之前的高频面试题整合中有简单介绍原型和原型链
,这篇文章再来深入探究一下。
JavaScript 与传统面向对象语言(如 Java 或 C++)不同,它采用基于原型的继承机制,而非基于类的继承。虽然 ES6 引入了
class
语法,但本质上仍是对原型继承的语法糖。
1. 什么是原型(Prototype)
1.1 原型的定义
在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]]
,这是语言规范中定义的隐藏属性,用于指向该对象的原型。虽然我们无法直接访问 [[Prototype]]
,但大多数浏览器实现提供了一个非标准的访问接口 __proto__
来操作它。
这个被 [[Prototype]]
指向的对象就是当前对象的原型。通过原型,对象可以访问它本身未定义但其原型中存在的属性和方法。
⚠️ 注意:虽然
__proto__
可以访问或修改原型,但它是早期实现遗留下来的方式,不推荐在现代代码中使用。
1.2 原型的访问方式
方法 | 说明 |
---|---|
obj.__proto__ | 非标准属性,现代浏览器支持,但不推荐使用 |
Object.getPrototypeOf(obj) | 标准方法,用于获取对象的原型 |
Object.setPrototypeOf(obj, proto) | 标准方法,用于设置对象的原型(慎用,性能较差) |
1.3 原型查找机制
当访问对象的某个属性时,JavaScript 会按照如下步骤查找:
- 检查对象本身是否拥有该属性;
- 若没有,则查找其原型(即
[[Prototype]]
指向的对象); - 若仍未找到,则继续沿原型链向上查找;
- 一直查找到原型链末端
null
为止,若仍未找到,则返回undefined
。
1.4 示例代码
let obj = { name: "Alice" };
console.log(obj.hasOwnProperty("name")); // true
console.log(obj.hasOwnProperty("toString")); // false
console.log(obj.toString()); // Object.prototype.toString
此例中:
name
是obj
自身的属性;toString
不是obj
的属性,但它的原型是Object.prototype
,其中定义了toString
方法。
2. 原型链(Prototype Chain)
2.1 定义
原型链是多个对象通过 [[Prototype]]
属性串联起来的引用链。这条链从对象本身开始,一直延伸到 null
。
通过这条链,JavaScript 实现了类似“继承”的效果。一个对象可以通过其原型访问另一个对象的属性和方法。
2.2 结构
let obj = {};
↓
obj.__proto__ === Object.prototype
↓
Object.prototype.__proto__ === null
2.3 原型链中的属性查找规则
let a = {};
console.log(a.toString()); // 从 Object.prototype 继承
当访问 a.toString()
:
a
没有toString
;- 查找
a.__proto__
(即Object.prototype
); - 找到
toString
方法并调用; - 若仍未找到,则查找
Object.prototype.__proto__
,即null
,此时终止查找。
3. 构造函数与 prototype 属性
3.1 构造函数的定义
JavaScript 中的构造函数是普通函数,只要通过 new
关键字调用,它就会生成一个新对象并将该对象的 [[Prototype]]
设置为构造函数的 prototype
属性。
3.2 构造函数示例
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
let p1 = new Person("Alice");
p1.greet(); // Hello, my name is Alice
流程说明:
- 执行
new Person("Alice")
时,会创建一个新对象p1
; p1.[[Prototype]]
被自动设置为Person.prototype
;p1.greet()
会查找p1
→Person.prototype
,找到greet
并调用。
3.3 prototype
与 [[Prototype]]
的区别
属性名 | 所属对象 | 类型 | 作用 |
---|---|---|---|
prototype | 构造函数 | 对象 | 用于设置由该构造函数创建的实例对象的原型 |
[[Prototype]] (或 __proto__ ) | 实例对象 | 对象 | 引用其原型对象 |
总结:
prototype
是构造函数自带的属性,而[[Prototype]]
是对象的内部属性。通过构造函数创建的对象,其[[Prototype]]
会自动指向构造函数的prototype
。
4. 原型链继承的实现方式
JavaScript 的继承并非基于类,而是通过原型链实现。你可以使用构造函数 + Object.create()
等方式来模拟传统语言的继承结构。
4.1 示例:Animal → Dog
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound.`);
};
function Dog(name, breed) {
Animal.call(this, name); // 继承属性
this.breed = breed;
}
// 继承方法
Dog.prototype = Object.create(Animal.prototype);
// 修正 constructor 指向
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
console.log(`${this.name} barks.`);
};
const dog1 = new Dog("Buddy", "Golden Retriever");
dog1.speak(); // Buddy barks.
4.2 原型链查找路径图
dog1
↓ [[Prototype]]
Dog.prototype
↓ [[Prototype]]
Animal.prototype
↓ [[Prototype]]
Object.prototype
↓
null
5. ES6 class 与原型的关系
虽然 ES6 引入了 class
语法,但它只是对原型链继承的语法糖封装,底层依然依赖 [[Prototype]]
。
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} meows.`);
}
}
const c = new Cat("Milo");
c.speak(); // Milo meows.
等价于:
function Cat(name) {
this.name = name;
}
Cat.prototype.speak = function() {
console.log(`${this.name} meows.`);
};
console.log(Object.getPrototypeOf(c) === Cat.prototype); // true
6. Object.create() 的继承方式
Object.create()
是另一种创建对象并设置原型的方式,尤其适用于纯粹对象之间的继承关系。
const animal = {
speak() {
console.log(`${this.name} makes a sound.`);
}
};
const dog = Object.create(animal);
dog.name = "Buddy";
dog.speak(); // Buddy makes a sound.
结构图:
dog
↓ [[Prototype]]
animal
↓ [[Prototype]]
Object.prototype
7. 常见误区 ⚠️
- ❌ 所有对象都有
prototype
属性
实际上,只有函数对象(即构造函数)才具有
prototype
属性。普通对象如{}
没有该属性,但它们有内部的[[Prototype]]
。
- ❌ 使用
__proto__
是标准做法
__proto__
是历史遗留的非标准属性,虽然现代浏览器普遍支持,但不推荐使用。应使用Object.getPrototypeOf()
和Object.setPrototypeOf()
来获取和设置原型。
8. 性能与实践建议
- 避免频繁使用
Object.setPrototypeOf()
:它会阻碍引擎优化,应尽量在对象创建时设置原型。 - 继承链不要太深:原型链越长,属性查找成本越高,影响性能。
- 方法共享建议写在原型上:可节省内存空间,避免重复绑定。
结语 ✒️
欢迎补充或纠正✍🏻,持续更新中……🚀🚀🚀