标准操作 for 循环
相信大部分搬砖工作者都写过类似的代码:
- var arr = ['element1', 'element2', 'element3'];
- for (var i = 0, len = arr.length; i < len; i++) {
- console.log(arr[i]);
- }
- // element1
- // element2
- // element3
这是一段标准的 for 循环代码, 通过变量 i 去追踪数组 arr 的索引, 达到访问数组中每一位元素的目的.
但不得不说, 这是非常原始的一种方法, 存在着几个显著缺点:
无法只关注元素本身, 需要花费精力去维护变量 i 以及边界 len;
当存在着多重嵌套时, 将需要跟踪维护多个变量 i, 代码会非常复杂;
需要花费精力去处理越界问题, 一些编译型语言在遇到数组索引越界时会报错, 而 JavaScript 引擎将不会告诉你任何错误信息, 错误定位成本会比较高.
数组自带方法 forEach
当然我们可以使用数组自带的 forEach 方法进行数组的遍历:
- var arr = ['element1', 'element2', 'element3'];
- arr.forEach(function(value, index, arr) {
- console.log(value);
- });
- // element1
- // element2
- // element3
一切看起来都是那样的完美, 即不需要花费精力去追踪索引, 又无需担心越界问题, 简直美滋滋. But, 如果遍历到某个特定条件想退出咋整?
- var arr = ['element1', 'element2', 'element3'];
- arr.forEach(function(value, index, arr) {
- if (index === 1) {
- break;
- } else {
- console.log(value);
- }
- });
- // Uncaught SyntaxError: Illegal break statement
比较遗憾的是以上的代码未能按照我们的预期运行(break,continue 等语句跨越了函数边界), 所以这个看似完美的办法实际上只能一条道走到黑, 像吃了炫迈一样根本停不下来. 当数组很大, 没办法通过提前终止遍历来节省资源.
for in 循环
饭要一口一口的吃(饭桶请忽略), 我们可以先解决花费时间防止数组越界的问题, 例如可以使用 for in 循环:
- var arr = ['element1', 'element2', 'element3'];
- for (var i in arr) {
- console.log(arr[i]);
- }
- // element1
- // element2
- // element3
for in 语句是一种精准的迭代语句, 可以枚举对象的所有可枚举属性 (可以使用 Object.getOwnPropertyDescriptor(targetObj, attrName) 方法来查看对象的某个属性是否可枚举).It means that, 可以用它来遍历对象:
- var obj = {
- a: 1,
- b: 1,
- c: 1
- };
- for (let attr in obj) {
- console.log(attr, obj[attr]);
- }
- // a 1
- // b 1
- // c 1
除了遍历对象, 数组之外, for in 循环还可兼职遍历字符串:
- var str = 'I am a handsome boy!';
- for (var i in str) {
- console.log(str[i]);
- }
- // 太帅 (chang) 了, 结果就不打印了
当然, 也支持 break,continue 的操作, 例子我就不写了.
这玩意看起来非常的牛 *, 简直就是万能的. But, 一般看上去什么都会的人, 实际上什么都做不精(我就不一样了, 我不仅看上去什么都不会, 还做不好), 这个玩意也是一样的, 看看例子:
- var father = {
- fatherAttr: 1
- };
- // 以 father 为原型创建对象实例 instance
- var instance = Object.create(father);
- instance.a = 1;
- instance.b = 1;
- instance.c = 1;
- for (var attr in instance) {
- console.log(attr, instance[attr]);
- }
- // a 1
- // b 1
- // c 1
- // fatherAttr 1
- // 获取 instance 实例的自有属性名
- console.log(Object.getOwnPropertyNames(instance));
- // ["a", "b", "c"]
上面这个例子中, 首先以 father 对象为原型创建了一个对象实例 instance, 然后为这个实例 instance 添加了 a,b,c 三个属性, 接着使用 for in 循环遍历这个对象. 通过查看 instance 的自有属性可以发现, fatherAttr 并不是 instance 的属性, 而是其原型 father 的属性, for in 循环会将对象的原型属性也一并列举出来. 故使用此方法去遍历对象属性的时候, 需要加多一层判断:
- for (var attr in obj) {
- if (obj.hasOwnProperty(attr)) {
- // 是对象的自有属性, 可以尽情的玩耍了
- }
- }
for in 循环枚举原型属性这个弊端, 在操作数组上也是有同样的问题, 但是一般情况下, 使用它遍历数组还是比较保险的; 毕竟数组的原型是 JavaScript 内建对象 Array,Array 对象的默认属性都是不可枚举的; 但如果你连 Array 对象都敢修改的话, 这个小小的 bug 对你来说也不是事了.
不得不说, for in 循环还是比较普遍使用的遍历对象的方法, 这主要得益于其兼容性. 当然, 遍历对象还有其他的方法, 稍迟再讲. 我们继续审判 for in 循环:
- var str = 'ac';
- for (let index in str) {
- console.log(str[index]);
- }
- // a
- // 无法用言语描述的字符
- // 无法用言语描述的字符
- // c
ES5 及之前处理字符串时, 是以 16 位编码单位为基础的; 16 位编码显然无法给世界上所有的字符编码, 所以某些字符就需要使用 32 位进行编码了, 例如''字.
所以上面的例子出现打印四个字符的结果就不难理解了. 按照道理来说, 这不应该是 for in 循环的锅, 但是有些时候就是不想讲道理.
虽然 es6 处理字符串强制使用 UTF-16 字符串来解决上述的问题(下面会有相关的例子), 但 for in 循环依旧会存在上述的问题, 如果你的程序需要兼容不支持 es6 的浏览器, 可以戳这里
for of 循环
通过 for in 循环可以解决传统 for 循环需要维护边界的问题, 但也引入了一些新问题, 跟搬砖工作者的日常操作 "解决 3 个 bug, 引入 8 个新 bug" 场景极度相似.
所以换个 es6 定义的 for of 循环操作试试:
- let str = 'a c';
- for (let char of str) {
- if (char === ' ') {
- continue;
- } else {
- console.log(char);
- }
- }
- // a
- //
- // c
从上面的例子来看, 效果简直是 perfect: 索引去掉了, 边界去掉了, 想继续就继续, 想退出就退出, 还能顺便解决一下字符串的编码问题.
for of 循环是一种依赖对象迭代器 (迭代器的相关内容放在下一篇) 的遍历方法, 每一次执行都会执行迭代器的 next 方法, 返回正确的值. 通过 for of 循环, 无需花费精力去追踪复杂的条件, 降低了出错的概率.
根据先褒后贬的套路, 接下来看看其一些限制性:
运行环境为 ES6 及以上版本, 所以兼容性没有 for in 循环以及传统的操作好, 如果需要考虑兼容上世纪的浏览器, 就不能使用这个东西
只能用于遍历可迭代对象, 即存在迭代器的对象, 如果用于遍历不可迭代对象, 分分钟报错没商量. 可以通过检测对象的 Symbol.iterator 属性 (相关内容将放在下一篇) 是否为函数来判断对象是否可迭代.
- let arr = ['a', 'b', 'c'];
- // 判断其 Symbol.iterator 属性是否为函数
- if ((typeof arr[Symbol.iterator]).toUpperCase() === 'FUNCTION') {
- for (let element of arr) {
- console.log(element);
- }
- } else {
- console.log('此对象不可迭代');
- }
- // a
- // b
- // c
实际上, 大多数 JavaScript 的内置对象都支持迭代, 例如: Array,Set,Map,String 等, 当使用 for of 循环遍历上述对象时,会使用其默认的迭代器:
- let map = new Map([['a', 1], ['b', 1], ['c', 1], ['d', 1]]);
- // 正经操作
- for (let item of map) {
- console.log(item);
- }
- // ["a", 1]
- // ["b", 1]
- // ["c", 1]
- // ["d", 1]
- // 使用解构, 方便读取值
- for (let [key, value] of map) {
- console.log(key, value);
- }
- // a 1
- // b 1
- // c 1
- // d 1
上面的例子使用了 for of 遍历了 Map 类型实例 map, 迭代对象为 Map 类型的默认迭代器. 当然, 像 Array,Set,Map 类型还提供了一些特殊的迭代器, 可以让搬砖工作者更方便的去处理其想关注的内容:
entries() 返回一个迭代器, 其返回值为键值对数组(Map 集合的默认迭代器; 对于 Set 集合, 返回值数组的元素相同, 即 value)
keys() 返回一个迭代器, 其返回值为集合的键名(对于 Set 集合, 此迭代器跟 values 迭代器返回值相同; 对于数组, 此迭代器返回值为索引)
values() 返回一个迭代器, 其返回值为集合的值(Array,Set 集合的默认迭代器)
- let arr = ['a', 'b', 'c', 'd']
- let set = new Set(arr);
- for (let item of set.entries()) {
- console.log(item);
- }
- for (let item of arr.entries()) {
- console.log(item);
- }
- // ["a", "a"]
- // ["b", "b"]
- // ["c", "c"]
- // ["d", "d"]
- // [0, "a"]
- // [1, "b"]
- // [2, "c"]
- // [3, "d"]
- for (let item of set.keys()) {
- console.log(item);
- }
- for (let item of arr.keys()) {
- console.log(item);
- }
- // a
- // b
- // c
- // d
- // 0
- // 1
- // 2
- // 3
- for (let item of set.values()) {
- console.log(item);
- }
- for (let item of arr.values()) {
- console.log(item);
- }
- // a
- // b
- // c
- // d
- // a
- // b
- // c
- // d
除了 JavaScript 的内置对象, 一些 DOM 标准的类型如 NodeList 也可以使用 for of 循环进行遍历:
- let containers = document.querySelectorAll('.container');
- for (let node of containers) {
- // 搞事情专用注释
- }
很遗憾的是, for of 循环居然不支持自定义对象的遍历(心中一万匹 *** 奔腾而过......), 所以如果不想使用 for in 循环遍历对象, 只能转个弯了.
遍历对象的转弯操作
Object.keys()获取键名数组
使用 Object.keys()可以获取到对象实例的所有可枚举属性, 其返回值为一个数组, 数组元素为对象的键名:
- let father = {
- fatherAttr: 1
- };
- // 以 father 为原型创建对象实例 instance
- let instance = Object.create(father);
- instance.a = 1;
- instance.b = 1;
- instance.c = 1;
- Object.defineProperty(instance, 'd', {
- writable: true,
- value: 1,
- enumerable: false,
- configurable: true
- });
- for (let key of Object.keys(instance)) {
- console.log(key);
- }
- // a
- // b
- // c
从上面的例子中可以看出, Object.keys()方法并不会获取对象的原型属性以及自身不可枚举属性, 这个是比较符合我们的需求的; 并且, 这个玩意是 ES5 的特性, 兼容性还是比较好的, 是笔者比较喜欢使用的方法.
当然, 如果作死, 往这个方法传入非对象参数(如字符串), 其在 ES5 环境和 ES6 环境的表现是不一样的:
- console.log(Object.keys('I am a handsome boy!'));
- // ES5 直接报错, 但说不定是浏览器嫉妒我的帅气才会报错的
- // ES6 估计见多了大风大浪, 没啥感觉了
- // ["0", "1", "2", "3", "4", "5", ...]
另外, 需要注意的一点, ES 标准没有规定这个枚举顺序, 也就是说此方法的返回值的顺序是不确定的(包括下面的各种方法), 如果对顺序有要求, 可以尽量使用 map 或者 set 集合进行操作.
Object.getOwnPropertyNames()获取键名数组
此方法跟 keys 方法表现一样, 所不同的是, 其返回的数组包含了对象的不可枚举属性:
- let father = {
- fatherAttr: 1
- };
- let instance = Object.create(father);
- instance.a = 1;
- instance.b = 1;
- instance.c = 1;
- Object.defineProperty(instance, 'd', {
- writable: true,
- value: 1,
- enumerable: false,
- configurable: true
- });
- for (let key of Object.getOwnPropertyNames(instance)) {
- console.log(key);
- }
- // a
- // b
- // c
- // d
如果你还是想作死, 试试传入一个字符串会发生什么事, 可以自己去试试, 然后评论区留下实验结果.
Object.entries()获取键值对数组
这个方法返回什么东西就无需多言了吧, 看例子:
- let father = {
- fatherAttr: 1
- };
- let instance = Object.create(father);
- instance.a = 1;
- instance.b = 1;
- instance.c = 1;
- Object.defineProperty(instance, 'd', {
- writable: true,
- value: 1,
- enumerable: false,
- configurable: true
- });
- for (let key of Object.entries(instance)) {
- console.log(key);
- }
- // ["a", 1]
- // ["b", 1]
- // ["c", 1]
所以当使用一个对象初始化一个 Map 实例时, 可以使用这个方法:
- let obj = { a: 1, b: 1, c: 1 },
- map = new Map(Object.entries(obj));
- console.log(map.get('a'));
- console.log(map.get('b'));
- console.log(map.get('c'));
- // 1
- // 1
- // 1
Object.values()获取对象的属性值数组
嗯, 写例子好累, 不写了, 各位看官自己搞吧.
Object.getOwnPropertySymbols()获取 Symbol 属性名
上面提到的几个方法都无法获取到对象实例的 Symbol 类型的属性名, 如果需要遍历这个玩意,需要使用 Object.getOwnPropertySymbols()方法:
- let father = {
- fatherAttr: 1
- };
- let instance = Object.create(father);
- instance.a = 1;
- instance.b = 1;
- instance.c = 1;
- instance[Symbol('I am a handsome boy!')] = 1;
- for (let key of Object.keys(instance)) {
- console.log(key);
- }
- // a
- // b
- // c
- for (let key of Object.getOwnPropertySymbols(instance)) {
- console.log(key);
- }
- // Symbol(I am a handsome boy!)
上面列举了一些遍历的方法, 一般可以满足日常的工作需要. 但是上面都是 ES 内置的方法, 不能定制化. 在这个个性张扬的时代, 如果你想搞点特殊, 可以自定义一个迭代器; 通过一些封装, 甚至可以使用 for of 循环枚举对象. 相关内容将在下一篇博文.
来源: https://juejin.im/post/5bfbbe2df265da61407e95a3