本文详细对比JavaScript中for循环与for...in循环的使用场景与差异。重点解析在遍历HTMLCollection等实时DOM集合时,缓存数组长度对提升循环性能的关键作用。同时深入探讨for...in循环遍历原型链属性的潜在风险,并提供使用hasOwnProperty()及Object.prototype.hasOwnProperty.call()进行安全过滤的最佳实践,帮助开发者写出更高效、安全的迭代代码。
JavaScript迭代方式概述
在JavaScript开发中,迭代对象通常有两种主要方式:
- 基础的for循环;
- 以及for...in循环;
for循环的性能优化策略
一、for循环
这种基础写法存在以下不足:
主要问题在于每次迭代时都需要重新获取数组的长度; 此外,循环的终止条件需要明确指定; 在for循环中,你可以遍历数组或类数组对象(如arguments和HTMLCollection对象)。常见的循环写法如下:
// 次佳的循环
for (var i = 0; i < myarray.length; i++) {
// 使用myarray[i]做点什么
}
上述写法的缺点在于,每次循环迭代时都要重新计算数组长度。这会降低代码执行性能,特别是当myarray不是普通数组,而是HTMLCollection对象时,性能损耗更为明显。
HTMLCollection是指由DOM方法返回的集合对象,例如:
document.getElementsByName() document.getElementsByClassName() document.getElementsByTagName()
此外,还有一些在DOM标准制定前引入并沿用至今的HTMLCollections,包括:
document.images: 页面中所有图片元素 document.links : 所有超链接元素 document.forms : 页面中所有表单 document.forms[0].elements : 第一个表单内的所有输入域
这些集合的麻烦之处在于它们会实时查询底层文档(HTML页面)。这意味着每次你访问集合的length属性时,都会触发一次对DOM的实时查询,而DOM操作通常开销较大。
因此,在循环获取值时,缓存数组(或集合)的长度是一种更优的做法,如下面代码所示:
for (var i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做点什么
}
通过这种方式,在循环过程中,你只需要检索一次长度值。
在所有浏览器中,循环时缓存HTMLCollections的长度都能显著提升速度,性能提升幅度在2倍(Safari3)到190倍(IE7)之间。//此数据貌似很老
需要注意的是,当你明确需要在循环中修改集合(例如动态添加更多DOM元素)时,你可能希望长度值能实时更新,而不是使用常量。
结合单var声明形式,你可以将变量提取到循环外部,如下所示:
function looper() {
var i = 0,
max,
myarray = [];
// ...
for (i = 0, max = myarray.length; i < max; i++) {
// 使用myarray[i]做点什么
}
}
这种形式的好处是保持一致性,因为你遵循了单var声明风格。其不足在于代码重构时,复制和粘贴整个循环会变得困难。例如,当你将一个函数中的循环复制到另一个函数时,你必须确保能将i和max引入新函数(如果在新函数中未使用,则可能需要从原函数中删除它们)。
最后,需要对循环进行的调整是使用以下表达式之一替换i++。
i = i + 1
i += 1
JSLint提示这样做,原因是++和--促进了“过分棘手(excessive trickiness)”。如果你直接忽略它,JSLint的plusplus选项将设为false(默认值为default)。
两种变化形式:
- 减少一个变量(无需max)
- 递减至0通常更快,因为与0进行比较比与数组长度或其他非0值比较更有效率01
//第一种变化的形式:
var i, myarray = [];
for (i = myarray.length; i–-;) {
// 使用myarray[i]做点什么
}
//第二种使用while循环:
var myarray = [],
i = myarray.length;
while (i–-) {
// 使用myarray[i]做点什么
}
这些微小的改进仅体现在性能上,此外JSLint会对使用i--加以抱怨。
for...in循环与原型链陷阱
二、for …in 循环—也被称为“枚举”
for …in 循环常用于迭代对象的属性或数组的每个元素。在for…in循环中,循环计数器是字符串,而不是数字,它包含当前属性的名称或当前数组元素的索引。下面直接给出几个例子:
当遍历一个对象时,变量 i 即循环计数器代表对象的属性名:
//使用for..in循环遍历对象属性
varperson={
name: "Admin",
age: 21,
address:"shandong"
};
for(var i in person){
console.log(i);
}
执行结果为:
name
age
address
当遍历一个数组时,变量 i 即循环计数器代表当前数组元素的索引:
//使用for..in循环遍历数组
vararray = ["admin","manager","db"]
for(vari in array){
console.log(i);
}
执行结果:
0
1
2
然而,现在看来for .. in循环似乎挺好用的,但别高兴得太早,请看下面的例子:
var array =["admin","manager","db"];
//给Array的原型添加一个name属性
Array.prototype.name= "zhangsan";
for(var i in array){
alert(array[i]);
}
运行结果:
admin
manager
db
zhangsan 咦,奇怪了,怎么平白无故地冒出来一个zhangsan 现在,再看看使用for循环会怎样?
vararray = ["admin","manager","db"];
//给Array的原型添加一个name属性
Array.prototype.name = "zhangsan";
for(var i =0 ; i<array.length; i++){
alert(array[i]);
};
运行结果:
admin
manager
db 哦,现在明白了,for..in循环会把某个类型的原型(prototype)中的方法与属性也给遍历出来,这可能会导致代码中出现意外的错误。为了避免这个问题,我们可以使用对象的hasOwnProperty()方法来过滤,如果对象的属性或方法是非继承的,那么hasOwnProperty()方法返回true。即这里的检查不涉及从其他对象继承的属性和方法,只会检查在特定对象自身中直接创建的属性。
vararray = ["admin","manager","db"];
Array.prototype.name= "zhangshan";
for(var i in array){
//如果不是该对象自身直接创建的属性(也就是该属//性是原型中的属性),则跳过显示
if(array.hasOwnProperty(i)){
alert(array[i]);
}
}
运行结果:
admin
manager
db 另一种使用hasOwnProperty()的形式是直接引用Object.prototype上的方法,如下所示:
// 对象
var man = {
hands: 2,
legs: 2,
heads: 1
};
for (var i in man) {
if (Object.prototype.hasOwnProperty.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
}
其好处在于,当man对象重新定义hasOwnProperty时,可以避免命名冲突。同时也避免了长属性查找对象的所有方法,你可以使用局部变量“缓存”它。
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
if (hasOwn.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
}
严格来说,不使用hasOwnProperty()并不构成错误。根据任务需求以及你对代码的自信程度,你可以跳过它以换取些许的循环速度提升。但是,当你对当前对象内容(及其原型链)不确定时,添加hasOwnProperty()检查会更加保险。
格式化的变化(这通不过JSLint检测)会直接忽略掉花括号,把if语句放到同一行上。其优点在于循环语句读起来像一个完整的想法(每个元素都有一个自己的属性”X”,使用”X”干点什么):
// 警告: 通不过JSLint检测
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // 过滤
console.log(i, ":", man[i]);
}
以上就是介绍了JavaScript提供的两种方式迭代对象:for循环和for...in循环,希望这篇文章对大家的学习有所帮助。