在JavaScript
中,原型链作为一个基础,老生长谈,今天我们就来深入的解读一下原型链。
本章主要讲的是下面几点,可以根据需要进行阅读:
- 函数与对象
- 对于
prototype
的认识 - 对于<code>__proto__</code>的的认识
-
prototype
和<code>__proto__</code>的关系 -
instanceof
操作符到底是怎么穿梭的 -
[[prototype]]
链属性的访问 -
[[prototype]]
链上的属性设置与属性屏蔽 - 关于
prototype
中的constructor
属性 - 当我们在使用
new
的时候到底发生了什么 - 应用:两种继承的设计模式
- 函数与对象到底是什么关系
1. 函数与对象
我们都知道,JavaScript
中,一切都是对象,函数也是对象,数组也是对象,但是数组是对象的子集,而对于函数来说,函数与对象之间有一种“鸡生蛋蛋生鸡”的关系,我们会在最后进行总结。
- 所有的对象<b>都是</b>由
Object
继承而来,而Object
对象却是一个函数。
- 对象<b>都是</b>由函数来创建的。
对于上述的第一点,前半部分会在后面的解释中讲到,而对于后半部分,在控制台中输入typeof Object
,显然输出的是function
。
上述的第二点,我们可以看一下下面的例子。
var obj = { a: 1, b: 2}
var arr = [2, 'foo', false]
表面上来看,好像不存在函数创建对象,而实际上,以上的过程是这样子的:
var obj = new Object()
obj.a = 1
obj.b = 2
var arr = new Array()
arr[0] = 2
arr[1] = 'foo'
arr[2] = false
//typeof Object === 'function'
//typeof Array === 'function'
2. 对于prototype
的认识
每一个<b>函数</b>都有一个属性叫做prototype
,它的属性值是一个对象,在这个对象中默认有一个constructor
属性,指向这个函数的本身。如下图:
3. 对于<code>__proto__</code>的的认识
<code>__proto__</code>是隐式原型,通常也写作[[prototype]]
每一个<b>对象</b>都有一个这样的隐藏属性,<b>它引用了创建这个对象的函数的prototype
。</b>(注:并不是所有浏览器都实现了对于对象的隐式原型的提供!)
需要注意的是,函数也是对象,自然它也有__proto__
。
可见,<code>__proto__</code>和prototype
并不相同(有例外,存在指向相同的情况),那两者有什么样的联系呢,继续往下看。
4. prototype
和<code>__proto__</code>的关系
前面我们讲到了两个很重要的点:
- 每一个<b>函数</b>都有一个属性叫做
prototype
,它的属性值是一个对象。
- 每一个<b>对象</b>都有一个隐式原型<code>__proto__</code>,它引用了创建这个对象的函数的
prototype
。
所以,下面让我们来看一段代码看看两者之间的关系:
var o1 = new Object()
var o2 = new Object()
上面的Object
作为构造函数创建了两个对象o1
和o2
。
看一下图解:
结合上面的两句话:
-
function Object
在这里作为一个构造函数,毫无疑问它是一个函数,那么自然有一个prototype
属性。 - <code>__proto__</code>引用了创建这个对象的函数的
prototype
。于是,o1
和o2
对象都是由function Object
创建出来的,那么自然的,它就指向(引用)了创建它们的函数(Object
)的prototype
属性。
那我们再来看如果是一个普通的构造函数而不是内置的呢?一样的道理,这里我们就不再赘述。
function foo() {}
var f1 = new foo()
var f2 = new foo()
<b>注意:这里有一个特例!</b>
对于Object.prototype
来说,它的__proto__
是null
,这是一个<b>特例。</b>
同时,我们要注意图里面有一个Foo.prototype
,它的__proto__
指向了Object.prototype
。这个是因为:<b>一切的对象都是由Object
继承而来</b>,也就是说Foo.prototype
这个对象也是由Object
构造的,所以说Foo.prototype.__proto__
指向(引用)了Object.prototype
,这个也符合我们上面所述的<b>每一个<b>对象</b>都有一个隐式原型<code>__proto__</code>,它引用了创建这个对象的函数的prototype
</b>。
到这里,似乎prototype
和__proto__
关系已经很明朗的,但是你有没有发现还有一个坑,我们从头到尾都在围绕function Object()
这个东西,那我们会不会考虑🤔这个鬼东西是从哪里来的呢?
难道凭空出现?显然,不存在的!毕竟,存在即合理。
那函数是怎么创建出来的呢?我们继续来看一段代码,这段代码可能你很少见,但是如果你读过红宝书函数的一章,你一定不会感到陌生!
function foo(a, b) {
return a + b
}
console.log(foo(1, 2)) //3
var boo = new Function('a', 'b', 'return a + b') //Function大写
console.log(boo(1,2)) //3
以上,第二种写法出现了大写的Function
。(不推荐这么写。因为这是一种创建动态函数的写法,原因参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function )
从上面的代码可知,函数是被Function
创建的。
所以,function Object
是由Function
创建的,那么Object.__proto === Function.prototype
也就不言而喻了,于是就有下面的一张图。
在这张图中,Foo
和Object
这两个函数的__proto__
就是指向Function.prototype
了。
这里又有一个<b>特例</b>!(相信我,这是最后一个特例了🙄~)
没错,你会发现一个坑?为什么Function.__proto__
指向了Function.prototype
🤔?这又是什么操作?
我们来理一下思路:函数是由Function
创建的,那么Function
也是一个函数,那么它有没有可能是自己搞自己的呢😎?
答案是肯定的。
于是,函数是由Function
创建的,那么Function
由自身创建,所以Function.__proto__
就指向了创建它的函数(也就是自己)的prototype
。
那最后,把Foo.prototype
、Object.prototype
、Function.prototype
的__proto__
连起来,就可以得到下面这一张图。(红色标识即为特例)
最后,再次总结一下:
- 所有的对象<b>都是</b>由
Object
继承而来,对象<b>都是</b>由函数来创建的。 - 每一个<b>函数</b>都有一个属性叫做
prototype
,它的属性值是一个对象。 - 每一个<b>对象</b>都有一个隐式原型<code>__proto__</code>,它引用了创建这个对象的函数的
prototype
。
5. instanceof
操作符到底是怎么穿梭的
既然讲到了__proto__
和prototype
,那么密不可分的就是instanceof
操作符了。
对于
A instanceof B
来说,它的判断规则是:沿着A
的__proto__
这条线来找,同时沿着B的prototype
这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true
。如果找到终点还未重合,则返回false
。
所以,不熟悉的🙊就可以通过上面的那个总图来进行判断到底是返回true
还是false
。
那么,我们来举个🌰:
function fn {}
var f1 = new fn();
console.log(f1 instanceof Object);//true
console.log(f1 instanceof fn);//true
显然,沿着链条穿梭成立!
再来看几个喜闻乐见的:
console.log(Object instanceof Function);//true
console.log(Function instanceof Object);//true
console.log(Function instanceof Funciton);//true
所以,instanceof
操作符机制就不言而喻了。
6. [[prototype]]
链属性的访问
众所周知,JavaScript
中的继承是通过[[prototype]]
链来实现的(也叫原型链)。
看下面代码:
function foo (){}
var f1 = new foo()
f1.a = 10
foo.prototype.a=1
foo.prototype.b=2
console.log(f1.a) //10
console.log(f1.b) //2
console.log(f1.c) // undefined
访问一个对象的属性时,先在这个对象自身属性中查找,如果没有,再沿着
__proto__
这条链向上找,这就是[[prototype]]
链(原型链),如果一直找不到,那么最后会返回undefined
。
那如何区分这个属性是实例对象中的(比如说上面new
出来的对象f1
)还是通过[[prototype]]
链找到的呢?
答案就是hasOwnProperty
,同时,在for...in
循环中,要注意该遍历会遍历出包括原型的所有属性。
我们可以对上面代码的a
和b
进行检测:
function foo (){}
var f1 = new foo()
f1.a = 10
foo.prototype.a=1
foo.prototype.b=2
console.log(f1.a)//10
console.log(f1.b)//2
console.log(f1.hasOwnProperty('a')) //true
console.log(f1.hasOwnProperty('b')) //false
在这里,本身f1
是没有hasOwnProperty
方法的,并且,foo.prototype
也是没有的。那其实它是从Object.prototype
中继承而来的。可见,<b>[[prototype]]
链最终的位置就是Object.prototype
</b>。以下是Object.prototype
的一些属性和方法。
7. [[prototype]]
链上的属性设置与属性屏蔽
先来看一下这段代码:
var parentObject = {
a: 1,
b: 2
};
var childObject = {};
console.log(childObject); // > Object {}
childObject.__proto__ = parentObject;
console.log(childObject); // > Object {}
childObject.c = 3;
childObject.a = 2;
console.log(parentObject); // Object {a: 1, b: 2}
console.log(childObject); // > Object {c: 3, a: 2}
这是一个很简单的属性设置,但是其实里面存在着[[prototype]]
链属性设置的机制🙃。
如下:
- 如果属性
c
不是直接存于childObject
上,[[Prototype]]
链就会被遍历,如果[[Prototype]]
链上找不到c
,c
这时就会被直接添加到childObject
上。 - 如果这时属性
a
存在于原型链上层而不存在于childObject
中,赋值语句childObject.a = 2
却不会修改到parentObjec
t中的a
,而是直接把a
作为一个新属性添加到了childObject
上。
于此同时,也就发生了属性屏蔽😭。
此时会发现,赋值完了以后,parentObject
的a
属性没有被修改,而childObject
中新增了一个a
属性,所以现在就会出现一个问题,parentObject
的a
属性再也不能通过childObject.a
的方式被访问到了。
在这里,就发生了属性屏蔽,childObject
中包含的a
属性会屏蔽原型链上层所有的a
属性,因为childObject.a
总会选择原型链中最底层的a
属性。
但实际上,屏蔽比我们想象中的更复杂。下面我们一起来分析一下a
不直接存在于childObject
中,而是存在于原型链上层时, 执行childObject.a = 2
语句会出现的三种情况。
如果在
[[Prototype]]
链上层存在名为a
的普通数据访问属性,并且没有被标记为只读(writable: false
),那就会直接在childObject
中添加一个名为a
的新属性,它是屏蔽属性,这个情况就是上文例子中发生的情况。如果在
[[Prototype]]
链上层存在a
,但它被标记为只读(writable: true
),那么无法修改已有属性或者在childObject
上创建屏蔽属性,严格模式下执行这个操作还会抛出错误。
var parentObject = {};
Object.defineProperty(parentObject, "a", {
value: 2,
writable: false, // 标记为不可写
enumerable: true //可遍历
});
var childObject = {
b: 3
};
childObject.__proto__ = parentObject; // 绑定原型
childObject.a = 10;
console.log(childObject.a); // 2
console.log(childObject); // > Object {b: 3}
console.log(parentObject); // Object {a: 2}
- 如果在
[[Prototype]]
链上层存在a
并且它被定义成了一个setter
函数,那就一定会调用这个setter
函数。a
不会被添加到childObject
,上层的setter
也不会被重新定义。
var parentObject = {
set a(val) { //这是set函数,相当于赋值
this.aaaaaa = val * 2;
}
};
var childObject = {
b: 3
};
childObject.__proto__ = parentObject;
childObject.a = 10;
console.log(childObject); //Object {b: 3, aaaaaa: 20}
console.log(parentObject); //Object {}
另外,属性屏蔽还有一种很容易被忽略的情况😩:
var parentObject = {
a: 2
};
var childObject = Object.create( parentObject ); // 这句话相当于先定义一个空对象,再绑定原型
console.log(parentObject.a); // 2
console.log(childObject.a); // 2
console.log(parentObject.hasOwnProperty('a')); // true
console.log(childObject.hasOwnProperty('a')); // false
console.log(parentObject); // > Object {a:2}
childObject.a++; // 这时候迭加的应是原型链上parentObject的a
console.log(parentObject.a); // 2
console.log(childObject) // > Object { a: 3 }
console.log(childObject.a); // 3
console.log(childObject.hasOwnProperty('a')); // true
childObject.a
访问的应是parentObject
上的a
属性,然而执行迭加后却产生了上面这个结果,原型链上的a
并没有被修改到。 原因就是,在执行childObject.a++
时,发生了隐式的属性屏蔽,因为childObject.a++
实际上就相当于childObject.a = childObject.a + 1
。
8. 关于prototype
中的constructor
属性
上面有介绍说到constructor
是函数原型的一个属性,指向函数的本身。
function Foo() {
this.name = 'dog';
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
当a.constructor === Foo
的时候,其实这时候并不能够说明a
是由Foo
构造而成的。实际上,a.constructor
的引用是被委托给了Foo.prototype
(本身a
自身是没有这个属性的),所以才会出现等价的情况,而并不能说明a
是由Foo
构造而成的。
而对于constructor
来说,这个属性其实就是[[prototype]]
上一个简单的默认属性,没有writable:false
也不是setter
,只是有一个默认行为。
继续看下面的代码:
function Foo() {
this.name = 'dog';
}
Foo.prototype = {
h: 'hhh'
};
var a1 = new Foo();
a1.constructor === Foo; // false
a1.constructor === Object; // true
a1 instanceof Foo //true
这里由于Foo.prototype
的默认属性被清空了,所以constructor
不存在,可是__proto__
构成的原型链是不变的,所以a1.constructor
的引用被委托到Object.prototype.constructor
,所以第一个返回false
,第二个返回true
。
所以,我们应该怎么对待constructor
这个属性呢😶?
它并不是什么神秘的属性,Foo.prototype
的constructor
属性只是Foo
函数在声明时的默认属性。一定程度上可以用.constructor
来判断原型指向,但它并不安全,除了有这个默认行为之外,<b>它和我们平常自定义的属性,再也没什么区别了。</b>
9. 当我们在使用new
的时候到底发生了什么
在JavaScript
中,构造函数只是一些使用new
操作符时被调用的函数,它们并不会属于某个类,也不会实例化一个类。所以,实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
当使用new
来调用函数时,会自动执行以下操作:
- 创建一个全新的对象
- 这个新对象会被执行
[[prototype]]
连接 - 这个新对象会绑定到函数调用的
this
- 如果函数没有返回其他对象,那么
new
表达式中的函数调用会自动返回这个新对象。
看下面的例子:
function SuperType(name) { // 定义了一个超类,供下面的子类继承
this.name = name;
}
function SubType() { // 定义了子类1,继承了超类,无返回值
SuperType.call(this, "Cong1");
this.age = 29;
}
function SubType2() { // 定义了子类2,继承了超类,返回了一个引用类型的值
SuperType.call(this, "Cong2");
this.age = 29;
return { a: 2 };
}
function SubType3() { // 定义了子类3,继承了超类,返回了一个值类型的值
SuperType.call(this, "Cong3");
this.age = 29;
return 3;
}
/* 下面比较有new操作符和无new操作符调用子类的区别 */
var instance1_nonew = SubType();
var instance2_nonew = SubType2();
var instance3_nonew = SubType3();
var instance1_hasnew = new SubType();
var instance2_hasnew = new SubType2();
var instance3_hasnew = new SubType3();
// 依次打印六个变量
console.log(…);
得到的结果是:
instance1_nonew
undefined
instance2_nonew
> Object {a: 2}
instance3_nonew
3
instance1_hasnew
> SubType {name: "Cong1", age: 29}
instance2_hasnew
> Object {a: 2}
instance3_hasnew
> SubType3 {name: "Cong3", age: 29}
没有new
操作符的语句,就像我们平常调用函数一样,得到的肯定是函数的返回值,所以前3个_nonew
变量就会得到图示所示的结果。
而看到下面3个_hasnew
变量,行为却有点不同,没有返回值的1_hasnew
就直接构造了一个实例对象,而2_hasnew
和3_hasnew
都是有返回值的,两者的表现却不同了。
根据上面所说的原理再来分析一下这个过程:
- 首先新建一个对象:
var instance = new Object()
- 给这个对象设置
[[prototype]]
链:
instance.__proto__ = SubType.prototype
- 绑定
this
,将SubType
中的this
指向instance
,执行SubType
中的语句进行赋值。 - 返回值,这里要根据
SubType
的返回类型来判断😷:
- 如果是一个引用类型(对象),那么就替换掉
instance
本身的这个对象。(如:instance2_hasnew
) - 如果是值类型,那么直接丢弃它,返回
instance
对象本身。(如:instance3_hasnew
)
10. 应用:两种继承的设计模式
在JavaScript中没有类的概念,使用的是原型继承。而有两种常见的设计模式,一种是面向对象模式,而另外一种是对象关联模式。
在使用的过程中,都用到了Object.create()
,它会创建一个新对象并把它关联到我们指定的对象,也就是进行[[prototype]]
连接。
- “原型”面向对象风格
function Foo(who) {
this.me = who
}
Foo.prototype.identify = function() {
return "I am " + this.me
}
function Bar(who) {
Foo.call(this,who)
}
Bar.prototype = Object.create(Foo.prototype)
Bar.prototype.speak = function() {
console.log("Hello, " + this.identify() + ".")
}
var b1 = new Bar("b1")
var b2 = new Bar("b2")
b1.speak() //Hello, I am b1.
b2.speak() //Hello, I am b2.
关系图如下:
- 对象关联风格
Foo = {
init: function(who) {
this.me = who
},
identify: function() {
return "I am " + this.me
}
}
Bar = Object.create(Foo)
Bar.speak = function() {
console.log("Hello, " + this.identify() + ".")
}
var b1 = Object.create(Bar)
b1.init("b1")
var b2 = Object.create(Bar)
b2.init("b2")
b1.speak() //Hello, I am b1.
b2.speak() //Hello, I am b2.
关系图如下:
以上两种继承的设计,明显发现第二种更加的简洁。
在“原型面向对象风格”中,需要时刻的留意prototype
的情况,[[prototype]]
“游走”于函数的prototype
之间。
而对于“对象关联风格”,它只关心一件事,那就是对象之间的关联情况,不将方法写于函数的prototype
上。
虽然实现的原理是相同的,但是不同的思维方式,更利于理解,代码风格更为友好🤗。
11. 函数与对象到底是什么关系
其实,这个问题也是困扰了我很久😪。
我们都知道:
- 一切对象继承于
Object
。(当然Object.prototype
除外) -
Object.prototype.__proto__
指向了null
。 - 对象都是由函数创建的。
以上,看似并没有什么用,那现在我们来缕一下思路。
- (这里先不考虑
Object.prototype
)一切对象继承于Object
,所以说,对象的原型链(__proto__
)最终的位置应该是Object.prototype
。所以一切的老大应该是Object.prototype
。 -
Object.prototype.__proto__
指向了null
。既然__proto__
的指向是创建这个对象的函数原型,可是这里Object.prototype.__proto__
却指向了null
。那么,唯一可能就是Object.prototype
是由JavaScript
引擎创造出来的。 - 所以,<b>最终
[[prototype]]
链的位置应该是null
而不是Object.prototype
</b>。 - 对象都是由函数创建的。(这里的对象同样是不考虑
Object.prototype
的)也就是说,所有的对象都是由Function
构造出来,那么他们的[[prototype]]
都应该经过Function.prototype
。 - 于是,引用类型等构造函数(如:
Array()
、Object()
等)以及普通的函数对象,甚至Function
,他们的__proto__
应该是指向Function.prototype
。 - 那
Function.prototype
的__proto__
指向了哪里?由第一点可知,当然是指向了Object.prototype
,所以Function.prototype
就是老二。
所以,简而言之:
- 首先有的应该是
Object.prototype
,它是由JavaScript
引擎创造出来的。 - 紧接着才有
Function.prototype
,并把它的__proto__
连接到了Object.prototype
。 - 接下来,将各种内置引用类型的构造函数的
__proto__
连接到了Function.prototype
。 - 执行
Function.__proto__
连接到Function.prototype
的操作。 - 执行
Object.__proto__
连接到Object.prototype
的操作。 - 最后再是对
Function
和Object
实例的挂载。
注:以上为个人的见解,欢迎指正😉。
😉这是一条可爱的分割线😉。
以上,就是本次博客的全部内容(终于结束了)感谢你耐心的阅读😉
第一次写博客,如有理解错误的地方,师请改正😳。
参考资料:
书籍:《你不知道的JavaScript(上卷)》
博客:http://www.cnblogs.com/wangfupeng1988/p/4001284.html
博客:http://www.yangzicong.com/article/1