你不知道的JavaScript(五)|this和对象原型

[[Prototype]]
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。

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

[[Prototype]]引用由什么用呢?当你试图引用对象的属性时会触发[[Get]]操作,比如myObject.a。对于默认的[[Get]]操作来说,第一步是检查对象本身是都有这个属性,如果有的话就使用它。但是如果a不在myObject中,就需要使用对象的[[Prototype]]链了。
对于默认的[[Get]]操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的[[Prototype]]链:

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

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

Object.prototype
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的“普通”对象都“源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype对象,所有它包含JavaScript中许多通用的功能。

属性设置和屏蔽
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。

myObject.foo = "bar";

如果myObject对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值。
如果foo不是直接存在于myObject中,[[Prototype]]链就会被遍历,类似[[Get]]操作。如果原型链上找不到foo,foo就会被直接添加到myObject上。
然而,如果foo存在于原型链上层,赋值语句myObject.foo="foo"的行为就会有些不同。
如果属性名foo既出现在myObject中也出现在myObject的[[Prototype]]链上层,那么就会发生屏蔽。myObject中包含的foo属性会屏蔽原型链上层的所有foo属性,因为myObject.foo总是会选择原型链中最低层的foo属性。
屏蔽比我们想想中更加复杂。下面分析下如果foo不直接存在于myObject中而是存在于原型链上层时myObject.foo=“bar”会出现的三种情况:
1、如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性。
2、如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略,总之,不会发生屏蔽。
3、如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这个setter。
如果你希望在第二种和第三中情况下也屏蔽foo,那就不能使用=操作符来赋值,而是使用Object.defineProperty(..)来向myObject添加foo。
有些情况下回隐式产生屏蔽,一定要当心:

var anotherObject = {
    a: 2
};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("a"); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty("a"); // true

尽管myObject.a++看起来应该(通过委托)查找并增加anotherObject.a属性,但是别忘了++操作相当于myObject.a=myObject.a+1。因此++操作首先会通过[[Prototype]]查找属性a并从anotherObject.a获取当前属性值2,然后给这个值加1,接着用[[Put]]将值3赋给myObject中新建的屏蔽属性a。
修改委托属性时一定要小心。如果想让anotherObject.a的值增加,唯一的办法是anotherObject.a++。

“类函数”
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开发者不知道的秘密是:new Foo()这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。
继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。委托这个术语可以更加准确滴描述JavaScript中对象的关联机制。还有个偶尔会用到的JavaScript术语差异继承。基本原则是在描述对象行为时,使用其不同于普遍描述的特性。

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

Foo.prototype默认(在代码中第一行声明时!)有一个公有并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数。此外,可以看到通过“构造函数”调用new Foo()创建的对象也有一个.constructor属性,指向“创建这个对象的函数”。
上一段代码很容易让人认为Foo是一个构造函数,因为我们使用new来调用它并且看到它“构造”了一个对象。实际上,Foo和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数调用前面加上new关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。换句话说,在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。

技术

function Foo(name) {
    this.name = name;
}
Foo.prototype.myName = function () {
    return this.name;
};
var a = new Foo("a");
var b = new Foo("b");
a.myName(); // "a"
b.myName(); // "b"

在这段代码中,看起来似乎创建a和b时会把Foo.prototype对象复制到这两个对象中,然而事实并不是这样。在前面介绍默认[[Get]]算法时介绍过[[Prototype]]链,以及当属性不直接存在于对象中时如何通过它来进行查找。
因此,在创建的过程中,a和b的内部[[Prototype]]都会关联到Foo.prototype上。但a和b中无法找到myName时,它会(通过委托)在Foo.prototype上找到。

回顾“构造函数”
之前讨论.constructor属性时我们说过,看起来a.constructor===Foo为真意味着a确实有一个指向Foo的.constructor属性,但是事实不是这样。实际上,.constructor引用同样被委托给了Foo.prototype,而Foo.prototype.constructor默认指向Foo。
Foo.prototype的.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属性(不过默认的Foo.prototype对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的Object.prototype。这个对象有.constructor属性,指向内置的Object(..)函数。
当然,你可以给Foo.prototype添加一个.constructor属性,不过这需要手动添加一个符合正常行为的不可枚举属性。

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
// 需要在Foo.prototype 上“修复”丢失的.constructor 属性
// 新对象属性起到Foo.prototype 的作用
Object.defineProperty(Foo.prototype, "constructor", {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo // 让.constructor 指向Foo
});

原型风格

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 )。调用Object.create(..)会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象(本例中是Foo.prototype)。换句话说,这条语句的意思是:“创建一个新的Bar.prototype对象并把它关联到Foo.prototype”。
注意:下面这两种方式是常见的错误做法,实际上它们都存在一些问题:

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

Bar.prototype=Foo.prototype并不会创建一个关联到Bar.prototype的新对象,它只是让Bar.prototype直接引用Foo.prototype对象。因此当你执行类似Bar.prototype.
myLabel = ...的赋值语句时会直接修改Foo.prototype对象本身。显然这不是你想要的结果,否则你根本不需要Bar对象,直接使用Foo就可以了,这样代码也会更简单一些。
Bar.prototype = new Foo()的确会创建一个关联到Bar.prototype的新对象。但是它使用了Foo(..)的“构造函数调用”,如果函数Foo有一些副作用(比如写日志、修改状态、注册到其他对象,给this添加数据属性,等等)的话,就会影响到Bar()的“后代”,后果不堪设想。
因此,要创建一个合适的关联对象,我们必须使用Object.create(..)而不是使用具有副作用的Foo(..)。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。
如果能有一个标准并且可靠的方法来修改对象的[[Prototype]]关联就好了。在ES6之前,我们只能通过设置.proto属性来实现,但是这个方法并不是标准并且无法兼容所有浏览器。ES6添加了辅助函数Object.setPrototypeOf(..),可以用标准并且考考的方法来修改关联。
对比下两种把Bar.prototype关联到Foo.prototype的方法:

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

如果忽略掉Object.create(..)方法带来的轻微性能损失(抛弃的对象需要进行垃圾回收),它实际上比ES6及其之后的方法更短并且可读性更高。不过无论如何,这是两种完全不同的语法。

检查“类”关系
假设有对象a,如何找到对象a委托的对象(如果存在的话)呢?在传统的面相类环境中,检查一个实例(JavaScript中的对象)的继承(JavaScript中的委托关联)通常被称为内省(或者反射)。

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

我们如何通过内省找出a的“祖先”(委托关联)呢?第一种方法是站在“类”的角度来判断:a instanceof Foo;//true
instanceof操作符的左操作数是一个普通的对象,右操作数是一个函数。instanceof回答的问题是:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象?
可惜,这个方法只能处理对象(a)和函数(带.prototype引用的Foo)之间的关系。如果你想判断两个对象(比如a和b)之间是否通过[[Prototype]]链关联,只用instanceof无法实现。
第二种判断[[Prototype]]反射的方法,它更加简洁:

Foo.prototype.isPrototypeOf( a ); // true
//在a的整条[[Prototype]]链中是否出现过Foo.prototype?
// 非常简单:b 是否出现在c 的[[Prototype]] 链中?
b.isPrototypeOf( c );

我们也可以直接获取一个对象的[[Prototype]]链。在ES5中,标准的方法是:```JavaScript
Object.getPrototypeOf(a)

可以验证一下,这个对象引用是否和我们想的一样:
```JavaScript
Object.getPrototypeOf( a ) === Foo.prototype; // true

绝大多数(不是所有!)浏览器也支持一种非标准的方法来访问内部[[Prototype]]属性:

a.__proto__ === Foo.prototype; // true

这个奇怪的.__proto__(在ES6之前并不是标准!)属性“神奇地”引用了内部的[[Prototype]]对象,如果你想直接查找(甚至可以通过.__proto__.__ptoto__...来遍历)原型链的话,这个方法非常有用。
和我们之前说过的.constructor一样,.proto实际上并不存在于你正在使用的对象中(本例中是a)。实际上,它和其他的常用函数(.toString()、.isPrototypeOf(..),等等)一样,存在于内置的Object.prototype中,它们是不可枚举的。
.proto看起来很像一个属性,但是实际上它更像一个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()(调用getter函数)。虽然getter函数存在于Object.prototype对象中,但是它的this指向对象a(this的绑定规则),所以和Object.getPrototypeOf(a)结果相同。

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

创建关联

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),这样我们就可以充分发挥[[Prototype]]机制的威力(委托)并且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype和.constructor引用)。
Object.create(null)会创建一个拥有空(或者说null)[[Prototype]]链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以instanceof操作符无法进行判断,因此总是会返回false。这些特殊的空[[Prototype]]对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。
Object.create()的polyfill代码
Object.create(..)是在ES5中新增的函数,所以在ES5之前的环境中(比如就IE)如果要支持这个功能的话就需要使用一段简单的polyfill代码,它部分实现了Object.create(..)的功能:

if (!Object.create) {
    Object.create = function (o) {
        function F() { }
        F.prototype = o;
        return new F();
    };
}

这段代码polyfill代码使用了一个一次性函数F,我们通过改写它的.prototype属性使其指向想要关联的对象,然后再使用new F()来构造一个新对象进行关联。

关联关系是备用

var anotherObject = {
    cool: function () {
        console.log("cool!");
    }
};
var myObject = Object.create(anotherObject);
myObject.cool(); // "cool!"

由于存在[[Prototype]]机制,这段代码可以正常工作。但是如果你这样写只是为了让myObject在无法处理属性或者方法时可以使用备用的anotherObject,那么你的软件就会变得有点“神奇”,而且很难理解和维护。
这并不是说任何情况下都不应该选择备用这种设计模式,但是这在JavaScript中并不是很常见。所以如果你使用的是这种模式,那或许应当退后一步并重新思考一下这种模式是否合适。
当你给开发者设计软件时,假设要调用myObject.cool(),如果myObject中不存在cool()时这条语句也可以正常工作的话,那你的API设计就会变得很“神奇”,对于未来维护你软件的开发者来说这可能不太好理解。
但是你可以让你的API设计不那么“神奇”,同时仍然能发挥[[Prototype]]关联的威力:

var anotherObject = {
    cool: function () {
        console.log("cool!");
    }
};
var myObject = Object.create(anotherObject);
myObject.doCool = function () {
    this.cool(); // 内部委托!
};
myObject.doCool(); // "cool!"

这里我们调用的myObject.doCool()是实际存在于myObject中的,这可以让我们的API设计更加清晰。从内部来说,我们的实现遵循的是委托设计模式,通过[[Prototype]]委托到anotherObject.cool()。

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

推荐阅读更多精彩内容