编写高质量JS代码的68个有效方法(十)
No.46、使用数组而不要使用字典来存储有序集合
Tips:
- 使用for...in 循环来枚举对象属性应当与顺序无关
- 如果聚集运算字典中的数据,确保聚集操作与顺序无关
- 使用数组而不是字典来存储有序集合
由于标准允许JavaScript引擎自由选择顺序,那么如果用字典存储有序数据,就会导致兼容性问题。
No.47、绝不要在Object.prototype中增加可枚举的属性
Tips:
- 避免在Object.prototype中增加属性
- 考虑编写一个函数代理Object.prototype方法
- 如果你是在需要在prototype中增加属性,请使用ES5中的Object.defineProperty方法将它们定义为不可枚举的属性
for...in
循环非常便利,但是容易受到原型污染。如果在Object.prorotype中增加可枚举属性的话,将会导致大多数for...in
循环受到污染。
如果是在是要在Object.prototype上定义属性的话,可以使用如下代码:
Object.defineProperty(Object.prototype, 'allKeys', {
value: function(){
var arr = [];
for(var key in this){
arr.push(key);
}
return arr;
},
writable: true,
enumerable: false, //设置属性为不可枚举
configurable: true
});
测试代码:
var obj = {a: 1, b: 2};
console.log(obj.allKeys()); // ['a', 'b']
No.48、避免在枚举期间修改对象
Tips:
- 当使用
for...in
循环枚举一个对象的属性时,确保不要修改该对象 - 当迭代一个对象时,如果该对象的内容可能会在循环期间被改变,应该使用while循环或经典的for循环来代替
for...in
- 为了在不断变化的数据结构中能够预测枚举,考虑使用一个有序的数据结构,例如数组,而不要使用字典
在大部分编译型语言中,如果在迭代时修改对象属性,是会出现编译错误的。在js中,没有这样的编译机制,但是也尽量保证不要修改迭代对象。
如果在被枚举时添加了新对象,并不一定能保证新添加的对象能被访问到:
var obj = {a: 1, b: 2};
for(var p in obj){
console.log(p);
obj[p + '1'] = obj[p] + 1;
}
遇到这样的场景,应当使用while和标准的for循环。
No.49、数组迭代要优先使用for循环而不是for...in
循环
Tips:
- 迭代数组的索引属性应当总是使用for循环而不是
for...in
循环 - 考虑在循环之前将数组的长度存储在一个局部变量中以避免重新计算数组长度
猜测下面一段代码的结果?
var arr = [5, 6, 8, 10, 9];
var sum = 0;
for(var a in arr){
sum += a;
}
console.log(sum);
要达到正确的结果,那么应该使用for循环
var arr = [5, 6, 8, 10, 9];
var sum = 0;
for(var i = 0, len = arr.length; i < len; i++){
sum += arr[i];
}
console.log(sum); //38
再看一个比较极端的例子:
var arr = [5, 6, 8, 10, 9];
arr.len = 4;
for(var p in arr){
console.log(p);
}
这个时候用for...in
,完全是达不到预期效果的
再来看一个对于数组长度缓存的测试代码:
var count = 0;
console.time('t1');
while(count < 10000){
var arr = [5, 6, 8, 10, 9];
var sum = 0;
count++;
for(var i = 0, len = arr.length; i < len; i++){
sum += arr[i];
}
}
console.timeEnd('t1');
count = 0;
console.time('t2');
while(count < 10000){
var arr = [5, 6, 8, 10, 9];
var sum = 0;
count++;
for(var i = 0; i < arr.length; i++){
sum += arr[i];
}
}
console.timeEnd('t2');
结果,请自行复制代码执行。。。
No.50、迭代方法优于循环
Tips:
- 使用迭代方法(如Array.prototype.forEach和Array.prototype.map)替换for循环使得代码更可读,并且避免了重复循环控制逻辑
- 使用自定义的迭代函数来抽象未被标准库支持的常见循环模式
- 在需要提前终止循环的情况下,仍然推荐使用传统的循环。另外some和every方法也可用于提前退出
在使用循环的时候,在确定循环的终止条件时容易引入一些简单的错误:
for(var i = 0; i <= n; i++){}
for(var i = 1; i< n; i++){}
比较庆幸的是,闭包是一种为这些模式建立迭代抽象方便的、富有表现力的手法。
我们可以用以下代码来代替:
var arr = [1, 2, 3];
arr.forEach(function(v, i){
console.log(v);
});
如果要创建新数组,那么可以用以下方式:
var arr = [1, 2, 3];
var arrNew = [];
//方式一
arr.forEach(function(v, i){
arrNew.push(v);
});
//方式二
for(var i = 0, len = arr.length; i < len; i++){
arrNew.push(arr[i]);
}
为了简化这种普遍操作,ES5中引入了Array.prototype.map方法:
var arr = [1, 2, 3];
var arrNew = arr.map(function(v){
return v;
});
同样,如果想提取满足条件的元素,ES5也提供了filter方法:
var arr = [1, 2, 3];
var arrNew = arr.filter(function(v){
return v > 1;
});
console.log(arrNew);
在ES5中,针对数组也提供了some和every ,可以用来终止循环,但是实际意义等同于C#的Linq方法All和Any:
var arr = [1, 2, 3];
//数组元素有一个>1就返回true,并终止循环
var b = arr.some(function(a){
return a>1;
});
console.log(b); //true
//数组元素每个都<3,则返回true,否则返回false,并提前终止循环
b = arr.every(function(a){
return a<3;
});
console.log(b); //false