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

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

资料来自


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

ini 复制代码
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字段,指向自己的构造函数。

代码说明

javascript 复制代码
const dog = new Animal();
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true
console.log(Animal.prototype.constructor === Animal); // true

引擎自动完成的过程(伪代码)

ini 复制代码
const dog = {};
dog.[[Prototype]] = Animal.prototype;
Animal.prototype.constructor = Animal;

构造函数和 new 关键字

构造函数只是一个普通函数,区别于其他函数,它的首字母采用大写(例如Animal),并且通常采用new的形式调用。

在用new调用时做了三件事:

  • 创建一个新对象
  • 把这个新对象的 [[Prototype]] 设置为构造函数的 prototype
  • this 绑定到这个新对象,然后执行构造函数

代码示例

javascript 复制代码
function Animal(name) {
  this.name = name;
}

const dog = new Animal("旺财");
console.log(Object.getPrototypeOf(dog) === Animal.prototype);
console.log(dog.name); // "旺财"

效果上,等价于

javascript 复制代码
function Animal(name) {
  // new 会把 this 绑定到新创建的对象上
  // 普通函数写法无法还原这一步,这里省略
  return { name }
}

const dog = Animal("旺财");
Object.setPrototypeOf(dog, Animal.prototype)

prototype[[Prototype]] 的区别

prototype :是一个显式属性 。只有函数 (特别是构造函数)才拥有它。当你定义一个函数时,JS 会自动为它创建一个 prototype 对象。

代码示例

javascript 复制代码
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修改原型指向

代码示例

javascript 复制代码
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的语法创建了一个对象

scala 复制代码
class Animal {
	static planet = "Earth";
	
	eat() {
		console.log("eating..."); 
	}
}

class Dog extends Animal {
	bark() {
		console.log("woof!"); 
	}
}

const myDog = new Dog();

但其写法等价于

javascript 复制代码
// 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();

验证

arduino 复制代码
console.log(Dog.planet); // "Earth"
console.log(Dog.planet === Animal.planet); // true

总结

在js中,函数也是一种特殊形式的对象,也正因如此,js才能用基于原型链的形式实现构造实例、继承等操作。

因为是对象,所以我们可以随时修改 prototype 指向,或者用 Object.setPrototypeOf 强行改变血缘。这种"动态修改对象属性"的能力,赋予了 JS 极高的灵活性。

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

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

相关推荐
AlienZHOU5 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc8 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师10 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen10 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒11 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow11 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81811 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007811 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明11 小时前
第 6 章 组件进阶
前端·vue.js
江华森11 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端