夯实基础-迭代器与生成器

1. 迭代器介绍

在Javascript中,存在很多循环方法,例如数组的forEachmapsome等等。还有更通用的for...in以及for循环。在ES6之前,我们常常使用这些方法完成循环的功能。但这些方法存在明显的缺陷:

  1. 我们需要记住各种各样的api来达到不同的需求。数组上的方法不可以用于遍历对象,尤其在ES6引入MapSet等后,如果再像数组那样将遍历方法放在Map或者Set上,会给开发者带来心智负担。因此需要一个统一的迭代方法,使得迭代更轻松。
  2. ES5的一些方法,例如forEach,使用起来容易令人误解,在forEach中无法使用breakcontinue中断循环。

在Javascript中,可以使用for...of...来使用迭代器:

javascript 复制代码
const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value); // 依次打印1, 2, 3
}

for循环一样,在for...of...中也可以使用continuebreak来中断循环。

javascript 复制代码
const arr = [1, 2, 3];
for (const value of arr) {
  if (value > 2) break; // value为3时跳出循环
  console.log(value); // 依次打印1, 2
}

在Javascript中,以下数据类型上存在迭代器:

  • 数组
  • 字符串
  • Map
  • Set
  • NodeList
  • arguments参数

除了for...of...,还可以使用Array.from解构扩展运算符等方式使用迭代器:

javascript 复制代码
const nodes = Array.from(document.getElementsByTagName('a')); // 将NodeList转成了数组
const [a, b] = [1, 2];
console.log(a, b); // 打印1和2

function fn () {
  console.log([...arguments]); //打印参数列表: [1, 2, 3]
}

fn(1, 2, 3)

2. 迭代器原理

所有迭代器均实现了Symbol.iterator接口,判断一个数据类型是否能够使用迭代器,只需要判断在这个数据上是否存在这个接口即可。

javascript 复制代码
// 判断一个数据是否可以使用迭代器
function isIterable (value) {
    if (value == null) return false;
    return typeof value[Symbol.iterator] === 'function';
}

const arr = [1, 2];
const obj = { a: 1, b: 2};
console.log(`arr能否使用迭代器:${isIterable(arr)}`); // arr能否使用迭代器:true
console.log(`obj能否使用迭代器:${isIterable(obj)}`); // obj能否使用迭代器:false

Symbol.iterator函数返回一个对象(这个对象可以称为迭代器对象),迭代器对象中必须存在一个next方法,next方法执行后返回一个对象,包含donevalue两个属性。done用来表示迭代是否结束,value则是当前迭代的值。

javascript 复制代码
const arr = [1, 2];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: true, value: undefined }
// 迭代结束后,继续执行next始终返回相同的结果
console.log(iterator.next()); // { done: true, value: undefined }

从上面代码可以推断出,迭代器的迭代原理就是依次执行next方法,直到done为true时结束迭代。基于这个原理,我们可以给一些不存在内置迭代器的数据类型添加迭代器:

javascript 复制代码
Object.prototype[Symbol.iterator] = function () {
  const obj = this;
  const keys = Object.keys(obj);
  let i = 0;
  return {
      next () {
          return {
              done: i >= keys.length,
              value: obj[keys[i ++]]
          }
      }
  }
}

const obj = { a: 1, b: 2 };
for (const value of obj) {
    console.log(value); // 依次打印1, 2
}

除了next方法,迭代器对象中还可以存在return方法,执行return方法必须返回一个对象。大部分内置数据类型的迭代器中都不存在return方法(后面要讲的生成器对象中存在return方法),当使用breakcontinue中断迭代,或者使用解构时,会触发return方法执行(如果存在的话),也可以直接执行return方法中断迭代。

javascript 复制代码
const obj = {
  a: 1,
  b: 2,
  [Symbol.iterator] () {
      let i = 0;
      const self = this;
      const keys = Object.keys(self);
      
      return {
          next () {
              return {
                  done: i >= keys.length,
                  value: self[keys[i ++]]
              }
          },
          return () {
              console.log('触发了return方法');
              return { done: true, value: undefined } // 必须返回一个对象,否则会报错
          }
      }
  }
};

for (const v of obj) {
    if (v === 2) break; // 触发了return方法
}

const [a] = obj; // 触发了return方法

内置的迭代器对象中,也实现了迭代器接口,且迭代器方法返回的对象等于当前迭代器对象:

javascript 复制代码
const arr = [1, 2];
const it = arr[Symbol.iterator]();
console.log(typeof it[Symbol.iterator] === 'function'); // true
console.log(it[Symbol.iterator]() === it); // true

因此也可以迭代这个对象遍历数组值:

javascript 复制代码
const arr = [1, 2];
const it = arr[Symbol.iterator]();
for (const v of it) {
    console.log(v); // 1, 2
}

如果中断了迭代,下次迭代从中断的下一个位置继续迭代:

javascript 复制代码
const arr = [1, 2, 3, 4];
const it = arr[Symbol.iterator]();
let v;

for (v of it) {
    console.log(v); // 1, 2
    if (v === 2) break;
}

for (v of it) {
    console.log(v); // 3, 4
}

3. 使用迭代器

  1. 访问SetMap的第一项:
javascript 复制代码
const set = new Set([1, 2]);
const map = new Map([
    ['name', 'zhangsan'],
    ['age', 18]
]);

console.log(set[Symbol.iterator]().next().value); // 1
console.log(map.entries()[Symbol.iterator]().next().value); // ['name', 'zhangsan']
  1. 自定义迭代器
javascript 复制代码
class Company {
    #members = [];
    constructor (name) {
        this.name = name;
    }
    
    addMember (member) {
        this.#members.push(member);
        return this;
    }
    
    [Symbol.iterator] () {
        let i = 0;
        const self = this;
        
        return {
            next () {
                return {
                    done: i >= self.#members.length,
                    value: self.#members[i ++]
                }
            }
        }
    }
}

const baidu = new Company('baidu');
baidu.addMember('zhangsan').addMember('lisi');
for (const member of baidu) {
    console.log(member); // 'zhangsan', 'lisi'
}

4. 生成器介绍

生成器是一种可以暂停执行的函数,使用yield关键字暂停执行函数,使用next恢复函数执行:

javascript 复制代码
function * fn () {
    console.log('start');
    yield 1;
    console.log('step1');
    yield 2;
    console.log('step2');
    yield 3;
    console.log('end');
}

const gen = fn();
gen.next(); // 'start'
gen.next(); // 'step1'
gen.next(); // 'step2';
gen.next(); // 'end'

生成器函数返回一个生成器对象,包含nextreturnthrow方法。和迭代器一样,next执行后返回一个包含donevalue的对象,生成器函数的返回值会作为最后一次迭代结果的value值:

javascript 复制代码
function * fn () {
    yield 1;
    yield 2;
    yield 3;
    return 4;
}

const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.next()); // { done: false, value: 2 }
console.log(gen.next()); // { done: false, value: 3 }
console.log(gen.next()); // { done: true, value: 4 }
console.log(gen.next()); // { done: true, value: undefined }

生成器对象也实现了迭代器接口:

javascript 复制代码
function * fn () {
    yield 1;
    yield 2;
    yield 3;
}

const gen = fn();
const it = gen[Symbol.iterator]();
console.log(gen === it); // true

for (const value of gen) {
    console.log(value); // 依次打印1, 2, 3
}

next函数接收一个参数,作为上一次yield的返回值:

javascript 复制代码
function * fn () {
    console.log(yield 1);
    console.log(yield 2);
    console.log(yield 3);
}

const gen = fn();
gen.next(0); // 首次执行,没有上一个yield,因此参数被忽略
gen.next(1); // 打印1
gen.next(2); // 打印2
gen.next(3); // 打印3

return函数可以中断迭代,接受一个参数,作为return函数的返回值中的value

javascript 复制代码
function * fn () {
    console.log(yield 1);
    console.log(yield 2);
    console.log(yield 3);
}

const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.return('end')); // { done: false, value: 'end' }
console.log(gen.next()); // { done: true, value: undefined }

throw函数也可以中断迭代,同时抛出一个错误,throw函数接受一个参数作为错误原因:

javascript 复制代码
function * fn () {
   yield 1;
   yield 2;
}

const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
gen.throw('error: stop'); // Uncaught error: stop

如果在生成器中使用try...catch...捕获了这个错误,则迭代不会中断,而是跳过当前yield(实际在chrome中,迭代被直接结束了):

javascript 复制代码
function * fn () {
    try {
        yield 1;
        yield 2;
        yield 3;
    } catch (e) {
        console.log(e);
    }
}

const gen = fn();

// 以下为chrome中执行结果,与红宝书描述不一致
console.log(gen.next()); // { done: false, value: 1 }
gen.throw('error: stop'); // error: stop
console.log(gen.next()); // { done: true, value: undefined }
console.log(gen.next()); // { done: true, value: undefined }

如果生成器还未开始执行就调用了throw方法,则相当于在函数外部抛出错误,会中断代码执行:

javascript 复制代码
function * fn () {
    try {
        yield 1;
        yield 2;
        yield 3;
    } catch (e) {
        console.log(e);
    }
}

const gen = fn();

// 以下为chrome中执行结果
gen.throw('error: stop'); // 代码执行被中断,并抛出错误:Uncaught error: stop
// 下面代码不会执行
console.log(gen.next());
console.log(gen.next());

生成器中还可以使用yield *直接消费一个可迭代对象:

javascript 复制代码
function * fn () {
    yield * [1, 2, 3];
    // 等价于
    // yield 1;
    // yield 2;
    // yield 3;
}

for (const v of fn()) {
    console.log(v); // 依次执行1, 2, 3
}

5. 使用生成器

生成器最大的作用在于可以中断代码执行,利用这个特性可以实现类似async await的效果。事实上来说,async await就是一个底层语法糖,内部实现原理就是利用了生成器。可以查看co这个库了解生成器的使用。

javascript 复制代码
import co from 'co';

co(function *(){
  // yield any promise
  var result = yield Promise.resolve(true);
}).catch(onerror);

co(function *(){
  // resolve multiple promises in parallel
  var a = Promise.resolve(1);
  var b = Promise.resolve(2);
  var c = Promise.resolve(3);
  var res = yield [a, b, c];
  console.log(res);
  // => [1, 2, 3]
}).catch(onerror);
相关推荐
鱼樱前端5 分钟前
2025前端跨窗口通信最佳实践(多种方案选择参考)
前端·javascript
前端开发爱好者8 分钟前
Vue 3.6 将正式进入「无虚拟 DOM」时代!
前端·javascript·vue.js
Mike_jia15 分钟前
Snapdrop:开源跨平台文件传输的革命者——从极简部署到企业级实战全解析
前端
晴殇i16 分钟前
一行代码生成绝对唯一 ID:告别 Date.now() 的不可靠方案
前端·javascript·面试
徐小夕20 分钟前
pxcharts-pro, 支持百万数据渲染的多维表格编辑器
前端·javascript·vue.js
独立开阀者_FwtCoder22 分钟前
Vue 3.6 将正式进入「无虚拟 DOM」时代!
前端·javascript·后端
山河木马28 分钟前
前端学C++可太简单了:-> 操作符
前端·javascript·c++
独立开阀者_FwtCoder34 分钟前
前端开发的你,其实并没有真的掌握img标签!
前端·vue.js·github
Deng94520131439 分钟前
快捷订餐系统
前端·css·css3
十五_在努力42 分钟前
参透JavaScript —— 花十分钟搞懂作用域
前端·javascript