CodeReview问题与总结第一期

常用的遍历方法有哪些?每种适用于什么场景,各有什么优缺点?

常用遍历方法

for循环

语法:

for (语句 1; 语句 2; 语句 3) { 被执行的代码块 }
语句 1 在循环(代码块)开始前执行
语句 2 定义运行循环(代码块)的条件
语句 3 在循环(代码块)已被执行之后执行

for…in

for...in语句以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。

语法:

for (variable in object) {…}
variable:在每次迭代时,将不同的属性名分配给变量。
object:被迭代枚举其属性的对象。

注意:for...in不应该用于迭代一个 Array

  • for...in不仅可以遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。(即:for...in循环语句将返回所有可枚举属性,包括非整数类型的名称和继承的那些。)
  • 不能保证for...in将以任何特定的顺序返回索引。

应用:

1
2
3
4
5
var obj = {a:1, b:2, c:3};

for (var prop in obj) {
console.log("obj." + prop + " = " + obj[prop]);
}

for…of

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

语法:

for (variable of iterable) {
//statements
}
variable:在每次迭代中,将不同属性的值分配给变量。
iterable:可枚举其枚举属性的对象。

注意:可以使用break跳出循环
应用:

1
2
3
4
5
6
7
8
9
10
let iterable = [10, 20, 30];

for(let a of iterable){
if(a > 20) {
break
}
console.log(a)
}
// 10
// 20

forEach()

forEach方法对数组的每个元素执行一次提供的函数。
语法:

array.forEach(callback(currentValue, index, array){
//do something
}, this)
array.forEach(callback[, thisArg])

参数:

currentValue(当前值):数组中正在处理的当前元素。
index(索引):数组中正在处理的当前元素的索引。
array:forEach()方法正在操作的数组。
thisArg:可选参数。当执行回调 函数时用作this的值(参考对象)。

应用:

1
2
3
4
//打印数组
const arr = ['a', 'b', 'c'];

arr.forEach( element => console.log(element));

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//复制对象
function copy(obj) {
var copy = Object.create(Object.getPrototypeOf(obj));
var propNames = Object.getOwnPropertyNames(obj);

propNames.forEach(function(name) {
var desc = Object.getOwnPropertyDescriptor(obj, name);
Object.defineProperty(copy, name, desc);
});

return copy;
}

var obj1 = { a: 1, b: 2 };
var obj2 = copy(obj1);

缺点:
不能中途跳出循环,如果数组在迭代时被修改了,则其他元素会被跳过。

1
2
3
4
5
6
7
8
9
10
var words = ["one", "two", "three", "four"];
words.forEach(function(word) {
console.log(word);
if (word === "two") {
words.shift();
}
});
// one
// two
// four

map()

map方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
语法:

arr.map(function callback(currentValue, index, array) {
// Return element for new_array
}[, thisArg])

参数:

callback: 生成新数组元素的函数,使用三个参数:
currentValue callback: 的第一个参数,数组中正在处理的当前元素。
index callback: 的第二个参数,数组中正在处理的当前元素的索引。
array callback: 的第三个参数,map方法被调用的数组。
thisArg: 可选的。执行 callback 函数时 使用的this 值。

返回值:
一个新数组,每个元素都是回调函数的结果。

应用:

1
2
3
4
//重新格式化数组
var numbers = [1, 4, 9];
var roots = numbers.map(Math.sqrt);
// roots的值为[1, 2, 3], numbers的值仍为[1, 4, 9]

缺点: 不能中途跳出循环。

性能测试

测试环境:node v9.8.0
测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
测试环境:node v9.8.0

JavaScript
var number = 100; // array大小
var array = [];
for (let i = 0; i < number; i++) {
array[i] = i + 1;
}
var len = array.length;

console.log('数组长度为:', number)
// 正常for循环
console.time('normal for');
for (let i = 0; i < len; i++) {
array[i] + 1;
}

console.timeEnd('normal for');

// 倒序for循环
console.time('reverse for');
for (let i = len - 1; i--;) {
array[i] + 1;
}
console.timeEnd('reverse for');

// while循环
console.time('while');
let i = 0;
while (i < len) {
array[i] + 1;
i++;
}
console.timeEnd('while');

// for-in循环
console.time('for-in');
for (let i in array) {
array[i] + 1;
}
console.timeEnd('for-in');

// for each 循环
console.time("for each");
array.forEach(e => e + 1);
console.timeEnd("for each")

// map循环
console.time("map");
array.map(e => e + 1);
console.timeEnd("map")

测试结果(ms为单位):

数组长度 100 1000 10000 100000 1000000
普通for循环 0.101 0.125 0.343 0.224 4.778
倒序for循环 0.009 0.030 0.255 8.606 4.391
while循环 0.007 0.028 0.24 23.957 4.445
for-in 0.032 0.351 2.466 22.203 163.261
for-each 0.030 0.054 0.296 1.986 14.921
map 0.029 0.055 0.323 19.905 189.387

思考:我们选择循环的多个角度——业务需要、代码可读性、性能。
小结: 多数情况下前端不会涉及到大数量级的运算,如果有,那么应该考虑业务代码的正确性或者是否该将运算交到后端处理,在小数据量的运算中优先考虑代码的可读性、可维护性:采用for-eachmap方法。

延伸阅读: 为什么for-in效率相比而言这么低?
for...in 一般是用在对象属性名的遍历上的,由于每次迭代操作会同时搜索实例本身的属性以及原型链上的属性,所以效率肯定低下。for-in实际上效率是最低的。这是因为 for...in有一些特殊的要求,具体包括:

  1. 遍历所有属性,不仅是 ownproperties 也包括原型链上的所有属性。
  2. 忽略 enumerable(可枚举) 为 false 的属性。
  3. 必须按特定顺序遍历,先遍历所有数字键,然后按照创建属性的顺序遍历剩下的。

相关链接:
吹毛求疵的追求优雅高性能JavaScript
Array-MDN

Vue是如何实现响应式的?监测变化时应注意什么?

如何追踪变化

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

vue

监测变化时应注意:

  1. Vue 不能检测到对象属性的添加或删除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var vm = new Vue({
    data: {
    a: 1
    }
    })
    // `vm.a` 现在是响应式的

    vm.b = 2
    // `vm.b` 不是响应式的
  2. 不能检测以下变动的数组:

  • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

相关链接:
Vue细节与最佳实践
Vue对象更改检测注意事项

0%