继承和原型链:js如何实现继承

0 阅读5分钟

注:本文是学习事件循环后的个人笔记,建议配合以下参考资料一起阅读。

资料来自


日常使用js里经常遇到一个需求:用map对数组做映射 比如把数组内每一个值都加上后缀:

const arr = [10, 20, 30, 40, 50];
const res = arr.map(item => item + "%");

但在控制台打印 arr,展开后会发现,它自己身上并没有 map 方法,不过有一个[[Prototype]]image.png

再次展开[[Prototype]],就能找到map: image.png

为什么明明arr里没有写关于map的定义,但却能直接使用?这里的[[Prototype]]又是什么,为什么map会在它上面?这就涉及到了一个叫做”原型链“的概念,下文会详细讲解。

[[Prototype]] 是什么

[[Prototype]] 是用于指定对象的原型对象的内部插槽,它指向自己的原型。使用 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数分别访问和修改 [[Prototype]] 内部插槽。

注:内部插槽"是 ECMAScript 规范的术语,意思是引擎内部实现用的,不能直接用 obj.[[Prototype]] 这样的语法访问,只能通过 Object.getPrototypeOf() 来读取。

原型对象和原型链

每个对象内部都有一个 [[Prototype]],指向另一个对象,这个被指向的对象就叫做它的原型对象

原型对象本身也是一个普通对象,也有自己的 [[Prototype]],指向另一个对象……这样一路链接下去,直到 null 为止,这条链就叫做原型链。

如图所示: image.png

为什么会有原型对象和原型链

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提供了classextends语法,可以实现让写法更接近 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 极高的灵活性。

附录:构造函数原型链完整图

image.png

注:.__proto__是实例用于访问自己原型的访问器,最初是非标准的浏览器私有实现,后来因为使用太广泛而被纳入规范,但标注为遗留特性。推荐使用标准的 Object.getPrototypeOf() 替代。