10分钟了解原型链
2024-12-09 11:13:16

原型链,从来都没去理解过,为了面试特意查了一下,发现早就是在工作里边用的熟悉了,只是我没有去深入了解这块。

正文

工作中,为了调试,我们常常输出对象查看内容。

例如,我在vue中输出内容,经常会发现会经常看到Prototype,__proto__,.constructor等意义不明的东西。

后来工作久了,细细查了一下,才知道这玩意是原型链。

毕竟我不是正规前端出身,做事儿总是奉行能用就行,所以后来一直没详细整理这块的内容。

如今闲下来了,针对这块内容,这里梳理一下,也防止面试被问到。

必要概念

在正式进入原型链之前,先简单了解一些简单的概念。

构造函数

构造函数和普通函数本质上没什么区别,只不过使用了new关键字创建对象的函数,被叫做了构造函数。

构造函数命名通常采用首字母大写的方式,以便与普通函数和变量进行区分。

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
this.species = '人类';
this.say = function () {
console.log("Hello");
}
}

let per1 = new Person('xiaoming', 20);

Object

在JavaScript中,Object构造函数创建一个对象包装器。

如果给定值是null或undefined,将会创建并返回一个空对象,否则,将返回一个与给定值对应类型的对象。

当以非构造函数(即,没有使用new)的方式被调用时,Object函数将转换为对象。

它可以将任何值转换为对象,这种方式通常被用于将基本数据类型(如数字、字符串和布尔值)转换为相应的对象。

以下代码举例,可以看出Object的转换能力。

1
2
3
4
5
let num = 123;
let obj = Object(num);

console.log(obj); // 输出:Number {123}
console.log(typeof obj); // 输出:"object"

根据上面构造函数的定义,Object是满足当做一个构造函数的要求的。

既然是个函数了,那么就会有prototype属性。

这里先埋一个伏笔,到现在只需要记住,Object是JS运行时就创建好了的,是JS内置的,并且Object身上有个prototype属性即可

Function

Function是一个特殊的构造函数,它是在JavaScript运行时就创建的一个对象

Function是所有函数的构造函数,先通过代码的方式举例一下通过new Function()的方式创建一个函数。

1
2
3
4
5
const sum = new Function('a', 'b', 'return a + b;');
console.log(sum(2, 3)); // 输出 5

const greet = new Function('name', 'console.log("Hello, " + name + "!");');
greet('John'); // 输出 "Hello, John!"

到这里我们知道了new Function() 构造函数可以动态地创建函数,从这里先解释一下上面有关Object的解释中的伏笔。

既然所有函数都是Function生产出来的,那么Object这个构造函数是不是也是Function生产出来的呢?

答案是的。

那Function自身怎么来的呢?

答案是Function创造了Function,没错这是一个特殊的情况,因为万物都有个源头,Function和Object一样,都是JS在运行时就创建好了。

例如下面代码,可以先略过。看完下面有关constructor 的解释再回来看这个就理解了。

1
2
3
function abc () {}
console.log(abc.constructor === Function) // 输出 true
console.log(Function.constructor === Function) // 输出 true

这段代码就印证了,所有函数都是Function生产的(包括Function自身也是)

显式原型(prototype)

显示原型就是利用prototype属性查找原型,只是这个是函数类型数据的属性。

当然,这个东西在我们那个时候叫显示原型,现在叫做原型对象,本质上是一个东西。

个人感觉现在的说法更贴切,因为这东西本质上指向的内容是对象,所以叫做原型对象,更贴切。

  1. Prototype是函数的一个属性
  2. 是个对象
  3. 创建函数的时候时候,自带该属性

隐式原型(__proto__)

隐式原型是利用__proto__属性查找原型,这个属性指向当前对象的构造函数的原型对象

1
2
console.log(per1.__proto__ === Person.prototype); // true
console.log(per2.__proto__ === Person.prototype); // true
  1. 对象的属性
  2. 指向构造函数Prototype

构造器(Constructor)

对于原型对象Prototype来说,它有个constructor属性,指向它的构造函数。

1
2
console.log(Object.\__proto__ === Function.prototype) // 结果为true

经典图解

在了解了上述的名词之后,我们再看下边的这张图,应该就会清晰很多了。

以下这张图,代入核心知识点。

为了清晰的解释这个关系,我们先在脑海中抽象出两个线路,一个是函数线,一个对象线

这两个线,只是暂时的把函数和对象两个概念做个区分,但实际上,函数属于对象,只是一个特殊的对象而已。

在JS中一切皆对象,而所有的对象原型最终指向的是null

1
牢记下面我抽象出来的两个知识点

函数prototype__proto__ , 对象__proto__

写一段伪代码,只是帮助你理解原型链,但是这个核是一定要记住的。

1
2
3
4
5
6
7
Function = {
prototype,
__proto__
}
Object= {
__proto__
}

image-20240527221624949

个人拓展

之前看到有人在群里装逼,说隐式原型(__proto__)已经被废止,说已经使用新的[[Prototype]]做标识,听他说的信誓旦旦,我还真信了。

后来查了一下MDN的相关文档,大致闹明白了是怎么回事儿。

[[Prototype]] 是什么

ECMA-262使用一些内部特性来描述属性的特征,这里放一下官方文档的原文。

1
2
3
4
5
遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。
内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数来访问。
这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器。
为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)。
它不应与函数的 func.prototype 属性混淆,后者指定在给定函数被用作构造函数时分配给所有对象实例的 [[Prototype]]。我们将在后面的小节中讨论构造函数的原型属性。

这段话说的挺绕,但实际就很简单的意思:这些特性是由为JavaScript实现引擎的规范定义的,因此,开发者不能在JavaScript中直接访问这些特性。

为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Prototype]]、[[Enumerable]]等。

[[Prototype]]就是这个一个内部属性, 它指的是对象的原型,脚本中没有访问这个[[Prototype]]特性的标准方式,但Firefox、Safari和Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型。

因此,所谓的__proto__被废除,纯粹是瞎说,我亲自在控制台上敲过,用的很正常。

而[[Prototype]] ,也只是内部属性,并不能在控制台中被执行。

所以,原型链这种基础的东西,几乎不太可能动的,有些人纯粹的是胡编的。

结语

虽然很绕,但是其实一旦理解了就很容易明白,而明白之后,再看那张经典的原型链图就会非常明白。

B站的【前端八股文】原型和原型链,还有掘金的你可能不太理解的JavaScript - 原型与原型链这篇文档都解释的很不错,我算是照猫画虎的理解了一下。

当然,我也翻看了现代MDN的文档,发现似乎隐式原型(__proto__)被官方弃用了,现在似乎

只是,这块的知识,除非是涉及到基础框架开发,不然大多数时候是用不上的,属于一看就能明白是什么意思的基础问题,但开发根本用不到太多。

参考

【前端八股文】原型和原型链

你可能不太理解的JavaScript - 原型与原型链

原型和原型链–图解

继承与原型链 - JavaScript | MDN (mozilla.org)