欢迎来我的博客阅读:「JavaScript 原型中的哲学思想」
记得当年初试前端的时候,学习JavaScript过程中,原型问题一直让我疑惑许久,那时候捧着那本著名的红皮书,看到有关原型的讲解时,总是心存疑虑。
当在JavaScript世界中走过不少旅程之后,再次萌发起研究这部分知识的欲望,翻阅了不少书籍和资料,才搞懂__proto__
和prototype
的概念。
故以作此笔记,日后忘了可以回来看看。
如果你看的过程中觉得理解有些困难,把例子在代码中跑一跑,亲手试一试也许能解决不少疑惑。
一切皆为对象
殊不知,JavaScript的世界中的对象,追根溯源来自于一个 null
「一切皆为对象」,这句着实是一手好营销,易记,易上口,印象深刻。
万物初生时,一个null
对象,凭空而生,接着Object
、Function
学着null
的模样塑造了自己,并且它们彼此之间喜结连理,提供了prototype
和constructor
,一个给子孙提供了基因,一个则制造万千子子孙孙。
在JavaScript中,null
也是作为一个对象存在,基于它继承的子子孙孙,当属对象。
乍一看,null
像是上帝,而Object
和Function
犹如JavaScript世界中的亚当与夏娃。
原型指针 __proto__
在JavaScript中,每个对象都拥有一个原型对象,而指向该原型对象的内部指针则是__proto__
,通过它可以从中继承原型对象的属性,原型是JavaScript中的基因链接,有了这个,才能知道这个对象的祖祖辈辈。从对象中的__proto__
可以访问到他所继承的原型对象。
var a = new Array();
a.__proto__ === Array.prototype // true
上面代码中,创建了一个Array的实例a
,该实例的原型指向了Array.prototype
。
Array.prototype
本身也是一个对象,也有继承的原型:
a.__proto__.__proto__ === Object.prototype // true
// 等同于 Array.prototype.__proto__ === Object.prototype
这就说了明了,Array本身也是继承自Object的,那么Object的原型指向的是谁呢?
a.__proto__.__proto__.__proto__ === null // true
// 等同于 Object.prototype.__proto__ === null
所以说,JavaScript中的对象,追根溯源都是来自一个null对象。佛曰:万物皆空,善哉善哉。
除了使用.__proto__
方式访问对象的原型,还可以通过Object.getPrototypeOf
方法来获取对象的原型,以及通过Object.setPrototypeOf
方法来重写对象的原型
。
值得注意的是,按照语言标准,__proto__
属性只有浏览器才需要部署,其他环境可以没有这个属性,而且前后的两根下划线,表示它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeof
和Object.setPrototypeOf
,进行原型对象的读写操作。
这里用__proto__
属性来描述对象中的原型,是因为这样来得更加形象,且容易理解。
原型对象 prototype
函数作为JavaScript中的一等公民,它既是函数又是对象,函数的原型指向的是Function.prototype
var Foo = function() {}
Foo.__proto__ === Function.prototype // true
函数实例除了拥有__proto__
属性之外,还拥有prototype
属性。
通过该函数构造的新的实例对象,其原型指针__proto__
会指向该函数的prototype
属性。
var a = new Foo();
a.__proto__ === Foo.prototype; // true
而函数的prototype
属性,本身是一个由Object
构造的实例对象。
Foo.prototype.__proto__ === Object.prototype; // true
prototype
属性很特殊,它还有一个隐式的constructor
,指向了构造函数本身。
Foo.prototype.constructor === Foo; // true
a.constructor === Foo; // true
a.constructor === Foo.prototype.constructor; // true
原型链
概念
原型链作为实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
每个构造函数都有一个原型对象(prototype
),原型对象都包含一个指向构造函数的指针(constructor
),而实例都包含一个指向原型对象的内部指针(__proto__
)。
那么,假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。
如此层层递进,就构造了实例与原型的链条,这就是原型链的基本概念。
意义
“原型链”的作用在于,当读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。以此类推,如果直到最顶层的Object.prototype还是找不到,则返回undefine
亲子鉴定
在JavaScript中,也存在鉴定亲子之间DNA关系的方法:
- instanceof 运算符返回一个布尔值,表示一个对象是否由某个构造函数创建。
- Object.isPrototypeOf() 只要某个对象处在原型链上,isProtypeOf都返回true
var Bar = function() {}
var b = new Bar();
b instanceof Bar // true
Bar.prototype.isPrototypeOf(b) // true
Object.prototype.isPrototypeOf(Bar) // true
要注意,实例b
的原型是Bar.prototype
而不是Bar
一张历史悠久的图
这是一张描述了Object
、Function
以及一个函数实例Foo
他们之间原型之间联系。如果理解了上面的概念,这张图是不难读懂。
从上图中,能看到一个有趣的地方。
Function.prototype.__proto__
指向了 Object.prototype
,这说明Function.prototype
是一个 Object
实例,那么应当是先有的Object
再有Function
。
但是Object.prototype.constructor.__proto__
又指向了 Function.prototype
。这样看来,没有Function
,Object
也不能创建实例。
这就产生了一种类「先有鸡还是先有蛋」的经典问题,到底是先有的Object
还是先有的Function
呢?
这么哲学向的问题,留给你思考了。
我只是感慨:
越往JavaScript的深处探索,越发觉得这一门语言很哲学。
这个是每个js开发必须了解的知识,尤其是做面向对象开发
学习了,感谢楼主分享干货
太棒了,谢谢楼主。收藏
赞
赞!
什么哲学。
就是设计时的安排和妥协。以及无意有意所导致的后果。语言设计者希望有函数式编程特点,有LISP语言特征,又想要有C++Java的面向对象特征,就采用了原型构造器的思路,然后各种揉捏、妥协、安排,并没有得到一个特别清晰简洁的方案,然后就是这个样子了。
真心不错
不错!
@LongHorn-C (逃
请问“指针”的叫法的出处?js有指针?? “并且constructor属性会随着构造函数构造实例的时候,赋值给实例对象,也就是说,实例对象也含有constructor属性,指向到构造函数。” constructor只是一个不可枚举属性而已,没什么特殊的,你给prototype添加其他属性,一样可以通过实例对象来访问(因为实例化时对象添加了__proto__为prototype所引用的对象的引用),nodejs没有对constructor做“constructor属性会随着构造函数构造实例的时候,赋值给实例对象”,constructor还是很老实地呆在prototype里。 设计哲学里似乎存在一个问题,Function.proto.constructor===Function,但Function.proto.proto!==Function.prototype,这与Function().constructor===Function,但Function().proto===Function.prototype的哲学不符(cnode的编辑框会把下划线给弄没了,光标也乱跑,这个bug修复下吧?)(估计是range/selection对象部分哪里写错了?。。。)
清晰易懂!
@DevinXian 是的,这是很基础的东西。 但是原型的东西对于初学者是很不容易搞懂, 写这样的文章只是希望能给别人一点帮助,以及说一下自己的理解罢了。
@LongHorn-C 本文纯属一己之见。
@d630209581 感谢指出!
Q1. 请问“指针”的叫法的出处?js有指针??
「指针」的叫法,《JavaScript高级程序设计》以及《JavaScript语言精粹》有过这样的用法。
这里我用「指针」想表达的是:__proto__
指向的是XXX.prototype
指向的同一块内存。这种思想,我还没有找到比「指针」更容易让人理解的形容了。
Q2. constructor问题
这是我的疏忽,感谢指正,已修正原文。:-)
对象的实例并没有直接继承构造函数原型的’constructor’属性,用hasOwnProperty
方法就能检查出来。
Q3. 设计哲学里似乎存在一个问题,Function.__proto__.constructor===Function
,但Function.__proto__.proto!==Function.prototype
,与Function().constructor===Function
,但Function().__proto__===Function.prototype
的哲学不符
我并不是很能理解你所说的问题,但是Function.__proto__
指向的是 Function.prototype
,而Function.__proto__.__proto__
同等于Function.prototype.__proto
,指向的是Object.prototype
。所以Function.__proto__.proto!==Function.prototype
必须成立。
Q4. cnode的编辑框会把下划线给弄没了 下划线是markdown的语法,用来强调作用。CNode的评论框是支持markdown解析的,所以要使用下划线,最好使用: `` 包裹起来。
@JerryC8080
Q3的解释有问题,以错误导致的结果来回答错误的原因,js目前的规则是,如果a对象的constructor为b对象的引用,那么a对象的__proto__
就应该为b对象prototype所引用对象的引用,例子:Number().constructor===Number//true
,
Number().__proto__===Number.prototype//true
,
那么问题来了:
Q4 没搞过md,用了``光标还是会漂移,除非打个回车换行再打字
这是一个问题
马克。。。 From Noder
mark
@nanfengbobo 我这是我写的:
这个是你自己幻想的吧,我并没有这么写:
你的这个例子:跟我的例子是一个意思,即“如果a对象的constructor为b对象的引用,那么a对象的__proto__就应该为b对象prototype所引用对象的引用”,但是这一规则对于Number.__proto__并不适用,所以我认为设计哲学里出了“问题”,当然可能是故意的,不过麻烦你多看几遍我的代码再来回复我。。。
JavaScript 没有类的概念,所谓的对象就是实例 函数也是一种对象,但是和普通对象不一样 只有函数才有原型,普通对象没有原型
var a = new Array() //a 是一个对象,Array 是一个函数,constructor 函数
mark
js里面的哲学思想是
typeof NaN (Not a Number) == 'number'