[翻译]JavaScript秘密花园 - Array, Array Constructor, for in loop, typeof, instanceOf
数组遍历与属性
虽然在 JavaScript 中数组是是对象,但是没有好的理由去使用 `for in` 循环 遍历数组。
相反,有一些好的理由不去使用 for in
遍历数组。
注意: JavaScript 中数组不是 关联数组。 JavaScript 中只有对象 来管理键值的对应关系。但是关联数组是保持顺序的,而对象不是。
由于 for in
循环会枚举原型链上的所有属性,唯一过滤这些属性的方式是使用 `hasOwnProperty` 函数,
因此会比普通的 for
循环慢上好多倍。
遍历(Iteration)
为了达到遍历数组的最佳性能,推荐使用经典的 for
循环。
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
上面代码有一个处理,就是通过 l = list.length
来缓存数组的长度。
虽然 length
是数组的一个属性,但是在每次循环中访问它还是有性能开销。
可能最新的 JavaScript 引擎在这点上做了优化,但是我们没法保证自己的代码是否运行在这些最近的引擎之上。
实际上,不使用缓存数组长度的方式比缓存版本要慢很多。
`length` 属性(The `length` property)
length
属性的 getter 方式会简单的返回数组的长度,而 setter 方式会截断数组。
var foo = [1, 2, 3, 4, 5, 6];
foo.length = 3;
foo; // [1, 2, 3]
foo.length = 6;
foo; // [1, 2, 3]
译者注: 在 Firebug 中查看此时 foo 的值是: [1, 2, 3, undefined, undefined, undefined] 但是这个结果并不准确,如果你在 Chrome 的控制台查看 foo 的结果,你会发现是这样的: [1, 2, 3] 因为在 JavaScript 中 undefined 是一个变量,注意是变量不是关键字,因此上面两个结果的意义是完全不相同的。
// 译者注:为了验证,我们来执行下面代码,看序号 5 是否存在于 foo 中。
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 false
foo[5] = undefined;
5 in foo; // 不管在 Firebug 或者 Chrome 都返回 true
为 length
设置一个更小的值会截断数组,但是增大 length
属性值不会对数组产生影响。
结论(In conclusion)
为了更好的性能,推荐使用普通的 for
循环并缓存数组的 length
属性。
使用 for in
遍历数组被认为是不好的代码习惯并倾向于产生错误和导致性能问题。
Array
构造函数
由于 Array
的构造函数在如何处理参数时有点模棱两可,因此总是推荐使用数组的字面语法 - []
- 来创建数组。
[1, 2, 3]; // 结果: [1, 2, 3]
new Array(1, 2, 3); // 结果: [1, 2, 3]
[3]; // 结果: [3]
new Array(3); // 结果: []
new Array('3') // 结果: ['3']
译者注:这里的模棱两可指的是数组的两种构造函数语法 var arr1 = new Array(arrayLength); var arr2 = new Array(element0, element1, ..., elementN);
// 译者注:因此下面的代码将会使人很迷惑
new Array(3, 4, 5); // 结果: [3, 4, 5]
new Array(3) // 结果: [],此数组长度为 3
由于只有一个参数传递到构造函数中(译者注:指的是 new Array(3);
这种调用方式),并且这个参数是数字,构造函数会返回一个 length
属性被设置为此参数的空数组。
需要特别注意的是,此时只有 length
属性被设置,真正的数组并没有生成。
译者注:在 Firebug 中,你会看到 [undefined, undefined, undefined],这其实是不对的。在上一节有详细的分析。
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, 数组还没有生成
这种优先于设置数组长度属性的做法只在少数几种情况下有用,比如需要循环字符串,可以避免 for
循环的麻烦。
new Array(count + 1).join(stringToRepeat);
// 译者注:new Array(3).join('#') 将会返回 "##"
结论(In conclusion)
应该尽量避免使用数组构造函数创建新数组。推荐使用数组的字面语法。它们更加短小和简洁,因此增加了代码的可读性。
for in
循环
和 in
操作符一样,for in
循环同样在查找对象属性时遍历原型链上的所有属性。
注意:
for in
循环不会遍历那些enumerable
设置为false
的属性;比如数组的length
属性。
// 修改 Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // 输出两个属性:bar 和 moo
}
由于不可能改变 for in
自身的行为,因此有必要过滤出那些不希望出现在循环体中的属性,
这可以通过 Object.prototype
原型上的 `hasOwnProperty` 函数来完成。
注意: 由于
for in
总是要遍历整个原型链,因此如果一个对象的继承层次太深的话会影响性能。
使用 `hasOwnProperty` 过滤(Using `hasOwnProperty` for filtering)
// foo 变量是上例中的
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
这个版本的代码是唯一正确的写法。由于我们使用了 hasOwnProperty
,所以这次只输出 moo
。
如果不使用 hasOwnProperty
,则这段代码在原生对象原型(比如 Object.prototype
)被扩展时可能会出错。
一个广泛使用的类库 Prototype 就扩展了原生的 JavaScript 对象。
因此,但这个类库被包含在页面中时,不使用 hasOwnProperty
过滤的 for in
循环难免会出问题。
最佳实践(Best practices)
推荐总是使用 hasOwnProperty
。不要对代码运行的环境做任何假设,不要假设原生对象是否已经被扩展了。
typeof
操作符
typeof
操作符(和 `instanceof` 一起)或许是 JavaScript 中最大的设计缺陷,
因为几乎不可能从它们那里得到想要的结果。
尽管 instanceof
还有一些极少数的应用场景,typeof
只有一个实际的应用(译者注:这个实际应用是用来检测一个对象是否已经定义或者是否已经赋值),
而这个应用却不是用来检查对象的类型。
注意: 由于
typeof
也可以像函数的语法被调用,比如typeof(obj)
,但这并是一个函数调用。 那两个小括号只是用来计算一个表达式的值,这个返回值会作为typeof
操作符的一个操作数。 实际上不存在名为typeof
的函数。
JavaScript 类型表格(The JavaScript type table)
Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
上面表格中,Type 一列表示 typeof
操作符的运算结果。可以看到,这个值在大多数情况下都返回 "object"。
Class 一列表示对象的内部属性 [[Class]]
的值。
JavaScript 标准文档中定义:
[[Class]]
的值只可能是下面字符串中的一个:Arguments
,Array
,Boolean
,Date
,Error
,Function
,JSON
,Math
,Number
,Object
,RegExp
,String
.
为了获取对象的 [[Class]]
,我们需要使用定义在 Object.prototype
上的方法 toString
。
对象的类定义(The Class of an object)
JavaScript 标准文档只给出了一种获取 [[Class]]
值的方法,那就是使用 Object.prototype.toString
。
function is(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
}
is('String', 'test'); // true
is('String', new String('test')); // true
上面例子中,Object.prototype.toString
方法被调用,this 被设置为了需要获取 [[Class]]
值的对象。
译者注:Object.prototype.toString
返回一种标准格式字符串,所以上例可以通过 slice 截取指定位置的字符串,如下所示:
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(2) // "[object Number]"
ES5 提示: 在 ECMAScript 5 中,为了方便,对
null
和undefined
调用Object.prototype.toString
方法, 其返回值由Object
变成了Null
和Undefined
。
译者注:这种变化可以从 IE8 和 Firefox 4 中看出区别,如下所示:
// IE8
Object.prototype.toString.call(null) // "[object Object]"
Object.prototype.toString.call(undefined) // "[object Object]"
// Firefox 4
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
测试为定义变量(Testing for undefined variables)
typeof foo !== 'undefined'
上面代码会检测 foo
是否已经定义;如果没有定义而直接使用会导致 ReferenceError
的异常。
这是 typeof
唯一有用的地方。
结论(In conclusion)
为了检测一个对象的类型,强烈推荐使用 Object.prototype.toString
方法;
因为这是唯一一个可依赖的方式。正如上面表格所示,typeof
的一些返回值在标准文档中并未定义,
因此不同的引擎实现可能不同。
除非为了检测一个变量是否已经定义,我们应尽量避免使用 typeof
操作符。
instanceof
操作符
instanceof
操作符用来比较两个操作数的构造函数。只有在比较自定义的对象时才有意义。
如果用来比较内置类型,将会和 typeof 操作符 一样用处不大。
比较自定义对象(Comparing custom objects)
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// 如果仅仅设置 Bar.prototype 为函数 Foo 本省,而不是 Foo 构造函数的一个实例
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
`instanceof` 比较内置类型(Using `instanceof` with native types)
new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true
'foo' instanceof String; // false
'foo' instanceof Object; // false
有一点需要注意,instanceof
用来比较属于不同 JavaScript 上下文的对象(比如,浏览器中不同的文档结构)时将会出错,
因为它们的构造函数不会是同一个对象。
结论(In conclusion)
instanceof
操作符应该仅仅用来比较来自同一个 JavaScript 上下文的自定义对象。
正如 `typeof` 操作符一样,任何其它的用法都应该是避免的。