JavaScript 中的面向对象编程 (OOP) 有几个层次:
每一层都只依赖于之前的层,使您能够逐步学习 JavaScript OOP。第一层和第二层构成了一个简单的核心,当您对更复杂的第三层和第四层感到困惑时,可以参考它们。
粗略地说,JavaScript 中的所有对象都是从字符串到值的映射(字典)。对象中的一个(键,值)条目称为属性。属性的键始终是文本字符串。属性的值可以是任何 JavaScript 值,包括函数。方法是其值为函数的属性。
属性有三种
[[Prototype]]
保存对象的原型,并且可以通过 Object.getPrototypeOf()
读取。JavaScript 的对象字面量允许您直接创建普通对象(Object
的直接实例)。以下代码使用对象字面量将一个对象赋值给变量 jane
。该对象有两个属性:name
和 describe
。 describe
是一个方法:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
// (1)
},
// (2)
};
this
来引用当前对象(也称为方法调用的接收者)。您可能会认为对象只是从字符串到值的映射。但它们不止于此:它们是真正的通用对象。例如,您可以在对象之间使用继承(请参阅第二层:对象之间的原型关系),并且您可以保护对象不被更改。直接创建对象的能力是 JavaScript 的突出特性之一:您可以从具体对象开始(不需要类!),并在以后引入抽象。例如,构造函数是对象的工厂(如第三层:构造函数——实例的工厂中所述),它们大致类似于其他语言中的类。
点运算符提供了一种用于访问属性的紧凑语法。属性键必须是标识符(请参阅合法标识符)。如果要读取或写入具有任意名称的属性,则需要使用方括号运算符(请参阅方括号运算符 ([]):通过计算键访问属性)。
本节中的示例使用以下对象
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
点运算符允许您“获取”属性(读取其值)。以下是一些示例
> jane.name // get property `name` 'Jane' > jane.describe // get property `describe` [Function]
获取不存在的属性将返回 undefined
> jane.unknownProperty undefined
您可以使用赋值运算符 (=
) 来设置通过点表示法引用的属性的值。例如:
> jane.name = 'John'; // set property `name` > jane.describe() 'Person named John'
如果属性尚不存在,则设置它会自动创建它。如果属性已存在,则设置它会更改其值。
delete
运算符允许您从对象中完全删除属性(整个键值对)。例如:
> var obj = { hello: 'world' }; > delete obj.hello true > obj.hello undefined
如果仅将属性设置为 undefined
,则该属性仍然存在,并且对象仍然包含其键:
> var obj = { foo: 'a', bar: 'b' }; > obj.foo = undefined; > Object.keys(obj) [ 'foo', 'bar' ]
如果删除该属性,则其键也会消失
> delete obj.foo true > Object.keys(obj) [ 'bar' ]
delete
仅影响对象的直接(“自身”,非继承)属性。它的原型不会被触及(请参阅删除继承的属性)。
谨慎使用 delete
运算符。如果由构造函数创建的实例的“形状”没有改变(粗略地说:没有删除或添加属性),则大多数现代 JavaScript 引擎都会优化它们的性能。删除属性会阻止这种优化。
如果该属性是自身属性但无法删除,则 delete
返回 false
。在所有其他情况下,它都返回 true
。以下是一些示例。
作为准备工作,我们创建一个可以删除的属性和另一个无法删除的属性(通过描述符获取和定义属性解释了 Object.defineProperty()
)
var
obj
=
{};
Object
.
defineProperty
(
obj
,
'canBeDeleted'
,
{
value
:
123
,
configurable
:
true
});
Object
.
defineProperty
(
obj
,
'cannotBeDeleted'
,
{
value
:
456
,
configurable
:
false
});
对于无法删除的自身属性,delete
返回 false
> delete obj.cannotBeDeleted false
在所有其他情况下,delete
返回 true
> delete obj.doesNotExist true > delete obj.canBeDeleted true
即使 delete
没有改变任何内容(继承的属性永远不会被删除),它也会返回 true
> delete obj.toString true > obj.toString // still there [Function: toString]
虽然您不能使用保留字(例如 var
和 function
)作为变量名,但您可以将它们用作属性键:
> var obj = { var: 'a', function: 'b' }; > obj.var 'a' > obj.function 'b'
数字可以在对象字面量中用作属性键,但它们会被解释为字符串。点运算符只能访问键为标识符的属性。因此,您需要使用方括号运算符(如下例所示)来访问键为数字的属性:
> var obj = { 0.7: 'abc' }; > Object.keys(obj) [ '0.7' ] > obj['0.7'] 'abc'
对象字面量还允许您使用任意字符串(既不是标识符也不是数字)作为属性键,但您必须将它们用引号引起来。同样,您需要使用方括号运算符来访问属性值
> var obj = { 'not an identifier': 123 }; > Object.keys(obj) [ 'not an identifier' ] > obj['not an identifier'] 123
点运算符适用于固定的属性键,而方括号运算符允许您通过表达式引用属性。
方括号运算符允许您通过表达式计算属性的键:
> var obj = { someProperty: 'abc' }; > obj['some' + 'Property'] 'abc' > var propKey = 'someProperty'; > obj[propKey] 'abc'
这也允许您访问键不是标识符的属性
> var obj = { 'not an identifier': 123 }; > obj['not an identifier'] 123
请注意,方括号运算符会将其内部内容强制转换为字符串。例如
> var obj = { '6': 'bar' }; > obj[3+3] // key: the string '6' 'bar'
调用方法的工作方式与您预期的一样:
> var obj = { myMethod: function () { return true } }; > obj['myMethod']() true
设置属性的工作方式类似于点运算符:
> var obj = {}; > obj['anotherProperty'] = 'def'; > obj.anotherProperty 'def'
删除属性的工作方式也类似于点运算符:
> var obj = { 'not an identifier': 1, prop: 2 }; > Object.keys(obj) [ 'not an identifier', 'prop' ] > delete obj['not an identifier'] true > Object.keys(obj) [ 'prop' ]
这不是一个常见的用例,但有时您需要将任意值转换为对象。Object()
,用作函数(而不是构造函数),提供了此服务。它产生以下结果:
值 | 结果 |
(不带参数调用) |
|
|
|
|
|
布尔值 |
|
数字 |
|
字符串 |
|
对象 |
|
以下是一些示例
> Object(null) instanceof Object true > Object(false) instanceof Boolean true > var obj = {}; > Object(obj) === obj true
以下函数检查value
是否为对象:
function
isObject
(
value
)
{
return
value
===
Object
(
value
);
}
请注意,如果 value
不是对象,则上述函数会创建一个对象。您可以通过 typeof
实现相同的功能,而无需这样做(请参阅陷阱:typeof null)。
您也可以将 Object
作为构造函数调用,这会产生与将其作为函数调用相同的结果:
> var obj = {}; > new Object(obj) === obj true > new Object(123) instanceof Number true
当您调用函数时,this
始终是一个(隐式)参数:
即使普通函数不需要 this
,它仍然作为特殊变量存在,其值始终是全局对象(浏览器中的 window
;请参阅全局对象)
> function returnThisSloppy() { return this } > returnThisSloppy() === window true
this
始终为 undefined
> function returnThisStrict() { 'use strict'; return this } > returnThisStrict() === undefined true
this
指的是调用该方法的对象
> var obj = { method: returnThisStrict }; > obj.method() === obj true
对于方法,this
的值称为方法调用的接收者。
请记住,函数也是对象。因此,每个函数都有自己的方法。本节介绍了其中的三种方法,它们有助于调用函数。以下几节将使用这三种方法来解决调用函数时遇到的一些陷阱。接下来的示例都引用以下对象 jane
:
var
jane
=
{
name
:
'Jane'
,
sayHelloTo
:
function
(
otherName
)
{
'use strict'
;
console
.
log
(
this
.
name
+
' says hello to '
+
otherName
);
}
};
第一个参数是在被调用函数内部,this
将会拥有的值;其余的参数作为参数传递给被调用函数。以下三种调用是等效的:
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
call
(
jane
,
'Tarzan'
);
var
func
=
jane
.
sayHelloTo
;
func
.
call
(
jane
,
'Tarzan'
);
对于第二次调用,您需要重复 jane
,因为 call()
不知道您是如何获得它被调用函数的。
第一个参数是在被调用函数内部,this
将会拥有的值;第二个参数是一个数组,它提供了调用的参数。以下三种调用是等效的:
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
apply
(
jane
,
[
'Tarzan'
]);
var
func
=
jane
.
sayHelloTo
;
func
.
apply
(
jane
,
[
'Tarzan'
]);
对于第二次调用,您需要重复 jane
,因为 apply()
不知道您是如何获得它被调用函数的。
构造函数的 apply() 解释了如何将 apply()
与构造函数一起使用。
此方法执行部分函数应用,这意味着它创建一个新函数,该函数按以下方式调用 bind()
的接收者:this
的值为 thisValue
,参数从 arg1
开始,直到 argN
,后面跟着新函数的参数。换句话说,新函数在调用原始函数时,会将它的参数追加到 arg1, ..., argN
。让我们看一个例子:
function
func
()
{
console
.
log
(
'this: '
+
this
);
console
.
log
(
'arguments: '
+
Array
.
prototype
.
slice
.
call
(
arguments
));
}
var
bound
=
func
.
bind
(
'abc'
,
1
,
2
);
数组方法 slice
用于将 arguments
转换为数组,这对于记录它是必要的(此操作在 类数组对象和泛型方法 中解释)。bound
是一个新函数。以下是交互
> bound(3) this: abc arguments: 1,2,3
以下三种调用 sayHelloTo
都是等效的
jane
.
sayHelloTo
(
'Tarzan'
);
var
func1
=
jane
.
sayHelloTo
.
bind
(
jane
);
func1
(
'Tarzan'
);
var
func2
=
jane
.
sayHelloTo
.
bind
(
jane
,
'Tarzan'
);
func2
();
假设 JavaScript 有一个三点运算符(...
),它可以将数组转换为实际参数。这样的运算符将允许您将 Math.max()
(请参阅其他函数)与数组一起使用。在这种情况下,以下两个表达式将是等效的
Math
.
max
(...[
13
,
7
,
30
])
Math
.
max
(
13
,
7
,
30
)
对于函数,您可以通过 apply()
实现三点运算符的效果
> Math.max.apply(null, [13, 7, 30]) 30
三点运算符对于构造函数也是有意义的
new
Date
(...[
2011
,
11
,
24
])
// Christmas Eve 2011
唉,这里 apply()
不起作用,因为它只对函数或方法调用有帮助,而对构造函数调用没有帮助。
我们可以分两步模拟 apply()
。
通过方法调用将参数传递给 Date
(它们还没有在数组中)
new
(
Date
.
bind
(
null
,
2011
,
11
,
24
))
前面的代码使用 bind()
创建一个没有参数的构造函数,并通过 new
调用它。
使用 apply()
将数组传递给 bind()
。因为 bind()
是一个方法调用,所以我们可以使用 apply()
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
,
2011
,
11
,
24
]))
前面的数组包含 null
,后面跟着 arr
的元素。我们可以使用 concat()
通过将 null
预先添加到 arr
来创建它
var
arr
=
[
2011
,
11
,
24
];
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
].
concat
(
arr
)))
前面的手动解决方法的灵感来自 Mozilla 发布的库方法。以下是它的一个稍微编辑过的版本
if
(
!
Function
.
prototype
.
construct
)
{
Function
.
prototype
.
construct
=
function
(
argArray
)
{
if
(
!
Array
.
isArray
(
argArray
))
{
throw
new
TypeError
(
"Argument must be an array"
);
}
var
constr
=
this
;
var
nullaryFunc
=
Function
.
prototype
.
bind
.
apply
(
constr
,
[
null
].
concat
(
argArray
));
return
new
nullaryFunc
();
};
}
以下是使用中的方法
> Date.construct([2011, 11, 24]) Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
另一种方法是通过 Object.create()
创建一个未初始化的实例,然后通过 apply()
调用构造函数(作为函数)。这意味着您实际上是在重新实现 new
运算符(省略了一些检查)
Function
.
prototype
.
construct
=
function
(
argArray
)
{
var
constr
=
this
;
var
inst
=
Object
.
create
(
constr
.
prototype
);
var
result
=
constr
.
apply
(
inst
,
argArray
);
// (1)
// Check: did the constructor return an object
// and prevent `this` from being the result?
return
result
?
result
:
inst
;
};
前面的代码不适用于大多数内置构造函数,这些构造函数在作为函数调用时总是生成新的实例。换句话说,第 (1) 行中的步骤没有按预期设置 inst
。
如果您从对象中提取一个方法,它将再次成为一个真正的函数。它与对象的连接被切断,通常不再正常工作。以以下对象 counter
为例:
var
counter
=
{
count
:
0
,
inc
:
function
()
{
this
.
count
++
;
}
}
提取 inc
并调用它(作为函数!)失败
> var func = counter.inc; > func() > counter.count // didn’t work 0
解释如下:我们已经将 counter.inc
的值作为函数调用。因此,this
是全局对象,我们执行了 window.count++
。window.count
不存在,并且是 undefined
。对其应用 ++
运算符会将其设置为 NaN
> count // global variable NaN
如果方法 inc()
处于严格模式,您将收到警告
> counter.inc = function () { 'use strict'; this.count++ }; > var func2 = counter.inc; > func2() TypeError: Cannot read property 'count' of undefined
原因是当我们调用严格模式函数 func2
时,this
是 undefined
,从而导致错误。
感谢 bind()
,我们可以确保 inc
不会丢失与 counter
的连接
> var func3 = counter.inc.bind(counter); > func3() > counter.count // it worked! 1
在 JavaScript 中,有许多接受回调的函数和方法。浏览器中的示例是 setTimeout()
和事件处理。如果我们将 counter.inc
作为回调传入,它也会作为函数被调用,从而导致刚才描述的相同问题。为了说明这种现象,让我们使用一个简单的回调调用函数:
function
callIt
(
callback
)
{
callback
();
}
通过 callIt
执行 counter.count
会触发警告(由于严格模式)
> callIt(counter.inc) TypeError: Cannot read property 'count' of undefined
和以前一样,我们通过 bind()
解决问题
> callIt(counter.inc.bind(counter)) > counter.count // one more than before 2
每次调用 bind()
都会创建一个新函数。当您注册和取消注册回调时(例如,用于事件处理),这会产生后果。您需要将注册的值存储在某个地方,并在取消注册时也使用它。
您经常在 JavaScript 中嵌套函数定义,因为函数可以是参数(例如,回调),并且可以通过函数表达式就地创建。当一个方法包含一个普通函数,并且您想在后者内部访问前者的 this
时,这就会出现问题,因为方法的 this
被普通函数的 this
遮蔽了(后者甚至不需要它自己的 this
)。在下面的例子中,(1) 处的函数试图访问 (2) 处方法的 this
:
var
obj
=
{
name
:
'Jane'
,
friends
:
[
'Tarzan'
,
'Cheeta'
],
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
// (1)
console
.
log
(
this
.
name
+
' knows '
+
friend
);
// (2)
}
);
}
};
这失败了,因为 (1) 处的函数有它自己的 this
,这里它是 undefined
> obj.loop(); TypeError: Cannot read property 'name' of undefined
有三种方法可以解决这个问题。
我们将 this
赋值给一个在嵌套函数内部不会被遮蔽的变量
loop
:
function
()
{
'use strict'
;
var
that
=
this
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
that
.
name
+
' knows '
+
friend
);
});
}
以下是交互
> obj.loop(); Jane knows Tarzan Jane knows Cheeta
我们可以使用 bind()
为回调提供一个固定的 this
值,即方法的 this
(第 (1) 行):
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
}.
bind
(
this
));
// (1)
}
两个对象之间的原型关系是关于继承的:每个对象都可以有另一个对象作为其原型。然后,前一个对象继承其原型的所有属性。对象通过内部属性 [[Prototype]]
指定其原型。每个对象都有这个属性,但它可以是 null
。由 [[Prototype]]
属性连接的对象链称为原型链(图 17-1)。
为了了解基于原型的(或原型)继承是如何工作的,让我们看一个例子(使用发明的语法来指定 [[Prototype]]
属性)
var
proto
=
{
describe
:
function
()
{
return
'name: '
+
this
.
name
;
}
};
var
obj
=
{
[[
Prototype
]]
:
proto
,
name
:
'obj'
};
对象 obj
从 proto
继承属性 describe
。它还有一个所谓的自有(非继承的、直接的)属性 name
。
obj
继承了属性 describe
;您可以访问它,就好像对象本身具有该属性一样:
> obj.describe [Function]
每当您通过 obj
访问属性时,JavaScript 都会从该对象开始搜索它,并继续搜索其原型、原型的原型,依此类推。这就是为什么我们可以通过 obj.describe
访问 proto.describe
的原因。原型链的行为就好像它是一个单一对象一样。当您调用一个方法时,这种错觉会一直保持:this
的值始终是搜索方法开始的对象,而不是找到方法的对象。这允许方法访问原型链的所有属性。例如
> obj.describe() 'name: obj'
在 describe()
内部,this
是 obj
,这允许方法访问 obj.name
。
在原型链中,对象中的一个属性覆盖“后面”对象中具有相同键的属性:前一个属性首先被找到。它隐藏了后一个属性,该属性不能再被访问。例如,让我们覆盖 obj
中的 proto.describe()
方法:
> obj.describe = function () { return 'overridden' }; > obj.describe() 'overridden'
这类似于基于类的语言中方法的覆盖方式。
原型非常适合在对象之间共享数据:多个对象获得相同的原型,该原型保存所有共享的属性。让我们看一个例子。对象 jane
和 tarzan
都包含相同的方法 describe()
。这是我们希望通过使用共享来避免的事情:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
tarzan
=
{
name
:
'Tarzan'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
这两个对象都是人。它们的 name
属性不同,但我们可以让它们共享 describe
方法。我们通过创建一个名为 PersonProto
的公共原型并将 describe
放入其中来实现这一点(图 17-2)。
以下代码创建了共享原型 PersonProto
的对象 jane
和 tarzan
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
以下是交互
> jane.describe() Person named Jane > tarzan.describe() Person named Tarzan
这是一种常见的模式:数据驻留在原型链的第一个对象中,而方法驻留在后面的对象中。JavaScript 的原型继承风格旨在支持这种模式:设置属性只影响原型链中的第一个对象,而获取属性则考虑整个链(请参阅 设置和删除只影响自有属性)。
到目前为止,我们一直假设您可以从 JavaScript 访问内部属性 [[Prototype]]
。 但是该语言不允许您这样做。相反,它提供了一些函数,用于读取原型和使用给定原型创建新对象。
此 调用:
Object
.
create
(
proto
,
propDescObj
?
)
创建一个原型为 proto
的对象。可以选择通过描述符添加属性(在属性描述符 中解释)。在以下示例中,对象 jane
获取原型 PersonProto
和一个可变属性 name
,其值为 'Jane'
(通过属性描述符指定)
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
Object
.
create
(
PersonProto
,
{
name
:
{
value
:
'Jane'
,
writable
:
true
}
});
以下是交互过程
> jane.describe() 'Person named Jane'
但您通常只创建一个空对象,然后手动添加属性,因为描述符很冗长
var
jane
=
Object
.
create
(
PersonProto
);
jane
.
name
=
'Jane'
;
此方法 调用:
Object
.
getPrototypeOf
(
obj
)
返回 obj
的原型。继续前面的示例
> Object.getPrototypeOf(jane) === PersonProto true
此 语法:
Object
.
prototype
.
isPrototypeOf
(
obj
)
检查方法的接收者是否是 obj
的(直接或间接)原型。换句话说:接收者和 obj
是否在同一个原型链中,并且 obj
是否在接收者之前?例如
> var A = {}; > var B = Object.create(A); > var C = Object.create(B); > A.isPrototypeOf(C) true > C.isPrototypeOf(A) false
以下函数 迭代对象 obj
的属性链。它返回第一个具有键为 propKey
的自有属性的对象,如果没有这样的对象,则返回 null
:
function
getDefiningObject
(
obj
,
propKey
)
{
obj
=
Object
(
obj
);
// make sure it’s an object
while
(
obj
&&
!
{}.
hasOwnProperty
.
call
(
obj
,
propKey
))
{
obj
=
Object
.
getPrototypeOf
(
obj
);
// obj is null if we have reached the end
}
return
obj
;
}
在前面的代码中,我们 以泛型方式调用了方法 Object.prototype.hasOwnProperty
(请参阅 泛型方法:从原型借用方法)。
一些 JavaScript 引擎有一个特殊属性,用于 获取和设置对象的原型:__proto__
。它为该语言带来了对 [[Prototype]]
的直接访问:
> var obj = {}; > obj.__proto__ === Object.prototype true > obj.__proto__ = Array.prototype > Object.getPrototypeOf(obj) === Array.prototype true
关于 __proto__
,您需要了解以下几点
__proto__
读作“dunder proto”,是“double underscore proto”的缩写。这种读音是从 Python 编程语言借鉴而来的(由 Ned Batchelder 在 2006 年提出)。带有双下划线的特殊变量在 Python 中非常常见。__proto__
不是 ECMAScript 5 标准的一部分。因此,如果您希望代码符合该标准并在当前的 JavaScript 引擎中可靠运行,则不得使用它。__proto__
,它将成为 ECMAScript 6 的一部分。以下表达式检查引擎是否支持将 __proto__
作为特殊属性
Object
.
getPrototypeOf
({
__proto__
:
null
})
===
null
只有获取属性时才会考虑 对象的完整原型链。设置和删除会忽略继承,并且仅影响自有属性。
设置属性会创建一个自有属性,即使 存在具有该键的继承属性也是如此。例如,给定以下源代码:
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
从 proto
继承 foo
> obj.foo 'a' > obj.hasOwnProperty('foo') false
设置 foo
会产生预期的结果
> obj.foo = 'b'; > obj.foo 'b'
但是,我们创建了一个自有属性,并没有更改 proto.foo
> obj.hasOwnProperty('foo') true > proto.foo 'a'
其基本原理是,原型属性旨在由多个对象共享。这种方法允许我们以非破坏性的方式“更改”它们——只有当前对象会受到影响。
您只能删除自有属性。 让我们再次设置一个对象 obj
,其原型为 proto
:
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
删除继承的属性 foo
没有任何效果
> delete obj.foo true > obj.foo 'a'
有关 delete
运算符的更多信息,请参阅删除属性。
如果要更改继承的属性, 则必须先找到拥有该属性的对象(请参阅 查找定义属性的对象),然后对该对象执行更改。例如,让我们从前面的示例中删除属性 foo
> delete getDefiningObject(obj, 'foo').foo; true > obj.foo undefined
true
或 false
的标志。可枚举性很少重要,通常可以忽略(请参阅 可枚举性:最佳实践)。您可以列出自有属性键,列出所有可枚举属性键,以及检查属性是否存在。以下小节将展示如何操作。
您可以列出所有自有 属性键,也可以仅列出可枚举的属性键:
Object.getOwnPropertyNames(obj)
返回 obj
的所有自有属性的键。Object.keys(obj)
返回 obj
的所有 可枚举自有属性的键。 请注意,属性通常是可枚举的(请参阅可枚举性:最佳实践),因此您可以使用 Object.keys()
,尤其是对于您创建的对象。
如果要列出对象的全部属性(包括自有属性和继承属性),则有两种选择。
选项 1 是使用循环
for
(
«
variable
»
in
«
object
»
)
«
statement
»
迭代 object
的所有可枚举属性的键。有关更详细的说明,请参阅 for-in。
选项 2 是自己实现一个函数,该函数迭代所有属性(而不仅仅是可枚举属性)。例如
function
getAllPropertyNames
(
obj
)
{
var
result
=
[];
while
(
obj
)
{
// Add the own property names of `obj` to `result`
result
=
result
.
concat
(
Object
.
getOwnPropertyNames
(
obj
));
obj
=
Object
.
getPrototypeOf
(
obj
);
}
return
result
;
}
您可以检查对象是否具有某个属性, 或者某个属性是否存在于对象内部:
propKey in obj
obj
具有键为 propKey
的属性,则返回 true
。此测试包括继承的属性。Object.prototype.hasOwnProperty(propKey)
this
)具有 键为 propKey
的自有(非继承)属性,则返回 true
。 避免直接在对象上调用 hasOwnProperty()
,因为它可能会被覆盖(例如,被键为 hasOwnProperty
的自有属性覆盖)
> var obj = { hasOwnProperty: 1, foo: 2 }; > obj.hasOwnProperty('foo') // unsafe TypeError: Property 'hasOwnProperty' is not a function
相反,最好以泛型方式调用它(请参阅泛型方法:从原型借用方法)
> Object.prototype.hasOwnProperty.call(obj, 'foo') // safe true > {}.hasOwnProperty.call(obj, 'foo') // shorter true
var
proto
=
Object
.
defineProperties
({},
{
protoEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
protoEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
var
obj
=
Object
.
create
(
proto
,
{
objEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
objEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
Object.defineProperties()
在通过描述符获取和定义属性 中进行了解释,但它的工作原理应该相当明显:proto
具有自有属性 protoEnumTrue
和 protoEnumFalse
,而 obj
具有自有属性 objEnumTrue
和 objEnumFalse
(并继承 proto
的所有属性)。
请注意,对象(例如前面示例中的 proto
)通常至少具有原型 Object.prototype
(其中定义了标准方法,例如 toString()
和 hasOwnProperty()
)
> Object.getPrototypeOf({}) === Object.prototype true
在与 属性相关的操作中,可枚举性仅影响 for-in
循环 和 Object.keys()
(它也影响 JSON.stringify()
,请参阅 JSON.stringify(value, replacer?, space?))。
for-in
循环迭代所有可枚举属性的键,包括继承的属性(请注意,Object.prototype
的任何不可枚举属性都不会出现)
> for (var x in obj) console.log(x); objEnumTrue protoEnumTrue
Object.keys()
返回所有自有(非继承)可枚举属性的键
> Object.keys(obj) [ 'objEnumTrue' ]
如果需要所有自有属性的键,则需要使用 Object.getOwnPropertyNames()
> Object.getOwnPropertyNames(obj) [ 'objEnumTrue', 'objEnumFalse' ]
只有 for-in
循环(请参阅前面的示例) 和 in
运算符会考虑继承:
> 'toString' in obj true > obj.hasOwnProperty('toString') false > obj.hasOwnProperty('objEnumFalse') true
迭代 属性键:
以for-in 中描述的方式,将 for-in
与 hasOwnProperty()
结合使用。即使在较旧的 JavaScript 引擎上,这种方法也适用。例如
for
(
var
key
in
obj
)
{
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
key
))
{
console
.
log
(
key
);
}
}
将 Object.keys()
或 Object.getOwnPropertyNames()
与 forEach()
数组迭代结合使用
var
obj
=
{
first
:
'John'
,
last
:
'Doe'
};
// Visit non-inherited enumerable keys
Object
.
keys
(
obj
).
forEach
(
function
(
key
)
{
console
.
log
(
key
);
});
迭代属性值或(键,值)对
ECMAScript 5 允许 您编写方法,这些方法的调用看起来就像是在获取或设置属性。这意味着属性是虚拟的,而不是存储空间。例如,您可以禁止设置属性,并始终计算读取属性时返回的值。
以下 示例使用对象字面量为属性 foo
定义设置器和获取器:
var
obj
=
{
get
foo
()
{
return
'getter'
;
},
set
foo
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
};
以下是交互
> obj.foo = 'bla'; setter: bla > obj.foo 'getter'
指定获取器和设置器的另一种方法 是通过属性描述符(请参阅 属性描述符)。以下代码定义与前面的字面量相同的对象
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
// object with property descriptors
foo
:
{
// property descriptor
get
:
function
()
{
return
'getter'
;
},
set
:
function
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
}
}
);
获取器和 设置器是从原型继承的:
> var proto = { get foo() { return 'hello' } }; > var obj = Object.create(proto); > obj.foo 'hello'
属性特性和属性描述符是一个高级主题。您通常不需要知道它们是如何工作的。
在本节中,我们将研究 属性的内部结构:
属性的所有状态,包括其数据和元数据,都存储在 特性 中。它们是属性所具有的字段,就像对象具有属性一样。特性键通常用双括号括起来。特性对于普通属性和访问器(getter 和 setter)都很重要。
以下特性特定于普通属性
[[Value]]
保存属性的值,即其数据。[[Writable]]
保存一个布尔值,指示是否可以更改属性的值。[[Get]]
保存 getter,这是一个在读取属性时调用的函数。该函数计算读取访问的结果。[[Set]]
保存 setter,这是一个在将属性设置为某个值时调用的函数。该函数接收该值作为参数。属性描述符是一种用于以编程方式处理特性的数据结构。它是一个编码属性特性的对象。描述符的每个属性都对应于一个特性。例如,以下是一个只读属性的描述符,其值为 123:
{
value
:
123
,
writable
:
false
,
enumerable
:
true
,
configurable
:
false
}
您可以通过访问器实现相同的目标,即不变性。然后描述符如下所示
{
get
:
function
()
{
return
123
},
enumerable
:
true
,
configurable
:
false
}
定义属性的含义取决于属性是否已存在
如果属性不存在,则创建一个新属性,其特性由描述符指定。如果描述符中没有与特性对应的属性,则使用默认值。默认值由特性名称的含义决定。它们与通过赋值创建属性时使用的值相反(然后该属性是可写的、可枚举的和可配置的)。 例如:
> var obj = {}; > Object.defineProperty(obj, 'foo', { configurable: true }); > Object.getOwnPropertyDescriptor(obj, 'foo') { value: undefined, writable: false, enumerable: false, configurable: true }
我通常不依赖默认值,而是显式声明所有特性,以便完全清楚。
如果属性已存在,则更新描述符指定的属性特性。如果描述符中没有与特性对应的属性,则不要更改它。以下是一个示例(续前例)
> Object.defineProperty(obj, 'foo', { writable: true }); > Object.getOwnPropertyDescriptor(obj, 'foo') { value: undefined, writable: true, enumerable: false, configurable: true }
Object.getOwnPropertyDescriptor(obj, propKey)
返回 obj
的自身(非继承)属性的描述符,其键为 propKey
。如果没有这样的属性,则返回 undefined
> Object.getOwnPropertyDescriptor(Object.prototype, 'toString') { value: [Function: toString], writable: true, enumerable: false, configurable: true } > Object.getOwnPropertyDescriptor({}, 'toString') undefined
Object.defineProperty(obj, propKey, propDesc)
创建或更改 obj
的属性,其键为 propKey
,其特性由 propDesc
指定。返回修改后的对象。例如
var
obj
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
123
,
enumerable
:
true
// writable: false (default value)
// configurable: false (default value)
});
Object.defineProperties(obj, propDescObj)
Object.defineProperty()
的批量版本。 propDescObj
的每个属性都包含一个属性描述符。属性的键及其值告诉 Object.defineProperties
要在 obj
上创建或更改哪些属性。例如:
var
obj
=
Object
.
defineProperties
({},
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
Object.create(proto, propDescObj?)
首先,创建一个原型为 proto
的对象。然后,如果指定了可选参数 propDescObj
,则向其添加属性 - 方式与 Object.defineProperties
相同。最后,返回结果。例如,以下代码段生成与前一个代码段相同的结果
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
以下函数执行此类复制
function
copyObject
(
orig
)
{
// 1. copy has same prototype as orig
var
copy
=
Object
.
create
(
Object
.
getPrototypeOf
(
orig
));
// 2. copy has all of orig’s properties
copyOwnPropertiesFrom
(
copy
,
orig
);
return
copy
;
}
属性通过此函数从 orig
复制到 copy
function
copyOwnPropertiesFrom
(
target
,
source
)
{
Object
.
getOwnPropertyNames
(
source
)
// (1)
.
forEach
(
function
(
propKey
)
{
// (2)
var
desc
=
Object
.
getOwnPropertyDescriptor
(
source
,
propKey
);
// (3)
Object
.
defineProperty
(
target
,
propKey
,
desc
);
// (4)
});
return
target
;
};
以下是涉及的步骤
source
的所有自身属性键的数组。target
中创建自身属性。请注意,此函数与 Underscore.js 库中的 _.extend()
函数非常相似。
defineProperty()
和 defineProperties()
定义属性(请参阅通过描述符获取和定义属性)。=
为属性赋值。但是,有一些细微的差别
为属性赋值 prop
意味着更改现有属性。过程如下:
prop
是 setter(自身或继承的),则调用该 setter。prop
是只读的(自身或继承的),则抛出异常(在严格模式下)或不执行任何操作(在宽松模式下)。下一节将更详细地解释这种(稍微出乎意料的)现象。prop
是自身的(并且是可写的),则更改该属性的值。prop
,要么它是继承的并且是可写的。在这两种情况下,都定义一个自身属性 prop
,它是可写的、可配置的和可枚举的。在后一种情况下,我们只是覆盖了继承的属性(非破坏性地更改了它)。在前一种情况下,自动定义了缺少的属性。这种自动定义是有问题的,因为赋值中的拼写错误可能难以检测。如果对象 obj
从原型继承了属性 foo
,并且 foo
是不可写的,则无法为 obj.foo
赋值:
var
proto
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
'a'
,
writable
:
false
});
var
obj
=
Object
.
create
(
proto
);
obj
从 proto
继承了只读属性 foo
。在宽松模式下,设置该属性无效
> obj.foo = 'b'; > obj.foo 'a'
在严格模式下,您会收到异常
> (function () { 'use strict'; obj.foo = 'b' }()); TypeError: Cannot assign to read-only property 'foo'
这符合赋值更改继承的属性但非破坏性的想法。如果继承的属性是只读的,则您要禁止所有更改,即使是非破坏性的更改。
请注意,您可以通过定义自身属性来绕过此保护(请参阅上一小节,了解定义和赋值之间的区别)
> Object.defineProperty(obj, 'foo', { value: 'b' }); > obj.foo 'b'
一般规则是,系统创建的属性是不可枚举的,而用户创建的属性是可枚举的:
> Object.keys([]) [] > Object.getOwnPropertyNames([]) [ 'length' ] > Object.keys(['a']) [ '0' ]
对于内置实例原型的 方法尤其如此
> Object.keys(Object.prototype) [] > Object.getOwnPropertyNames(Object.prototype) [ hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString' ]
可枚举性的主要目的是告诉 for-in
循环它应该忽略哪些属性。正如我们刚才在查看内置构造函数的实例时所见,所有不是由用户创建的内容都对 for-in
隐藏。
受可枚举性影响的操作只有
for-in
循环Object.keys()
(列出自身属性键)JSON.stringify()
(JSON.stringify(value, replacer?, space?))以下是一些要记住的最佳实践
for-in
循环(最佳实践:迭代数组)。Object
.
preventExtensions
(
obj
)
使无法向 obj
添加属性。例如
var
obj
=
{
foo
:
'a'
};
Object
.
preventExtensions
(
obj
);
现在,在宽松模式下添加属性会静默失败
> obj.bar = 'b'; > obj.bar undefined
并在严格模式下抛出错误
> (function () { 'use strict'; obj.bar = 'b' }()); TypeError: Can't add property bar, object is not extensible
但是,您仍然可以删除属性
> delete obj.foo true > obj.foo undefined
您可以通过以下方式检查对象是否可扩展
Object
.
isExtensible
(
obj
)
通过以下方式密封
Object
.
seal
(
obj
)
防止扩展并将所有属性设置为“不可配置”。后者意味着属性的特性(请参阅属性特性和属性描述符)不能再更改。例如,只读属性将永远保持只读。
以下示例演示了密封如何使所有属性都不可配置
> var obj = { foo: 'a' }; > Object.getOwnPropertyDescriptor(obj, 'foo') // before sealing { value: 'a', writable: true, enumerable: true, configurable: true } > Object.seal(obj) > Object.getOwnPropertyDescriptor(obj, 'foo') // after sealing { value: 'a', writable: true, enumerable: true, configurable: false }
您仍然可以更改属性 foo
> obj.foo = 'b'; 'b' > obj.foo 'b'
但您不能更改其特性
> Object.defineProperty(obj, 'foo', { enumerable: false }); TypeError: Cannot redefine property: foo
您可以通过以下方式检查对象是否已密封
Object
.
isSealed
(
obj
)
通过以下方式冻结:
Object
.
freeze
(
obj
)
它使所有属性都不可写,并密封 obj
。换句话说,obj
是不可扩展的,并且所有属性都是只读的,并且无法更改。让我们看一个例子:
var
point
=
{
x
:
17
,
y
:
-
5
};
Object
.
freeze
(
point
);
同样,在宽松模式下,您会遇到静默失败
> point.x = 2; // no effect, point.x is read-only > point.x 17 > point.z = 123; // no effect, point is not extensible > point { x: 17, y: -5 }
在严格模式下,您会收到错误
> (function () { 'use strict'; point.x = 2 }()); TypeError: Cannot assign to read-only property 'x' > (function () { 'use strict'; point.z = 123 }()); TypeError: Can't add property z, object is not extensible
您可以通过以下方式检查对象是否已冻结
Object
.
isFrozen
(
obj
)
保护对象是浅层的:它会影响自身属性,但不会影响这些属性的值。例如,请考虑以下对象:
var
obj
=
{
foo
:
1
,
bar
:
[
'a'
,
'b'
]
};
Object
.
freeze
(
obj
);
即使您已经冻结了 obj
,它也不是完全不可变的——您仍然可以更改属性 bar
的(可变)值。
> obj.foo = 2; // no effect > obj.bar.push('c'); // changes obj.bar > obj { foo: 1, bar: [ 'a', 'b', 'c' ] }
此外,obj
具有原型 Object.prototype
,它也是可变的。
一个构造函数(简称:构造器)有助于生成在某种程度上相似的对象。它是一个普通函数,但它的命名、设置和调用方式都不同。本节解释了构造函数的工作原理。它们对应于其他语言中的类。
我们已经看到了两个相似对象的例子(在通过原型在对象之间共享数据)
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
对象 jane
和 tarzan
都被认为是“人”,并且共享原型对象 PersonProto
。让我们把这个原型变成一个构造函数 Person
,它可以创建像 jane
和 tarzan
这样的对象。构造函数创建的对象称为它的实例。这样的实例具有与 jane
和 tarzan
相同的结构,由两部分组成
jane
和 tarzan
)。PersonProto
)。构造函数是一个通过 new
运算符调用的函数。按照惯例,构造函数的名称以大写字母开头,而普通函数和方法的名称以小写字母开头。函数本身设置了第一部分
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person.prototype
中的对象成为 Person
的所有实例的原型。它贡献了第二部分
Person
.
prototype
.
describe
=
function
()
{
return
'Person named '
+
this
.
name
;
};
让我们创建并使用 Person
的一个实例
> var jane = new Person('Jane'); > jane.describe() 'Person named Jane'
我们可以看到 Person
是一个普通函数。它 只有在通过 new
调用时才成为构造函数。new
运算符执行以下步骤:
Person.
prototype
。Person
接收该对象作为隐式参数 this
并添加实例属性。图 17-3 显示了实例 jane
的样子。Person.prototype
的属性 constructor
指向构造函数,这在实例的 constructor 属性中进行了解释。
instanceof
运算符允许我们检查一个对象是否是给定构造函数的实例
> jane instanceof Person true > jane instanceof Date false
如果您要手动实现 new
运算符,它大致如下所示:
function
newOperator
(
Constr
,
args
)
{
var
thisValue
=
Object
.
create
(
Constr
.
prototype
);
// (1)
var
result
=
Constr
.
apply
(
thisValue
,
args
);
if
(
typeof
result
===
'object'
&&
result
!==
null
)
{
return
result
;
// (2)
}
return
thisValue
;
}
在第 (1) 行中,您可以看到由构造函数 Constr
创建的实例的原型是 Constr.prototype
。
第 (2) 行揭示了 new
运算符的另一个特性:您可以从构造函数返回一个任意对象,它将成为 new
运算符的结果。如果您希望构造函数返回子构造函数的实例,这将非常有用(从构造函数返回任意对象中给出了一个例子)。
不幸的是,术语原型在 JavaScript 中的使用 具有歧义:
一个对象可以是另一个对象的原型
> var proto = {}; > var obj = Object.create(proto); > Object.getPrototypeOf(obj) === proto true
在前面的例子中,proto
是 obj
的原型。
prototype
的值每个构造函数 C
都有一个 prototype
属性,它引用一个对象。该对象成为 C
的所有实例的原型:
> function C() {} > Object.getPrototypeOf(new C()) === C.prototype true
通常,上下文会清楚地表明指的是哪种原型。如果需要消除歧义,那么我们只能使用原型来描述对象之间的关系,因为该名称已经通过 getPrototypeOf
和 isPrototypeOf
进入了标准库。因此,我们需要为 prototype
属性引用的对象找到一个不同的名称。一种可能是构造函数原型,但这有问题,因为构造函数也有原型
> function Foo() {} > Object.getPrototypeOf(Foo) === Function.prototype true
因此,实例原型是最好的选择。
默认情况下,每个函数 C
都包含一个实例原型对象 C.prototype
,其属性 constructor
指向 C
:
> function C() {} > C.prototype.constructor === C true
因为 constructor
属性是由每个实例从原型继承的,所以您可以使用它来获取实例的构造函数
> var o = new C(); > o.constructor [Function: C]
在以下 catch
子句中,我们根据捕获的异常的构造函数采取不同的操作:
try
{
...
}
catch
(
e
)
{
switch
(
e
.
constructor
)
{
case
SyntaxError
:
...
break
;
case
CustomError
:
...
break
;
...
}
}
此方法仅检测给定构造函数的直接实例。相反,instanceof
检测直接实例和所有子构造函数的实例。
例如
> function Foo() {} > var f = new Foo(); > f.constructor.name 'Foo'
并非所有 JavaScript 引擎都支持函数的 name
属性。
这就是您创建一个新对象 y
的方法,该对象与现有对象 x
具有相同的构造函数
function
Constr
()
{}
var
x
=
new
Constr
();
var
y
=
new
x
.
constructor
();
console
.
log
(
y
instanceof
Constr
);
// true
这个技巧对于必须处理子构造函数实例并且想要创建一个类似于 this
的新实例的方法来说非常方便。然后您就不能使用固定的构造函数
SuperConstr
.
prototype
.
createCopy
=
function
()
{
return
new
this
.
constructor
(...);
};
一些继承库将超原型分配给子构造函数的属性。例如,YUI 框架通过Y.extend
提供子类化
function
Super
()
{
}
function
Sub
()
{
Sub
.
superclass
.
constructor
.
call
(
this
);
// (1)
}
Y
.
extend
(
Sub
,
Super
);
第 (1) 行中的调用有效,因为 extend
将 Sub.superclass
设置为 Super.prototype
。由于 constructor
属性,您可以将超构造函数作为方法调用。
instanceof
运算符(参见instanceof 运算符)不依赖于属性 constructor
。
确保对于每个构造函数 C
,以下断言成立:
C
.
prototype
.
constructor
===
C
默认情况下,每个函数 f
已经有一个正确设置的属性 prototype
> function f() {} > f.prototype.constructor === f true
因此,您应该避免替换此对象,而只向其添加属性
// Avoid:
C
.
prototype
=
{
method1
:
function
(...)
{
...
},
...
};
// Prefer:
C
.
prototype
.
method1
=
function
(...)
{
...
};
...
如果确实要替换它,则应手动为 constructor
分配正确的值
C
.
prototype
=
{
constructor
:
C
,
method1
:
function
(...)
{
...
},
...
};
请注意,JavaScript 中没有任何关键内容依赖于 constructor
属性;但最好设置它,因为它启用了本节中提到的技术。
instanceof
运算符
value
instanceof
Constr
确定 value
是否由构造函数 Constr
或子构造函数创建。它通过检查 Constr.prototype
是否在 value
的原型链中来实现。因此,以下两个表达式是等效的:
value
instanceof
Constr
Constr
.
prototype
.
isPrototypeOf
(
value
)
以下是一些示例
> {} instanceof Object true > [] instanceof Array // constructor of [] true > [] instanceof Object // super-constructor of [] true > new Date() instanceof Date true > new Date() instanceof Object true
正如预期的那样,对于原始值,instanceof
始终为 false
> 'abc' instanceof Object false > 123 instanceof Number false
最后,如果 instanceof
的右侧不是函数,则会抛出异常
> [] instanceof 123 TypeError: Expecting a function in instanceof check
几乎所有对象都是 Object
的实例,因为 Object.prototype
在它们的原型链中。但也有一些对象并非如此。以下是两个例子:
> Object.create(null) instanceof Object false > Object.prototype instanceof Object false
前一个对象在dict 模式:没有原型的对象是更好的映射中有更详细的解释。后一个对象是大多数原型链结束的地方(它们必须在某个地方结束)。这两个对象都没有原型
> Object.getPrototypeOf(Object.create(null)) null > Object.getPrototypeOf(Object.prototype) null
但 typeof
正确地将它们分类为对象
> typeof Object.create(null) 'object' > typeof Object.prototype 'object'
对于 instanceof
的大多数用例来说,这个陷阱并不是一个破坏因素,但您必须意识到它。
在 Web 浏览器中,每个框架和窗口都有自己的域,其中包含独立的全局变量。这会阻止 instanceof
对跨域的对象起作用。要了解原因,请查看以下代码:
if
(
myvar
instanceof
Array
)
...
// Doesn’t always work
如果 myvar
是来自不同域的数组,则其原型是该域的 Array.prototype
。因此,instanceof
将不会在 myvar
的原型链中找到当前域的 Array.prototype
,并将返回 false
。ECMAScript 5 具有函数 Array.isArray()
,它 始终有效:
<head>
<script>
function
test
(
arr
)
{
var
iframe
=
frames
[
0
];
console
.
log
(
arr
instanceof
Array
);
// false
console
.
log
(
arr
instanceof
iframe
.
Array
);
// true
console
.
log
(
Array
.
isArray
(
arr
));
// true
}
</script>
</head>
<body>
<iframe
srcdoc=
"<script>window.parent.test([])</script>"
>
</iframe>
</body>
显然,对于非内置构造函数,这也是一个问题。
除了使用 Array.isArray()
之外,您还可以采取多种措施来解决此问题
postMessage()
方法,该方法可以将对象复制到另一个域,而不是传递引用。检查实例的构造函数的名称(仅适用于支持函数的 name
属性的引擎)
someValue
.
constructor
.
name
===
'NameOfExpectedConstructor'
使用原型属性将实例标记为属于类型 T
。您可以通过多种方式实现此目的。检查 value
是否是 T
的实例如下所示
value.isT()
:T
实例的原型必须从此方法返回 true
;公共超构造函数应返回默认值 false
。'T' in value
:您必须使用其键为 'T'
(或更唯一的内容)的属性标记 T
实例的原型。value.TYPE_NAME === 'T'
:每个相关原型都必须具有一个具有适当值的 TYPE_NAME
属性。本节提供了一些实现构造函数的技巧。
如果您在使用构造函数时忘记了 new
,则您 是将其作为函数而不是构造函数调用的。在草率模式下,您不会获得实例,并且会创建全局变量。不幸的是,所有这些都是在没有警告的情况下发生的:
function
SloppyColor
(
name
)
{
this
.
name
=
name
;
}
var
c
=
SloppyColor
(
'green'
);
// no warning!
// No instance is created:
console
.
log
(
c
);
// undefined
// A global variable is created:
console
.
log
(
name
);
// green
在严格模式下,您会收到异常
function
StrictColor
(
name
)
{
'use strict'
;
this
.
name
=
name
;
}
var
c
=
StrictColor
(
'green'
);
// TypeError: Cannot set property 'name' of undefined
class
Expression
{
// Static factory method:
public
static
Expression
parse
(
String
str
)
{
if
(...)
{
return
new
Addition
(...);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
}
...
Expression
expr
=
Expression
.
parse
(
someStr
);
在 JavaScript 中,您可以简单地从构造函数返回所需的任何对象。因此,前面代码的 JavaScript 版本如下所示
function
Expression
(
str
)
{
if
(...)
{
return
new
Addition
(..);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
...
var
expr
=
new
Expression
(
someStr
);
这是一个好消息:JavaScript 构造函数不会限制您,因此您可以随时改变主意,决定构造函数是应该返回直接实例还是其他内容。
本节说明,在大多数情况下,您不应将数据放在原型属性中。但是,该规则也有一些例外。
原型包含由多个对象共享的属性。因此,它们适用于方法。此外,使用接下来描述的技术,您还可以使用它们为实例属性提供初始值。稍后我将解释为什么不建议这样做。
构造函数通常将实例属性设置为初始值。如果其中一个值是默认值,则您不需要创建实例属性。您只需要一个具有相同键的原型属性,其值为默认值。例如
/**
* Anti-pattern: don’t do this
*
* @param data an array with names
*/
function
Names
(
data
)
{
if
(
data
)
{
// There is a parameter
// => create instance property
this
.
data
=
data
;
}
}
Names
.
prototype
.
data
=
[];
参数 data
是可选的。如果缺少它,则实例不会获得属性 data
,而是继承 Names.prototype.data
。
这种方法通常有效:您可以创建 Names
的实例 n
。获取 n.data
会读取 Names.prototype.data
。设置 n.data
会在 n
中创建一个新的自有属性,并在原型中保留共享的默认值。只有当我们更改默认值(而不是用新值替换它)时,才会出现问题
> var n1 = new Names(); > var n2 = new Names(); > n1.data.push('jane'); // changes default value > n1.data [ 'jane' ] > n2.data [ 'jane' ]
在前面的示例中,push()
更改了 Names.prototype.data
中的数组。由于该数组由没有自有属性 data
的所有实例共享,因此 n2.data
的初始值也已更改。
鉴于我们刚刚讨论的内容,最好不要共享默认值,而是始终创建新的默认值
function
Names
(
data
)
{
this
.
data
=
data
||
[];
}
显然,如果默认值是不可变的(所有基本类型都是如此;请参阅基本类型值),则不会出现修改共享默认值的问题。但为了保持一致性,最好坚持使用一种设置属性的方法。我还更喜欢保持通常的关注点分离(请参阅第 3 层:构造函数 - 实例的工厂):构造函数设置实例属性,原型包含方法。
ECMAScript 6 将使其更加成为最佳实践,因为构造函数参数可以具有默认值,并且您可以通过类定义原型方法,但不能定义具有数据的原型属性。
有时,创建属性值是一项开销很大的操作(在计算或存储方面)。在这种情况下,您可以按需创建实例属性:
function
Names
(
data
)
{
if
(
data
)
this
.
data
=
data
;
}
Names
.
prototype
=
{
constructor
:
Names
,
// (1)
get
data
()
{
// Define, don’t assign
// => avoid calling the (nonexistent) setter
Object
.
defineProperty
(
this
,
'data'
,
{
value
:
[],
enumerable
:
true
,
configurable
:
false
,
writable
:
false
});
return
this
.
data
;
}
};
我们无法通过赋值将属性 data
添加到实例,因为 JavaScript 会抱怨缺少设置器(当它只找到获取器时就会这样做)。因此,我们通过 Object.defineProperty()
添加它。请参阅属性:定义与赋值以查看定义和赋值之间的区别。在第 (1) 行中,我们确保正确设置了属性 constructor
(请参阅实例的构造函数属性)。
显然,这是相当多的工作,所以您必须确保它是值得的。
如果相同的属性(相同的键、相同的语义、通常不同的值)存在于多个原型中,则称其为多态的。然后,通过实例读取属性的结果是通过该实例的原型动态确定的。非多态使用的原型属性可以用变量替换(这更好地反映了它们的非多态性质)。
例如,您可以将常量存储在原型属性中,并通过 this
访问它
function
Foo
()
{}
Foo
.
prototype
.
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
this
.
FACTOR
;
};
此常量不是多态的。因此,您也可以通过变量访问它
// This code should be inside an IIFE or a module
function
Foo
()
{}
var
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
FACTOR
;
};
以下是一个具有不可变数据的多态原型属性示例。通过原型属性标记构造函数的实例,您可以将它们与其他构造函数的实例区分开来:
function
ConstrA
()
{
}
ConstrA
.
prototype
.
TYPE_NAME
=
'ConstrA'
;
function
ConstrB
()
{
}
ConstrB
.
prototype
.
TYPE_NAME
=
'ConstrB'
;
由于多态“标记” TYPE_NAME
,即使在跨领域时,您也可以区分 ConstrA
和 ConstrB
的实例(然后 instanceof
不起作用;请参阅陷阱:跨领域(框架或窗口))。
JavaScript 没有用于管理对象的私有数据的专用方法。本节将介绍三种解决此限制的技术:
此外,我将解释如何通过 IIFE 保持全局数据私有。
调用构造函数时,会创建两个东西:构造函数的实例和一个环境(请参阅环境:管理变量)。该实例将由构造函数初始化。环境保存构造函数的参数和局部变量。在构造函数内部创建的每个函数(包括方法)都将保留对环境的引用,即创建它的环境。这种函数和环境的组合称为闭包(闭包:函数与其诞生作用域保持连接)。因此,构造函数的环境是独立于实例的数据存储,并且仅因为两者同时创建才与其相关。为了正确连接它们,我们必须拥有同时存在于两个世界中的函数。使用Douglas Crockford 的术语,实例可以具有与其关联的三种值(请参阅图 17-4)
以下各节更详细地解释了每种值。
请记住,给定一个构造函数 Constr
,有两种属性是公共的,每个人都可以访问。首先,原型属性存储在 Constr.prototype
中,并由所有实例共享。原型属性通常是方法:
Constr
.
prototype
.
publicMethod
=
...;
其次,实例属性对于每个实例都是唯一的。它们在构造函数中添加,通常保存数据(而不是方法):
function
Constr
(...)
{
this
.
publicData
=
...;
...
}
构造函数的环境由参数和局部变量组成。它们只能从构造函数内部访问,因此对实例是私有的:
function
Constr
(...)
{
...
var
that
=
this
;
// make accessible to private functions
var
privateData
=
...;
function
privateFunction
(...)
{
// Access everything
privateData
=
...;
that
.
publicData
=
...;
that
.
publicMethod
(...);
}
...
}
私有数据非常安全,外部无法访问,因此原型方法无法访问它。但是,离开构造函数后,您如何使用它呢?答案是特权方法:在构造函数中创建的函数作为实例方法添加。这意味着,一方面,它们可以访问私有数据;另一方面,它们是公共的,因此原型方法可以看到它们。换句话说,它们充当私有数据和公共数据(包括原型方法)之间的中介:
function
Constr
(...)
{
...
this
.
privilegedMethod
=
function
(...)
{
// Access everything
privateData
=
...;
privateFunction
(...);
this
.
publicData
=
...;
this
.
publicMethod
(...);
};
}
以下是使用 Crockford 隐私模式实现的 StringBuilder
:
function
StringBuilder
()
{
var
buffer
=
[];
this
.
add
=
function
(
str
)
{
buffer
.
push
(
str
);
};
this
.
toString
=
function
()
{
return
buffer
.
join
(
''
);
};
}
// Can’t put methods in the prototype!
以下是交互过程
> var sb = new StringBuilder(); > sb.add('Hello'); > sb.add(' world!'); > sb.toString() ’Hello world!’
以下是使用Crockford 隐私模式时需要考虑的一些事项:
对于大多数非安全关键型应用程序,隐私更像是对 API 客户端的提示:“您不需要看到这些。”这是封装的关键好处 - 隐藏复杂性。即使幕后发生了更多事情,您也只需要了解 API 的公共部分。命名约定的想法是通过标记属性的键让客户端了解隐私。前缀下划线通常用于此目的。
让我们重写前面的 StringBuilder
示例,以便将缓冲区保存在属性 _buffer
中,该属性是私有的,但仅按照约定
function
StringBuilder
()
{
this
.
_buffer
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
.
_buffer
.
push
(
str
);
},
toString
:
function
()
{
return
this
.
_buffer
.
join
(
''
);
}
};
以下是通过标记属性键实现隐私的一些优缺点
私有属性命名约定的一个问题是键可能会冲突(例如,构造函数中的键与子构造函数中的键,或 mixin 中的键与构造函数中的键)。您可以通过使用更长的键来降低此类冲突的可能性,例如,包括构造函数的名称。然后,在前面的例子中,私有属性 _buffer
将被称为 _StringBuilder_buffer
。如果这样的键对您来说太长,您可以选择具体化它,将其存储在一个变量中:
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
我们现在通过 this[KEY_BUFFER]
访问私有数据
var
StringBuilder
=
function
()
{
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
[
KEY_BUFFER
].
push
(
str
);
},
toString
:
function
()
{
return
this
[
KEY_BUFFER
].
join
(
''
);
}
};
return
StringBuilder
;
}();
我们在 StringBuilder
周围包装了一个 IIFE,以便常量 KEY_BUFFER
保持局部性并且不会污染全局命名空间。
具体化的属性键使您能够在键中使用 UUID(通用唯一标识符)。例如,通过 Robert Kieffer 的 node-uuid
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
KEY_BUFFER
每次代码运行时都有不同的值。例如,它可能看起来像这样
_StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1
带有 UUID 的长键使键冲突几乎不可能发生。
本小节介绍如何通过 IIFE(参见通过 IIFE 引入新作用域)将全局数据保持为单例对象、构造函数和方法的私有数据。这些 IIFE 创建新的环境(请参阅环境:管理变量),您可以在其中放置私有数据。
您不需要构造函数来将对象与环境中的私有数据相关联。以下示例显示了如何通过将 IIFE 包装在单例对象周围来将其用于相同目的:
var
obj
=
function
()
{
// open IIFE
// public
var
self
=
{
publicMethod
:
function
(...)
{
privateData
=
...;
privateFunction
(...);
},
publicData
:
...
};
// private
var
privateData
=
...;
function
privateFunction
(...)
{
privateData
=
...;
self
.
publicData
=
...;
self
.
publicMethod
(...);
}
return
self
;
}();
// close IIFE
某些全局数据仅与构造函数和原型方法相关。通过将 IIFE 包装在两者周围,您可以将其隐藏在公共视图之外。具有具体化键的属性中的私有数据给出了一个示例:构造函数 StringBuilder
及其原型方法使用常量 KEY_BUFFER
,其中包含一个属性键。该常量存储在 IIFE 的环境中
var
StringBuilder
=
function
()
{
// open IIFE
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
// Omitted: methods accessing this[KEY_BUFFER]
};
return
StringBuilder
;
}();
// close IIFE
请注意,如果您正在使用模块系统(请参阅第 31 章),您可以通过将构造函数和方法放在模块中来以更简洁的代码实现相同的效果。
有时您只需要单个方法的全局数据。您可以通过将其放在包装该方法的 IIFE 的环境中来保持其私有性。例如:
var
obj
=
{
method
:
function
()
{
// open IIFE
// method-private data
var
invocCount
=
0
;
return
function
()
{
invocCount
++
;
console
.
log
(
'Invocation #'
+
invocCount
);
return
'result'
;
};
}()
// close IIFE
};
以下是交互过程
> obj.method() Invocation #1 'result' > obj.method() Invocation #2 'result'
在本节中,我们将研究如何继承构造函数:给定一个构造函数 Super
,我们如何编写一个新的构造函数 Sub
,它具有 Super
的所有功能以及它自己的一些功能?不幸的是,JavaScript 没有用于执行此任务的内置机制。因此,我们必须做一些手动工作。
图 17-5 说明了这个想法:子构造函数 Sub
应该拥有 Super
的所有属性(原型属性和实例属性)以及它自己的属性。因此,我们对 Sub
的外观有了一个粗略的了解,但不知道如何实现。我们需要弄清楚几件事,我将在接下来解释
instanceof
工作:如果 sub
是 Sub
的实例,我们也希望 sub instanceof Super
为真。Sub
中调整 Super
的方法之一。Super
的方法之一,我们可能需要从 Sub
中调用原始方法。实例属性在构造函数本身中设置,因此继承超类构造函数的实例属性涉及调用该构造函数:
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Super
.
call
(
this
,
prop1
,
prop2
);
// (1)
this
.
prop3
=
prop3
;
// (2)
this
.
prop4
=
prop4
;
// (3)
}
当通过 new
调用 Sub
时,其隐式参数 this
指的是一个新的实例。它首先将该实例传递给 Super
(1),后者添加其实例属性。之后,Sub
设置它自己的实例属性(2,3)。诀窍是不要通过 new
调用 Super
,因为这会创建一个新的超类实例。相反,我们将 Super
作为函数调用,并将当前(子类)实例作为 this
的值传入。
共享属性(例如方法)保存在实例原型中。因此,我们需要找到一种方法让 Sub.prototype
继承 Super.prototype
的所有属性。解决方案是为 Sub.prototype
提供原型 Super.prototype
。
是的,JavaScript 术语在这里令人困惑。如果您感到困惑,请参阅术语:两种原型,其中解释了它们的区别。
这是实现这一点的代码
Sub
.
prototype
=
Object
.
create
(
Super
.
prototype
);
Sub
.
prototype
.
constructor
=
Sub
;
Sub
.
prototype
.
methodB
=
...;
Sub
.
prototype
.
methodC
=
...;
Object.create()
生成一个新对象,其原型为 Super.prototype
。之后,我们添加 Sub
的方法。如实例的 constructor 属性中所述,我们还需要设置属性 constructor
,因为我们已经替换了原始实例原型,其中包含正确的值。
图 17-6 显示了 Sub
和 Super
现在是如何相关的。 Sub
的结构确实类似于我在图 17-5中绘制的内容。该图未显示实例属性,这些属性由图中提到的函数调用设置。
“确保 instanceof
工作”意味着 Sub
的每个实例也必须是 Super
的实例。 图 17-7 显示了 subInstance
(Sub
的实例)的原型链是什么样的:它的第一个原型是 Sub.prototype
,它的第二个原型是 Super.prototype
。
让我们从一个更简单的问题开始:subInstance
是 Sub
的实例吗?是的,因为以下两个断言是等效的(后者可以被认为是前者的定义)
subInstance
instanceof
Sub
Sub
.
prototype
.
isPrototypeOf
(
subInstance
)
如前所述,Sub.prototype
是 subInstance
的原型之一,因此两个断言都为真。类似地,subInstance
也是 Super
的实例,因为以下两个断言成立
subInstance
instanceof
Super
Super
.
prototype
.
isPrototypeOf
(
subInstance
)
我们通过向 Sub.prototype
添加一个同名方法来覆盖 Super.prototype
中的方法。 methodB
就是一个例子,在图 17-7 中,我们可以看到它是如何工作的:对 methodB
的搜索从 subInstance
开始,并在找到 Super.prototype.methodB
之前找到了 Sub.prototype.methodB
。
要理解超级调用,您需要知道术语宿主对象。方法的宿主对象是拥有该方法作为其属性值的对象。例如,Sub.prototype.methodB
的宿主对象是 Sub.prototype
。超级调用方法 foo
涉及三个步骤:
foo
的方法。this
调用该方法。基本原理是超级方法必须与当前方法一起使用相同的实例;它必须能够访问相同的实例属性。因此,子方法的代码如下所示。它超级调用自身,它调用它覆盖的方法
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Super
.
prototype
.
methodB
.
call
(
this
,
x
,
y
);
// (1)
return
this
.
prop3
+
' '
+
superResult
;
}
读取 (1) 处的超级调用的一种方法如下:直接引用超级方法并使用当前的 this
调用它。但是,如果我们将其分为三个部分,我们会发现前面提到的步骤
Super.prototype
:在 Super.prototype
中开始搜索,它是 Sub.prototype
(当前方法 Sub.prototype.methodB
的宿主对象)的原型。methodB
:查找名称为 methodB
的方法。call(this, ...)
:调用上一步中找到的方法,并维护当前的 this
。到目前为止,我们始终 通过提及超构造函数名称来引用超方法和超构造函数。这种硬编码会降低代码的灵活性。您可以通过将超原型分配给 Sub
的属性来避免这种情况:
Sub
.
_super
=
Super
.
prototype
;
然后调用超构造函数和超方法如下所示
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Sub
.
_super
.
constructor
.
call
(
this
,
prop1
,
prop2
);
this
.
prop3
=
prop3
;
this
.
prop4
=
prop4
;
}
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Sub
.
_super
.
methodB
.
call
(
this
,
x
,
y
);
return
this
.
prop3
+
' '
+
superResult
;
}
设置 Sub._super
通常由一个实用函数来处理,该函数还将子原型连接到超原型。例如
function
subclasses
(
SubC
,
SuperC
)
{
var
subProto
=
Object
.
create
(
SuperC
.
prototype
);
// Save `constructor` and, possibly, other methods
copyOwnPropertiesFrom
(
subProto
,
SubC
.
prototype
);
SubC
.
prototype
=
subProto
;
SubC
.
_super
=
SuperC
.
prototype
;
};
此代码使用辅助函数 copyOwnPropertiesFrom()
,该函数在 复制对象 中显示和解释。
将“子类”读作动词:SubC
子类化 SuperC
。这样的实用函数可以减轻创建子构造函数的痛苦:需要手动执行的操作更少,并且永远不会重复提及超构造函数的名称。以下示例演示了它如何简化代码。
作为一个具体示例,假设 构造函数 Person
已经存在:
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
.
describe
=
function
()
{
return
'Person called '
+
this
.
name
;
};
我们现在想创建构造函数 Employee
作为 Person
的子构造函数。我们手动执行此操作,如下所示
function
Employee
(
name
,
title
)
{
Person
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
=
Object
.
create
(
Person
.
prototype
);
Employee
.
prototype
.
constructor
=
Employee
;
Employee
.
prototype
.
describe
=
function
()
{
return
Person
.
prototype
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
以下是交互过程
> var jane = new Employee('Jane', 'CTO'); > jane.describe() Person called Jane (CTO) > jane instanceof Employee true > jane instanceof Person true
上一节中的实用函数 subclasses()
使 Employee
的代码稍微简单一些,并避免了对超构造函数 Person
进行硬编码
function
Employee
(
name
,
title
)
{
Employee
.
_super
.
constructor
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
.
describe
=
function
()
{
return
Employee
.
_super
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
subclasses
(
Employee
,
Person
);
内置构造函数使用本节中描述的相同子类化方法。 例如,Array
是 Object
的子构造函数。因此,Array
实例的原型链如下所示:
> var p = Object.getPrototypeOf > p([]) === Array.prototype true > p(p([])) === Object.prototype true > p(p(p([]))) === null true
在 ECMAScript 5 和 Object.create()
之前,一种常用的解决方案是通过调用超构造函数来创建子原型:
Sub
.
prototype
=
new
Super
();
// Don’t do this
在 ECMAScript 5 下不建议这样做。原型将具有 Super
的所有实例属性,而这些属性对它没有用处。因此,最好使用前面提到的模式(涉及 Object.create()
)。
几乎所有对象在其 原型链中都有 Object.prototype
:
> Object.prototype.isPrototypeOf({}) true > Object.prototype.isPrototypeOf([]) true > Object.prototype.isPrototypeOf(/xyz/) true
以下小节描述了 Object.prototype
为其原型提供的 方法。
以下两种 方法用于将对象转换为原始值:
Object.prototype.toString()
返回对象的字符串表示形式 :
> ({ first: 'John', last: 'Doe' }.toString()) '[object Object]' > [ 'a', 'b', 'c' ].toString() 'a,b,c'
Object.prototype.valueOf()
这是将对象转换为数字的首选方法。默认实现返回 this
> var obj = {}; > obj.valueOf() === obj true
valueOf
被包装器构造函数覆盖以返回包装的原始值
> new Number(7).valueOf() 7
转换为数字和字符串(无论是隐式还是显式)都建立在转换为原始值的基础上(有关详细信息,请参阅 算法:ToPrimitive() - 将值转换为原始值)。这就是为什么您可以使用前面提到的两种方法来配置这些转换。转换为数字时首选 valueOf()
> 3 * { valueOf: function () { return 5 } } 15
转换为字符串时首选 toString()
> String({ toString: function () { return 'ME' } }) 'Result: ME'
转换为布尔值是不可配置的;对象始终被认为是 true
(请参阅 转换为布尔值)。
此方法 返回对象的特定于区域设置的字符串表示形式。默认实现调用 toString()
。大多数引擎对此方法的支持都不止于此。但是,ECMAScript 国际化 API(请参阅 ECMAScript 国际化 API),它得到许多现代引擎的支持,会覆盖它以用于多个内置构造函数。
以下方法有助于原型继承 和属性:
Object.prototype.isPrototypeOf(obj)
如果接收器是 obj
的原型链的一部分,则返回 true
> var proto = { }; > var obj = Object.create(proto); > proto.isPrototypeOf(obj) true > obj.isPrototypeOf(obj) false
Object.prototype.hasOwnProperty(key)
如果 this
拥有 其键为 key
的属性,则返回 true
。“拥有”表示该属性存在于对象本身中,而不是存在于其原型之一中。
您通常应该以通用方式(而不是直接)调用此方法,尤其是在您静态不知道其属性的对象上。原因和方式在 属性的迭代和检测 中进行了解释
> var proto = { foo: 'abc' }; > var obj = Object.create(proto); > obj.bar = 'def'; > Object.prototype.hasOwnProperty.call(obj, 'foo') false > Object.prototype.hasOwnProperty.call(obj, 'bar') true
Object.prototype.propertyIsEnumerable(propKey)
如果接收器具有键为 propKey
的属性,则返回 true
,该属性 是可枚举的,否则返回 false
:
> var obj = { foo: 'abc' }; > obj.propertyIsEnumerable('foo') true > obj.propertyIsEnumerable('toString') false > obj.propertyIsEnumerable('unknown') false
有时,实例原型 具有对更多对象有用的方法,而不仅仅是从它们继承的对象。本节介绍如何在不继承原型的情况下使用原型的方法。例如,实例原型 Wine.prototype
具有方法 incAge()
:
function
Wine
(
age
)
{
this
.
age
=
age
;
}
Wine
.
prototype
.
incAge
=
function
(
years
)
{
this
.
age
+=
years
;
}
交互如下
> var chablis = new Wine(3); > chablis.incAge(1); > chablis.age 4
方法 incAge()
适用于任何具有属性 age
的对象。我们如何在不是 Wine
实例的对象上调用它?让我们看一下前面的方法调用
chablis
.
incAge
(
1
)
实际上有两个参数
chablis
是方法调用的接收器,通过 this
传递给 incAge
。1
是一个参数,通过 years
传递给 incAge
。我们不能用任意对象替换前者 - 接收器必须是 Wine
的实例。否则,将找不到方法 incAge
。但是,前面的方法调用等效于(请参阅 在设置 this 时调用函数:call()、apply() 和 bind())
Wine
.
prototype
.
incAge
.
call
(
chablis
,
1
)
使用前面的模式,我们可以使一个对象成为接收器(call
的第一个参数),它不是 Wine
的实例,因为接收器不用于查找方法 Wine.prototype.incAge
。在以下示例中,我们将方法 incAge()
应用于对象 john
> var john = { age: 51 }; > Wine.prototype.incAge.call(john, 3) > john.age 54
可以以这种方式使用的函数称为 泛型方法;它必须准备好 this
不是“其”构造函数的实例。因此,并非所有方法都是泛型的;ECMAScript 语言规范明确规定了哪些方法是泛型的(请参阅 所有泛型方法的列表)。
调用方法 非常冗长:
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
您可以通过由空对象字面量 {}
创建的 Object
实例访问 hasOwnProperty
来缩短此过程
{}.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
同样,以下两个表达式是等效的
Array
.
prototype
.
join
.
call
(
str
,
'-'
)
[].
join
.
call
(
str
,
'-'
)
这种模式的优点是不那么冗长。但它也不那么容易理解。性能应该不是问题(至少从长远来看),因为引擎可以静态确定字面量不应该创建对象。
以下是一些 正在使用的泛型方法的示例:
使用 apply()
(请参阅 Function.prototype.apply(thisValue, argArray))推送数组(而不是单个元素;请参阅 添加和删除元素(破坏性))
> var arr1 = [ 'a', 'b' ]; > var arr2 = [ 'c', 'd' ]; > [].push.apply(arr1, arr2) 4 > arr1 [ 'a', 'b', 'c', 'd' ]
此示例是关于将数组转换为参数,而不是关于从另一个构造函数中借用方法。
将数组方法 join()
应用于字符串(它不是数组)
> Array.prototype.join.call('abc', '-') 'a-b-c'
将数组方法 map()
应用于字符串:[17]
> [].map.call('abc', function (x) { return x.toUpperCase() }) [ 'A', 'B', 'C' ]
以泛型方式使用 map()
比使用 split('')
更有效,后者会创建一个中间数组
> 'abc'.split('').map(function (x) { return x.toUpperCase() }) [ 'A', 'B', 'C' ]
将字符串方法应用于非字符串。toUpperCase()
将接收器转换为字符串并对结果进行大写
> String.prototype.toUpperCase.call(true) 'TRUE' > String.prototype.toUpperCase.call(['a','b','c']) 'A,B,C'
在普通对象上使用泛型数组方法可以让您深入了解它们的工作原理
在伪数组上调用数组方法
> var fakeArray = { 0: 'a', 1: 'b', length: 2 }; > Array.prototype.join.call(fakeArray, '-') 'a-b'
查看数组方法如何转换它视为数组的对象
> var obj = {}; > Array.prototype.push.call(obj, 'hello'); 1 > obj { '0': 'hello', length: 1 }
JavaScript 中有一些对象感觉像数组,但实际上不是。 这意味着,虽然它们具有索引访问权限和 length
属性,但它们没有任何数组方法(forEach()
、push
、concat()
等)。这很不幸,但正如我们将看到的,泛型数组方法提供了一种解决方法。类数组对象的示例包括:
特殊变量 arguments
(请参阅 按索引获取所有参数:特殊变量 arguments),它是一个重要的类数组对象,因为它是 JavaScript 的基本组成部分。arguments
看起来像一个数组
> function args() { return arguments } > var arrayLike = args('a', 'b'); > arrayLike[0] 'a' > arrayLike.length 2
但是没有可用的数组方法
> arrayLike.join('-') TypeError: object has no method 'join'
这是因为 arrayLike
不是 Array
的实例(并且 Array.prototype
不在原型链中)
> arrayLike instanceof Array false
浏览器 DOM 节点列表,由 document.getElementsBy*()
(例如,getElementsByTagName()
)、document.forms
等返回
> var elts = document.getElementsByTagName('h3'); > elts.length 3 > elts instanceof Array false
字符串,也是类数组的
> 'abc'[1] 'b' > 'abc'.length 3
术语 类数组 也可以看作是泛型数组方法和对象之间的契约。对象必须满足某些要求;否则,这些方法将无法在它们上工作。要求是
类数组对象的元素必须可以通过方括号和从 0 开始的整数索引进行访问。所有方法都需要读取访问权限,而某些方法还需要写入访问权限。请注意,所有对象都支持这种索引:方括号中的索引将转换为字符串,并用作查找属性值的键
> var obj = { '0': 'abc' }; > obj[0] 'abc'
length
属性,其值是其元素的数量。某些方法要求 length
是可变的(例如,reverse()
)。长度不可变的值(例如,字符串)不能与这些方法一起使用。以下模式 对于使用类数组对象很有用:
将类数组对象转换为数组
var
arr
=
Array
.
prototype
.
slice
.
call
(
arguments
);
不带任何参数的方法 slice()
(请参阅 连接、切片、连接(非破坏性))会创建类数组接收器的副本
var
copy
=
[
'a'
,
'b'
].
slice
();
要迭代类数组对象的所有元素,可以使用简单的 for
循环
function
logArgs
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
i
+
'. '
+
arguments
[
i
]);
}
}
但您也可以借用 Array.prototype.forEach()
function
logArgs
()
{
Array
.
prototype
.
forEach
.
call
(
arguments
,
function
(
elem
,
i
)
{
console
.
log
(
i
+
'. '
+
elem
);
});
}
在这两种情况下,交互如下所示
> logArgs('hello', 'world'); 0. hello 1. world
以下 列表包含 ECMAScript 语言规范中提到的所有泛型方法:
Array.prototype
(参见 数组原型方法)
concat
every
filter
forEach
indexOf
join
lastIndexOf
map
pop
push
reduce
reduceRight
reverse
shift
slice
some
sort
splice
toLocaleString
toString
unshift
Date.prototype
(参见 日期原型方法)
toJSON
Object.prototype
(参见 所有对象的的方法)
Object
方法自动是泛型的——它们必须适用于所有对象。)
String.prototype
(参见 字符串原型方法)
charAt
charCodeAt
concat
indexOf
lastIndexOf
localeCompare
match
replace
search
slice
split
substring
toLocaleLowerCase
toLocaleUpperCase
toLowerCase
toUpperCase
trim
由于 JavaScript 没有用于映射的内置数据结构,因此对象通常用作从字符串到值的映射。唉,这比看起来更容易出错。本节解释了此任务中涉及的三个陷阱。
读取属性的操作可以 分为两种:
读取对象作为映射的条目时,需要在这些操作之间仔细选择。要了解原因,请考虑以下示例
var
proto
=
{
protoProp
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
.
ownProp
=
'b'
;
obj
是一个对象,它有一个自有属性,其原型是 proto
,它也有一个自有属性。 proto
的原型是 Object.prototype
,就像所有由对象字面量创建的对象一样。因此,obj
从 proto
和 Object.prototype
继承属性。
我们希望将 obj
解释为具有单个条目的映射
ownProp: 'b'
也就是说,我们希望忽略继承的属性,只考虑自有属性。让我们看看哪些读取操作以这种方式解释 obj
,哪些不解释。请注意,对于作为映射的对象,我们通常希望使用存储在变量中的任意属性键。这排除了点符号。
in
运算符检查对象 是否具有给定键的属性,但它会考虑继承的属性:
> 'ownProp' in obj // ok true > 'unknown' in obj // ok false > 'toString' in obj // wrong, inherited from Object.prototype true > 'protoProp' in obj // wrong, inherited from proto true
我们需要检查以忽略继承的属性。 hasOwnProperty()
可以满足我们的需求
> obj.hasOwnProperty('ownProp') // ok true > obj.hasOwnProperty('unknown') // ok false > obj.hasOwnProperty('toString') // ok false > obj.hasOwnProperty('protoProp') // ok false
在遵守我们将 obj
解释为映射的同时,我们可以使用哪些操作来查找 obj
的所有键? for-in
看起来可能有效。但是,唉,它不起作用:
> for (propKey in obj) console.log(propKey) ownProp protoProp
它考虑了继承的可枚举属性。这里没有显示 Object.prototype
的属性的原因是它们都是不可枚举的。
相比之下,Object.keys()
仅列出自有属性
> Object.keys(obj) [ 'ownProp' ]
此方法仅返回可枚举的自有属性;ownProp
是通过赋值添加的,因此默认情况下是可枚举的。如果要列出所有自有属性,则需要使用 Object.getOwnPropertyNames()
。
对于读取属性的值,我们只能在点运算符和括号运算符之间进行选择。我们不能使用前者,因为我们有存储在变量中的任意键。这给我们留下了括号运算符,它会考虑继承的属性:
> obj['toString'] [Function: toString]
这不是我们想要的。没有用于仅读取自有属性的内置操作,但您可以轻松地自己实现一个
function
getOwnProperty
(
obj
,
propKey
)
{
// Using hasOwnProperty() in this manner is problematic
// (explained and fixed later)
return
(
obj
.
hasOwnProperty
(
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
使用该函数,将忽略继承的属性 toString
> getOwnProperty(obj, 'toString') undefined
函数 getOwnProperty()
调用了 obj
上的方法 hasOwnProperty()
。通常,这很好
> getOwnProperty({ foo: 123 }, 'foo') 123
但是,如果向 obj
添加一个键为 hasOwnProperty
的属性,则该属性将覆盖方法 Object.prototype.hasOwnProperty()
,并且 getOwnProperty()
将停止工作
> getOwnProperty({ hasOwnProperty: 123 }, 'foo') TypeError: Property 'hasOwnProperty' is not a function
您可以通过直接引用 hasOwnProperty()
来解决此问题。这避免了通过 obj
来找到它
function
getOwnProperty
(
obj
,
propKey
)
{
return
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
我们已经以泛型方式调用了 hasOwnProperty()
(参见 泛型方法:从原型借用方法)。
在许多 JavaScript 引擎中,属性 __proto__
(参见 特殊属性 __proto__)很特殊:获取它会检索对象的原型,而设置它会更改对象的原型。 这就是对象无法在键为 '__proto__'
的属性中存储映射数据的原因。如果要允许映射键 '__proto__'
,则必须在将其用作属性键之前对其进行转义:
function
get
(
obj
,
key
)
{
return
obj
[
escapeKey
(
key
)];
}
function
set
(
obj
,
key
,
value
)
{
obj
[
escapeKey
(
key
)]
=
value
;
}
// Similar: checking if key exists, deleting an entry
function
escapeKey
(
key
)
{
if
(
key
.
indexOf
(
'__proto__'
)
===
0
)
{
// (1)
return
key
+
'%'
;
}
else
{
return
key
;
}
}
我们还需要转义 '__proto__'
(等)的转义版本以避免冲突;也就是说,如果我们将键 '__proto__'
转义为 '__proto__%'
,那么我们还需要转义键 '__proto__%'
,这样它就不会替换 '__proto__'
条目。这就是第 (1) 行发生的事情。
Mark S. Miller 在 一封电子邮件 中提到了这个陷阱的现实意义
认为这个练习是学术性的,不会出现在真实的系统中?正如在一个支持线程中观察到的那样,直到最近,在所有非 IE 浏览器上,如果您在新 Google 文档的开头键入“__proto__”,您的 Google 文档就会挂起。这被追溯到将对象错误地用作字符串映射。
您可以创建一个没有原型的对象,如下所示:
var
dict
=
Object
.
create
(
null
);
这样的对象比普通对象是更好的映射(字典),这就是为什么这种模式有时被称为 dict 模式(dict 代表 dictionary,即字典)的原因。让我们首先检查普通对象,然后找出为什么无原型对象是更好的映射。
通常,您在 JavaScript 中创建的每个对象在其原型链中至少都有 Object.prototype
。 Object.prototype
的原型是 null
,所以大多数原型链都在这里结束
> Object.getPrototypeOf({}) === Object.prototype true > Object.getPrototypeOf(Object.prototype) null
无原型对象作为映射有两个优点
in
运算符来检测属性是否存在,并使用括号来读取属性。 __proto__
将被禁用。在 ECMAScript 6 中,如果 Object.prototype
不在对象的原型链中,则特殊属性 __proto__
将被禁用。您可以预期 JavaScript 引擎会慢慢迁移到这种行为,但它现在还不常见。唯一的缺点是您将失去 Object.prototype
提供的服务。例如,dict 对象不能再自动转换为字符串
> console.log('Result: '+obj) TypeError: Cannot convert object to primitive value
但这并不是一个真正的缺点,因为直接在 dict 对象上调用方法是不安全的。
将 dict 模式用于快速破解和作为库的基础。在(非库)生产代码中,库更可取,因为您可以确保避免所有陷阱。下一节列出了一些这样的库。
将对象用作映射有很多应用程序。 如果所有属性键都是静态已知的(在开发时),那么您只需要确保忽略继承并仅查看自有属性。如果可以使用任意键,则应求助于库以避免本节中提到的陷阱。以下是两个示例:
对象字面量(参见 对象字面量)
var
jane
=
{
name
:
'Jane'
,
'not an identifier'
:
123
,
describe
:
function
()
{
// method
return
'Person named '
+
this
.
name
;
},
};
// Call a method:
console
.
log
(
jane
.
describe
());
// Person named Jane
点运算符 (.)(参见 点运算符 (.):通过固定键访问属性)
obj
.
propKey
obj
.
propKey
=
value
delete
obj
.
propKey
括号运算符 ([])(参见 括号运算符 ([]):通过计算键访问属性)
obj
[
'propKey'
]
obj
[
'propKey'
]
=
value
delete
obj
[
'propKey'
]
获取和设置原型(参见 获取和设置原型)
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
getPrototypeOf
(
obj
)
Object
.
keys
(
obj
)
Object
.
getOwnPropertyNames
(
obj
)
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
propKey
in
obj
通过描述符获取和定义属性 (参见 通过描述符获取和定义属性)
Object
.
defineProperty
(
obj
,
propKey
,
propDesc
)
Object
.
defineProperties
(
obj
,
propDescObj
)
Object
.
getOwnPropertyDescriptor
(
obj
,
propKey
)
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
preventExtensions
(
obj
)
Object
.
isExtensible
(
obj
)
Object
.
seal
(
obj
)
Object
.
isSealed
(
obj
)
Object
.
freeze
(
obj
)
Object
.
isFrozen
(
obj
)
Object
.
prototype
.
toString
()
Object
.
prototype
.
valueOf
()
Object
.
prototype
.
toLocaleString
()
Object
.
prototype
.
isPrototypeOf
(
obj
)
Object
.
prototype
.
hasOwnProperty
(
key
)
Object
.
prototype
.
propertyIsEnumerable
(
propKey
)