字节面试官:如何让var[a,b]={a:1,b:2}成功解构赋值?

1,543 阅读9分钟

前言

字节面试官问出这样一道面试题:let [a, b] = { a: 1, b: 2 },请把它成功解构。

我:咋一看咋不对劲啊?这是要我解构吗,可是为啥左边是数组右边是对象啊?有点懵了。。。

这是一道字节的面试题,大家第一眼看到这个面试题的时候,是不是脑袋有点懵?有一种熟悉又陌生的感觉?我当时也是这样的

在 JavaScript 中,解构赋值语法的左侧是一个数组,而右侧则应该是一个具有迭代器接口的对象(如数组、Map、Set等)。因此,将对象 {a: 1, b: 2} 解构赋值给 [a, b] 会导致语法错误,可偏偏面试官要求我们让这个解构赋值表达式成立,哎,我开始慌了......

不过既然面试官会问这道题,肯定没那么简单,也肯定有他的道理,接下来我就带大家来深入解决一下这道题。

思路

错误思路

既然将一个对象解构赋值给数组,是一个语法错误,那我们直接把这个解构语法变为对象的解构赋值语法不就好了。直接改成var { a, b } = { a: 1, b: 2 }; 如果你是这样做的话,哈哈哈哈哈哈,那么恭喜你,面试结束了。

这是字节的面试题,肯定有他考我们的道理,有他要考的考点在。

正确思路

咱们先来看看报错是怎么样的:

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

image-20240713171458667

TypeError: {(intermediate value)(intermediate value)} is not iterable

这个错误是类型错误,并且是对象有问题,因为对象是一个不具备迭代器属性的数据结构,通过这里咱们可以知道,这道字节面试题应该就是想要考验我们对于迭代器属性的认识和了解。

迭代器

迭代器的介绍

迭代器是某些数据结构的属性,并不是方法。可以被遍历的数据结构就会有迭代器属性,例如数组、Map和Set等,但是对象没有自带的迭代器属性。

迭代器就是一个对象,这个对象有一个next方法。每次调用next方法时会返回一个包含valuedone键值对的对象,其中value的值为当前迭代到的元素值、done的值为布尔值,当done的值为false表示迭代没有完成,当done的值为true表示迭代完成。

在js中有多种可以被称之为集合的数据解构(arr、obj、set、map),我们希望某些数据解构是可以被迭代的,于是官方就打造了一个属性Iterator,并设定具有Iterator属性的数据结构就是可迭代的。

同时迭代器属性的值必须是一个对象,且对象中必须拥有next方法,该next每次被调用,就会返回一个新对象{done:false,value:1}

手搓一个迭代器

function createIterator(items) {
    var i = 0;
    return {
        next: function () {
            var done = i >= items.length
            var value = !done ? items[i++] : undefined
            return {
                done: done,
                value: value
            }
        }
    }
}

var iterator = createIterator([1, 2, 3])

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

运行结果为:

image-20240713173024347

在手搓的迭代器中运用了闭包的原理。在createIterator方法运行完成后就应该会被垃圾回收机制给回收掉,但是iterator.next方法执行时会访问createIterator方法的执行上下文内的变量i,所以在createIterator方法的执行上下文被回收后仍然会在执行上下文栈上留下一个内存空间存储变量i。每调用一次next方法就会让变量i自增,实现往后遍历。在没有遍历完时调用next方法就会返回{ done: false, value: 当前迭代到的元素值 },在遍历完后再调用next方法就会返回{ done: true, value: undefined }

for...of

在JavaScript中,for...of循环是一种遍历可迭代对象的简洁方式。这个循环结构是在es6中引入的,用来代替传统的for循环或其他循环机制,如forEach,当目标是遍历数组、类数组对象、映射(Maps)、集合(Sets)、字符串以及其他实现迭代器协议的自定义对象时。

语法

for (const variable of iterable) {
  // 执行代码块
}

其中:

  • variable 是一个临时变量,每次迭代都会被赋值为当前的元素。
  • iterable 是任何实现了迭代器协议的可迭代对象。

如何工作

for...of循环会调用可迭代对象的Symbol.iterator方法来获取一个迭代器。然后,它会不断调用迭代器的next方法直到返回的done属性为true,这意味着没有更多的元素可供迭代。在每次迭代中,value属性会被赋给variable

for...of的原理

for...of执行时会对循环的数据结构进行判断,判断该数据结构是否具有可迭代性。

在JavaScript中存在一个特殊的方法——[Symbol.iterator]方法,可以通过调用[Symbol.iterator]方法获取迭代器。因此for...of执行时会判断循环的数据结构是否拥有[Symbol.iterator]方法,进而判断该数据结构是否具有可迭代性。

for...of 遍历的其实是某结构上的迭代器对象

来举个例子

function createIterator(items) {
    var i = 0;
    return {
        next: function () {
            var done = i >= items.length
            var value = !done ? items[i++] : undefined

            return {
                done: done,
                value: value
            }
        }
    }
}

const obj = {
    value: 1
}
obj[Symbol.iterator] = function () {
    return createIterator([1, 2, 3])
}

通过手写一个迭代器和[Symbol.iterator]方法,让obj对象具有可迭代性。可以看出调用[Symbol.iterator]方法返回的值就是一个迭代器(一个对象)。

接下来看看通过给一个对象手写一个迭代器,并且让该对象具有[Symbol.iterator]方法后是否可以执行for...of循环。

for (let value of obj) {
    console.log(value);
}

结果为:

image-20240713194853019

可以看到给对象手写一个迭代器确实可以让它拥有该方法并执行for...of循环。

手写一个函数模拟for...of的逻辑
function forOf(obj, cb) {
    if (!obj[Symbol.iterator]) {
        throw new TypeError(obj + "is not iterable")
    }
    let iterator = obj[Symbol.iterator]()
    let res = iterator.next()
    while (!res.done) {
        cb(res.value)
        res = iterator.next()
    }
}

var colors = ['red', 'green', 'blue']

forOf(colors, function (value) {
    console.log(value);
})

我来给大家解释一下这段代码的意思

  1. 迭代性检查:
    • if (!obj[Symbol.iterator]) {: 检查传入的对象 obj 是否具有 Symbol.iterator 属性,这是所有可迭代对象必须具有的属性。
    • 如果 obj 不可迭代,函数将抛出一个 TypeError,指出该对象不是可迭代的。
  2. 迭代器获取:
    • let iterator = obj[Symbol.iterator](): 获取 obj 的迭代器。由于 obj 已经通过了可迭代性检查,我们可以安全地调用其 Symbol.iterator 方法,该方法返回一个迭代器对象。
  3. 迭代过程:
    • let res = iterator.next(): 调用迭代器的 next 方法,得到一个包含 { value, done } 属性的对象。value 是当前迭代的元素,done 是一个布尔值,表示是否已经迭代完所有元素。
    • while (!res.done) {: 进入一个循环,只要 donefalse,就继续执行循环体。
    • cb(res.value): 在循环体内,将当前元素传递给回调函数 cb 进行处理。
    • res = iterator.next(): 更新 res,以便下一次迭代。
  4. 使用示例:
    • var colors = ['red', 'green', 'blue']: 创建一个颜色数组。
    • forOf(colors, function (value) {: 调用 forOf 函数,传入颜色数组和一个回调函数。
    • console.log(value);: 回调函数将接收到数组中的每一个元素,并将其打印到控制台。

面试题的解

要想解出这道题,除了需要了解迭代器的原理,还需要了解解构的知识。

解构的核心原理

解构的核心原理在于模式匹配和赋值。模式由解构语法定义,它描述了你希望如何从数据结构中提取信息。当模式与数据结构匹配时,相应的值就被提取出来并赋给相应的变量。这种机制简化了数据的提取和使用,使得代码更加清晰和易于维护。

//解构赋值的过程中也涉及到了迭代器
const newArr = ['red', 'black']
const [a, b] = newArr
//解构赋值的逻辑
var iterator = newArr[Symbol.iterator]()
a = iterator.next().value
b = iterator.next().value

通过刚刚上面的报错我们可以知道对象不具有迭代属性,如果面试题中等号右边是一个数组,那就是数组的结构了

let [a, b] = [1,2 ],

数组的解构其实就是靠迭代器属性不断去迭代,把数组里的值一个一个取出来,然后赋值给这个a和这个b.

解构这个语法的真谛并不是真的帮你把数组里的1赋给a,把2赋给b,他是把这个数组的迭代器属性里面的那个迭代对象里面执行一次next得到的value的value值为1赋给变量a,在执行一次next将得到的value值为2赋值给变量b.其实解构的原理是这样的。

回到面试题

要使该解构赋值可以成功,那么就需要给对象手搓一个迭代器,所以咱们就给该对象添加一个[Symbol.iterator]方法,但是不能直接在该1对象里添加,而是在该对象的原型上添加。

Object.prototype[Symbol.iterator] = function () {
    return {}
}
var [a, b] = {a: 1, b: 2}
console.log(a,b);

那返回的迭代器要怎么办呢?有迭代器的数据结构有数组,可以将对象和数组进行关联,然后再通过调用数组的[Symbol.iterator]方法的返回值作为该对象的[Symbol.iterator]方法的返回值。

Object.prototype[Symbol.iterator] = function () {
    return Object.values(this)[Symbol.iterator]()
}
var [a, b] = {a: 1, b: 2}
console.log(a,b);

可以通过Object.values(this)返回一个由对象的键值构成的数组,再通过Object.values(this)[Symbol.iterator]()作为返回值。

来看结果:

image-20240713210326498

nice!成功的将对象里面的属性解构到数组上去啦,解决了这道字节面试题!

总结

解决完这道字节的面试题,也说明了往往大厂最爱考的总是一些基础知识的底层原理。通过这道题,不仅需要了解迭代器的原理知识,还需要掌握解构的核心原理,相信通过今天这道题的学习,你一定收获不少叭!可以点个免费的赞赞嘛,感谢感谢! gitee.com/Luo-zhao-fa