浅谈JavaScript原型链


一、概念理解

首先抛出直接解释

[[Prototype]]是对象中引用其他对象的一个内部链接(属性),如果在对象上没有找到需要的属性(或函数引用),引擎继续在[[Prototype]]指向的对象上查找。以此类推,如果在所指向的对象中也没有找到需要的引用就会继续查找它的[[Prototype]],这些对象链接就是原型链。

代码体现:


var o={a:1,b:2}

var b=Object.create(o)

var c=Object.create(b)

console.log(b.a,c.b)//1,2

console.log(b.hasOwnProperty('a'))//false

函数Object.create()会创建一个对象并把这个对象的[[Prototype]]关联到指定的对象。如上代码所示,新创建的对象b,c并不具有a和b的属性,所以引用时会继续访问对象的[[Prototype]]链直到访问到或者链至null,又比如hasOwnProperty这个“方法”引用取自原型链顶端Object.prototype,稍后会详细介绍。设置属性的情况则比较复杂,会发生屏蔽以及属性为setter等情况,这里不做展开。

接下来介绍一些概念。


[[Prototype]]:JavaScript中对象的内置属性,代表对于其他对象的引用。通常我们把这个“其他对象”称为原型对象,该原型对象又具有一个[[Prototype]]指向它的原型,如此层层向上直到对象的原型为null,这些对象的单向链接形成“原型链”,null没有原型,是该原型链的最后一环。([[Prototype]]链的顶端被设置为Object.prototype,该对象的原型为null)虽然myObject.[[Prototype]]表示对象myObject原型,但无法直接访问,可以借由.__proto__和Object.getPrototypeOf()函数访问以及Object.prototype.isPrototypeOf()来判断。大多数对象在创建时[[Prototype]]属性会被赋予一个非空值。

__proto__:对象属性,大部分浏览器所支持的访问内部[[Prototype]]的非标准方式。可以这么理解:.__proto__之所以能够访问内部[[Prototype]]属性在于该属性的作用是调用Object.getPrototypeOf()(括号解释可略过:作为Object.prototype属性的__proto__是访问描述符,其getter为函数调用Object.getPrototypeOf(this),用以暴露内部[[Prototype]];
setter近似为function(o){Object.setPrototypeOf(this,o);return o;})表明了.__proto__的可设置,但一般不需要)。另外__proto__前后是均是double下划线_,注意长度。

prototype:函数对象属性,所有 函数只有个别例外)默认拥有的公有不可枚举属性(敲黑板),指向某个对象。为了便于叙述,统一以 函数 bar()为例。如果将 bar.prototype 所指向的对象叫做bar的原型了(一般是这样称呼的),那么bar.__proto__( bar.[[Prototype]] )又应该叫什么。如果是我就会把前者称作bar.[ˈprəʊtətaɪp]而非原型(o(*≧▽≦)ツ┏━┓)。读者应该注意到讨论对象的原型链时,函数对象的特别之处,即 函数拥有除去__proto__之外的prototype属性。

new操作符:使用new来调用函数,会创建一个全新的对象,这个对象的[[Prototype]]指向函数原型(.prototype),对象则绑定到函数调用的this,如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。后文介绍Object.create()函数时可结合这点来理解。

类型与构造函数:在js中object是六种主要类型之一,其余简单基本类型(string、boolean、number、null和undefined)并非对象,而对象object又拥有一些诸如 String、Number、Object、Function之类的对象子类型,但其实它们只是内置函数,在原型链的理解中,把它们当做一系列构造函数。

一般而言,函数都可称之为“构造函数”。因为会有这样的使用场景:

function bar(who){this.me=who} 

var n=new bar("n")

var m=new String("I'm a string")

console.log(n.__proto__===bar.prototype)//true

console.log(m.__proto__===String.prototype)//true

实际上函数并非构造函数,但是当使用new关键字时,函数调用变成了“构造函数调用”

无论是声明的函数bar(who){ ... }还是内置函数String(){ [native code] }似乎都被当做了提供类机制的语言(比如Java)中的 构造函数 来创建一个对象。并且被创建对象(n,m)的 [[Prototype]] (属性__proto__)链接到了构造函数的.prototype所指向的对象。

这里我避免了将bar.prototype和String.prototype称作bar或者String的原型,新对象的内部链接[[Prototype]]指向的就是 其构造函数.prototype(比如bar.prototype) 这个对象。

在前面提到过,几乎所有的函数均拥有一个prototype属性,这个属性指向 某个对象。String.prototype指向的这个对象(注意是对象)拥有slice(),concat(),indexOf()等字符串对象常用的诸多方法,其[[Prototype]](String.prototype.__proto__)则指向了顶层的Object.prototype。而对象Object.prototypeObject.[ˈprəʊtətaɪp],如果你不会混淆就直接称它Object的原型吧)就是由内置函数Object()构造的对象的[[Prototype]]所指向的东西。

而bar.prototype这类自定义函数的prototype属性指向对象又是怎样的呢?这类对象不特别定义的话,非常简陋(此句存疑),一般只有constructor__proto__两个属性。constructor引用了函数本身(bar()),而__proto__指向了Object.prototype。像Number.prototypeString.prototype这些对象肯定早就布局于js中了,而通过声明定义的函数的prototype则是在使用new符号构造时与新对象一同创建的,可以被任意设置(bar.prototype={ ... })。

通过上面的例子与叙述,你可能发现了一些规律或者产生了一些疑惑,接下来将总结一番。

1.所有内置函数包括Object(),Function()等以及自定义声明的函数对象,它们的__proto__属性([[Prototype]])都指向了Function.prototype函数(浏览器显示ƒ () { [native code] }),可以认为这个函数是所有函数的源头,拥有常见的bind()、apply()、call()等可以被所有函数对象通过原型链调用的方法。

可以这样理解:所有的函数均由new Funtion()构造(实际应用请采用声明定义),故这些函数的__proto__被链向了Function.prototype

"构造函数".prototype指向的应该是对象,但是Function.prototype比较特殊,typeof运算符下为function而非object,但仍可被当做对象理解)

2.所有函数的“原型”(prototype属性指向对象)可以被看作由构造函数Object()创建的对象,故"构造函数".prototype这个对象(包括 Function.prototype)的[[Prototype]]链至 Object.prototype

3.一般而言,对象可以由 字面量、new、Object.create() 三种方式创建,实际上都可以看做是由 new 方式构建,文字形式和构造形式的关联毋庸赘述,来看一看 Object.create() 的近似实现: function create(proto){ function() F{};F.prototype=proto;return new F();} new操作符将新对象的[[Prototype]]链至F.prototype,即传入参数对象proto,F() {}只是作为一个桥梁短暂地存在。而最上层的函数总是由 new Object() 构造的,故对象的原型链都可以上溯到Object.prototype
关系图一览:

image

Function()和Object()作为核心互相构造,f1()代表绝大部分函数。


补充:

constructor:函数.prototype 在函数声明时会拥有这个默认属性,它指向函数本身,.constructor这个属性会通过原型链被下级对象引用,所以函数与对象也具有.constructor引用,只不过并不代表一定具有这个属性;也可通过自定义 函数.prototype={/.../}被抹除掉或者被重新定义,所以它最好被当做一个不可靠普通的属性来理解而不是“构造函数”的引用。


二、原型链的使用

1.继承:如果读者此前未接触 类机制 的语言或者只想了解js中的原型链,请略过含有 继承 的句子。解释JavaScript中的继承会费一番口舌,因为JavaScript中的继承只是“形似”。尽管ES6中引入了语法糖class,但它仍旧没提供 子类得到父类复制的副本 这样的继承机制。

使用继承的思想需要我们将大多数任务都有的行为抽象出来作为基本父类,然后定义诸多子类继承父类并特殊化其行为(重写和多态)。来看下面一段代码:

function Vehicle(){ this.engines=1; }

Vehicle.prototype.ignition=function(){  console.log("Engine starts")  }

Vehicle.prototype.drive=function(){  console.log("moving forward")  }

function Car(type){ Vehicle.call(this); this.name=type}

Car.prototype=Object.create(Vehicle.prototype)

Car.prototype.myDrive=function(){ console.log(this.name);this.drive()  }

var  Rover=new Car("Rover")

Car.ignition() //Engine starts

Car.myDrive()//Rover moving forward

这段代码定义了父类 Vehicle和子类 Car,子类继承父类的属性和“方法”是通过原型链来实现的。Car.prototype继承Vehicle.prototype的方法,而新对象Rover通过new构造的原型链引用Car.prototype的属性。这样的继承逻辑确实没有问题,但令人感到别扭,你可能会问为什么要通过 .prototype之间的关联来实现继承,而不采用传统的一些定义方式。

你也许会使用语法糖这样定义:

class Vehicle{

constructor(type){ this.me=type }

ignition(){ ... }

drive(){ ... }

}

class Car extends Vehicle{

ignition(){ super.ignition() .../*another new code*/ }

drive(){ super.drive() .../*another new code*/}

}

遗憾的是,class中的方法仍旧定义在其prototype上(typeof运算符下class为function),即有

console.log(Object.getOwnPropertyNames(Vehicle.prototype))//["constructor","ignition","drive"]

console.log(Object.getOwnPropertyNames(Car.prototype))//["constructor","ignition","drive"]

Car.prototype.__proto__===Vehicle.prototype//true

Car.__proto__===Vehicle//true

并且子类、子类实例与父类、父类实例依然通过原型链联系在一起。


接下来是一些个人愚见:

要解释这个状况也许是基于一个前提:js中复制一个对象(函数)是个难以名状的问题,一般我们只能复制引用。所以传统 类机制所实现的子类父类方法互不影响在JavaScript中没有延续下来,进一步来说子类父类其实也不存在,有的只是对象。js也提供了一些模拟类复制行为的方法,我们来看其中一种:

function  mixin(sourceObj,targetObj){

    for(var key in sourceObj){

           if (!(key in targetObj)){  targetObj[key]=sourceObj[key];  }

    }

    return  targetObj;

}

var Vehicle={

    engines:1,

    ignition:function(){ console.log("Engine starts"); },

    drive:function(){  this.ignition();  console.log("moving forward");  },

};

var Car=mixin(Vehicle,{

    wheels:4,

    drive:function(){

            Vehicle.drive.call(this);

            console.log(this.wheels+"moving start");

    }

});

mixin函数充当关键字extends的功能,其中判断语句用以完成子类重写。子类Car的ignition函数实际为父类的引用,同时重写Car中drive方法时,由这样一个语句实现了多态:Vehicle.drive.call(this)。引用会在多处建立起关联,一旦修改共享的函数对象便会影响到多个类和实例。当类以及继承的数目和关系较复杂时,模拟类的这种方式可读性差,并且维护成本高。

我想这可能是class依旧采用原型链继承而非副本复制的部分原因。至于为什么一开始JavaScript没有类机制及其原型链的初衷则需进一步查阅资料。


2.行为委托:对于JavaScript中引入类与继承的现状各人看法不同。有人认为原型链真正的服务对象是一种名为 委托的设计模式。委托行为发生在某些对象在找不到属性或者方法引用时,会把这个请求委托给另一个对象。代码形式像这样:

VehicleAction={ 

     setType:function(type){  this.type=type;  },

      ignition:function(){  console.log(this.type+"  engine  starts")  },

}

Car=Object.create(VehicleAction);

Car.drive=function(){   this.ignition();  console.log("=>moving forward"); };

var c=Object.create(Car);

c.setType("Rover");

c.drive();//Rover engine starts  =>moving  forward

这段代码特点有两处,一是通过Object.create()建立的原型链注重对象之间关联,直接来创建对象,不再通过.prototype属性(当然,能舍弃.prototype和.constructor也依赖了Object.create()的优异性能);再有则是避免了在原型链不同环中使用相同的命名,即在设计之初理清对象行为的类型。

这样的设计模式关注的焦点是多方向的关联关系,而非垂直的继承关系。另外,有时候代码没有像这样直接使用 C.ignition()是有考虑到内部委托会让API设计更清晰的缘故。


参考:
1.You Don't Know JavaScript: Types & Grammar by Kyle Simpson(O'Reilly).Copyright 2015 Getify Solution,Inc.,978-1-491-90419
2.MDN web doc


请提出文中的不当表述和例子,我会尽量修改完善。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342