1. 简介
针对JavaScript内置的容器,尝试写出更好的代码。
此篇文中的语法、特性可能需要较新的ES版本来支持。
2. JavaScript中的遍历语法
2.1 传统for循环
仅适用于支持数字下标访问的容器,C、Java形式的循环。
let a = ['a', 'b', 'c'];
for (let i = 0; i < a.length; i++) {
console.log(`${i}: ${a[i]}`);
}
输出为
0: a
1: b
2: c
稳定易用少坑,只是看起来稍显啰嗦。
使用let i = 0
,保证i
的有效范围在for
循环内,如果使用var i = 0
,那么在循环过后仍然能够访问到i
的值。如果运行环境不支持let
关键字,那么在循环外独立声明i
,意思会更明确一些。
var a = ['a', 'b', 'c'],
i = 0;
for (i = 0; i < a.length; i++) {
console.log(`${i}: ${a[i]}`);
}
2.2. for-in
重要的话放在前边,for...in
有坑。
for...in
语法是用来遍历任何可迭代对象的。
let myObj = {
name: "Ming",
age: "20",
};
for (let prop in myObj) {
console.log(`${prop}: ${myObj[prop]}`);
}
输出为
name: Ming
age: 20
坑在于,使用for...in
循环,会遍历对象的所有可遍历属性,包括对象继承来的属性、prototype的属性等。表现可能与预期不同。
这里引用阮一峰《JavaScript 标准参考教程(alpha)》中的例子:
// name 是 Person 本身的属性
function Person(name) {
this.name = name;
}
// describe是Person.prototype的属性
Person.prototype.describe = function () {
return 'Name: '+this.name;
};
var person = new Person('Jane');
// for...in循环会遍历实例自身的属性(name),
// 以及继承的属性(describe)
for (var key in person) {
console.log(key);
}
输出为
name
describe
可以使用hasOwnProperty
方法,判断某个属性是不是自身的属性。
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
此时输出只有一个name
。
注意一般情况下应避免对Array使用for...in
循环。假设有这样的数组:
var a = [];
var a[2] = 1; // [undefined, undefined, 1]
a.extraProp = "this is a test extra props"; // 可以给数组或数组的ProtoType加一个属性
如果用传统i++
式的循环,能够得到[undefined, undefined, 1]
这样的结果,接下来我们试试使用for...in
循环:
for (let val in a) {
console.log(val);
}
得到结果为
2
extraProp
丢失了两个undefined
,多了一个不能通过下标访问的属性extraProp
。对数组进行遍历时,我们通常希望以此输出数组中每一个数字下标对应的元素,而这与我们一般的需求不符。
2.3. for...of
for...of
是ES6中新出现的语法。for...of
需要所遍历的对象实现了可迭代协议;并使用对象指定的迭代器进行遍历。换句话说,for...of
会输出什么,是由对象自己决定的,不同于for...in
那种,像是把对象直接肢解遍历。某种程度上说,JavaScript中的for...of
最为接近Python中的for...in
。
使用和for..in
同样的例子:
var a = [];
var a[2] = 1; // [undefined, undefined, 1]
a.extraProp = "this is a test extra props"; // 可以给数组或数组的ProtoType加一个属性
for (let val of a) {
console.log(val);
}
得到结果:
undefined
undefined
1
与使用传统i++
遍历方式得到的结果完全一致。
在允许ES6的情况下,应当优先考虑使用for...of
方式遍历元素。
3. 数组Array
3.1. 对Array进行for...of遍历
对于数组,前边已经提到了可以使用for...of
语法写出简洁的代码进行遍历:
let a = ['a', 'b', 'c'];
for (let val of a) {
console.log(val);
}
3.2. Array的keys与entries
如果在使用for...of
进行遍历时,希望拿到当前元素的下标引用(类似于Python中的enumerate
),可以使用Array.prototype.keys
以及 Array.prototype.entries
方法。
keys
返回一个新的Array Iterator,对其遍历得到数组中的索引,用于仅需要索引,而不需要元素本身的情况:
let a = ['a', 'b', 'c'];
for (let val of a.keys()) {
console.log(val);
}
输出结果为
0
1
2
entries
同样返回一个新的Array Iterator,对其遍历可以得到数组中的键值对:
let a = ['a', 'b', 'c'];
for (let [idx, val] of a.entries()) {
console.log(`${idx}: ${val}`);
}
输出结果为
0: a
1: b
2: c
3.3. Array的forEach
Array.prototype.forEach()
是ES5开始Array
类的一个遍历方法。其语法为:
arr.forEach(function callback(currentValue, index, array) {
//your iterator
}[, thisArg]);
即接受一个回掉函数作为参数,这样会对数组中的每一个元素执行此函数:
let a = ['a', 'b', 'c'];
a.forEach(function(element) {
console.log(element);
});
输出结果为
a
b
c
forEach
仅会对能够通过下标访问的元素进行遍历,为数组或数组的prototype增加属性不会影响遍历,但是如果某个索引对应的元素为空,那么会跳过这个元素:
let a = ['a', 'b', 'c'];
let a[4] = 'e'; // ['a', 'b', 'c', undefined, 'e'];
a.forEach(function(element) {
console.log(element);
});
输出结果为
a
b
c
e
另一个限制为,使用forEach
难于实现for
循环中的break
逻辑。
4. 使用Map和Set
Map
和set
是ES2015中加入的键访问容器(Keyed Collection),用于实现映射表和集合的功能。
在ES2015之前,Map
的功能通常可以用一个JavaScript对象来实现,但JavaScript对象有限制,包括key必须为字符串,对象的原型包含默认的键,可能在使用中导致冲突。
Set
的功能在以前通常使用Array
代替,并手动处理一些Set应有的特性(例如元素不可重复等),使用Array实现Set的功能运行效率十分低下。
顺便,在JavaScript中的Map
和Set
有一个隐藏小福利:对Map
或者Set
进行遍历的时候,元素的访问顺序就是元素插入的顺序。相比之下Python中需要有序字典需要使用collections.OrderedDict
,且Python的内置库中目前还没有找到专门的有序集合的实现。
对于Map常用操作如下:
Map.prototype.size
Map.prototype.set(key, value)
Map.prototype.get(key, value)
Map.prototype.has(key)
Map.prototype.delete(key)
Map.prototype.clear()
Map.prototype.keys() // 只包含key的迭代器
Map.prototype.values() // 只包含value的迭代器
Map.prototype.entries() // [key, value] 形式的迭代器
Map.prototype.forEach(callbackFn[, thisArg])
对于Set常用操作如下:
Set.prototype.size
Set.prototype.add(value)
Set.prototype.has(value)
Set.prototype.delete(value)
Set.prototype.clear()
Set.prototype.values() // 包含value的迭代器
Set.prototype.keys() // 包含value的迭代器,和Set.prototype.values()相同
Set.prototype.entries() // [value, value] 形式的迭代器
Set.prototype.forEach(callbackFn[, thisArg])
我踩过的一个坑是in
运算符(大概是由于Python的惯性...)。在JavaScript中in
操作符可以用于判断一个对象是否含有某个属性(var o = {name: "Ming"}
,那么name in o
会返回True),文档中也提到可以用于判断数组的某个下标是否在其长度以内(多数情况下,如果n < arr.length
且n >= 0
,那么n in arr
会返回True
,但也有例外,在2.2中提到,此处不该依赖in
操作符)。
实际上in
操作符对于Map
和Set
类型没有什么意义。要查找某个键是否在Map
或Set
中,请使用Map.prototype.has(key)
和Set.prototype.has(value)
。