在日常开发中时常会碰到类似于Object.prototype.toString.call()这样的涉及到JS原型的方法,又因自身对原型链没有过系统的整理,有时候会对这些用法产生疑惑,故记录此贴谈谈个人对JS原型链的见解。
你认为以下表达式会出现什么样的结果?
(其中Person为构造函数,person为该构造函数的实例)
1. console.log(Person.prototype.constructor === Person) //true or false?
2. console.log(Person.prototype._proto_.constructor === Object) //true or false?
3. console.log(Person.prototype._proto_._proto_ === null) //true or false?
4. console.log(person._proto_ === Person.prototype) //true or false?
5. console.log(person._proto_.constructor === Person) //true or false?
如果以上几题都能答对,那么基本上就可以说对原型链有了基本的认知,以上几题的答案都是true,不知道你答对了几题呢?🤔
如果你对这些题目有所疑问,那么不如听我为你分析一波😙
上面的这一张图简单地说明了实例对象、原型对象以及构造函数之间的关系,如果你看不懂,别急!且听我细细说来😎
首先需要明确的一点是实例对象、原型对象、构造函数三者并不相同!!!
let person = new Person()
console.log(person === Person) //false 实例不等于构造函数
console.log(person === Person.prototype) //false 实例不等于原型
console.log(Person.prototype === Person) //false 原型不等于构造函数
从图中可以看到实例与构造函数之间并不存在直接的联系,实例只与原型对象存在直接的联系,即实例与构造函数之间的联系需要以原型对象作为桥梁,这三者的关系可以理解为(以Person作为构造函数,person作为实例对象为例):
-
构造函数Person上存在有一个
prototype属性,这个属性指向了原型对象,即原型对象表示为Person.prototype -
原型对象作为实例与构造函数之间的桥梁,原型对象存在一个
constructor属性,该属性指向了构造函数,也就是说Person.prototype.constructor指向了Person,也就是文章开头第一个问题的结果console.log(Person.prototype.constructor === Person) //原型的constructor属性指向构造函数,答案是true -
实例对象可以通过
_proto_连接到原型对象,实际上这个属性指向了隐藏属性[[prototype]],(这个[[prototype]]指针指向了构造函数的原型对象,但是访问这个[[prototype]]特性的标准方式并不存在,所以Firefox、Safari以及Chrome在每个对象上暴露_proto_属性,通过这个属性来访问对象原型。)也就是说person._proto_指向了Person.prototype,也就是开头的第四个问题的结果:console.log(person._proto_ === Person.prototype) //实例对象的_proto_属性指向原型对象,答案是true console.log(person.constructor === Person) //实例对象可以直接访问包括constructor在内的原型对象上的属性,答案是true
相信看到这里你已经对于实例对象、原型对象以及构造函数之间的关系有了基本的认识,可能你会问那么剩下的那三题又是什么原理?在你弄懂三者的联系后还请接着往下看👾
-
原型是另一个类型的实例,原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数这样就在实例和原型之间构造了一条原型链。举例来说
Person.prototye这个原型对象除了constructor属性之外还有一个_proto_属性指向了另一个构造函数的原型对象,在这里例子里这个构造函数是Object,也就是说Person.prototype._proto_指向了Object的原型对象,由此点我们可以解出问题的第二点:console.log(Person.prototype._proto_ === Object.protype) // Person原型对象是Object原型对象的实例,答案是true console.log(Person.prototype._proto_.constructor === Object) //Person.prototype._proto_指向了Object原型对象,原型对象的constructor属性又指向了构造函数,即Object,所以答案是true -
正常原型链都会终止于
Object的原型对象,而Object原型的原型是nullconsole.log(Object.prototype._proto_ === null) // Object原型对象是null的实例,答案是true // 等同于 console.log(Person.prototype._proto_._proto_ === null) // Person.prototype._proto_等同于Object.prototype,所以答案是true
最后再看这一个进阶版本,你能够看懂下面这个原型链的逻辑关系了吗?能看懂你的原型链就过关了!✌
除此之外,不得不提的便是由原型链衍生而来的事件委托的设计模式了😎
我们需要明确的是在JavaScript中并不存在严格定义上的类(class)
接下来我们来观察下典型的(原型)面向对象风格和对象关联风格两者设计模式之间的差别:
//典型的(原型)面向对象风格
function Foo(who){
this.me = who;
}
Foo.prototype.identify = function(){
return "I am" + this.me;
}
function Bar(who){
Foo.call(this, who);
}
// 将Bar.prototype委托给Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function(){
alert("Hello," + this.identify() + ".");
}
// b1,b2为委托给Bar.prototype
var b1 = new Bar("b1");
var b2 = new Bar("b2");
b1.speak();
b2.speak();
// 对象关联风格
Foo = {
init:function(who){
this.me = who;
},
identify:function(){
return "I am" + this.me;
}
}
// 将Bar委托给Foo
Bar = Object.create(Foo);
Bar.speak = function(){
alert("Hello" + this.identify() + ".")
}
// 将b1,b2委托给Bar
var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");
b1.speak();
b2.speak();
两者起到的效果是相同的,但是对象关联风格的代码显然更加简洁,因为这种代码只关注一件事:对象之间的关联关系。对象关联可以更好地支持关注分离原则,创建和初始化并不需要合并为一个步骤。
此外也可以使用ES6中class语法糖写法来表示这个例子:
// ES6 class语法糖
class Foo{
constructor(who){
this.me = who;
}
identify(){
return "I am" + this.me;
}
}
class Bar extends Foo{
constructor(who){
super(who);
}
speak = function(){
alert("Hello," + this.identify() + ".");
}
}
var b1 = new Bar("b1");
var b2 = new Bar("b2");
b1.speak();
b2.speak();
ES6语法相比与面向对象风格的写法有以下优势:
- 无须再使用杂乱的
.prototype了- 直接使用
extends继承,不在需要通过Object.create()来替换.prototype对象,也不需要设置_proto_或者Object.setPrototypeOf()- 可以使用
super()实现相对多态,如此任何方法都可以引用原型链上方的同名方法,解决了构造函数不能互相引用的问题- class字面语法不能声明属性,只能声明方法
- 通过
extends可以很自然得对对象子类型进行扩展但同时class语法也存在很多深层问题,class并不会像传统面向类的语言一样在声明时静态复制所有行为。如果你修改或者替换了父“类”中的一个方法,那么子“类”和所有实例都会受到影响,因为它们在定义时并没有进行复制,只是基于
[[Prototype]]进行了实时委托。class基本上只是现有
[[Prototype]](委托)机制的一种语法糖,并非是真正的类!ES6的class想伪装成一种很好的语法问题的解决方案,但是实际上却让问题更难解决而且让JavaScript更加难以理解(它隐藏了JavaScript对象之间的实时委托关联机制)。
如果这一篇文章你觉得不错或是对你有所帮助的话,可以给笔者一个赞吗?你小小的一个赞便是对笔者莫大的鼓励😊,同时欢迎各位朋友们在评论区评论留言,如有错漏之处敬请指正,互勉💪
引用文献:
【2】MDN对象原型