本人从事前端开发的工做三年有余,我要向你坦白,时至今日我对JS原型仍然是只知其一;不知其二,当年的校招面试关于JS原型都是“临时抱佛脚”,死记硬背混过去鸟~ ~。html
在往后工做中,我已熟练的使用Function
去封装类,使用mixin
去丰富类,使用new
去实例化我钟意的对象(单身狗的悲哀),然而却忘了它们背后蕴含的原理。前端
痛定思痛,本文算做是还债帖。面试
在JS里,分了两种数据类型,分别为基本数据类型
和引用数据类型
。算法
目前基本数据类型已有:
number
string
boolean
undefined
null
symbol
bigint
浏览器
它们的区分点在存储位置的不一样。基本数据类型
是直接存储在栈内存中的,而引用数据类型
则存储在堆内存里的。markdown
在《天天都在写的JS判断语句,你真的了解吗?》一文中提到咱们可使用typeof
运算符去识别变量的数据类型。以下图:app
在其中,typeof null === 'object'
是个例外,它是惟一一个没法经过typeof
识别出来的基本数据类型。函数
计算机都是以二进制方式来存储数据,JS中若是二进制前三位都是0时会断定为
object
类型,而null
的二进制前三位刚好都是0,因此返回object
oop
除了null
这个特例,咱们发现全部引用数据类型又分为了两个阵营:function
和 object
。它们是否有内在联系呢?post
先抛出个问题,什么是对象? 我会给出这样的答案:所谓对象,都有本身的属性和方法
这是我本身的认识,不必定正确,欢迎你评论,说出你的想法。
那么,函数是对象吗?是!由于它符合上面的定义:
function Foo(){} Foo.name = 'Foo' Foo.getName = function(){return Foo.name} Foo.getName() // Foo 复制代码
在JS中,对象是“第一等公民”,那么怎样才算“第一等公民”?
显然,函数符合这四个条件,因此函数也是“第一等公民”。因此,函数即对象。
事实上,在ECMA标准中,已直接将函数划归到
Sandard Built-in ECMAScript Objects
既然“函数即对象”,JS又怎样具体区分出函数与对象呢?ECMA给出了答案: 若是一个对象有内部属性
[[Call]]
,则它是一个函数; 若是一个对象没有内部属性[[Call]]
,则它是一个普通对象;
函数肩负了两项职责:
new
操做符调用函数;function Person(name){ this.name = name } let p = new Person() typeof Person // function typeof p // object 复制代码
在JS中,已经为咱们准备好不少的内置构造函数,好比Function
Object
Array
Date
RegExp
等。当你使用typeof
操做符识别它们,它们都会返回function
:
typeof Function // function typeof Array // function 复制代码
你有没有发现,
new Date()
或Date()
你均可以获得一个 Date 实例化对象,你知道怎么作到的吗?欢迎评论 。
至此咱们有了基本认知,首先函数拥有对象相同的能力(函数即对象),同时函数还能实例化对象。在Function和Object背后必定有着“隐蔽”的内在联系。
计算机执行任何逻辑都须要成本:时间成本和内存成本。一样一件事两种作法,显然咱们会选择成本更低的那种作法。一样的,JS也“不笨”。
先看以下示例代码:
let dog1 = { name: '旺财', species: '犬', bark: function(){ console.log('汪汪!') } } let dog2 = { name: '大黄', species: '犬', bark: function(){ console.log('汪汪!') } } 复制代码
狗生艰难,都要背负属性species
和方法bark
,任重而道远。因而旺财和大黄商议,能不能有个”代理“的机制,将共性部分的属性都交给这个代理,由代理全权负责,本身只保留不一样的部分(name
)。因而有了这个”代理“:
let agent = { species: '犬', bark: function(){ console.log('汪汪!') } } let dog1 = { name: '旺财', __agent__: agent } let dog2 = { name: '大黄', __agent__: agent } 复制代码
当访问到一个属性,本身身上没有的,就去”代理“那里找。若是找到了,皆大欢喜;未果,万分抱歉。
”代理“就如同制做陶瓷碗的模具,它记录了陶瓷碗的高度、碗口大小等,这样保证制做出每一个碗都是同种规格。而各个碗又能够涂上不一样的色彩,保留了它的个性。
在JS中,这个”代理“就是原型prototype
。原型保存了同类型对象的共享属性和方法,实质上就是为了节省内存空间。从上面的示例中看到agent
是一个对象,一样JS中prototype
也是一个对象,即原型对象。
那么,这个prototype
对象应该放在哪里最合适呢?(小蝌蚪开始找妈妈了)
咱们从上一节中知道,对象能够由函数实例化,即用new
操做符运行函数,至关于同类型的对象也都是由一样的函数实例化的,此时函数变身为构造函数。-->(同规格的碗必定是从一个模子里倒出来的)
另外一方面,函数即对象,那么就能够在函数上挂载一些属性。Bingo!把prototype
挂载在构造函数上必定是最合适的,由于对象实例化必定会经历构造函数运行。 因而,修改一下上面的示例:
function Dog(name){ this.name = name } Dog.prototype = { species: '犬', bark: function(){ console.log('汪汪!') } } // Oops, 好像丢了什么关键信息? let dog1 = new Dog('旺财') let dog2 = new Dog('大黄') dog1.species === dog2.species // true 复制代码
细心的读者在这里应该会纠正我了,给Dog
构造函数直接赋值prototype
会丢失一些信息。哈,没错,就是contructor
属性,暂且不表。先要研究在new
的过程当中,是怎样操做这个原型的。
引自MDN中的定义,new运算符建立一个用户定义的对象类型的实例或具备构造函数的内置对象的实例。
管它是啥,咱们关心的是new
内部的猫腻。 以let dog = new Dog('旺财')
为例,new
进行了以下操做:
obj = {}
;obj
的内部属性[[Prototype]]
指向构造函数Dog
的原型,即obj.[[Prototype]] = Dog.prototype
;Dog
的this
指向这个空对象obj
;Dog
,函数内部对this
的操做等同于操做obj
对象;Dog
指定了返回值,则正常返回这个返回值;不然,返回obj
对象;以上就是new
操做符的黑魔法,它已经帮咱们完成了对象实例化后原型的绑定,完成了“代理”指定。 其中,提到了obj
空对象的内部属性[[Prototype]]
,它在new
过程当中会被指向构造函数的原型,至关于这个空对象obj
继承了原型对象。 依据ECMA标准中的描述,每一个对象都会有这个[[Prototype]]
内部属性,为的就是实现对象继承。
既然是内部属性,咱们天然没法直接经过obj.[[Prototype]]
访问到。目前有两个方式能够从一个对象obj
上获取到它继承的原型对象:
obj.__proto__
Object.getPrototypeOf(obj)
之因此将obj.__proto__
称为野路子是由于对象上的__proto__
属性是由各个浏览器本身实现的,目的是为了方便开发者调试代码,ECMA官方可不认可这种方式哦。
在ECMA2015中没有任何关于
__proto__
的说明,在最新的ECMA标准中你能够在附录中找到关于__proto__
的说明,但它是可配置的属性,便可能会被任意覆盖。所以切记:不要在生产环境中使用
__proto__
,请使用Object.getPrototypeOf(obj)
,当且仅在开发环境中可使用__proto__
。 若是你实在忍不住,也请作好字段校验const hasProto = '__proto__' in {}
。
prototype vs [[Prototype]]
这里势必要把二者拿出来比较一下,毕竟用了同一个单词嘛。请记住如下两句话:
prototype
属性,prototype
是一个对象(即,原型对象),对象上包含全部实例对象共享的属性或方法;[[Prototype]]
内部属性,它指向建立该对象的构造函数的原型对象;比较拗口,请多读几遍这两句话,理解其中含义!
依据“函数即对象”的定义,函数会同时拥有prototype
和[[Prototype]]
属性,对象不会有prototype
属性,只有[[Prototype]]
属性。
至此,函数与对象果真是有“一腿”的,依靠prototype
和[[Prototype]]
产生了“剪不断理还乱”的关系。
每一个对象都有构造函数!
上面提到咱们能够经过Object.getPrototyoeOf(obj)
方法方便的获取到任一对象的原型对象。那么咱们也须要方便的从一个对象上面轻松获取到该对象的构造函数。
读取对象obj.constructor
属性就能够得到它的构造函数,这里须要注意的是,constructor
是挂载在构造函数的原型对象prototype
上的。而在实例化对象上读取的constructor
属性都是从它的构造函数原型上继承来的。
上节中咱们漏掉了对constructor的修正,若是粗暴的直接修改Dog
函数的原型,会丢失constructor
属性,访问dog1.constructor
,获得的是从Object
构造函数原型上继承来的constructor
,显然是不正确的,须要将constructor
指向正确的Dog
函数上:
function Dog(name){ this.name = name } Dog.prototype = { constructor: Dog, species: '犬', bark: function(){ console.log('汪汪!') } } let dog1 = new Dog('旺财') dog1.constructor === Dog // true 复制代码
有个面试题:问
Dog.prototype = {xxx: yyy}
与Dog.prototype.xxx = yyy
的区别在哪? --> 前者须要修正constructor
真的须要修正constructor吗?
平心而论,我在写函数封装类的时候,常常会覆盖它的原型,而且忘记修正constructor
属性,但也没有任何异常的事情发生,长此以往,不修正constructor
属性成了天然。
没错,不会有什么异常的事情发生,前提是你不会显式的调用constructor
。
constructor
指向构造函数,因此constructor
是函数,能够被直接调用。 仍然是上面的示例(不修正constructor
),一般而言,咱们是直接调用new Dog()
去实例化对象,而不会直接调用constructor
,那若是直接调用constructor
呢?
let dog2 = new dog1.constructor('大黄') dog2.name // undefined dog2 instanceof Dog // false dog2 instanceof Object // true dog2 instanceof String // true 复制代码
为何
dog2
是String
的实例,留给你去思考啦
若是不显式的调用constructor
好像也没啥问题啊,那你能保证你使用的某个第三方库就不会显式的调用你传递给它的对象的constructor
吗?
养成习惯,及时修正你的constructor
。
从上节中,大体清楚了如何经过new
构造函数来实例化一个对象,那么做为普通函数和普通对象的构造函数Function
和Object
是怎样的关系?
首先,Function
和Object
是内置构造函数,因此它们是函数,毋庸置疑:
typeof Function // function typeof Object // function 复制代码
其次既然是函数,那么它们都有prototype
对象属性,即Function.prototype
Object.prototype
。 而后,prototype
是对象,就有[[Prototype]]
内部属性,指向建立该对象的构造函数的原型对象:
Function.prototype.__proto__ === Object.prototype // true Object.prototype.__proto__ === Object.prototype // Oops!糟了,若是这里要是成立的话,就会陷入死循环了 复制代码
JavaScript为了避免陷入死循环,所谓“恩恩怨怨什么时候了”,总归须要一个终点,不能无休止遍历下去,所以当访问到Object.prototype.__proto__
时,将强制返回null
,意思是你访问到头了,没有更多内容了,洗洗睡吧。(标准中关于Object Prototype的说明)
同时,咱们在标准中也获取到以下两条信息:
- 每一个内置函数或内置构造函数的
[[Prototype]]
都指向Function.prototype
;- 每一个内置原型对象的
[[Prototype]]
都指向Object.prototype
,除了Object.prototype
自身之外;
在前面,咱们知道函数的原型应该是对象,即typeof Foo.prototype === 'object'
是成立的。可是!Function.prototype
倒是一个例外,typeof Function.prototype === 'function'
,不是object
。
我从ECMA2015中找到了关于Function.prototype
的描述:
The Function prototype object is itself a Function object (its [[Class]] is "Function") that, when invoked, accepts any arguments and returns undefined. 即 Function.prototype是函数对象 (它的内部属性[[Class]] 是 ‘Function’)
自ECMA2015如此描述后,为了兼容它,之后的全部ECMA版本也都“将错就错”了。
ECMA:你大爷终究是你大爷,哈哈
至此,能够获得Function与Object的关系图以下:
讨论了这么多,思绪有点乱了,我将全部关系者整合到一张关系图中:
从这张关系图中,咱们能够概括出几个信息点:
prototype
属性,原型是一个对象,包含全部实例对象共享的属性或方法;[[Prototype]]
内部属性,指向原型对象,其中Object.prototype.[[Prototype]]
指向null
,以表示原型查找的终点;prototype
对象上挂载了constructor
属性,指向构造函数;那么当咱们在谈论原型链的时候,究竟咱们在谈论什么?
若是你想真实看到这条“链”,那么它就是关系图中的粉色虚线框所示,可是这里原型链只是宾语,谈论原型链时,不能够丢失主语,即foo对象的原型链
。直观的说,原型链就是对象间用[[Prototype]]
内部属性串联起来的单向链表。
因此,函数
Foo
的原型链是怎样的,我想你也能在关系图找到。 函数Foo
的原型链能够访问到Function.prototype.call
或Function.prototype.apply
在JS中,我理解的原型链实际上是一种算法,即在对象上查找属性的算法。 以运行foo.someFunc()
为例:
foo
对象上是否已定义someFunc
方法,未果,next;foo.[[Prototype]]
,即Foo.prototype
对象,查找对象上是否已定义someFunc
方法,未果,next;Foo.prototype.[[Prototype]]
,即Object.prototype
对象,查找对象上是否已定义someFunc
方法,未果,next;Object.prototype.[[Prototype]]
,发现是null
,宣告这次查找失败,抛出TypeError
错误,结束这次查找,Game over。码了这么多字,无非想和你达成以下共识:
prototype
属性,prototype
是一个对象(即,原型对象),对象上包含全部实例对象共享的属性或方法;[[Prototype]]
内部属性,它指向建立该对象的构造函数的原型对象prototype
;constructor
属性,指向原型绑定的构造函数;你是否有不一样的见解?欢迎评论。
预告
基于原型,JS是如何作到类继承的?ES6中,class A extends B
背后藏着什么猫腻?如何经过“混入mixin”来完成多重继承?
码字不易,若是:
您的支持与关注,是我持续创做的最大动力!