《你不知道的JavaScript(上)-原型对象》学习笔记
3.1 语法
- 对象的文字语法:
var myObj = {
key: value
// ...
};
- 构造形式:
var myObj = new Object();
myObj.key = value;
3.2 类型
- 在 JavaScript 中一共有六种主要类型:
string、boolean、number、null、undefined、object
。 - 注意,简单基本类型(
string、boolean、number、null、undefined
)本身并不是对象,不要与String、Number、Boolean
搞混。 - null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null 时会返回字符串 "object"。实际上,null 本身是基本类型。
- 有一种常见的错误说法是“JavaScript 中万物皆是对象”,这显然是错误的。
- 实际上,JavaScript 中有许多特殊的对象子类型,我们可以称之为复杂基本类型。
- 函数就是对象的一个子类型(从技术角度来说就是“可调用的对象”)。JavaScript 中的函数是“一等公民”,因为它们本质上和普通的对象一样(只是可以调用),所以可以像操作其他对象一样操作函数(比如当作另一个函数的参数)。
- 数组也是对象的一种类型,具备一些额外的行为。数组中内容的组织方式比一般的对象要 稍微复杂一些。
内置对象
- JavaScript 中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和 简单基础类型一样,不过实际上它们的关系更复杂。
-
内置对象:
String、Number、Boolean、Object、Function、Array、Data、RegExp、Error
。
在 JavaScript 中,它们实际上是一些内置函数。这些内置函数可以当作构造函数 (由 new 产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。
举例来说:
var strPrimitive = "I am a string";
typeof strPrimitive; // "string" strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 检查 sub-type 对象
Object.prototype.toString.call( strObject ); // [object String]
原始值 "I am a string" 并不是一个对象,它只是一个字面量,并且是一个不可变的值。 如果要在这个字面量上执行一些操作,比如获取长度、访问其中某个字符等,那需要将其转换为 String 对象。
幸好,在必要时语言会自动把字符串字面量转换成一个 String 对象,也就是说你并不需要显式创建一个对象。
例如:
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这 样做,是因为引擎自动把字面量转换成 String 对象,所以可以访问属性和方法。
- 因此,
1.对于字符串字面量(string)、数值字面量(number)、布尔字面量(boolean)
来说,我们都可以直接在其上面访问属性或者方法,之所以可以这样做,是因为引擎自动把字面量转换成String、Number、Boolean对象
,所以可以访问属性和方法。
2.null、undefined
没有对应的构造形式,它们只有文字形式。相反,Date
只有构造(new Date(..)),没有文字形式。
3.对于Object、Array、Function、RegExp
来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。
4.Error
对象很少在代码中显式创建,一般是在抛出异常时被自动创建。也可以使用 new Error(..) 这种构造形式来创建,不过一般来说用不着。
3.3 内容(对象的属性)
- 对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
- 需要强调的一点是,当我们说“内容”时,似乎在暗示这些值实际上被存储在对象内部, 但是这只是它的表现形式。在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说就是引用)一样,指向这些值真正的存储位置。
- 在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性 名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的 确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
3.3.1 可计算属性名
ES6 增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
可计算属性名最常用的场景可能是 ES6 的符号(Symbol)。简单来说,Symbol是一种新的基础数据类型,包含一个不透明且无法预测的值(从技术角度来说就是一个字符串)。一般来说你不会用到符号的实际值(因为理论上来说在不 同的 JavaScript 引擎中值是不同的),所以通常你接触到的是符号的名称,比如 Symbol. Something(这个名字是我编的):
var myObject = {
[Symbol.Something]: "hello world"
}
3.3.4 复制对象
- Array:
slice
、concat
、Array.from()
- Object:
Object.assign()
、JSON.parse(JSON.stringify(obj))
不过使用JSON.parse(JSON.stringify(obj))
的话,undefined
、任意的函数
、symbol
在序列化过程中会被忽略(出现在非数组对象的属性中时)或者被转换成null
(出现在数组中时)
3.3.5 属性描述符
writable(可写)
、 enumerable(可枚举)
、 configurable(可配置)
。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty(..)
来添加一个新属性或者修改一个已有属性(如果它是 configurable)并对特性进行设置。
举例来说:
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
1. Writable
writable 决定是否可以修改属性的值。
思考下面的代码:
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
如你所见,我们对于属性值的修改静默失败(silently failed)了。如果在严格模式下,这种方法会出错:
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可写!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
TypeError 错误表示我们无法修改一个不可写的属性。
2. Configurable
只要属性是可配置的,就可以使用 defineProperty(..)
方法来修改属性描述符:
var myObject = {
a:2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置!
enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty( myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
} ); // TypeError
最后一个 defineProperty(..) 会产生一个 TypeError 错误,不管是不是处于严格模式,尝 试修改一个不可配置的属性描述符都会出错。注意:如你所见,把 configurable 修改成 false 是单向操作,无法撤销!
要注意有一个小小的例外:即便属性是 configurable:false, 我们还是可以 把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。
除了无法修改,configurable:false 还会禁止删除这个属性:
var myObject = {
a:2
};
myObject.a; // 2
delete myObject.a;
myObject.a; // undefined
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true
} );
myObject.a; // 2
delete myObject.a;
myObject.a; // 2
如你所见,最后一个 delete 语句(静默)失败了,因为属性是不可配置的。
3. Enumerable
这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for..in
循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。
用户定义的所有的普通属性默认都是 enumerable,这通常就是你想要的。但是如果你不希 望某些特殊属性出现在枚举中,那就把它设置成 enumerable:false。
3.3.6 不变性
1. 对象常量
结合 writable:false
和 configurable:false
就可以创建一个真正的常量属性(不可修改、 重定义或者删除):
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
2. 禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用 Object.prevent Extensions(..)
:
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
在非严格模式下,创建属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
3. 密封
Object.seal(..)
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..)
并把所有现有属性标记为 configurable:false
。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
4. 冻结
Object.freeze(..)
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..)
并把所有“数据访问”属性标记为 writable:false
,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性
,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。
你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..)
, 然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)
。但是一定要小心,因 为这样做有可能会在无意中冻结其他(共享)对象。
3.3.7 [[Get]]
在语言规范中,myObject.a
在 myObject
上实际上是实现了 [[Get]]
操作(有点像函数调 用:[[Get]]()
)。对象默认的内置 [[Get]]
操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。
然而,如果没有找到名称相同的属性,按照 [[Get]]
算法的定义会执行另外一种非常重要的行为遍历可能存在的 [[Prototype]] 链, 也就是原型链。
如果无论如何都没有找到名称相同的属性,那 [[Get]]
操作会返回值 undefined
3.3.8 [[Put]]
[[Put]]
被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。
如果已经存在这个属性,[[Put]]
算法大致会检查下面这些内容。
- 属性是否是访问描述符(参见3.3.9节)?如果是并且存在setter就调用setter。
- 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
- 如果都不是,将该值设置为属性的值。
如果对象中不存在这个属性,[[Put]] 操作会更加复杂。我们会在第 5 章讨论 [[Prototype]] 时详细进行介绍。
3.3.9 Getter和Setter
对象默认的 [[Put]] 和 [[Get]] 操作分别可以控制属性值的设置和获取。
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法 应用在整个对象上。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏 函数,会在设置属性值时调用。
当你给一个属性定义 getter、setter 或者两者都有时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。对于访问描述符来说,JavaScript 会忽略它们的 value 和 writable 特性,取而代之的是关心 set 和 get(还有 configurable 和 enumerable)特性。
var myObject = {
// 给 a 定义一个 getter
get a() {
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 给 b 设置一个 getter
get: function(){
return this.a * 2
},
// 确保 b 会出现在对象的属性列表中
enumerable: true
}
);
myObject.a; // 2
myObject.b; // 4
不管是对象文字语法中的get a(){..}
,还是 defineProperty(..)
中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值。
为了让属性更合理,还应当定义 setter,和你期望的一样,setter 会覆盖单个属性默认的 [Put]操作。通常来说 getter 和 setter 是成对出现的(只定义一个的话 通常会产生意料之外的行为):
var myObject = {
// 给 a 定义一个 getter
get a() {
return this._a_;
},
// 给 a 定义一个 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
3.3.10 存在性
-
in 操作符
会检查属性是否在对象及其 [[Prototype]] 原型链中
-
hasOwnProperty(..)
只会检查属性是否在 myObject 对象中
,不会检查 [[Prototype]] 链
1. 枚举
-
for..in
循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。 -
propertyIsEnumerable(..)
会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足 enumerable:true。 -
Object.keys(..)
会返回一个数组,包含所有可枚举属性,只会查找对象直接包含的属性。 -
Object.getOwnPropertyNames(..)
会返回一个数组,包含所有属性,无论它们是否可枚举,只会查找对象直接包含的属性。
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 让 a 像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让 b 不可枚举
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
3.4 遍历
-
for..in
循环可以用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。 -
forEach(..)
、every(..)
、some(..)
-
for..of
,循环每次调用 myObject 迭代器对象的 next() 方法时,内部的指针都会向前移动并 返回对象属性列表的下一个值。
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator](); // 使用 ES6 中的符号 Symbol.iterator 来获取对象的 @@iterator 内部属 性。
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
注:和数组不同,普通的对象没有内置的 @@iterator,所以无法自动完成 for..of 遍历。
第4章 混合对象“类”
类、继承、实例化、多态
4.1.1 “类”设计模式
- 面向对象设计模式,比如迭代器模式、观察者模式、工厂模式、单例模式,等等。
- 最好使用类把过程化风格的“意大利面代码”转换成结构清晰、组织良好的代码。
4.2 类的机制
- 一个类就是一张蓝图。为了获得真正可以交互的对象,我们必须按照类来建造(也可以说实例化)一个东西,这个东西通常被称为实例。这个对象就是类中描述的所有特性的一份副本。
4.3.1 多态
多态是说父类的通用行为可以被子类用更特殊的行为重写。
多态并不表示子类和父类有关联,子类得到的只是父类的一份副本。类的继承其实就是复制。
第5章 原型
5.1 [[Prototype]]
- 几乎所有的对象在创建时
[[Prototype]]
属性都会被赋予一个非空的值。 - 对于默认的
[[Get]]
操作来说,如果无法在对象本身找到需要的属性,就会继续访问对象的[[Prototype]]
链。这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]]
链。如果是后者的话,[[Get]]
操作的返回值是undefined
。 - 使用
for..in
遍历对象时,使用in
操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链
5.1.1 [[Prototype]] 的“尽头”
所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype,所以它包含 JavaScript 中许多通用的功能。
5.1.2 属性设置和屏蔽
如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = "bar" 会出现的三种情况。
- 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性(参见第3章)并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
- 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 如果在[[Prototype]]链上层存在foo并且它是一个setter(参见第3章),那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。
有些情况下会隐式产生屏蔽,一定要当心。思考下面的代码:
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++。
5.2 “类”
5.2.1 “类”函数
所有的函数默认都会拥有一个名为 prototype
的公有并且不可枚举的属性,它会指向另一个对象:
function Foo() {
// ...
}
Foo.prototype; // { }
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
new Foo() 只是间接完成我们的目标:一个关联到其他对象的新对象。
5.2.2 “构造函数”
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
// a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo
Foo.prototype 默认有一个公有并且不可枚举的属性 .constructor,这个属性引用的是对象关联的函数(本例中是 Foo)。此外,我们可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 .constructor 属性,指向 “创建这个对象的函数”。
实际上 a 本身并没有 .constructor 属性。而且,虽然 a.constructor 确实指向 Foo 函数,但是这个属性并不是表示 a 由 Foo“构造”。实际上,.constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo。a.constructor 只是通过默认的 [[Prototype]] 委托指向 Foo,这和“构造”毫无关系。
5.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 )
。调用 Object.create(..)
会凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你指定的对象(本例中是 Foo.prototype)。
- ES6 添加了辅助函数
Object.setPrototypeOf(..)
,可以用标准并且可靠的方法来修改对象的 [[Prototype]] 关联。
我们来对比一下两种把 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 及其之后的方法更短而且可读性更高。不过无论如何,这是两种完全不同的语法。
检查“类”关系
思考下面的代码:
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
- 第一种方法是站在“类”的角度来判断:
a instanceof Foo; // true
instanceof
回答的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?
instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。
可惜,这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系。如 果你想判断两个对象(比如 a 和 b)之间是否通过 [[Prototype]] 链关联,只用 instanceof 无法实现。
- 第二种判断 [[Prototype]] 反射的方法:
Foo.prototype.isPrototypeOf( a ); // true
isPrototypeOf(..)
回答的问题是:在 a 的整条 [[Prototype]] 链中是否出现过Foo.prototype ?
- 我们也可以直接获取一个对象的 [[Prototype]] 链。在 ES5 中,标准的方法是:
Object.getPrototypeOf( a )
可以验证一下,这个对象引用是否和我们想的一样:
Object.getPrototypeOf( a ) === Foo.prototype; // true
- 绝大多数(不是所有!)浏览器也支持一种非标准的方法来访问内部 [[Prototype]] 属性:
__proto__
a.__proto__ === Foo.prototype; // true
.proto 看起来很像一个属性,但是实际上它更像一个 getter/setter:
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this ); },
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );
5.4 对象关联
[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他
对象。
通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就 会继续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到需要的 引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
5.4.1 创建关联
Object.create(..)
- Object.create(..) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样 我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。
- Object.create(null) 会 创 建 一 个 拥 有 空( 或 者 说 null)[[Prototype]] 链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以 instanceof 操作符(之前解释过)无法进行判断,因此总是会返回 false。
- Object.create()的polyfill代码(兼容旧IE)
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
}; }
- Object.create()的扩展
Object.create(..) 的第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符:
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject, {
b: {
enumerable: false,
writable: true,
configurable: false,
value: 3
},
c: {
enumerable: true,
writable: false,
configurable: false,
value: 4
}
});
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
myObject.hasOwnProperty( "c" ); // true
myObject.a; // 2
myObject.b; // 3
myObject.c; // 4
5.5 小结
如果要访问对象中并不存在的一个属性,[[Get]] 操作就会查找对象内部[[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。
所有普通对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。toString()、valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。
关联两个对象最常用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤(第 2 章)中会创建一个关联其他对象的新对象。
使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不一样。
虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但 是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。
出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的真实机制(不仅仅是限制我们的思维模式)。
相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。
第6章 行为委托
类模型(面向对象风格)
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
委托模型(对象关联风格)
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
6.6 小结
在软件架构中你可以选择是否使用类和继承设计模式。大多数开发者理所当然地认为类是 唯一(合适)的代码组织方式,但是本章中我们看到了另一种更少见但是更强大的设计模式:行为委托。
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。 对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。