渐深学习中,让解构成立引发的思考

219 阅读8分钟

整理了一些对比论和考点,可直接移步知识点梳理

面试原题

// 让下面的代码成立
var [a, b] = {
    a: 3,
    b: 4
}
console.log(a, b) // 3, 4

我们先直接执行这段代码,会得到什么呢

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

答案是会报错,因为对象不是可迭代的,所以解构赋值会报错

引发第一个思考

什么是可迭代对象

满足可迭代协议的对象,就是可迭代对象。

可迭代协议

  • 对象属性有 Symbol.iterator, 值是函数,返回一个迭代器,迭代器的属性有 next 方法,返回一个对象,对象有 done 和 value 两个属性,done 表示是否迭代完成,value 表示迭代的值
  • 一个对象既可以是 可迭代对象(拥有[Symbol.iterator]),又可以是迭代器(自身含有next()方法)。
{
    [Symbol.iterator]: function (){
        return {
            next:function () {
                return {
                    value: value/undefined,
                    done: true/false => 完成/未完成
                }
            }
        }
    }
}

由此我们可以进行尝试,将题目的对象变成一个可迭代对象,然后解构赋值

var [a, b] = {
    a: 3,
    b: 4,
    [Symbol.iterator]: function () {
        // 利用数组的迭代器
        return Object.values(this)[Symbol.iterator]()
    }
}
console.log(a, b)

题目的要求不能改变源代码,所以我们可以在对象原型上添加 Symbol.iterator 属性,开始尝试

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

在 ES6 中,还有一个生成器,也可用于此

Object.prototype[Symbol.iterator] = function* () {
    yield* Object.values(this)
}

ES6 内建可迭代类型

  • Array、 String、 Map、 Set、 arguments、 NodeList DOM 集合
  • 解构、for of、展开运算符(...)
  • ECMAScript 的迭代机制基于可迭代协议统一了对数据结构的访问方式,使得各种集合类型数据结构可以通过一致的语法进行遍历。

追加下一个问题,什么是迭代器

此处文献来自掘金

在 JavaScrip 中有非常多的集合,比如数组、set、map等,他们是用来存放数据的集合。数据必然存在遍历,通常会使用for来遍历,就需要书写比较长的代码。官方就提供了一个属性 Iterable,并设定具有 Iterable 属性的数据结构就是可迭代的。

手搓一个迭代器

了解内部结构,助于理解迭代器原理

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

let iterator = MyIterator([1, 2, 3]);
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }

for of

只要是拥有迭代器属性的数据结构,那么他就可以被 for of 遍历,也就是说我们只要在某个数据结构上添加迭代器属性,那么这个数据结构就可以被 for of 遍历。

for of 遍历的机制就是,先获取迭代器,然后通过 next 方法获取迭代的值,直到迭代完成,for of 遍历结束。

我们来看这段代码

let obj = {
    value: 1
}
for (let item of obj) {
    console.log(item)
}

这段代码的执行结果会报错,obj is not iterable, 这点其实也证实了 for of 遍历的是这个数据结构身上的迭代器属性,我们尝试给 obj 添加迭代器属性

let obj = {
    value: 1,
}
obj[Symbol.iterator] = function () {
    return MyIterator([1, 2])
}
for (let item of obj) {
    console.log(item) // 1, 2
}

可以看到 obj 可以被 for of 遍历了,但是并不是遍历的 Obj 身上的属性,而是传进去的数组,因为 for of 遍历的是 obj 上的迭代器对象。

继续手撕一个 for of

for of 首先检索对象身上是否有迭代器属性,如果没有则会报错,如果有,则通过 next 方法获取迭代的值,直到迭代完成,for of 遍历结束。

function myForOf(obj, callback) {
    if (!obj[Symbol.iterator]) {
        throw new Error(obj + ' is not iterable');
    }
    let iterator = obj[Symbol.iterator]();
    let item = iterator.next();
    while (!item.done) {
        callback(item.value);
        item = iterator.next();
    }
}

let arr = [1, 2, 3];
myForOf(arr, (item) => {
    console.log(item);
})

持续追问,什么是生成器(Generator)

  • 生成器是 ES6 引入的一种特殊函数形式,具备“暂停执行”和“恢复执行”的能力。它不是一次性执行到底,而是通过 yield 分段执行,由调用者控制流程。
  • 生成器的本质是状态机,其内部维护着上下文信息,在多个 yield 之间保持函数状态,是构建复杂控制流(如携程、异步流程控制)的基础

基本语法

1.定义生成器函数
function* generatorFunction() {
    yield 'hello';
    yield 'world';
}
  • 使用 function* 定义
  • 使用 yield 暂停和返回数据
2. 调用方式
let generator = generatorFunction();
console.log(generator.next()); // { value: 'hello', done: false }
console.log(generator.next()); // { value: 'world', done: false }
console.log(generator.next()); // { value: undefined, done: true }

生成器的工作机制

关键词说明
yield暂停函数执行,返回数据
next()恢复函数执行,传入数据给上一个 yield 表达式
done表示函数是否执行完毕
return()提前结束生成器,并返回值
throww()向生成器内部抛出异常

示例:带参数的 .next()

function* calc() {
    const x = yield 10;
    const y = yield x + 5;
    return y;
}

const it = calc();
console.log(it.next()); // { value: 10, done: false }
console.log(it.next(20)); // { value: 25, done: false }
console.log(it.next(99)); // { value: 99, done: true } 

生成器 vs 普通函数

特性普通函数生成器函数
是否可以中断执行是(yield 暂停)
是否可多次返回值是(通过多次 yield)
返回值类型任意Iterator 迭代器对象
控制权函数内部调用方可控制执行流程

生成器的用途与应用场景

  • 惰性求值/无线序列

    节省内存,在需要的时候再生成结果

    function* fibonacci() {
      let i = 0;
      while (true) yield i++;
    }
    
  • 自定义迭代器

    const MyIterator = {
      *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;  
      }
    }
    
  • 控制异步流程

    function* getData() {
      const user = yield fetchUser();
      const posts = yield fetchPosts(user.id);
    }
    

常见面试高频问题

生成器和 async/await 的区别
特性Generatorasync/await
本质可中断函数异步语法糖
是否返回 Promise
控制流程手动调用 .next()自动执行
场景同步流程、数据流异步流程
yield*的作用?

委托执行另一个生成器或可迭代对象

function* inner() {
    yield 1;
    yield 2;
}

function* outer() {
    yield 'start';
    yield* inner(); // 委托 inner 的全部 yield
    yield 'end';
}

注意事项

  • 生成器不能用箭头函数(function* 是关键字)
  • 生成器本质是 “暂停式函数”,但每次恢复执行时上下文仍然保留
  • yield 只能在生成器函数中使用
  • 每次调用 next() 都会推进一次 yield
  • 可以使用 return() 提前终止生成器。
  • throw() 可以向生成器内部抛异常。

生成器速记口诀

function 星星起手式,yield 中断给值时;
next 叫醒继续跑,return throw 提前跑;
惰性状态都能搞,async 之前它称王。

生成器与迭代器

生成器是“自动构建迭代器的工厂函数”

function* gen() {
    yield 1;
    yield 2;
}

const it = gen();
typeof it.next === 'function'; // true
it[Symbol.iterator] === it; // true
  • 生成器返回的对象天然遵守迭代器协议
  • 同时也是可迭代对象

生成器 + 迭代器组合使用场景

实现复杂自定义迭代器(优雅替代next()写法
const obj = {
    data: [1, 2, 3],
    * [Symbol.iterator]() {
        for (let item of this.data) {
            yield item * 2;
        }
    }
}

for (let item of obj) {
    console.log(item); // 2, 4, 6
}
用生成器实现延迟计算(惰性求值)
function* lazyMap(arr, fn) {
    for (let item of arr) {
        yield fn(item);
    }
}

const square = lazyMap([1, 2, 3], x => x * x);

for (let item of square) {
    console.log(item); // 1, 4, 9
}
避免无限循环内存爆炸
function* forMap() {
    let i = 0;
    while (true) {
        yield i++;
    }
}

// 通过 for of 控制迭代数量

let count = 0;
for (let item of forMap()) {
    if (count++ > 10) break;
    console.log(item);
}
多层遍历嵌套 yield* 委托
function* gen() {
    yield 1;
    yield* inner();
    yield 3;
}

function* inner() {
    yield 2;
}

for (let item of gen()) {
    console.log(item); // 1, 2, 3
}

yield* 可委托数组、Set、Map、字符串、生成器等可迭代对象

知识点梳理

一、基础识记类

问题答案要点
什么是可迭代协议?实现了 Symbol.iterator() 方法,返回一个迭代器。
什么是迭代器协议?拥有 .next() 方法,返回 { value, done }
for...of 要求什么?对象必须是 iterable(实现 Symbol.iterator())。
Symbol.iteratornext() 区别?Symbol.iterator() 返回迭代器,.next() 控制执行。
如何判断可迭代?typeof obj[Symbol.iterator] === 'function'
yield 作用?暂停执行,向外返回值,可双向通信。

二、 原理机制类

问题答案要点
生成器 vs 普通函数?可暂停、返回迭代器、使用 yield
为什么生成器是状态机?yield 为状态切换点,每次 .next() 推进状态。
yield.next() 怎么通信?.next(val)val 作为上一个 yield 的返回值。
yield* 作用?委托另一个可迭代对象的迭代过程。
手动实现生成器执行器?递归调用 .next() 推进流程。
为什么说生成器是迭代器工厂?返回对象即是迭代器,实现自定义控制流程。

三、 实战应用类

应用场景示例
无限整数流function* infinite() { let i = 0; while(true) yield i++; }
惰性 mapfunction* lazyMap(arr, fn) { for (let a of arr) yield fn(a); }
给对象加迭代能力*[Symbol.iterator]() { for (let d of this.data) yield d; }
分帧执行for (let i=0; i<10000; i++) { yield; }
模拟 async/await使用 run(gen) 推进器配合 Promise 实现异步控制

四、 注意事项类

问题答案要点
生成器能用箭头函数吗?不能,箭头函数不支持 yield
yield 可在普通函数中用吗?不可,语法错误。
.return().throw().return(val) 结束迭代;.throw(err) 抛异常。
提前终止生成器后果?后续 yield 不再执行,注意资源清理(用 finally)。

五、对比迁移类

生成器 vs async 函数
对比项生成器async 函数
暂停方式yieldawait
执行控制手动 .next()自动推进
异步控制需结合执行器和 Promise内建异步控制
返回值迭代器对象Promise
生成器 vs Promise
对比项生成器Promise
控制的是执行流程异步值
执行方式手动推进自动执行
状态多次暂停/恢复单次完成/拒绝
可迭代对象 vs 类数组对象
对比项可迭代对象类数组对象
是否可 for...of不可(除非手动实现)
是否有 Symbol.iterator不可
是否有索引和 length不一定一定有