正确、更好地使用JavaScript容器对象

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

Mapset是ES2015中加入的键访问容器(Keyed Collection),用于实现映射表和集合的功能。

在ES2015之前,Map的功能通常可以用一个JavaScript对象来实现,但JavaScript对象有限制,包括key必须为字符串,对象的原型包含默认的键,可能在使用中导致冲突。

Set的功能在以前通常使用Array代替,并手动处理一些Set应有的特性(例如元素不可重复等),使用Array实现Set的功能运行效率十分低下。

顺便,在JavaScript中的MapSet有一个隐藏小福利:对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.lengthn >= 0,那么n in arr会返回True,但也有例外,在2.2中提到,此处不该依赖in操作符)。

实际上in操作符对于MapSet类型没有什么意义。要查找某个键是否在MapSet中,请使用Map.prototype.has(key)Set.prototype.has(value)

    原文作者:raaay0608
    原文地址: https://www.jianshu.com/p/3e7a62e04751
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞