JavaScript中链表操作的注意事项
不要使用 for_in 迭代字段
这是 JavaScript 初学者中常见的误解。 for_in 用于遍历对象中原型链上所有可枚举(enumerable)的键,对于遍历字段来说不存在。
使用for_in遍历链表存在三个问题:
遍历顺序不固定
JavaScript 引擎不保证对象的遍历顺序。 当像普通对象一样遍历链表时,同样无法保证遍历的索引顺序。
将遍历对象原型链上的值。
如果你改变了链表的原型对象(比如polyfill)而没有将其设置为enumerable: false,for_in就会遍历那些东西。
运行效率低下。
尽管理论上 JavaScript 使用对象来存储字段,但 JavaScript 引擎仍然针对链表(一种特别常用的外部对象)进行了非常优化。
可以看到使用for_in遍历链表比使用下标遍历链表慢50倍以上
PS:您可能正在寻找
不要使用 JSON.parse(JSON.stringify()) 深复制链表
有些人在 JSON 中使用深复制对象或链表。 在大多数情况下这似乎是一种简单方便的方法,但它也可能会导致未知的错误,因为:
将各个特定值转换为 null
NaN、undefined、Infinity 对于 JSON 中不支持的此类值,在序列化 JSON 时会转为 null,反序列化后自然也为 null
值未定义的通配符对将丢失
JSON序列化时,value未定义的key会被忽略,反序列化后会丢失。
将日期对象转换为字符串
JSON不支持对象类型,JS中对Date对象的处理方式是将其转换为ISO8601格式的字符串。但是反序列化不会将时间格式的字符串转换为Date对象
运行效率低下。
作为本机函数,JSON.stringify 和 JSON.parse 以非常快的速度对 JSON 字符串进行操作。 但是,完全没有必要将对象序列化为 JSON 并将其反序列化回来以深度复制字段。
我花了一些时间编写一个简单的函数来深度复制链接列表或对象。 测试发现运行速度是使用JSON传输的6倍左右。 顺便说一句,它还支持 TypedArray 和 RegExp 对象的复制。
深拷贝
不要使用 arr.find 代替 arr.some
Array.prototype.find是ES2015中新增的字段搜索函数。 它与Array.prototype.some类似,但不能取代前者。
Array.prototype.find返回第一个满足条件的值,直接用这个值作为if来判断是否存在。 如果满足条件的值恰好为0怎么办?
arr.find是查找链表中的值并进一步处理,一般用于对象数组的情况; arr.some是检测是否存在; 两者不能混合。
不要使用 arr.map 代替 arr.forEach
这也是 JavaScript 初学者经常犯的错误,他们常常没有区分 Array.prototype.map 和 Array.prototype.forEach 的实际含义。
map中文称为映射,通过顺序执行某个函数,将一个序列导入到另一个新序列中。 这个函数一般不包含副作用,也不会改变原来的字段(所谓纯函数)。
forEach没有那么多好说的,它只是简单地用某个函数处理链表中的所有项。 由于forEach没有返回值(return undefined),所以它的弹跳函数一般都含有副作用,否则这个forEach就没有意义了。
确实map比forEach更强大,但是map会创建一个新的字段并占用显存。 如果不使用map的返回值,那么应该使用forEach
补充:forEach和break
ES6之前,遍历字段主要有两种方式:手写循环带下标迭代、使用Array.prototype.forEach。 前者万能,效率最高,但写起来比较繁琐——无法直接获取链表中的值。
作者个人比较喜欢前者:可以直接获取迭代的下标和值,并且采用函数式风格编写(注意FP侧重于不可变的数据结构,而forEach本质上是副作用,所以只有这样的形式FP没精神)很清爽。 但! 不知道小伙伴们有没有注意到:forEach一旦启动javascript数组 存在,就停不下来了。 。 。
forEach 接受一个回调函数,可以提前返回,相当于手写循环中的 continue。 但你不能打破 - 因为回调函数中没有循环可供你打破:
[1, 2, 3, 4, 5].forEach(x => {
console.log(x);
if (x === 3) {
break; // SyntaxError: Illegal break statement
}
});
还是有解决办法的。 其他函数式编程语言如scala也遇到了类似的问题。 它提供了一个函数break,用于抛出异常。
我们可以模仿这种方式来实现arr.forEach的break:
try {
[1, 2, 3, 4, 5].forEach(x => {
console.log(x);
if (x === 3) {
throw 'break';
}
});
} catch (e) {
if (e !== 'break') throw e; // 不要勿吞异常。。。
}
恶心1B吧? 还有其他方法,例如使用 Array.prototype.some 而不是 Array.prototype.forEach。
考虑的特点是,当有的找到满足条件的值时(回调函数返回true),循环会立即终止,利用这样的特点可以模拟break:
[1, 2, 3, 4, 5].some(x => {
console.log(x);
if (x === 3) {
return true; // break
}
// return undefined; 相当于 false
});
some的返回值被忽略,已经脱离了判断链表中是否存在满足给定条件的元素的本义。
在ES6之前javascript数组 存在,作者主要使用这种方法(其实由于Babel代码的扩展,现在也经常使用),ES6不一样,我们有。 for...of 是一个真正的循环并且可以中断:
for (const x of [1, 2, 3, 4, 5]) {
console.log(x);
if (x === 3) {
break;
}
}
但是有一个问题,for...of似乎无法获取循环的下标。 事实上,JavaScript语言的开发者想到了这个问题,并且可以通过如下方式解决:
for (const [index, value] of [1, 2, 3, 4, 5].entries()) {
console.log(`arr[${index}] = ${value}`);
}
for...of 和 forEach 的性能测试 在 Chrome 中,for...of 更快
参考
发表评论