II-第5章 原型
这篇文章是JS的重中之重,要用心研读~~
能学到的知识:
-
对象查找和设置属性背后发生了什么
-
原型链形成的机制和特点
前面说到在对象里查找属性时,如果在对象本身找不到,会继续访问对象的prototype链。如果都没有的话返回undefined
var anotherObject = {
a:2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
console.log(myObject); // {}
myObject.a; // 2
现在myObject的prototype关联到anotherObject,myObject.a并不存在,但myObject.a结果为2
for..in遍历对象时原理和查找原型链类似。任何可通过原型链访问到(并且是enumerable)的属性都会被枚举
当你通过各种语法进行属性查找时都会查找原型链,直到找到属性或者查找完整条原型链。
原型链的尽头是Object.prototype
toString()、valueOf()和其他一些通用的功能 都存在于Object.prototype对象上,因此语言中所有的对象都可以使用它们。
属性设置和屏蔽
给一个对象设置属性的过程并不简单。通过例子来看myObject.foo = "bar";
一、 如果 myObject 对象中包含名为 foo 的普通数据访问属性,这条赋值语句只会修改已有的属性值。
二、 如果 foo 不是直接存在于 myObject 中,原型链就会被遍历。如果原型链上找不到 foo,foo 就会被直接添加到 myObject 上。
三、 如果 foo 存在于原型链上层,赋值语句 myObject.foo = "bar" 的行为就会有些不同。
如果属性名 foo 既出现在 myObject 中也出现在 myObject 的原型链上层,那么就会发生屏蔽。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性,因为 myObject.foo 总是会选择原型链中最底层的 foo 属性。
屏蔽:在当前作用域添加属性,以隔绝访问原型链上层的同样属性
发生屏蔽的三种情况:
- 如果在原型链上层存在名为foo的普通数据访问属性并且没有被标记为只读,那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
- 如果在原型链上层存在foo,但是它被标记为只读,那么无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 如果在原型链上层存在foo并且它是一个setter,那就一定会调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这个 setter。
向原型链上层已经存在的属性赋值,不一定会触发屏蔽(第二三种)。 如果希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(..)来向 myObject 添加 foo。
只读属性会阻止原型链下层屏蔽同名属性。这样做主要是为了模拟类属性的继承。你可以把原型链上层的 foo 看作是父类中的属性,它会被 myObject 继承(复制),这样一来 myObject 中的 foo 属性也是只读,所以无法创建。但是一定要注意,实际上并不会发生类似的继承复制。这看起来有点奇怪,myObject 对象竟然会因为其他对象中有一个只读 foo 就不能包含 foo 属性。更奇怪的是,这个限制只存在于 = 赋值中,使用 Object. defineProperty(..) 并不会受到影响。
有些情况下会隐式产生屏蔽,如下:
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过原型链查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着将值 3 赋给 myObject 中新建的屏蔽属性 a。
修改委托属性时一定要小心。如果想让 anotherObject.a 的值增加,唯一的办法是 anotherObject.a++。
类函数
函数的一种特殊特性:所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象: Foo的原型。我们通过名为 Foo.prototype 的属性引用来访问它。
function Foo() {
// ...
}
Foo.prototype; // { }
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
调用 new Foo() 时会创建 a,其中的一步就是给 a 一个内部的原型链,关联到 Foo.prototype 指向的那个对象。
在 JavaScript 中,不能创建一个类的多个实例,只能创建多个对象,它们的原型链关联的是同一个对象。在默认情况下多次实例化一个类不会进行复制, 因此这些对象之间并不会完全失去联系,它们是互相关联的。
通过new Foo()得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。
实际上,绝大多数 JavaScript 开发者不知道的秘密是,new Foo() 这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo() 只是间接完成了我们的目标:一个关联到其他对象的新对象。
通过Object.create(..)可以直接做到这一点。
通常我们讲原型继承。继承意味着复制操作。JS默认不会复制对象属性。会在两个对象之间创建关联,这样一个对象可以通过委托访问另一个对象的属性和函数。
差异继承?
构造函数
在 JavaScript 中对于“构造函数”最准确的解释是,所有带 new 的函数调用。
函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”。
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype 默认(在代码中第一行声明时)有一个公有并且不可枚举的属性.constructor,这个属性引用的是对象关联的函数(本例中是 Foo)。
此外,我们可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 .constructor 属性,指向 “创建这个对象的函数”。
a.constructor 只是通过默认的原型委托指向 Foo,这和构造毫无关系。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
Object(..) 并没有“构造”a1,看起来应该是 Foo()“构造”了它。
大部分开发者 都认为是 Foo() 执行了构造工作,但是问题在于,如果你认为constructor表示“由...... 构造”的话,a1.constructor 应该是 Foo,但是它并不是 Foo !
a1 并没有 .constructor 属性,所以它会委托原型链上的 Foo. prototype。但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这 个属性!), 所以它会继续委托,这次会委托给委托链顶端的 Object.prototype。这个对象 有 .constructor 属性,指向内置的 Object(..) 函数。
对象的 .constructor 会默认指向一个函数,这个函数可以通过对象的 .prototype 引用。
constructor 并不表示被构造 .constructor 并不是一个不可变属性。它是不可枚举的,但是它的值是可写的。此外,你可以给任意原型链中的任意对象添加一个名为 constructor 的属性或者对其进行修改,你可以任意对其赋值。
原型继承
Bar.prototype = Object.create()
Object.create(..) 会凭空创建一个“新”对象并把新对象内部的原型链关联到指定的对象(本例中是 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() 的“后代”,后果 不堪设想。
对比一下两种把 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 及其之后的方法更短而且可读性更高。不过无论如何,这是两种完全不同的语法。
检查类关系
instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。
在 a 的整条原型链中是否有指向 Foo.prototype 的对象?
这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系。如果你想判断两个对象(比如 a 和 b)之间是否通过原型链关联,只用 instanceof 无法实现。
第二种判断反射的方法:
Foo.prototype.isPrototypeOf( a ); // true
b 是否出现在 c 的原型链中?
b.isPrototypeOf( c );
直接获取一个对象原型链的方法Object.getPrototypeOf(a)
Object.getPrototypeOf( a ) === Foo.prototype; // true
非标准的方法a.__proto__ === Foo.prototype; // true
.constructor和.__proto__一样,并不存在于你正在使用的对象中 (本例中是 a)。
它和其他的常用函数(.toString()、.isPrototypeOf(..),等等)
一样,存在于内置的 Object.prototype 中。是不可枚举的。
.proto 的实现大致上是这样的
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this ); },
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
});
对象关联
Object.create(..) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),
这样我们就可以充分发挥原型链机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。
Object.create()的polyfill代码
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}