本文首发于我的 Github,欢迎 issue / fxxk。前端
ES6
的第一个版本发布于 15
年 6
月,而本文最先创做于 16
年,那也是笔者从事前端的早期。在那个时候,ES6
的众多特性仍处于 stage
阶段,也远没有如今这么普及,为了更轻松地写JavaScript
,笔者曾花费了整整一天,仔细理解了一下原型——这个对于一个成熟的JavaScript
开发者必需要跨越的大山。git
ES6
带来了太多的语法糖,其中箭头函数掩盖了 this
的神妙,而 class
也掩盖了本文要长篇谈论的 原型
。github
最近,我重写了这篇文章,经过本文,你将能够学到:windows
ES5
模拟类;prototype
和 __proto__
;JavaScript
这门语言。在 JavaScript
中,一直有这么一种说法,万物皆对象。事实上,在 JavaScript
中,对象也是有区别的,咱们能够将其划分为 普通对象
和 函数对象
。Object
和 Function
即是 JavaScript
自带的两个典型的 函数对象
。而函数对象就是一个纯函数,所谓的 函数对象
,其实就是使用 JavaScript
在 模拟类
。浏览器
那么,究竟什么是普通对象
,什么又是函数对象
呢?请看下方的例子:函数
首先,咱们分别建立了三个 Function
和 Object
的实例:post
function fn1() {}
const fn2 = function() {}
const fn3 = new Function('language', 'console.log(language)')
const ob1 = {}
const ob2 = new Object()
const ob3 = new fn1()
复制代码
打印如下结果,能够获得:ui
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof ob1); // object
console.log(typeof ob2); // object
console.log(typeof ob3); // object
console.log(typeof fn1); // function
console.log(typeof fn2); // function
console.log(typeof fn3); // function
复制代码
在上述的例子中,ob1
、ob2
、ob3
为普通对象(均为 Object
的实例),而 fn1
、fn2
、fn3
均是 Function
的实例,称之为 函数对象
。this
如何区分呢?其实记住这句话就好了:spa
Function
的实例都是函数对象
,而其余的都是普通对象
。说到这里,细心的同窗会发表一个疑问,一开始,咱们已经提到,Object
和 Function
均是 函数对象
,而这里咱们又说:全部Function
的实例都是函数对象
,难道 Function
也是 Function
的实例?
先保留这个疑问。接下来,对这一节的内容作个总结:
从图中能够看出,对象自己的实现仍是要依靠构造函数。那 原型链
究竟是用来干吗的呢?
众所周知,做为一门面向对象(Object Oriented)的语言,一定具备如下特征:
而原型链最大的目的, 就是为了实现继承
。
原型链到底是如何实现继承的呢?首先,咱们要引入介绍两兄弟:prototype
和 __proto__
,这是在 JavaScript
中无处不在的两个变量(若是你常常调试的话),然而,这两个变量并非在全部的对象上都存在,先看一张表:
对象类型 | prototype |
__proto__ |
---|---|---|
普通对象(NO) | ❎ | ✅ |
函数对象(FO) | ✅ | ✅ |
首先,咱们先给出如下结论:
函数对象
具备 prototype
这个属性;prototype
和 __proto__
都是 JavaScript
在定义一个函数或对象时自动建立的 预约义属性
。接下来,咱们验证上述的两个结论:
function fn() {}
console.log(typeof fn.__proto__); // function
console.log(typeof fn.prototype); // object
const ob = {}
console.log(typeof ob.__proto__); // function
console.log(typeof ob.prototype); // undefined,哇!果真普通对象没有 prototype
复制代码
既然是语言层面的预置属性,那么二者究竟有何区别呢?咱们依然从结论出发,给出如下两个结论:
prototype
被实例的 __proto__
所指向(被动)__proto__
指向构造函数的 prototype
(主动)哇,也就是说如下代码成立:
console.log(fn.__proto__ === Function.prototype); // true
console.log(ob.__proto__ === Object.prototype); // true
复制代码
看起来很酷,结论瞬间被证实,感受是否是很爽,那么问题来了:既然 fn
是一个函数对象,那么 fn.prototype.__proto__
到底等于什么?
这是我尝试去解决这个问题的过程:
typeof
获得 fn.prototype
的类型:"object"
"object"
,那 fn.prototype
岂不是 Object 的实例?根据上述的结论,快速地写出验证代码:console.log(fn.prototype.__proto__ === Object.prototype) // true
复制代码
接下来,若是要你快速地写出,在建立一个函数时,JavaScript
对该函数原型的初始化代码,你是否是也能快速地写出:
// 实际代码
function fn1() {}
// JavaScript 自动执行
fn1.protptype = {
constructor: fn1,
__proto__: Object.prototype
}
fn1.__proto__ = Function.prototype
复制代码
到这里,你是否有一丝恍然大悟的感受?此外,由于普通对象就是经过 函数对象
实例化(new
)获得的,而一个实例不可能再次进行实例化,也就不会让另外一个对象的 __proto__
指向它的 prototype
, 所以本节一开始提到的 普通对象没有 prototype 属性
的这个结论彷佛很是好理解了。从上述的分析,咱们还能够看出,fn1.protptype
就是一个普通对象,它也不存在 protptype
属性。
再回顾一下上一节,咱们还遗留一个疑问:
Function
也是 Function
的实例?是时候去掉应该
让它成立了。那么此刻,please show me your code!
console.log(Function.__proto__ === Function.prototype) // true
复制代码
上一节咱们详解了 prototype
和 __proto__
,实际上,这两兄弟主要就是为了构造原型链而存在的。
先上一段代码:
const Person = function(name, age) {
this.name = name
this.age = age
} /* 1 */
Person.prototype.getName = function() {
return this.name
} /* 2 */
Person.prototype.getAge = function() {
return this.age
} /* 3 */
const ulivz = new Person('ulivz', 24); /* 4 */
console.log(ulivz) /* 5 */
console.log(ulivz.getName(), ulivz.getAge()) /* 6 */
复制代码
解释一下执行细节:
1
,建立了一个构造函数 Person
,要注意,前面已经提到,此时 Person.prototype
已经被自动建立,它包含 constructor
和 __proto__
这两个属性;2
,给对象 Person.prototype
增长了一个方法 getName()
;3
,给对象 Person.prototype
增长了一个方法 getAge()
;4
, 由构造函数 Person
建立了一个实例 ulivz
,值得注意的是,一个构造函数在实例化时,必定会自动执行该构造函数。5
的输出,即 ulivz
应该是:{
name: 'ulivz',
age: 24
__proto__: Object // 实际上就是 `Person.prototype`
}
复制代码
结合上一节的经验,如下等式成立:
console.log(ulivz.__proto__ == Person.prototype) // true
复制代码
6
的时候,因为在 ulivz
中找不到 getName()
和 getAge()
这两个方法,就会继续朝着原型链向上查找,也就是经过 __proto__
向上查找,因而,很快在 ulviz.__proto__
中,即 Person.prototype
中找到了这两个方法,因而中止查找并执行获得结果。这即是 JavaScript
的原型继承。准确的说,JavaScript
的原型继承是经过 __proto__
并借助 prototype
来实现的。
因而,咱们能够做以下总结:
__proto__
指向 Function.prototype
;(复习)prototype
指向 instance.__proto__
;(复习)__proto__
指向 Object.prototype
;(复习)prototype
属性;(复习)ob.__proto__
, 也就是访问该对象的构造函数的原型 obCtr.prototype
,若仍找不到,会继续查找 obCtr.prototype.__proto__
,像依次查找下去。若在某一刻,找到了该属性,则会马上返回值并中止对原型链的搜索,若找不到,则返回 undefined
。为了检验你对上述的理解,请分析下述两个问题:
console.log(ulivz.__proto__ === Function.prototype)
复制代码
Person.__proto__
和 Person.prototype.__proto__
分别指向何处?前面已经提到,在 JavaScript
中万物皆对象。Person
很明显是 Function
的实例,所以,Person.__proto__
指向 Function.prototype
:
console.log(Person.__proto__ === Function.prototype) // true
复制代码
由于 Person.prototype
是一个普通对象,所以 Person.prototype.__proto__
指向Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype) // true
复制代码
为了验证 Person.__proto__
所在的原型链中没有 Object
,以及 Person.prototype.__proto__
所在的原型链中没有 Function
, 结合如下语句验证:
console.log(Person.__proto__ === Object.prototype) // false
console.log(Person.prototype.__proto__ == Function.prototype) // false
复制代码
上一节,咱们实际上还遗留了一个疑问:
咱们能够快速地利用如下代码验证:
function Person() {}
const ulivz = new Person()
console.log(ulivz.name)
复制代码
很显然,上述输出 undefined
。下面简述查找过程:
ulivz // 是一个对象,能够继续
ulivz['name'] // 不存在,继续查找
ulivz.__proto__ // 是一个对象,能够继续
ulivz.__proto__['name'] // 不存在,继续查找
ulivz.__proto__.__proto__ // 是一个对象,能够继续
ulivz.__proto__.__proto__['name'] // 不存在, 继续查找
ulivz.__proto__.__proto__.__proto__ // null !!!! 中止查找,返回 undefined
复制代码
哇,原来路的尽头是一场空。
最后,再回过头来看看上一节的那演示代码:
const Person = function(name, age) {
this.name = name
this.age = age
} /* 1 */
Person.prototype.getName = function() {
return this.name
} /* 2 */
Person.prototype.getAge = function() {
return this.age
} /* 3 */
const ulivz = new Person('ulivz', 24); /* 4 */
console.log(ulivz) /* 5 */
console.log(ulivz.getName(), ulivz.getAge()) /* 6 */
复制代码
咱们来画一个原型链图,或者说,将其整个原型链图画出来?请看下图:
PS:手贱把chl(个人中文名缩写)改为了 ulivz(Github名),因此这张图中的chl实际上就是ulivz,画这张图的时候, 我还在用windows = =
画完这张图,基本上全部以前的疑问均可以解答了。
与其说万物皆对象, 万物皆空彷佛更形象。
前面已经有所说起,但只有原型对象才具备 constructor
这个属性,constructor
用来指向引用它的函数对象。
Person.prototype.constructor === Person //true
console.log(Person.prototype.constructor.prototype.constructor === Person) //true
复制代码
这是一种循环引用。固然你也能够在上一节的原型链图中画上去,这里就不赘述了。
经过前文的论述,结合相应的代码验证,整理出如下原型链图:
因而可知,咱们更增强化了这两个观点:
- 任何内置函数对象(类)自己的
__proto__
都指向Function
的原型对象;- 除了
Oject
的原型对象的__proto__
指向null
,其余全部内置函数对象的原型对象的__proto__
都指向object
。
为了减小读者敲代码的时间,特给出验证代码,但愿可以促进你的理解。
Array:
console.log(arr.__proto__)
console.log(arr.__proto__ == Array.prototype) // true
console.log(Array.prototype.__proto__== Object.prototype) // true
console.log(Object.prototype.__proto__== null) // true
复制代码
RegExp:
var reg = new RegExp;
console.log(reg.__proto__)
console.log(reg.__proto__ == RegExp.prototype) // true
console.log(RegExp.prototype.__proto__== Object.prototype) // true
复制代码
Date:
var date = new Date;
console.log(date.__proto__)
console.log(date.__proto__ == Date.prototype) // true
console.log(Date.prototype.__proto__== Object.prototype) // true
复制代码
Boolean:
var boo = new Boolean;
console.log(boo.__proto__)
console.log(boo.__proto__ == Boolean.prototype) // true
console.log(Boolean.prototype.__proto__== Object.prototype) // true
复制代码
Number:
var num = new Number;
console.log(num.__proto__)
console.log(num.__proto__ == Number.prototype) // true
console.log(Number.prototype.__proto__== Object.prototype) // true
复制代码
String:
var str = new String;
console.log(str.__proto__)
console.log(str.__proto__ == String.prototype) // true
console.log(String.prototype.__proto__== Object.prototype) // true
复制代码
来几句短总结:
A
经过new
建立了B
,则 B.__proto__ = A.prototype
;__proto__
是原型链查找的起点;B.a
,若在B
中找不到a
,则会在B.__proto__
中,也就是A.prototype
中查找,若A.prototype
中仍然没有,则会继续向上查找,最终,必定会找到Object.prototype
,假若还找不到,由于Object.prototype.__proto__
指向null
,所以会返回undefined
;Object.prototype.__proto__ ——> null
。最后,给你留下一个疑问:
JavaScript
实现类的继承呢?请看个人原型系列的下一篇《深刻JavaScript继承原理》 分晓。
以上,全文终。
注:此外本文属于我的总结,部分表达可能会有疏漏之处,若是您发现本文有所欠缺,为避免误人子弟,请放心大胆地在评论中指出,或者给我提 issue,感谢~