注:本文是学习事件循环后的个人笔记,建议配合以下参考资料一起阅读。
资料来自
- MDN:[继承与原型链](继承与原型链 - JavaScript | MDN)
- 和 Claude 的互动式问答的结果
日常使用js里经常遇到一个需求:用map对数组做映射 比如把数组内每一个值都加上后缀:
const arr = [10, 20, 30, 40, 50];
const res = arr.map(item => item + "%");
但在控制台打印 arr,展开后会发现,它自己身上并没有 map 方法,不过有一个[[Prototype]]:
再次展开[[Prototype]],就能找到map:
为什么明明arr里没有写关于map的定义,但却能直接使用?这里的[[Prototype]]又是什么,为什么map会在它上面?这就涉及到了一个叫做”原型链“的概念,下文会详细讲解。
[[Prototype]] 是什么
[[Prototype]] 是用于指定对象的原型对象的内部插槽,它指向自己的原型。使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数分别访问和修改 [[Prototype]] 内部插槽。
注:内部插槽"是 ECMAScript 规范的术语,意思是引擎内部实现用的,不能直接用 obj.[[Prototype]] 这样的语法访问,只能通过 Object.getPrototypeOf() 来读取。
原型对象和原型链
每个对象内部都有一个 [[Prototype]],指向另一个对象,这个被指向的对象就叫做它的原型对象。
原型对象本身也是一个普通对象,也有自己的 [[Prototype]],指向另一个对象……这样一路链接下去,直到 null 为止,这条链就叫做原型链。
如图所示:
为什么会有原型对象和原型链
JavaScript 在设计之初选择了基于原型的继承模式——对象直接从其他对象继承,而非像传统的基于类的语言那样必须先定义类。这让语言在保持轻量的同时,天然支持了动态、灵活的代码复用。
注:Javascript 跟 Java 没有任何关系,只是为了蹭 Java 热度而把名字从 LiveScript 改成了JavaScript。
原型对象是从哪来
在创建对象的阶段,无论是直接创建对象字面量const obj = {},还是通过构造函数创建实例const dog = new Animal(),js引擎都会自动设置其 [[Prototype]],指向它的原型对象,以及设置constructor字段,指向自己的构造函数。
代码说明
const dog = new Animal();
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true
console.log(Animal.prototype.constructor === Animal); // true
引擎自动完成的过程(伪代码)
const dog = {};
dog.[[Prototype]] = Animal.prototype;
Animal.prototype.constructor = Animal;
构造函数和 new 关键字
构造函数只是一个普通函数,区别于其他函数,它的首字母采用大写(例如Animal),并且通常采用new的形式调用。
在用new调用时做了三件事:
- 创建一个新对象
- 把这个新对象的
[[Prototype]]设置为构造函数的prototype - 把
this绑定到这个新对象,然后执行构造函数
代码示例
function Animal(name) {
this.name = name;
}
const dog = new Animal("旺财");
console.log(Object.getPrototypeOf(dog) === Animal.prototype);
console.log(dog.name); // "旺财"
效果上,等价于
function Animal(name) {
// new 会把 this 绑定到新创建的对象上
// 普通函数写法无法还原这一步,这里省略
return { name }
}
const dog = Animal("旺财");
Object.setPrototypeOf(dog, Animal.prototype)
prototype 和 [[Prototype]] 的区别
prototype:是一个显式属性。只有函数(特别是构造函数)才拥有它。当你定义一个函数时,JS 会自动为它创建一个 prototype 对象。
代码示例
function Person(name) {
this.name = name;
const sayHi = () => {
console.log(`hello, I am ${name}`);
};
}
// 打印后会看到{},这就是Person的原型对象,是随着构造函数的定义的时同时创建的
console.log(Person.prototype)
[[Prototype]]:是一个隐式插槽(Internal Slot)。所有对象(包括函数、数组、普通对象)都拥有它。 它是引擎内部使用的私有属性,指向自己的原型对象,无法直接通过.[[Prototype]]访问,只能通过Object.getPrototypeOf函数访问,和Object.setPrototypeOf修改原型指向
代码示例
function Person(name) {
this.name = name;
const sayHi = () => {
console.log("hello")
}
}
const jack = new Person("jack")
// 无法访问,直接报错,因为只是内部插槽,不是真实存在的属性
console.log(jack.[[Prototype]])
// 可用于访问原型,会显示原型,该例子里为一个空对象
Object.getPrototypeOf(jack)
// 定义一个新的构造函数
function AnotherPerson(name) {
this.name = name;
}
// 在这个新原型上加一个特殊方法
AnotherPerson.prototype.identify = function() {
console.log("我现在是 AnotherPerson 的实例了!");
};
// 指定新的原型
Object.setPrototypeOf(jack, AnotherPerson.prototype)
// 验证
console.log(Object.getPrototypeOf(jack) === AnotherPerson.prototype); // true
jack.identify(); // "我现在是 AnotherPerson 的实例了!"
继承,class extends 的底层
在es6中,js提供了class和extends语法,可以实现让写法更接近 Java 等语言,让构建对象的过程更符合直觉,但其本质依旧是原型链,下文中将会进行说明。
下方的代码使用class的语法创建了一个对象
class Animal {
static planet = "Earth";
eat() {
console.log("eating...");
}
}
class Dog extends Animal {
bark() {
console.log("woof!");
}
}
const myDog = new Dog();
但其写法等价于
// 1. 定义父类构造函数
function Animal() {}
// 静态属性直接挂在函数名上
Animal.planet = "Earth";
// 实例方法挂在原型上,供所有实例共享
Animal.prototype.eat = function() {
console.log("eating...");
};
// 2. 定义子类构造函数
function Dog() {}
// 实例方法挂在原型上,供所有实例共享
Dog.prototype.bark = function() {
console.log("woof!");
};
// 3. 实现继承的关键:设置原型链
// 让 Dog.prototype 的原型指向 Animal.prototype
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// 让 Dog 本身指向 Animal(为了继承静态属性 planet)
Object.setPrototypeOf(Dog, Animal);
const myDog = new Dog();
验证
console.log(Dog.planet); // "Earth"
console.log(Dog.planet === Animal.planet); // true
总结
在js中,函数也是一种特殊形式的对象,也正因如此,js才能用基于原型链的形式实现构造实例、继承等操作。
因为是对象,所以我们可以随时修改 prototype 指向,或者用 Object.setPrototypeOf 强行改变血缘。这种“动态修改对象属性”的能力,赋予了 JS 极高的灵活性。
附录:构造函数原型链完整图
注:.__proto__是实例用于访问自己原型的访问器,最初是非标准的浏览器私有实现,后来因为使用太广泛而被纳入规范,但标注为遗留特性。推荐使用标准的 Object.getPrototypeOf() 替代。