先记住这个答案
map.set(key, value) 和 set.add(value) 执行完后返回的是集合本身,所以可以写成 map.set('a', 1).set('b', 2),每一次调用都作用在同一个集合上。delete 返回布尔值,表示这个键原本存不存在、这次有没有真的删掉;clear 返回 undefined。它们都不返回集合,所以接在后面继续 .set() 会报 TypeError。另外,链式调用是原地修改,不会生成新集合,所有引用这个集合的地方都能看到变化。
set和add返回集合本身,可以连着写delete返回布尔值,表示这次有没有真的删掉clear返回undefined,不能再往后链- 链式调用修改的是原集合,不会生成新集合
各个方法到底返回什么
把每个方法的返回值直接打印出来,比背结论更直观。
const map = new Map()
console.log(map.set('a', 1).set('b', 2) === map) // set 返回 Map 本身
const set = new Set()
console.log(set.add('a').add('b') === set) // add 返回 Set 本身
console.log(map.delete('a'), map.delete('a')) // 第一次删成功,第二次没有可删的
console.log(map.clear() === undefined, map.size) // clear 没有返回值
console.log(set.size)查看输出与解释
true
true
true false
true 0
2前两行的 true 说明链式调用的结果和原集合是同一个对象,没有产生新集合。第三行对同一个键删两次,第一次返回 true,第二次这个键已经不在了,返回 false。所以 delete 的返回值是有用的信息,比如可以用它判断「这个任务是不是真的被取消了」。
如果写成 map.clear().set('c', 3),由于 clear() 返回 undefined,就等于在 undefined 上调用 set,会报错。
链式调用改的是原集合
set 返回的是原集合,所以 const next = map.set('x', 1) 之后,next 和 map 是同一个对象。这一点在 React 里特别容易出问题。
- React 里更新
Map状态 - 用
useState存一个Map,写成setCache(cache.set(key, value)),传进去的还是原来那个引用。React 用Object.is比较新旧状态,发现是同一个对象,就不会重新渲染。正确写法是先复制再改:setCache(prev => new Map(prev).set(key, value))。这里链式调用正好派上用场,new Map(prev)创建副本,.set()返回副本本身,一行就能写完。 注意new Map(prev)只复制了条目,值如果是对象,新旧Map里还是同一个对象。
容易答错的地方
- 以为链式调用每一步都返回新集合
- 链式调用的写法和数组的
map、filter很像,容易让人以为每一步都生成了新集合。其实全程只有一个集合在被修改,赋给新变量也只是多了一个引用。 - 把
delete写进链中间 map.delete('a').set('b', 2)会报错,因为delete返回的是布尔值。删除要单独写一行,需要的话检查它的返回值。
面试官还会怎么问?
Set.add 一个已有的值,size 会变吗?
不会。值已经存在时 add 什么都不改,但仍然返回这个 Set,所以链式调用不会断。也正因为返回值一样,不能靠返回值判断这次有没有真的新增,要在 add 之前用 has 判断。
自己写的类怎么支持链式调用?
在方法最后 return this 就行。比如一个查询构造器,where() 和 orderBy() 都返回 this,就能写成 query.where(...).orderBy(...)。但真正用来返回结果的方法,比如 run(),应该返回结果而不是 this。
链式调用中途报错,前面的修改还在吗?
在。比如 map.set('a', 1).set('b', compute()),当 compute() 抛错时,'a' 已经写进去了。需要要么全部成功要么全部不生效,就先在一个新 Map 里构建好,全部成功后再替换原来的引用。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。