深入理解JavaScript——循环都来这儿

85 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

写这篇文章真的很痛苦,因为我的心已经不在这里。前前后后花了一周时间才撬动键盘

循环集合令

JavaScript 中有各种循环,如 for ,for(reverse),for...in,for...of,forEach,map 等,这些循环各有什么特点呢?

for 循环

这是最常见的循环

for (var i = 0; i < 10; i++) {
    console.log(i)
}

它的特点是最常见,缺点是可读性差

for(reverse) 循环

顾名思义,它是 for 循环的翻转版

for (var i = 10; i > 0; i--) {
    console.log(i)
}

同样的可读性差

for...in

for 循环可以以任意顺序迭代一个对象的除 Symbol 以外的可枚举属性,包括继承的可枚举属性

它有两个特点,一是迭代的是对象,二是循环目标是可枚举属性,包括继承的属性

// 例子1:迭代对象
var obj = {a: 1, b: 2, c: 3}
for (var key in obj) {
    console.log("obj." + key + " = " + obj[key])
}
// obj.a = 1
// obj.b = 2
// obj.c = 3
// 例子2:迭代可枚举属性
Object.prototype.sayHello = function() { console.log('hello') }

// 数组也属于对象,数组也能用对象的方法
const iterable = [1, 2, 3]
iterable.name = 'hello'

for (let key in iterable) {
    console.log("key&value: " + key + " = " + iterable[key]) 
}
// key&value: 0 = 1
// key&value: 1 = 2
// key&value: 2 = 3
// key&value: name = hello
// key&value: sayHello = function() { console.log('hello') }

它的使用场景是需要检查其中的任何键是否为某值的情况

我们在 拷贝的秘密 中曾手写过简易深拷贝:

function deepClone(source) {
    if (typeof source !== 'object' || source === null) {
        return source
    }
    let target = Array.isArray(source) ? [] : {}
    for (let prop in target) {
        if (source.hasOwnProperty(prop)) {
            target[prop] = typeof source[prop] === 'object' ? deepClone(source[prop]) : source[prop]
        }
    }
    return target
}

通过 for...in 遍历所有可枚举属性,然后通过 hasOwnProperty 过滤非自身属性的属性,从而拿到所有自身可枚举的属性,完成深拷贝

与之相对应的是 for...of

for...of

for...in 是 ES5 是为了解决遍历对象的 key 而新出的 API,而 for...of 是 ES6 时支持的特性,它的用途是遍历可迭代的对象(包括 Array、Map、Set、String、arguments等)

以 for...in 中的例子2为例:

Object.prototype.sayHello = function() { console.log('hello') }
const iterable = [1, 2, 3]
iterable.name = 'hello'

for (let i of iterable) {
  console.log(i); // 3, 5, 7
}

如果说 for...in 是为了拿到对象的 key(因为 value 在 for 循环中都能获得),那么 for...of 就是更方便拿到对象的 value

forEach

ES5 时数组新增的 API,能对数组的每个元素执行一次给定的函数。遍历时,不能被 break 或 return 提前结束循环

先看看它的参数,共三点:

  • element:当前元素
  • index:当前元素的索引
  • array:原数组
const array1 = ['a', 'b', 'c'];

array1.forEach((str, i, origin) => {
     console.log(`${i}: ${str} / ${origin}`);
});
// 0: a / a,b,c
// 1: b / a,b,c
// 2: c / a,b,c

被调用时,不会改变原数组(重点)

const array1 = ['a', 'b', 'c'];
array1.forEach((element) => {
    element = element + 1
});
console.log(array1) // [a, b, c]

但往往会在项目开发时遇到这类例子:

const arr = [{
    name: 'johan',
    age: 29
}, {
    name: 'elaine',
    age: 29
}]
arr.forEach(ele => {
    if (ele.name === 'johan') {
        ele.age = 22
    }
})
console.log(arr) // [{name: 'johan', age: 22}, {name: 'elaine', age: 29}]

原数组被改变了,为什么呢?

因为虽然调用 forEach 不会改变原数组,但是在 callbackFn 可能会改变 element(当前对象)

在第一个例子中,因为当前元素是基本类型,所以对它赋值是原数组是无效的,而当当前元素是引用类型时,情况就不同的,引用类型存在堆内存中,共享一个引用地址,当改变指时,原数组也就发生了变化,可以回顾下拷贝的秘密

map

ES6 时新增 API。此方法能创建一个新数组。这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成

const array1 = [1, 4, 9, 16];

const map1 = array1.map(x => x * 2);

console.log(map1); // [2, 8, 18, 32]

如果说 for...in 常和 for...of 被人拿出来比较,那么 forEach 的比较对象就是 map

例如 forEach 没有返回值,而 map 会返回一个新数组;forEach 偶尔会改变原数组(在callbackFn 改变当前对象时),而因为 map 返回的是一个新数组,所以永远不会改变原数组(所以在函数式编程中常会使用到 map,它就是标准的纯函数)

衍生一道面试题

["1","2","3"].map(parseInt)

为什么这题的答案是 [1, NaN, NaN] 呢?

parseInt 的语法是 parseInt(string, radix)

也就是说

// parseInt(string, radix) => map(parseInt(value, index))
// 也就是说它的迭代过程是:
// 第一次迭代: index is 0: parseInt("1", 0)  // 1
// 第二次迭代: index is 1: parseInt("2", 1)  // NaN
// 第三次迭代: index is 2: parseInt("3", 2)  // NaN

radix 表示进制的基数,范围是2~36,一般常见的是二进制、八进制、十进制、十六进制

当二进制时,除了“0、1“外,其他数字都不是有效二进制数字

测速比较

一些屌丝面试题会问这些循环方法速度排序。太搞笑了,又不是造火箭,问这些的目的是什么呢?

不过,写都写到这里了,不妨测一下

例子:

const million = 10000000;
const arr = Array(million);
console.time('timer')

for (let i = 0; i < arr.length; i++) {} // for
for (let i = arr.length; i > 0; i--) {}  // for(reverse)
for (const v in arr) {} // for...in
for (const v of arr) {} // for...of
arr.forEach(v => v) // forEach
arr.map(v => v) // map
console.timeEnd('timer')

衍生

在知乎中有人提问:如何形象地解释 JavaScript 中 map、foreach、reduce 间的区别尤雨溪 有个巧妙的比喻:

  • forEach 是你按顺序让他们做什么事情
  • map 是你拿着盒子,让他们将钱包扔进去,结束时返回一个新数组,里面有大家的钱包
  • reduce 是拿着钱包,每个检查,把你和前面的综合都加起来,算总共多少钱
  • filter 是过滤钱少于100快的,结束时返回一个新数组,里面都是钱大于100快的

总结

循环就这三板斧,一对比 for...in 和 for...of ;二是对比 forEach 和 map;三是 ES6 中其他常用 API

写不动了,毁灭吧

参考资料

系列文章