先从构造函数说起
任意普通函数用于创建一个对象时,它就被称为构造函数。
但我更愿意将其称为自定义构造函数,因为这个函数之所以能够作为构造函数,是因为我赋予了它这个功能,是我定义出来的。
1 | function Person(name) { |
上述代码就把Person()
函数作为自定义构造函数,生成了两个实例对象。
既然有自定义构造函数,难不成还有非自定义构造函数?
带着这个问题,反问一句,通过function
关键词就能创造出来一个函数,那么function这个功能是谁来实现呢?
其实JS内部还有一个底层设计好的的Function()函数,作为创建其他函数的构造函数,它不用我们自己去设置,只需要通过关键词function
调用即可。
为什么要有prototype?
现在要为张三和李四添加一个相同效果的sayHello
方法,可以用以下代码
1 | person1.sayHello = function () { |
代码执行后,会分别为两个实例对象分别添加相应的sayHello
属性。里面存放着相应的方法(就叫sayHello()
方法吧)。
现在只是两个人,如果要创建50个人,500个,5000个甚至更多,每个人也都要有该方法,为每个对象单独使用一段代码添加方法显然就不现实了,此时可以修改构造函数如下:
1 | function Person(name) { |
这样,就能够在创建实例对象时直接添加上该方法。并且在创建实例对象时,会为每个实例对象的属性或方法创建单独的内存空间存放,即使代码及实现效果相同也互不影响。验证如下:
1 | console.log(person1.sayHello === person2.sayHello); // false,它们不是同一个方法,各自占有内存 |
![](1-添加相同方法.png)
但这样做有一个问题,每个sayHello()
方法都占据单独的一块空间,现在只是两个人,如果要创建50个人,500个,5000个甚至更多,每个人也都要有该方法,那么内存占用就会爆炸高,那么该如何解决这种问题?
对此,JS的方法就是:既然效果相同,干脆只留一份,大家共享这一个方法,把它单独放在一个地方,谁用谁去调。
那么放到哪里?这就要考虑这些实例对象有什么共同的联系——无疑就是它们都是由同一个构造函数创建的,那就把这段代码交给构造函数保管吧。
为此构造函数Person()
特意开辟了一块空间,将这些需要进行共享的方法或属性进行存放,之后把这个空间的地址告诉构造函数,构造函数用prototype这个属性值存放这个地址。这样构造函数就可以通过prototype
访问到这块空间并对其进行修改。这块空间就被称为显式原型(为了避免这些名词造成理解上的困难,后面就叫prototype空间了)
❗在JS中,所有函数都有相对应的显示原型(也就是prototype空间)
1 | //构造函数 |
![](2-prototype空间.png)
constructor是什么,在哪里?
constructor在JS中的作用很简单,通过对象.constructor
指向创建该对象的构造函数。
1 | console.log(person1.constructor); // Person(name) {this.name = name;} |
既然任意JS对象都可以通过对象.constructor
指向创建该对象的构造函数,这就意味着上述每个person实例对象都有该方法,既然大家都有,何不把该方法也交给Person.prototype保管呢?
因此构造函数的prototype空间中也保存着constructor
方法,该方法的作用就是指回构造函数本身,就这样,prototype和constructor形成了闭环,能够实现构造函数和它的prototype空间相互访问。
![](3-constructor.png)
__proto__是什么?如何使用的?
上面提到通过将共享的方法放到构造函数的prototype空间中,以方便实例对象进行调用,那么实例对象如何才能调用这些方法呢?
在JS中,每个实例对象在创建的时候都会被告知其构造函数的prototype空间的地址,通过__proto__
属性进行标记(在有些浏览器中显示为[[prototype]]
)。因此,通过 对象.__proto__
方法就能够访问该空间并使用存在在其中的方法。
在JS中,当对象身上没有要使用的方法时,会自动通过__proto__
进入到共享空间去查找有没有相应的方法。
![](4-proto.png)
因此person1.constructor
真正的执行过程是:首先在person1身上查找constructor方法,发现没有,就根据__proto__
进入共享空间去查找,发现此处存在该属性且为一个方法,因此去执行该方法,就找到了构造函数那里。
⚠️需要注意的是:
(1)__proto__
是实例对象身上的属性,用于访问其构造函数的prototype空间,被称为隐式原型。
(2)在JS中,任意自定义的(构造)函数本身也是由Function()构造函数实例化出来的,所以自定义的(构造)函数本身也是一个实例对象,也有__proto__
属性。
也是因为所有的自定义函数都是由Function()构造函数实例化出来的,所有的自定义函数的__proto__
其实都指向同一个空间,即Function()的prototype空间。
而Function()构造函数则是JS底层实现的,所以现在先不用纠结谁生成的它。
(3)单纯作为实例对象(如person1、person2),身上是不会有prototype属性的,因为它不需要也不会用来为其他对象提供共享的内容!(它只需要利用别人提供的服务就好,本身不再对外提供服务)
![](5-构造函数的隐式原型.png)
Object()和Object.prototype
从这里开始,所提到的东西就涉及到了JS底层的实现,所有的自定义函数、变量,数组等等最终都会回到这里!
前面提到,所有的构造函数都有一个prototype空间,那么这个空间是以什么样的形式存在的?
1 | console.log(Person.prototype); // {sayHello: ƒ, constructor: ƒ} |
从上面代码中可以看出,prototype空间本质上是一个对象(Object),那么这个对象是怎么生成的,由谁生成的?下面我们继续探究。
既然是一个对象,我们可以把这个对象看做是某个构造函数实例化之后的结果,那么这个函数是谁?
1 | console.log(Person.prototype.__proto__.constructor); // ƒ Object() { [native code] } |
通过__proto__
我们来到了一个共享空间,又通过constructor
找到了该空间所对应的构造函数。也正是这个函数创造了Person.prototype
这里可以看到是一个名为Object()
的函数作为构造函数创建了Person.prototype空间,因此我们为内存空间中添加上与Object()相关的内容。
![](6-Object.png)
可以看到,Object()
函数创造了自定义函数Person()
的prototype空间,其实所有自定义函数的__proto__
都指向Object.prototype
,因为所有自定义函数的prototype空间都是一个纯粹的对象,JS中凡是纯粹的对象都是由Object()函数创建的。
JS万物皆对象
说到这里其实有一句话——JS中万物皆对象——就要进行解释了。
抛去底层代码不谈,所有的自定义构造函数是怎么来的?其本质是Function()函数的实例化对象。
而所有的自定义实例对象怎么来的?是自定义构造函数的实例化对象!
甚至每个自定义构造函数本身的prototype空间是怎么来的?是Object()函数的实例化对象!!
可以说,通过Object()和Function()两个底层函数,实例化出了所有的JS其他内容,因此JS中“万物皆对象”,这个对象指的就是实例化对象。
所以JS中所有能看到的东西身上都有__proto__
属性,其指向无外乎三处:Function()的prototype空间、Object()的prototype空间、自定义构造函数的prototype空间。(null的问题下面再谈)
![](7-JS万物皆对象.png)
Object()和Function()
前面所谈内容都是有逻辑、有推理的,可以一环一环扣下来进行推导,到了这里,就进入JS底层的设计了。这里就是人为规定的一些规则,没有什么必然逻辑。
在JS中,既然“万物皆对象”那么Object()和Function()是谁生成的实例对象呢?
按照我自己的理解。答案是:没有人生成他们它们。
🔸Object()和Function()本身其实就是JS设计出来的纯粹的函数,只不过为了体现“万物皆对象”的思想,使其看起来像是和对象一样,分别为他们两个添加了__proto__
属性。
🔸于是设计了Object()的__proto__
指向Function()的prototype,毕竟Object()是个函数;
🔸设计了Function.prototype的__proto__
指向Object.prototype,毕竟它是个prototype空间,可以按照对象去理解,而对象的__proto__
就是要指向Object.prototype的;
🔸设计了Function()的__proto__
指向Function.prototype,也就是自己的prototype空间,毕竟Function()是个函数,所有函数的__proto__
最终会指向Function.prototype
🔸为了让原型链能够有尽头,设计了Object.prototype的__proto__
指向null(而不是像Function.prototype那样指向自身)。(原型链下一节介绍)
就这样,底层的Object()和Function()以及它们的prototype空间之间的关系被规定了下来。
![](8-Object与Function.png)
结合前面的内容,JS中对象、函数等之间的关系就可以用下面这张图来表示:
![](9-原型链.png)
原型链是什么?
介绍原型链是什么之前,先回顾一下显式原型(prototype)和隐式原型(__proto__
)。
这两个概念通常与构造函数、实例对象同时出现,其关系如下:
显示原型和隐式原型有时指一个东西,就是构造函数的prototype空间。
显示原型是构造函数对prototype空间的称呼,通过prototype
访问。
隐式原型是实例对象对prototype空间的称呼,通过__proto__
访问。
通过__proto__
,实例对象能够使用其构造函数身上的方法,从而实现了**“继承”**。
![](10-proto实现继承.png)
又因为构造函数的prototype空间(被看做对象时)也有__proto__
属性,于是可以继续共享向上一级的方法,因此通过两次甚至更多次的“继承”,其实例对象也就能够使用既不在自身,也不再构造函数身上的一些方法。这种通过原型(prototype空间)实现的多级之间的继承关系,就被称为原型链。
通过原型链的继承,使得JS中一些基础的方法在封装于底层的同时还能提供给自定义函数、构造函数、对象去使用。
原型链实例
下面通过实例进行介绍:
比如在Object.prototype空间中定义了a()方法,虽然Person()构造函数和person1实例对象中都没有定义该方法,但两者均可以使用a方法。其流程如下:
(1)对于person1.a()
查找自身是否存在a(),不存在;
通过__proto__
进入Person.prototype空间,查找是否存在a(),不存在;
继续通过__proto__
进入Object.prototype空间,查找是否存在a(),存在;
返回a的执行结果
(2)对于Person.a()
查找自身是否存在a(),不存在;
通过__proto__
进入Function.prototype空间,查找是否存在a(),不存在;
继续通过__proto__
进入Object.prototype空间,查找是否存在a(),存在;
返回a的执行结果
(3)对于person2.b()
查找自身是否存在b(),不存在;
通过__proto__
进入Person.prototype空间,查找是否存在b(),不存在;
继续通过__proto__
进入Object.prototype空间,查找是否存在b(),不存在;
继续通过__proto__
来到null,,不存在b()且不能继续查找下去,则返回person1.b is not a function
;
![](11-原型链上的proto实例.png)
参考文章
JS 究竟是先有鸡还是有蛋,Object与Function究竟谁出现的更早,Function算不算Function的实例等问题杂谈 - 听风是风 - 博客园 (cnblogs.com)
帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)_码飞_CC的博客-CSDN博客_js prototype
用自己的方式(图)理解constructor、prototype、__proto__和原型链_飞歌Fly的博客-CSDN博客