绷不住了,这道字节面试题:let [a, b] = {a: 1, b: 2}

549 阅读6分钟

let [a, b] = {a: 1, b: 2}

面试官:“请你让这行代码成立,我希望 a = 1, b = 2”。

你的内心:“你是怎么用37°的嘴问出如此冰冷的问题的”。想归想,但如果你无法清晰的解释这个问题。那么认真看完这篇文章,谁看谁会

首先让我们搞明白这个问题问的是什么

这是一份数组的解构,但又像是对象的解构,我们只知道

image.png

js提供给我们的解构语法就是这般,那要把一个对象解构给数组,面试官究竟要考察我什么。要理清楚这个问题,我们需要先弄懂以下几个知识点

1. Iterator 接口

首先我们要对Iterator这个接口有一个清晰的认识。js中提供了很多引用类型的数据,包括(对象,数组,Set,Map等等)

这些引用类型有各自独特的数据访问方式,但是在js中,我们希望对这些数据结构有一种统一的访问机制,于是官方就打造了 for...of 这样一个遍历方法,旨在用它能遍历这些数据结构。但是打造 for...of 的时候设定了这样一个规则,就是只有当这种数据结构拥有迭代器属性的时候,才能被 for...of 所遍历

这时候你可能会好奇为什么要打造 for...of, 直接用 for (let i = 0; i < x; i++) {}来便利不好吗,事实是,如果我们只需要访问数组中的元素,使用for语句会比 for...of 需要定义更多的变量,以及数组的长度的读取等操作,无疑让代码变得更复杂更不优雅

1.1 迭代器属性

什么是迭代器属性,官方定义的迭代器属性指的是一个形如 [Symbol.iterator] 样的属性,值为一个函数体。简言之就是,一个对象上如果实现了 [Symbol.iterator] 方法,就被认为是可迭代的,就可以使用 for...of 语句来遍历。

让我们看看数组上目前内置了[Symbol.iterator]属性的样子

image.png

当数组上的[Symbol.iterator]函数被调用,得到了一个对象,该对象中具有 next 方法,每次调用 next都会得到一个新的对象 { value: 1, done: false }。数组中的元素好像在以一种新的方式被我们得到 要解释这种新的方式我们就要细聊一聊关于迭代器协议

1.2 迭代器协议

迭代器本质上也是一个对象,但一个对象要成为迭代器,必须实现一个特定的协议,即 Iterator 协议。具体来说,该对象必须有一个 next() 方法,每次调用这个方法返回一个具有以下两个属性的对象:

  • value:迭代器返回的当前元素。
  • done:一个布尔值,表示迭代器是否已经完成遍历。

既然协议的规则如此,那我们便了可以很简单的手搓出来一个迭代器对象

image.png

哦~~ 原来新形如 myIterator 这样子的对象就叫做迭代器!

1.3 可迭代协议

那如何让一个对象成为可迭代对象呢?看过 1.1 中数组的样子你已经猜到了。一个对象要成为可迭代对象,它必须实现 [Symbol.iterator] 方法。这个方法返回一个迭代器对象。js中所有内置的集合类型(如 Array, Map, Set, String 等)都已经实现了可迭代协议。

如此说来,一个可迭代的对象便是长这般模样了:

image.png

2. 解构问题

搞明白了什么是迭代器,什么是可迭代的对象,接下来我们看看解构当中的细节,如果我们直接在v8中运行 let [a, b] = {a: 1, b: 2} 这行代码会发生什么呢?

image.png 给看官老爷翻译一下这行报错:类型错误: {(中间值)(中间值)} 不可迭代,也就是说报错内容为对象 {} 不是一个可迭代对象

从这里我们可以得出两个结论:

  • js中的普通对象不具有 [Symbol.iterator] 属性
  • 以数组为例,let [x, y] = [1, 2],解构其实是将数组中的迭代器属性得到的值不断地调用 next 再将得到的对象中的 value 赋值给变量 x,y

悟了悟了!

3. 回答面试官

明白了以上内容,再回过头来思考面试官提出的问题,现在的嘴角已是非AK能压的

let [a, b] = {a: 1, b: 2} 要让该代码成立的最重要的一步,不就是想办法让对象{a: 1, b: 2}具有[Symbol.iterator] 属性,并且遵照可迭代协议吗!

那我们顺势就能踏出这关键性的一步

// 往对象原型上添加迭代器属性,值为函数体
Object.prototype[Symbol.iterator] = function() {
    
}

let [a, b] = {a: 1, b: 2}  
console.log(a, b);

可是迭代器属性要返回一个迭代器对象,该对象中要具有next方法,那我们该如何打造这样一个迭代器对象呢?

再分析一下题目,题目希望我们将对象解构到数组上,这肯定是一个伪命题,其实是希望当v8开始执行解构语法时 代码能变成:let [a, b] = 数组的迭代器

那么答案就呼之欲出了,修改上述代码:

// 往对象原型上添加迭代器属性,值为函数体
Object.prototype[Symbol.iterator] = function() {
    return Object.values(this)[Symbol.iterator]()
}

let [a, b] = {a: 1, b: 2}  

console.log(a, b); // 1  2

解释一下这一行代码:

  1. 原型上函数体中的this指向同构造函数一样,都指向实例对象,所以这里的this指向的是 {a: 1, b: 2}
  2. Object.values(this) 获取到对象中所有的 value,以数组形式返回,所以得到 [1, 2]
  3. 直接返回数组自带的迭代器属性的值

那么当结构语句被v8执行的那一刻 let [a, b] = 数组的迭代器这个逻辑就成立了,完美解决!

4. for...of的实现原理

顺带我们再提一句 for...of 是如何工作的 image.png

5. 总结

一道优秀的面试题,一定是在尽可能少的问题中个考察面试者尽可能多的知识点,这个问题考察了我们以下知识点:

  1. 对ES6解构的语法原理的认识
  2. 对原型的理解
  3. 对迭代器的理解

我愿称之为是一道非常nice的题目,你绷住了嘛

如果学习的道路上你孤独前行,可以加个wx好友,备注掘金小伙伴,我们有一群志同道合为了奔赴大厂正在努力的小伙伴: psc1023wn