0
点赞
收藏
分享

微信扫一扫

用自己的方式(图)理解constructor、prototype、__proto__和原型链

爱情锦囊 2022-05-01 阅读 27

一开始,先说说为何这个知识点为什么理解起来这么乱

个人感觉原因有三:

  1. JS内函数即对象。
  2. Function对象和Object对象这两个内置对象的特殊性。
  3. 很多讲解图的指向一眼下去花里胡哨,看着都头疼[手动狗头]。

再说说,为何网上各位前辈的相关文章都难以参透

很多前辈在讲解相关知识点的时候都是从__proto__开始讲起,但在我看来,__proto__与prototype关系之密切是无法单独提出来讲的(单独讲就意味着难以理解);而prototype与constructor又有密切关系,这就造成一种很尴尬的处境,要先讲__proto__就必然需要同时讲解prototype和constructor属性,这个也就是为何对于小白的我们而言这些概念是那么的难以理解。(以上个人看法,仅供参考)

然后在讲讲我个人采取的理解方式

为了更轻松、更有动力地理解透,我采用从constructor到__proto__原型链一步步“拆解”的方式去理解,希望有好的效果。文章内容如下:

  1. 先理解为什么“函数即对象”
  2. constructor其实很纯粹
  3. prototype是为何而出现
  4. 真正的constructor属性藏在哪
  5. __proto__让实例能找到自己的原型对象
  6. 究竟何为原型链
  7. 原型链引出新的继承方式 
  8. 学了要用系列 | 手写一个new
  9. 总结

最后,讲讲往下看需要知道的那些小知识:

① 当任意一个普通函数用于创建一类对象时,它就被称作构造函数,或构造器。

function Person() {}
var person1 = new Person()
var person2 = new Person()
复制代码

上面代码Person( )就是person1和person2的构造函数。

② 可以通过对象.constructor拿到创建该实例对象的构造函数。

console.log(person1.constructor) // 结果输出: [Function: Person]
复制代码

Person函数就是person1对象的构造函数。

③ Function函数和Object函数是JS内置对象,也叫内部类,JS自己封装好的类,所以很多莫名其妙、意想不到的设定其实无需过分纠结,官方动作,神仙操作。

④ 原型对象即实例对象自己构造函数内的prototype对象。

一、先理解为什么“函数即对象”

先看以下代码:

function Person() {...}
console.log(Person.constructor) // 输出结果:[Function: Function]
// 上面是普通函数声明方法,生成具名函数,在声明时就已经生成对象模型。
console.log(Function.constructor) // 输出结果:[Function: Function]
console.log(Object.constructor) // 输出结果:[Function: Function]
复制代码

上面的代码构造了一个Person函数,我们能看出那些信息?

  1. Person虽被声明为一个函数,但它同样可以通过Person.constructor输出内容。输出内容说明Function函数是Person函数[普通声明的函数]的构造函数。
  2. Function函数同时是自己的构造函数。
  3. Function函数同样是Object这类内置对象的构造函数。

其实上面三点总结下来就是一句:在JS里,函数就是Function函数的实例对象。也就是我们说的函数即对象。上面的声明函数的代码其实几乎等同于下面代码:

// 使用Function构造器创建Function对象
var Person = new Function('...')
// 几乎?因为这种方式生成的函数是匿名函数[anonymous],并且只在真正调用时才生成对象模型。复制代码

在JS里,函数和对象包含关系如下:

二、constructor其实很纯粹

先忽略__proto__和prototype,直接理解constructor,代码例子:

function Person() {}
var person1 = new Person()
var person2 = new Person()
复制代码

下面一张图就画出了它们constructor的指向(忽略了__proto__和prototype):

图中,蓝色底是Person的实例对象,而Person、Function是函数(也是对象)。

首先,我们已经知道每个对象都可以通过对象.constructor指向创建该对象的构造函数。我们先假设每个对象上都有这么个constructor属性,然后理解如下:

  1. person1与person2是Person对象的实例,他们的constructor指向创建它们的构造函数,即Person函数;
  2. Person是函数,但同时也是Function实例对象,它的constructor指向创建它的构造函数,即Function函数;
  3. 至于Function函数,它是JS的内置对象,在第一点我们就已经知道它的构造函数是它自身,所以内部constructor属性指向自己。

所以constructor属性其实就是一个拿来保存自己构造函数引用的属性,没有其他特殊的地方。

在接下来的所有例子都将把Function对象视为Function对象自己的实例对象,通过去掉它的特殊性来更好理解相关概念。

三、prototype是为何而出现

上一步理解是很容易的,然后这时要求你去给Person的两个实例对象加上一个效果相同的方法,你写了以下代码:

// 下面是给person1和person2实例添加了同一个效果的方法sayHello
person1.sayHello = function() {
    console.log('Hello!')
}
person2.sayHello = function() {
    console.log('Hello!')
}
console.log(person1.sayHello === person2.sayHello) // false,它们不是同一个方法,各自占有内存复制代码

图示如下:

当你去对比这两个方法的时候,你会发现它们只是效果相同、名字相同,本质上却是各自都占用了部分内存的不同方法。这时候就出问题了,如果这时候有千千万万个实例(夸张)要这样效果同样的方法,那内存岂不是要炸。这时,prototype就出现解决问题了。

当需要为大量实例添加相同效果的方法时,可以将它们存放在prototype对象中,并将该prototype对象放在这些实例的构造函数上,达到共享、公用的效果。代码如下:

Person.prototype.sayHello = function() {
    console.log('Hello!')
}
console.log(person1.sayHello === person2.sayHello) // true,同一个方法复制代码

图示如下:

而之所以这种形式可以减少内存的浪费,是由于无需再拿出部分内存为同一类的实例单纯创建相关同一效果的属性或方法,而可以直接去构造函数的prototype对象上找并调用。

讲到这里,你需要知道的是,所有函数本身是Function函数的实例对象,所以Function函数中同样会有一个prototype对象放它自己实例对象的共享属性和方法。所以上面的图示是不完整的,应改成下图:

其实里面的sayHello也是个函数,也有自己的prototype,但不画出来了,免得头疼。

注意:接下来的用【原型对象】表示【创建自己的构造函数内部的prototype】!

四、真正的constructor属性藏在哪

看到上面,有些小伙伴就头疼了,你说的constructor属性为什么我就没在console出来的对象数据中看到呢?

我相信你们懂我的意思了,constructor是完全可以被当成一个共享属性存放在原型对象中,作用也依然是指向自己的构造函数,而实际上也是这么处理的。对象的constructor属性就是被当做共享属性放在它们的原型对象中,即下图:

这时候有人会拿个反例来问:如果是共享属性,那我将两个实例其中一个属性改了,为啥第二个实例没同步?如下面代码:

function Person() {}
var person1 = new Person()
var person2 = new Person()
console.log(person1.constructor) // [Function: Person]
console.log(person2.constructor) // [Function: Person]
person1.constructor = Function
console.log(person1.constructor) // [Function: Function]
console.log(person2.constructor) // [Function: Person] !不是同步为[Function: Function]复制代码

这个是因为person1.constructor = Function改的并不是原型对象上的共享属性constructor,而是给实例person1加了一个constructor属性。如下:

console.log(person1) // 结果:Function { constructor: [Function: Function] }复制代码

你可以看到person1实例中多了constructor属性。它原型对象上的constructor是没有改的。

嗯。嗯?嗯?!搞事?!! 这下共享属性能理解了,但上面的图解明显会造成很大的问题,我们根本不能通过一个对象.constructor找回创建自己的构造函数(之间没有箭头链接)!

好的,不急,第四点只是告诉你为什么constructor要待在创建自己的构造函数prototype上。接下来是该__proto__属性亮相了。

五、__proto__让实例能找到自己的原型对象

带着第四点的疑问,我们如果要去解决这个问题,我们自然会想到在对象内部创建一个属性直接指向自己的原型对象,那就可以找到共享属性constructor了,也就是下面的关系:

  1. 实例对象.__proto__ = 创建自己的构造函数内部的prototype(原型对象)
  2. 实例对象.__proto__.constructor = 创建自己的构造函数

也如下图所示:

上面说的__proto__属性实际上也的确是这样的设置的,对象的__proto__属性就是指向自己的原型对象。这里要注意,因为JS内所有函数都是Function函数的实例对象,所以Person函数也有个__proto__属性指向自己的原型对象,即Function函数的prototype。至于Function函数为何有个__proto__属性指向自己(蓝色箭头)也不用解释了吧,它拿自身作为自己的构造函数,反正就是个特例,不讲道理。

这个就是JS内部的操作了,当在一个实例对象上找不到某个属性时,JS就会去它的原型对象上找是否有相关的共享属性或方法,所以上面的例子中,person1对象内部虽然没有自己的constructor属性,但它的原型对象上有,所以能实现我们上面提到的效果。当然后面还涉及原型链,你只要知道上面一句话能暂时回答这个问题就好。

的确,它也是个对象,也的确有个__proto__指向自己的原型对象。那我们尝试用代码找出它的构造函数,如下:

function Person() {}
console.log(Person.prototype.__proto__.constructor) // [Function: Object]复制代码

因为__proto__指向原型对象,原型对象中的constructor又指向构造函数,所以Person.prototype.__proto__.constructor指向的就是Person中prototype对象的构造函数,上面的输出结果说明了prototype的构造函数就是Object函数(对象)。

下面一张图就画出了文章例子中所有__proto__指向,我们试试从中找出它的猫腻。

在第一点我们就讲了所有的函数都是Function函数的实例(包括Function自己),所以他们的__proto__自然也就都指向Function函数的prototype对象。

Object函数作为JS的内置对象,也是充当了很重要的角色。Object函数是所有对象通过原型链追溯到最根的构造函数。换句话说,就是官方动作,不讲道理的神仙操作。

这是由于Object函数的特殊性,有人会想,为什么Object函数不能像Function函数一样让__proto__属性指向自己的prototype?答案就是如果指向自己的prototype,那当找不到某一属性时沿着原型链寻找的时候就会进入死循环,所以必须指向null,这个null其实就是个跳出条件。

上面谈到原型链,有些小兄弟还不知道是什么东西,那接下来看看何为原型链,看懂了再回来重新理解一下猫腻三的解释。

六、究竟何为原型链

在让我告诉你何为原型链时,我先给你画出上面那个例子中所有的原型链,你看看能不能看出一些规律。上面的例子中一共有四条原型链,红色线连接起来的一串就是原型链

左边的图:原型链也就是将原型对象像羊肉串一样串起来成为一条链,好粗暴的解释,但的确很形象。

右边的图:之前说过Person函数(所有函数)其实是Function函数的实例,假设把它看成一个普通的实例对象,忽略它函数身份以及prototype对象,其实它和左边图中的person1没什么区别,只是它们的__proto__属性指向了各自的的原型对象。

左边的图:Function函数因为是个特殊的例子,它的构造函数就是自己,所以__proto__属性也指向自己的prototype对象;但它的特殊性并不影响它的prototype对象依然不出意外的是Object函数的实例

右边的图:这个理解起来就很难受,因为Object函数和别的函数一样也是Function函数的实例,所以它的__proto__属性毫无例外地是指向Function函数的prototype对象,但是问题是Function函数中的prototype本身又是Object函数的实例对象,所以Function函数中的prototype对象中的__proto__属性就指向Object函数的prototype对象,这就形成“我中有你,你中有我”的情况,也是造成难以理解的原因之一。

为了更好地理解原型链,我打算忽略掉那讨厌的特例,Function函数。

忽略掉Function函数后你会发现好清爽!相信大家也发现了,__proto__属性在其中起着关键作用,它将一个个实例和原型对象关联在一起,但由于所关联的原型对象也有可能是别人的实例对象,所以就形成了串连的形式,也就形成了我们所说的原型链。 

举报

相关推荐

0 条评论