JS:原型链

原型链

原型

1
2
var arr = []
arr.push('1')

arr明明是个空数组,为什么会有push方法?在这个过程中,我们看看浏览器做了什么,在声明一个空数组时,浏览器会默认给arr绑定一个隐藏的属性,即arr._proto_。然后把早已开辟好的数组的原型Array.prototype指向这个隐藏的属性。即arr._proto_ = Array.prototype将其赋值。而数组的原型有push方法,当写arr.push()时,会查找arr自身是否有push属性,如果没有就会到arr._proto_里去找,显然arr._proto_.push是存在的。一般情况下,可以省去_proto_,直接写成arr.push,浏览器默认会到原型里去找。
如果在Array.prototype里没找到,则会到下一层原型里去找。如果要调用arr.toString(),数组的原型Array.prototype._proto_指向对象的原型Object.prototype。对象的原型最终指向Null。

原型链是什么

当我们读取arr.toString时,JS引擎会做下面的事情:

  1. 看arr数组自身是否有toString属性,没有就走到下一步。
  2. 看arr.proto对象是否有toString属性,没有就继续下一步。
  3. 假如没有的话,看arr.proto.proto对象是否有toString属性。
  4. 如果还没有就继续往下找,看arr.proto.proto.proto对象有没有,知道找到头String属性,或者找到的值为null。
    在这个搜索过程中,是连着有proto链子走下去的,这条链子叫做原型链。
    在原型链里,数组和对象都有toString属性,但二者是不同的。在原型链的搜索过程中,找的是离得最近的那个属性。

    原型有什么用

    1
    function fn(){}

声明一个函数时,我们知道他有call方法,且存在于Function.prototype里,正因为有了原型,使得不需要每次声明函数时,都要给自身添加call属性。只需要放在原型里,让它成为公共的属性。

1
2
3
4
5
6
7
8
9
10
var container = []
var soldier
for(var i = 0;i<100;i++){
soldier = {
id : id,
walk: function(){},
die: functionn(){}
}
container.push(soldier)
}

上面代码中,通过for循环造了一百个士兵,每个士兵都有id属性、walk方法和die方法。这种写法显然会极大地浪费内存。因为我们知道walk方法和die方法是soldier共有的,id属性才是solidier私有的。我们可以把共有的属性放在原型里,而私有的属性放在对象自身里,JS代码改进如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var container = []
var soldier
soldier.prototype = {
walk: function(){},
die: function(){}
}
/*实际工作,不是这么写,_proto_不是soldier的标准属性*/
soldier._proto_ = soldier.prototype
for( var i=0;i<100;i++){
soldier = {
id: i
}
container.push(soldier)
}

通过创建soldier的原型,然后将soldier的proto指向这个原型,这样原型便解决了重复创建的问题。

原型链的应用

  1. 如何判断一个属性是公有还是私有的。
    使用arr.hasOwnProperty(‘toString’)
  • 返回false: 不是私有属性
  • 返回true: 是私有属性
  1. 函数的特殊性
    Function是一个构造函数,它既可以看成是一个函数也可以看成是一个对象。当看作函数是,会有prototype属性,即Function.prototype,表示构造函数的原型。当Function看成对象,又会有proto属性,即Function._proto_,表示该对象的原型指向,在查找时,由来标记原型链的。

    从上图中看到,对于任意函数来说,它们的原型指向都是Function.prototype,但是构造函数它也是属于函数,所以它的原型指向也是Function.prototype

    其实Function._proto_Function.prototype位于同一块内存里。

    对象.proto = 对象.constructor.prototype = 对象的构造函数.prototype

上面的公式适用于所有原型图。

1
2
3
Function作为对象:Function._proto_ = Function.constructor.prototype = Function.prototype
Function作为函数:Function.prototype = fn._proto_
//由于Function是某个对象的函数,则可以随便找一个函数,例如:Array、Number、String等等,fn是由Function构造出来的匿名函数。

Function.prototype是所有函数对象的原型,包括了构造函数。
我们以Array为例,如下图所示。

原型的终极奥义(原型图)

JavaScirpt里的对象可以分为3类,一类是用户创建的对象,一类是构造函数对象,另一类是原型对象。这三类对象中每一类都有proto属性,通过它可以遍历整个原型链,追溯到原型链的最底层。以下面代码为例:

1
2
3
function Foo(){}
var foo = new Foo()
var obj = new Object()

其中的复杂关系可以用下面的原型图解释。