放两个链接,强烈建议去读一读:
文一:JavaScript 原型精髓 #一篇就够系列
文二:用自己的方式(图)理解constructor、prototype、__proto__和原型链
在文一中我了解到为什么我一直看不懂各路大神的原型图,知道了原来要分成原型链、构造函数链两个来理解。
文二则帮我彻底理解了什么是构造函数链什么是原型链。
下面讲讲我所理解的原型链,看看我学成啥样了。如果对大家有所帮助就再好不过了!
初识三个属性
constructor
借鉴文二的顺序,先从constructor
讲起吧。
这个单词在不同场景下有不同的指代含义。
作为一个名词,它可以指「构造函数」这个函数类型。
在原型链这个场景下,它作为对象的一个属性名出现。
构造函数 constructor
construtor
谷歌翻译为构造函数
,所以会在有些场景中,他就指构造函数。
在ES6规范中,是这么形容constructor
的:
4.3.4 constructor
function object that creates and initializes objects
翻译:创建和初始化对象的函数对象
在这个语境中,construtor
指的就是构造函数
。
对象的属性 constructor
同时constructor
也是对象的属性
。
相信我们想知道的也不是构造函数相关的,更多想要理清作为对象属性的constructor
是什么。
对象的属性?
比如说一个函数对象fun
,我们能在他的属性里面发现起码三个跟constructor
相关的属性:
let Fun = function(){this.a = 'a'}
Fun.constructor // ƒ Function() { [native code] }
Fun.__proto__.constructor // ƒ Function() { [native code] }
Fun.prototype.constructor // ƒ ƒ (){this.a = 'a'}
let afun = new Fun
afun.constructor // ƒ (){this.a = 'a'}
afun.__proto__.constructor // ƒ (){this.a = 'a'}
不必知道他们是做什么的,不必知道为什么输出这些,也不必知道__proto__
和prototype
是什么,后面都会讲的,现在了解constructor
会在这三种地方出现即可。
constructor是做什么的呢?
从他的直译上也可见,该属性与构造函数相关,
用constructor
作为对象的属性名,用途则是指向该对象的构造函数
所以fun.constructor
指向的就是fun的构造函数:JS内置对象Function
:ƒ Function() { [native code] }
prototype
我们看规范中对prototype的定义(看不懂没关系,后面细讲):
4.3.5 prototype
object that provides shared properties for other objects
NOTE When a constructor creates an object, that object implicitly references the constructor’s
prototype
property for the purpose of resolving property references.翻译:
为其他对象提供共享属性的对象
注意:当构造函数创建一个对象的时候,这个对象会隐式引用这个构造函数的prototype属性中的属性,以便于解析属性的引用
我来解释一下吧:
首先要知道的一点是,`prototype`只存在于函数对象中,
下文说起【`prototype`】时代表的意思就是【构造函数的`prototype`属性】。
先看第一句:
prototype 是为其他对象
提供共享属性
的对象
prototype 是对象
这句话略去定语,剩下的就是:【prototype 是对象】。prototype
属性中存放一个对象,或者说prototype
属性是一个对象。
对象中有很多属性。
形式大概类似这样(当然不会是这些内容,这里仅仅打个样儿):
Fun.prototype = {
a: function(){ console.log('a') }, // 存放方法
b: function(){ console.log('b') },
c: 'c' // 存放具体值
...
}
再说这句话里面的【其他对象】是哪些对象:
是通过构造函数Fun创建出来的对象,
比如let obj1 = new Fun
中Fun
的实例obj1
就是这个【其他对象】。
提供 共享属性 是怎么回事?
通过new Fun
可以创造实例obj1
、自然也可以创造obj2、obj3...
,
这是一个老生常谈的问题,看一下这个场景:
function Fun(name){
this.name = name;
this.sayHi = function(){ console.log('hi~ i am' + this.name) }
}
let obj1 = new Fun('obj1Name')
let obj2 = new Fun('obj2Name')
let obj3 = new Fun('obj3Name')
如果按照上面这种方式为每一个实例添加name
属性,那么每创建一个实例,都会创建一个sayHi
方法。
上面创建了三个Fun
实例,每个实例都创建一个内存空间保存sayHi
,总共三个。
这仅仅三个实例,我们创建就创建了,但如果有成百上千个实例呢?这样就会很占内存。
怎么说呢,很没必要,很费劲。
那要怎么优化呢?
我们注意到,这三个实例的构造函数都是Fun
,那有没有办法能在Fun
上创建一个内存空间保存sayHi
,然后供它的实例们调用呢?
既然都这么问了,当然是有办法的,这时候就是prototype
闪亮登场了!
我们上面已经知道prototype
对象中保存了一些属性、方法。
第一步我们可以先把sayHi
方法保存在Fun.prototype
中。
第二步创建实例。
代码如下:
// 初始化 Fun
function Fun(name){
this.name = name;
}
// 将方法保存在Fun原型对象中
Fun.prototype.sayHi = function(){ console.log('hi~ i am' + this.name) }
// 创建实例
let obj1 = new Fun('obj1Name')
let obj2 = new Fun('obj2Name')
let obj3 = new Fun('obj3Name')
将sayHi
保存在Fun.prototype
中,则只在内存中创建了一次sayHi
方法,
但是方法创建在Fun.prototype
上,实例们该怎么调用呢?
是否应该有一个属性保存一下Fun.prototype
的地址呢(保存Fun.prototype
的引用)?
还是那句话,都这样问了,当然是要,不然找不到Fun.prototype
,谈何找到保存在Fun.prototype
的sayHi
方法呢。
【一个属性】是什么属性
讲到这儿,再说说
NOTE
中内容
注意:当构造函数创建一个对象的时候,这个对象会隐式引用这个构造函数的prototype属性中的属性,以便于解析属性的引用
当构造函数Fun
创建了一个实例对象obj1
时,obj1
中会有一个属性指向Fun.prototype
,obj1
也可以调用Fun.prototype
中保存的属性、方法等
为什么说是隐式?
因为obj1
中指向Fun.prototype
的那个属性按理来讲不应该被开发人员获取到,应该是规范中内部实现的一个属性,但是各个浏览器都不约而同实现了这个属性,可以被我们获取到,这个属性就是__proto__
.
关于【__proto__】和【prototype】翻译成中文是什么,可能大家都是随性而为,没有一个统一规范称呼它。
比较多的说法是:
称【__proto__】为【隐式原型】
称【prototype】为【原型对象】
obj1.__proto__ === Fun.prototype
,这就是obj1中隐式原型属性指向其构造函数Fun的原型对象。
看到这儿是不是有点似曾相识呢,这个__proto__
就是我们上面讲的要指向Fun.prototype
的那个「一个属性」呀!
既如此,上面那张图稍作修改:
__proto__
看到这儿应该对于obj.__proto__
是什么有所了解了:
我所理解的是:obj.__proto__
保存了obj的构造函数
的prototype属性
(原型对象)的地址
(引用)
这么一说,关于这个属性,反而没什么好说的了。
constructor 进阶
说是进阶,也可以叫做补充,上面也仅仅是说明了一下三个属性是什么罢了。
关于原型链、关于构造函数链,关于原型对象的constructor、隐式原型的constructor等等都没有提到。
但是不着急,就要聊到这里了。
fun的constructor真的是constructor属性吗
参考自:参考链接 - 文二的 【## 四、真正的constructor属性藏在哪】
先看跟我们上面说到的sayHi
类似的问题:
function Fun(name){
this.name = name;
this.sayHi = ...
this.constructor = function(){ return Fun };
}
let obj1 = new Fun('obj1Name')
...
说到底constructor
也是对象的一个属性,存储着返回其构造函数的方法,大概内容可能类似上面写的那样。
如果创建每个实例的时候都创建一遍constructor
函数,还是跟sayHi
一开始的问题一样:占内存!没必要!费劲!
那怎么办呢?sayHi
怎么解决的呢?放到构造函数的原型对象prototype
里面!
// 初始化 Fun
function Fun(name){
this.name = name;
}
// 将方法保存在Fun原型对象中
Fun.prototype.sayHi = ...
Fun.prototype.constructor = function(){ return Fun };
// 创建实例
let obj1 = new Fun('obj1Name')
...
那obj1.constructor
是个什么东西呢?
我认为跟obj1.__proto__
类似,也是保存地址(引用)的,
不过obj1.constructor
保存的引用是Fun.prototype.constructor
的地址(引用)!
Fun.prototype.__proto__
.constructor
prototype
是一个对象是吧,
对象都有隐式原型__proto__
这点也无可否认吧。
好的,那问题来了,Fun.prototype.__proto__.constructor
输出的是什么呢?
不要慌,我们一点点分析。Fun.prototype
是一个对象吧,我们把它看作一个整体:PrototypeFun
,
即:
let FunPrototype = Fun.prototype
那问题就变成了FunPrototype.__proto__.constructor
输出什么?
想一下上文说过的 :
obj.__proto__
保存了obj的构造函数
的prototype属性
(原型对象)的地址
(引用)
翻译成人话就是:obj.__proto__ === obj的构造函数.prototype
那就要找到FunPrototype
的构造函数了
中途插播:八一八Fun的原型对象「prototype」的构造函数
prototype
是JS的内置原型对象,依据在这里:Table 7.
再来看一段跟prototype
相关的规范(可以不用看翻译,有点晦涩难懂,一会我会翻译成人话):
17 ECMAScript Standard Built-in Objects
Unless otherwise specified every built-in prototype object has the Object prototype object, which is the initial value of the expression
Object.prototype
(19.1.3), as the value of its [[Prototype]] internal slot, except the Object prototype object itself.翻译:
17 ECMAScript 标准内置对象
除非另有说明,否则每个内置原型对象都有 Object 原型对象,它是表达式 Object.prototype (19.1.3) 的初始值,作为其 [[Prototype]] 内部槽的值,除了 Object 原型对象本身。
翻译成人话是什么呢?
每个prototype原型对象都有(保存着)一个Object的原型对象「prototype」,这个原型对象初始值就是(说的真啰嗦)Object.prototype
的值 ,那这个原型对象保存在哪里呢?保存在[[Prototype]]
里面(就是__proto__
)
翻译成人话就是:任何一个原型对象fun.prototype.__proto__ === Object.prototype
ok ,话题回到 Fun.prototype.__proto__.constructor
既然任何一个原型对象的隐式原型都指向Object.prototype
,
那么FunPrototype.__proto__ === Object.prototype
就是成立的!
FunPrototype.__proto__.constructor
返回什么呢?
返回FunPrototype
的构造函数!
它的构造函数是什么?
是内置对象Object
!
为什么呢?最开始我们就知道prototype
是一个对象,Fun.prototype
自然也是一个对象 (废话) 对象自然由Object
创建的。
构造函数链
构造函数链的尽头是Function
构造函数链的底层逻辑是:实例.__proto__ === 实例的构造函数.prototype
其中,下图,
obj2是(Fun的)实例,Fun是(obj2的)构造函数,
Fun是(Function的)实例,Function是(Fun的)构造函数。
我们可以看到有两条构造函数链:
一条是:obj2 - Fun - Function
另一条:Function - Function如果要问为什么Function-Function,只能说是规定,设计之初,总要能跳出构造函数的链条,那就让它的尽头是Function好了。
加上 Object
呢
我们可以看到有三条构造函数链:
一条是:obj2 - Fun - Function
另一条:Function - Function
第三条:Object - Function
「 Object是内置函数, new Object是对象
(通过new关键字返回的是一个对象)」
原型链
原型链的底层逻辑是从最底下的实例能一直追溯到null:实例.__proto__.__proto__....__proto__ === null
构造函数链就是这样的链,如果真的要像这样写出一个链条,只能是:
`实例.__proto__.constructor.__proto__.constructor...__proto__.constructor === Function.prototype`
当然,这个链条感觉写出来也没什么意义。
解释一下:
【实例.__proto__】 指向其 【构造函数1.prototype】
【构造函数1.prototype】 里面有一个属性 【constauctor】 指向构造函数1本身
那么 【构造函数1.__proto__】 表示出来就是 【实例.__proto__.constructor.__proto__ 】
原型链的终点是null
加上Function
就是:
原型链、构造函数链 图片对比
【 注意箭头的方向 】
补充解释:
构造函数链跟原型链要分开理解,我以前就是不知道为什么一个函数的实例既继承了Object的原型方法,又是Function的实例。
应该要分开理解,然后合在一起才对。
obj1、obj2 -> Fun -> Function这是构造函数链。
obj1、obj2 -> Fun -> Object,是原型链。
如果在Fun.prototype上添加‘aaFun’方法,
在Function.prototype上添加‘aaFunction’方法,
在Object.prototype上添加‘aaObject’方法,
那么 obj1、obj2 可以继承(不知道说继承对不对)aaFun、aaObject方法,
但是运行aaFunction就会返回undefined方法,因为这个aaFunction方法不在原型链!