梳理目录
- 为什么"函数即对象"
constructor其实很纯粹prototype是为何实现的- 真正的
constructor属性藏在哪里 __proto__让实例能够找到自己的原型对象- 究竟何为原型链
- 原型链引入新的继承方式
- 手写new
小知识
- 当任意一个普通函数用于创建一个类对象时,它就可以被称为构造函数,或者构造器
function Person(){}
let person1 = new Person()
let person2 = new Person()
上面的代码中 Person()就属于构造函数,person1,person2即属于Person()的实例
- 可以通过
对象.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虽然申明一个函数,当时它也可以通过Person.constructor输出其构造函数,它的构造函数为Function说明Function为Person的构造函数Function函数同样是自己的构造函数Function函数同样是内置对象Object类的构造函数
总结 在js里,函数就是Function的实例对象,也就是我们所说的函数即对象
上面的函数几乎等于下面这种:
// 使用Function构造器创建Function对象
var Person = new Function('...')
// 几乎?因为这种方式生成的函数是匿名函数[anonymous],并且只在真正调用时才生成对象模型。
在JS里,函数和对象包含关系如下:
总结:对象由函数创建,函数都是Function对象实例
二 constructor其实很纯粹
先忽略__proto__和prototype,直接理解contructor
代码列子:
function Person(){}
const person1 = new Person()
const person2 = new Person()
下面这张图就反映了他们constructor的指向(忽略__proto__和prototype)
图中,蓝色底是Person的实例对象,而Persons和Function的函数(也是对象)
首先,我们已经知道每个对象都可以通过对象.constructor找到自己的构造函数,我们先假设每个对象上面都有constructor属性,然后理解:
注意:
constructor属性不一定是对象本身的属性,这里只是方便理解。第三点会详细讲
person1,person2都是Person的实例,它们的constructor都指向它们的构造函数Person函数Person是函数,但同时他也是Function实例对象,它的constructor指向创建它的构造函数即Function函数Function函数为js的内置对象,它的构造函数是它本身,所以它的constuctor指向它本身
所以constructor属性其实就是拿来保存自己构造函数引用的属性,没有特殊地方
三 prototype是为何出现的
要求给上一步Person的两个实例上添加同一个方法,以下代码:
// 下面给a,b实例添加同一个效果的方法speak
person1.speak = function(){
console.log('Hello')
}
person2.speak = function(){
console.log('Hello')
}
console.log(person1.speak === person2.speak) // false 不是同一个方法,各自占自己内存
图示如下:
这2个方法效果相同,名字相同。本质上却是各自占用了部分内存的不同方法,如果有千千万万个这样实例。内存就炸了。这时prototype就出现了。
当需要为大量实例添加相同的效果的方法,可以将他们放入prototype对象当中,并且将该prototype对象放在这些实例的构造函数上面,达到共享,共用的效果,代码如下:
Person.prototype.speak = function(){
console.log('Hello');
}
console.log(person1.speak === person2.speak); //true
图示如下:
而之所以以这种形式可以减少内存浪费,由于无需再拿出内存为同一类的实列单纯创建相关同一效果的属相或者方法,而可以直接去哪构造函数prototype对象上面查找调用。
总结:
prototype对象用于放某同一类型实列的共享方法,实质上是为了内存找想。不用为同一种方法多次定义。
到这里你需要知道,所有函数本身是Function函数的实列对象,所以Function函数中同样会有一个prototype对象放他自己实列对象的共享属性和方法。所以完整图示如下:
注意:接下来的用【原型对象】表示【创建自己的构造函数内部的prototype】
四 真正的constructor属性藏在哪
对象的
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了,也就是下面的关系:
- 实列对象.
__proto__= 创建自己的构造函数内部的prototype(原型对象) - 实列对象.
__proto__.constructor= 创建自己的构造函数
如图所示
上面说的
__proto__确实是这样设置的,对象的__proto__属性就是指向自己的原型对象。这里要注意,应为JS内所有的函数都是Function函数的实列对象,所以Person函数也有__proto__属性指向自己的原型对象,Function函数的Prototype。至于Function函数为何有个__proto__属性指向自己,就是个特列。
疑惑:实列对象.constructor等于实列对象.proto.constructor?
这就是JS内部操作,当一个实列对象上面找不到这个属性时候,JS就会去他的原型对象上面找是否有相关的共享属性或者方法,所以上面的列子中,a对象内部虽然没有自己的constructor属性,但是在他的原型对象上面有,所以就实现上面的效果。
疑惑:prototype也是对象吧,他肯定也有__proto__吧?
是的,他也是个对象,也有
__proto__指向自己的原型对象
如下:
function Person() {}
console.log(Person.prototype.__proto__.constructor) // [Function: Object]
因为__proto__指向原型对象,原型对象中的constructor又指向构造函数,所以Person.prototype.__proto__.constructor指向就是Person中的prototype对象的构造函数,上面输出的结果说明了prototype的构造函数就是Object函数。
总结:函数内部的prototype也不过是普通的对象,并且默认也都是Object对象的实列
下面这张图画出了文章中所有__proto__指向,我们试着找出猫腻。
猫腻一:所有函数的__proto__指向他们的原型对象,即Function函数的Prototype对象
在第一点我们就讲了所有的函数都是
Function函数的实列,所以他们的__proto__自然也就指向Function函数的prototype对象
猫腻二:最后一个prototype对象是Object函数内的prototype对象
Object函数是所有对象通过原型链追溯到最根的构造函数。
猫腻三:Object函数的prototype中的__proto__指向null。
这是由于
Object函数的特殊性,有人会想,为什么Object函数不能像Function函数一样让__proto__属性指向自己的prototype?答案就是如果指向自己的prototype,那当找不到某一属性时沿着原型链寻找的时候就会进入死循环,所以必须指向null,这个null其实就是个跳出条件。
六 究竟何为原型链
在让我告诉你何为原型链时,我先给你画出上面那个例子中所有的原型链,你看看能不能看出一些规律。上面的例子中一共有四条原型链,红色线连接起来的一串就是原型链:
左边的图: Function函数因为是个特殊的例子,它的构造函数就是自己,所以__proto__属性也指向自己的prototype对象;但它的特殊性并不影响它的prototype对象依然不出意外的是Object函数的实例
右边的图: 这个理解起来就很难受,因为Object函数和别的函数一样也是Function函数的实例,所以它的__proto__属性毫无例外地是指向Function函数的prototype对象,但是问题是Function函数中的prototype本身又是Object函数的实例对象,所以Function函数中的prototype对象中的__proto__属性就指向Object函数的prototype对象,这就形成“我中有你,你中有我”的情况,也是造成难以理解的原因之一。
为了更好地理解原型链,我打算忽略掉那讨厌的特例,Function函数。
忽略掉
Function函数后你会发现好清爽!相信大家也发现了,__proto__属性在其中起着关键作用,它将一个个实例和原型对象关联在一起,但由于所关联的原型对象也有可能是别人的实例对象,所以就形成了串连的形式,也就形成了我们所说的原型链
七 原型链引出新的继承方式
首先回顾两点
prototype对象保存着构造函数给它的实例们调用的共享属性和方法- 实例对象当没有某一属性时,会通过
__proto__属性去找到创建它们的构造函数的prototype对象,并在里面找有没有相关的共享属性或方法。
那这时就很有趣了。prototype对象本身也有一个__proto__属性指向它自己的原型对象,上面有着构造函数留下的共享属性和方法。那这么说的话,假如当在自己原型对象上找不到相关的共享属性或方法时,对于它现在所在的prototype对象而言,也是一次寻值失败的情况,那它自然也会去它自己的原型对象上找,世纪大片图示如下:
现在来想想,假如Object函数内的prototype对象中__proto__属性不指向空,而指向自己的prototype?那不完了咯,死循环。
可能这时有小兄弟会问,这不就是一个不断找值的过程吗,有什么意义?但是就因为这种巧合,让一些可爱的人想到了一种新的继承方式:原型链继承。
请看下面代码:
function GrandFather() {
this.name = 'GrandFather'
}
function Father() {
this.age = 32
}
Father.prototype = new GrandFather() // Father函数改变自己的prototype指向
function Son() {}
Son.prototype = new Father() // Son函数改变自己的prototype指向
var son = new Son()
console.log(son.name) // 结果输出:GrandFather
console.log(son.age) // 结果输出:32
console.log(Son.prototype.constructor) // 结果输出:[Function: GrandFather]
相关指向图如下:
两边的图都是忽略了Function函数的,同时将一些没有必要展示出来的属性给忽略了,如各大函数的__proto__属性。
边的图: 在没有改变各个函数的prototype的指向时,默认就是左边的图片所示。每个函数的prototype都是默认情况下将它们内部的__proto__指向Object函数的(黑色箭头)。
右边的图: Father函数和Son函数都丢弃了它们各自的prototype对象,指向一个新的对象。这形成了三个新的有趣现象:
- Father函数中的
prototype指向了GrandFather的实例对象,这时候这个实例对象就成为了Father函数以后实例的原型对象,顺其自然GrandFather实例对象内的私有属性name就变成了Father函数以后实例的共享属性; - 同样的,Son函数中的
prototype指向了Father的实例对象,将Father的实例对象内的私有属性age就变成了Son函数以后实例的共享属性。 - 它们的
__proto__属性将它们串了起来,形成一条新的原型链。
上面的操作我们能看到Son函数以后的实例都能通过原型链找到name和age属性,也就是实现了我们所说的继承,继承了父类的属性。不过相信眼尖的我们会发现这种继承方式问题很大:
constructor的指向不可靠了,像Son实例对象.constructor最后得到的值是沿着原型链找到的GrandFather函数。可我们自己清楚Son实例对象就该是Son函数,但却不在我们的意料之中。- 所有所谓继承下来的属性全都是共享属性,好致命的问题。
八、学了要用系列 | 手写一个new
new关键词的作用一句话来说就是创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。而我们要去手动实现new关键词,无非就是组织一场认亲活动,环节有两个:
- 让一个对象承认自己的构造函数(爹)就是该构造函数
- 让这个构造函数承认这个对象就是它自己的实例(子)
实现
- 先走个Person构造函数(爹)做列子
function Person(identity){
this.identity = identity || 'Person'
}
- 爹有了,创建空对象
var obj = {}
上面的语句为字面式创建对象,实则等同于下面一句
var obj = new Object()
也即说明创建的空对象其实都是Object函数的实例,这么一看,完了吧,子不认爹。
还记得我们上面讲的吗,所谓的“空对象“内部并不是真正空空如也,它们内部都有一个__proto__属性指向自己的原型对象。而上面代码中的obj对象也是毫不例外有个__proto__属性指向Object对象中的prototype。
我们知道当创建某一构造函数的实例,创建出的实例应该将__proto__属性指向该构造函数内的prototype对象,那我们就走走形式,让它重新认爹。
- 手动实现实列中的
__proto__属性指向原型对象
obj.__proto__ = Person.prototype
图解如下:
你可以看到当指向变化后,Person函数中的
prototype成为实例对象obj的原型对象,而自然而然我们拿到的obj.constructor就对应变成了Person函数。换句话说,obj已经承认Person函数是它自己的构造函数,也就说我们完成了认亲活动的第一环节。
那问题来了,Person函数承认这个实例(子)吗?
如果Person函数内部没有设置像:this.identity = identity || 'Person'这些语句(设置私有属性/方法),其实它也就承认了,因为成为它儿子不需要别的资格。但是不巧,Person函数确实有设置,而这些语句就像在说:
“你要成为我儿子就需要有这个资格:拥有我设置的私有属性。但我认了你后,你改不改那个属性、要不要那个属性,我就不管了。 “
所以现在得进入第二环节:
4.在实列的执行环境内调用构造函数,添加构造函数设置私有属性/方法
Person.apply(obj,arguments) //arguments就是参数
我们先要知道构造函数为啥叫构造函数:
构造函数是一种特殊的方法,主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值。 看到关键作用了吗? “为对象成员变量赋初始值”。
再看回“老爹”,Person函数:
function Person(identity){
this.identity = identity || 'Person'
}
console.log(Person.identity) // 结果输出:undefined
// 注意不要拿name这个属性做例子,每个函数声明后都自带一个name属性用来保存函数名
疑惑:这里的this不是指向构造函数自身的吗?为什么Person函数没有identity属性?
回答:感觉说来话长,简化成一句就是:函数声明后函数体内的语句并不会立即执行,而是在真正调用时才执行。所以里面的this在没有调用时压根没指向,或者根本没被当成属性,只是个代码段,所以自然也不会立即给自己赋一个identity属性。其实说这么多,就是为了引出实例通过apply方法调用构造函数,让构造函数体内此时真实存在的this指向自己,并为自己赋相应的初始属性值。至于arguments就是相应的参数,可以看成用于调整初始值如何设置的参数。
整个过程结束后,实例也拥有了构造函数Person内部要求设置的属性和方法,如下图:
这时我们就完成了让这个Person构造函数承认这个
obj对象就是它自己的实例,也就是第二环节顺利完成。
- 整个过程代码如下
// 构造函数登场
function Person(identity){
this.identity = identity || 'Person'
}
// 实例对象登场
var obj = {}
// 环节一:让obj承认自己的构造函数(爹)就是Person函数
obj.__proto__ = Person.prototype
// 环节二:obj调用Person,拥有Person给孩子们设置的属性/方法
// 让Person函数承认这个对象就是它自己的实例(子)
Person.apply(obj, ['son'])
// End 完成,验证
console.log(obj.constructor) // 输出结果:[Function: Person]
console.log(obj.identity) // 输出结果:son
上面只是一个实例对象new出来的过程,真正实现new方法还需要我们将它封装起来,如下:
6.封装成new方法
// 构造函数登场
function Person(identity){
this.identity = identity || 'Person'
}
// 封装自己的new
function _new(Fuc) {
return function() {
var obj = {
__proto__: Fuc.prototype
}
Fuc.apply(obj, arguments)
return obj
}
}
// 封装完成,测试如下
var obj = _new(Person)('son')
console.log(obj.constructor) // 输出结果:[Function: Person]
console.log(obj.identity) // 输出结果:son
耶耶耶,完成
九 总结
最后希望大家支持原创,我只是按照思路来了一遍,自己清晰了很多。 原创地址