浅谈原型

1、[[Prototype]]
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。
思考下面代码:

var myobject = {
      a: 2
};
myObject.a; //2

当试图引用对象的属性时会触发[[Get]]操作,比如myObject.a。对于默认的[[Get]]操作来说,第一步就是检查对象本身是否有这个属性,如果有的话就使用它。但是如果a不在myObject中,就需要使用对象的[[Prototype]]链了。:

var anotherObject = {
      a:2
};
//创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
my.Object.a; //2

现在myObject对象的[[Prototype]]关联到了anotherObject。显然myObjext.a并不存在,但属性访问依然成功地找到了值2。但如果anotheObject中也找不到a并且[[Prototype]]链不为空的话。就会继续查找下去。这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]]链。如果是后者的话,[[Get]]操作的返回值是undefined。

引申到一个问题:哪里是[[Prototype]]的尽头呢?所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。

2、类
为什么一个对象需要关联到另一个对象?
首先来了解[[Prototype]]"不是"什么。

JavaScript和面向类的语言不同,它并没有类来作为对象的抽象模型,JavaScript只有对象。但是JavaScript却有模仿类的行为,模仿类的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为prototype的共有并且不可枚举的属性,它会指向另一个对象。

function Foo(){}
Foo.prototype; // {}

这个对象通常被称为Foo的原型,因为是通过名为Foo.prototype的属性引用来访问它。

这个对象到底是什么?最直接的解释就是,这个对象是在调用new Foo()时创建的,最后会被关联到这个Foo.prototype对象上。

function Foo(){}
var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; //true

调用new Foo()时会创建a,其中一步就是给a一个内部的[[Prototype]]链接,关联到Foo.prototype指向的那个对象。

在面向类的语言中,类可以被复制多次,就像用模具制作东西一样。之所以会这样是因为实例化一个类就意味着”把类的行为复制到物理对象中“,对于每一个新实例来说都会重复这个过程。

但是在JavaScript中,并没有类似的复制机制。不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。

new Foo()会生成一个新对象(a),这个新对象的内部链接[[Prototype]]关联的是Foo.prototype对象。这种机制通常被称为原型继承,它常常被视为动态语言版本的类继承。

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。

委托这个术语可以更加准确地描述JavaScript中对象的关联机制。

Foo.prototype还有另一个绝招:

function Foo(){}
Foo.prototype.constructor === Foo;//true
var a = new Foo();
a.constructor === Foo;//true

Foo.prototype默认有一个公有并且不可枚举的属性.condtructor,这个属性引用的是对象关联的函数。此外通过构造函数调用new Foo()创建的对象也有一个.constructor属性,指向”创建这个对象的函数“

看起来a.constructor === Foo为真意味着a的确有一个指向Foo.prototype.constructor默认指向Foo。其实是错的。实际上.constructor引用同样被委托给了Foo.prototype,而Foo.prototype.condtructor默认指向Foo。

举例来说,Foo.prototpe的.constructor属性只是Foo函数在声明时的默认属性,如果创建一个新对象并替换了函数默认的.prototype对象引用,那么新对象并不会自动获得.constructor属性。
思考下列代码:

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

a1并没有.constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype。但是这个对象也没有.constructor属性,所以会继续委托,这次委托给了链的顶端Object.prototype。这个对象有.constructor属性,指向内置的Object()函数。

3、继承
思考下列代码:

function Foo(name){
    this.name = name;
}
Foo.prototype.myName = function (){
    return this.name;
};
function Bar(name, label){
    Foo.call(this, name);
    this.label = label;
}
//我们创建了一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
// 注意! 现在没有 Bar.prototype.constructor 了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function (){
    return this.label;
};
var a = new Bar("a", "obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"

这段代码的核心语句Bar.prototype = Object.create( Foo.prototype )。调用Objec.create()会凭空创建一个新对象并把新对象内部的[[Prototype]]关联到你指定的对象。

下面有两种常见的错误做法

// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;
// 基本上满足你的需求, 但是可能会产生一些副作用 :(
Bar.prototype = new Foo();

第一种并不会创建一个关联到Bar.prototype的新对象,它只是让Bar.prototype直接引用Foo.prototype对象。因此当执行类似Bar.prototype.myLabel = ...的赋值语句时会直接修改Foo.prototype对象本身。
第二种的确会创建一个关联到Bar.prototype的新对象。但是它使用了Foo()的”构造函数调用“,如果foo有一些副作用的话,就会影响Bar()的后代。

因此,要创建一个合适的关联对象,我们必须使用Object.create()而不是使用具有副作用的Foo()。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。

在ES6前,只能通过设置.proto属性来修改对象的[[Prototype]]关联,但是这个方法并不是标准并且无法兼容所有浏览器,ES6添加了辅助函数Object.setPrototypeOf(),可以标准并且可靠的方法来修改关联。

来比对一下:

// ES6 之前需要抛弃默认的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 开始可以直接修改现有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );

忽略Object.create()方法带来的轻微性能损失,它实际上畀ES6及其之后的方法更短而且可读性更高。

假设有对象a,那么如何寻找对象a委托的对象呢?在传统的面向类环境中,检查一个实例的继承祖先通常被称为内省。

  1. 站在类的角度来判断:a instanceof Foo;//true
    instanceof回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象。但这个方法只能处理对象(a)和函数(Foo.prototype)之间的关系。
  2. Foo.prototype.isPrototypeOf(a); //true
    isPrototypeOf(..) 回答的问题是: 在 a 的整
    条 [[Prototype]] 链中是否出现过 Foo.prototype ?

我们也可以直接获取一个对象的[[Prototype]]链。在ES5中,标准的方法是:
Objcet.getPrototypeOf(a);
Object.getPrototypeOf( a ) === Foo.prototype; // true
绝大多数浏览器也支持一种非标准的方法来访问内部[[Prototype]]属性:
a.__proto__ === Foo.prototype;//true
这个奇怪的 .proto(在 ES6 之前并不是标准! ) 属性“神奇地” 引用了内部的[[Prototype]] 对象, 如果你想直接查找(甚至可以通过 .proto.ptoto... 来遍历)原型链的话, 这个方法非常有用。

和.constructor一样,.proto实际上并不存在与正在使用的对象中。实际上,它存在与内置的Objcet.prototype中,更像是一个getter/setter。

.proto 的实现大致上是这样的:

Object.defineProperty( Object.prototype,"__proto__", {
       get: function() {
              return Object.getPrototypeOf( this );
       },
       set: function(o) {
              // ES6 中的 setPrototypeOf(..)
              Object.setPrototypeOf( this, o );
              return o;
       }
} );

因此,访问a.proto时,实际上是调用了a.proto()。

4、对象关联
[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找它的[[Prototype]],以此类推,这一系列对象的链接被称为”原型链“

那[[Prototype]]机制的意义是什么?Object.create()的作用?

var foo = {
       something: function() {
              console.log( "Tell me something good..." );
       }
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...

Object.create()会创建一个新对象(bar)并把它关联到指定的对象(foo)。

假设要调用bar.something(),如果bar中不存在cool()时这条语句可以正常工作的话,那这API设计可能不被维护软件的开发者理解。

可以这样设计:

var foo = {
      something: function() {
            console.log( "Tell me something good..." );
      }
};
var bar = Object.create( foo );
bar.doSomething = function() {
      this.something(); // 内部委托!
};
bar.doSomething(); // "Tell me something good..."

这里调用的bar.doSomething()是实际存在与bar中的,这就更加清晰。

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

推荐阅读更多精彩内容