0
点赞
收藏
分享

微信扫一扫

【JS】132-重温基础:语法和数据类型

GG_lyf 2022-11-29 阅读 180

【JS】132-重温基础:语法和数据类型_迭代

本文是 重温基础 系列文章的第十三篇。
今日感受:每次自我年终总结,都会有各种情绪和收获。

本章节复习的是JS中的迭代器和生成器,常常用来处理集合。

前置知识:
JavaScrip已经提供多个迭代集合的方法,从简单的 ​​for​​循环到 ​map()​​和 ​filter()​​。
迭代器和生成器将迭代的概念直接带入核心语言,并提供一种机制来自定义 ​​for...of​循环的行为。

本文会将知识点分为两大部分,简单介绍和详细介绍
简单介绍,适合基础入门会使用的目标;
详细介绍,会更加深入的做介绍,适合理解原理;

1. 概述

当我们使用循环语句迭代数据时,需初始化一个变量来记录每一次迭代在数据集合中的位置:

  1. ​let a = ["aaa","bbb","ccc"];​
  2. ​for (let i = 0; i< a.length; i++){​
  3. ​    console.log(a[i]);​
  4. ​}​

这边的 ​i​就是我们用来记录迭代位置的变量,但是在ES6开始,JavaScrip引入了迭代器这个特性,并且新的数组方法新的集合类型(如 ​Set集合​​与 ​Map集合​)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的 ​for-of循环​​、展开运算符( ​...​),甚至连异步编程都可以使用迭代器。

本文主要会介绍ES6中新增的迭代器(Iterator)和生成器(Generator)。

2. 迭代器(简单介绍)

迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个 ​next()​​方法,每次调用都会返回一个结果对象。
这个结果对象,有两个属性:

  • value​: 表示下一个将要返回的值。
  • done​​: 一个布尔值,若没有更多可返回的数据时,值为 ​true​,否则 ​false​。

如果最后一个值返回后,再调用 ​next()​​,则返回的对象的 ​done​​值为 ​true​​,而 ​value​​值如果没有值的话,返回的为 ​undefined​。

ES5实现一个迭代器:

  1. ​function myIterator(list){​
  2. ​    var i = 0;​
  3. ​    return {​
  4. ​        next: function(){​
  5. ​            var done = i >= list.length;​
  6. ​            var value = !done ? list[i++] : undefined;​
  7. ​            return {​
  8. ​                done : done,​
  9. ​                value : value​
  10. ​            }​
  11. ​        }​
  12. ​    }​
  13. ​}​

  14. ​var iterator = myIterator([1,2,3]);​
  15. ​iterator.next();  // "{done: false, value: 1}"​
  16. ​iterator.next();  // "{done: false, value: 2}"​
  17. ​iterator.next();  // "{done: false, value: 3}"​
  18. ​iterator.next();  // "{done: true, value: undefined}"​
  19. ​// 以后的调用都一样​
  20. ​iterator.next();  // "{done: true, value: undefined}"​

从上面代码可以看出,ES5的实现还是比较麻烦,而ES6新增的生成器,可以使得创建迭代器对象的过程更加简单。

3. 生成器(简单介绍)

生成器是一种返回迭代器的函数,通过 ​function​​关键字后的星号( ​*​​)来表示,函数中会用到新的关键字 ​yield​​。星号可以紧挨着 ​function​关键字,也可以在中间添加一个空格。

  1. ​function *myIterator(){​
  2. ​    yield 1;​
  3. ​    yield 2;​
  4. ​    yield 3;​
  5. ​}​
  6. ​let iterator = myIterator();​
  7. ​iterator.next();  // "{done: false, value: 1}"​
  8. ​iterator.next();  // "{done: false, value: 2}"​
  9. ​iterator.next();  // "{done: false, value: 3}"​
  10. ​iterator.next();  // "{done: true, value: undefined}"​
  11. ​// 以后的调用都一样​
  12. ​iterator.next();  // "{done: true, value: undefined}"​

生成器函数最有趣的部分是,每当执行完一条 ​yield​​语句后函数就会自动停止执行,比如上面代码,当 ​yield1;​​执行完后,便不会执行任何语句,而是等到再调用迭代器的 ​next()​​方法才会执行下一个语句,即 ​yield2;​​.
使用 ​​yield​关键字可以返回任何值和表达式,因为可以通过生成器函数批量给迭代器添加元素:

  1. ​function *myIterator(list){​
  2. ​    for(let  i = 0; i< list.length ; i ++){​
  3. ​        yield list[i];​
  4. ​    }​
  5. ​}​

  6. ​var iterator = myIterator([1,2,3]);​
  7. ​iterator.next();  // "{done: false, value: 1}"​
  8. ​iterator.next();  // "{done: false, value: 2}"​
  9. ​iterator.next();  // "{done: false, value: 3}"​
  10. ​iterator.next();  // "{done: true, value: undefined}"​
  11. ​// 以后的调用都一样​
  12. ​iterator.next();  // "{done: true, value: undefined}"​

生成器的适用返回很广,可以将它用于所有支持函数使用的地方。

4. 迭代器(详细介绍)

4.1 Iterator迭代器概念

Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成迭代操作(即依次处理该数据结构的所有成员)。

Iterator三个作用

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • Iterator 接口主要供ES6新增的 ​for...of​消费;

4.2 Iterator迭代过程

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,迭代器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的 ​next​方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的 ​next​方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的 ​next​方法,直到它指向数据结构的结束位置。

每一次调用 ​next​​方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 ​value​​和 ​done​两个属性的对象。

  • value​属性是当前成员的值;
  • done​属性是一个布尔值,表示迭代是否结束;

模拟 ​next​方法返回值:

  1. ​let f = function (arr){​
  2. ​    var nextIndex = 0;​
  3. ​    return {​
  4. ​        next:function(){​
  5. ​            return nextIndex < arr.length ?​
  6. ​            {value: arr[nextIndex++], done: false}:​
  7. ​            {value: undefined, done: true}​
  8. ​        }​
  9. ​    }​
  10. ​}​

  11. ​let a = f(['a', 'b']);​
  12. ​a.next(); // { value: "a", done: false }​
  13. ​a.next(); // { value: "b", done: false }​
  14. ​a.next(); // { value: undefined, done: true }​

4.3 默认Iterator接口

若数据可迭代,即一种数据部署了Iterator接口。
ES6中默认的Iterator接口部署在数据结构的 ​​Symbol.iterator​​属性,即如果一个数据结构具有 ​Symbol.iterator​属性,就可以认为是可迭代
​​Symbol.iterator​​属性本身是函数,是当前数据结构默认的迭代器生成函数。执行这个函数,就会返回一个迭代器。至于属性名 ​Symbol.iterator​​,它是一个表达式,返回 ​Symbol​​对象的 ​iterator​属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。

原生具有Iterator接口的数据结构有

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

4.4 Iterator使用场景

  • (1)解构赋值
    对数组和 ​​Set​​ 结构进行解构赋值时,会默认调用 ​Symbol.iterator​方法。
  1. ​let a = new Set().add('a').add('b').add('c');​
  2. ​let [x, y] = a;       // x = 'a'  y = 'b'​
  3. ​let [a1, ...a2] = a;  // a1 = 'a' a2 = ['b','c']​
  • (2)扩展运算符
    扩展运算符( ​​...​)也会调用默认的 Iterator 接口。
  1. ​let a = 'hello';​
  2. ​[...a];            //  ['h','e','l','l','o']​

  3. ​let a = ['b', 'c'];​
  4. ​['a', ...a, 'd'];  // ['a', 'b', 'c', 'd']​
  • (2)yield*
    ​​yield*​后面跟的是一个可迭代的结构,它会调用该结构的迭代器接口。
  1. ​let a = function*(){​
  2. ​    yield 1;​
  3. ​    yield* [2,3,4];​
  4. ​    yield 5;​
  5. ​}​

  6. ​let b = a();​
  7. ​b.next() // { value: 1, done: false }​
  8. ​b.next() // { value: 2, done: false }​
  9. ​b.next() // { value: 3, done: false }​
  10. ​b.next() // { value: 4, done: false }​
  11. ​b.next() // { value: 5, done: false }​
  12. ​b.next() // { value: undefined, done: true }​
  • (4)其他场合
    由于数组的迭代会调用迭代器接口,所以任何接受数组作为参数的场合,其实都调用了迭代器接口。下面是一些例子。
  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如 ​newMap([['a',1],['b',2]])​)
  • Promise.all()
  • Promise.race()

4.5 for...of循环

只要数据结构部署了 ​Symbol.iterator​​属性,即具有 iterator 接口,可以用 ​for...of​​循环迭代它的成员。也就是说, ​for...of​​循环内部调用的是数据结构的 ​Symbol.iterato​​方法。
使用场景
​​for...of​可以使用在数组 ​Set​和 ​Map​结构类数组对象Genetator对象字符串

  • 数组
    ​​for...of​​循环可以代替数组实例的 ​forEach​方法。
  1. ​let a = ['a', 'b', 'c'];​
  2. ​for (let k of a){console.log(k)}; // a b c​

  3. ​a.forEach((ele, index)=>{​
  4. ​    console.log(ele);    // a b c​
  5. ​    console.log(index);  // 0 1 2 ​
  6. ​})​

与 ​for...in​​对比, ​for...in​​只能获取对象键名,不能直接获取键值,而 ​for...of​允许直接获取键值。

  1. ​let a = ['a', 'b', 'c'];​
  2. ​for (let k of a){console.log(k)};  // a b c​
  3. ​for (let k in a){console.log(k)};  // 0 1 2​
  • Set和Map
    可以使用数组作为变量,如 ​​for(let[k,v]of b){...}​。
  1. ​let a = new Set(['a', 'b', 'c']);​
  2. ​for (let k of a){console.log(k)}; // a b c​

  3. ​let b = new Map();​
  4. ​b.set('name','leo');​
  5. ​b.set('age', 18);​
  6. ​b.set('aaa','bbb');​
  7. ​for (let [k,v] of b){console.log(k + ":" + v)};​
  8. ​// name:leo​
  9. ​// age:18​
  10. ​// aaa:bbb​
  • 类数组对象
  1. ​// 字符串​
  2. ​let a = 'hello';​
  3. ​for (let k of a ){console.log(k)}; // h e l l o​

  4. ​// DOM NodeList对象​
  5. ​let b = document.querySelectorAll('p');​
  6. ​for (let k of b ){​
  7. ​    k.classList.add('test');​
  8. ​}​

  9. ​// arguments对象​
  10. ​function f(){​
  11. ​    for (let k of arguments){​
  12. ​        console.log(k);​
  13. ​    }​
  14. ​}​
  15. ​f('a','b'); // a b​
  • 对象
    普通对象不能直接使用 ​​for...of​会报错,要部署Iterator才能使用。
  1. ​let a = {a:'aa',b:'bb',c:'cc'};​
  2. ​for (let k in a){console.log(k)}; // a b c​
  3. ​for (let k of a){console>log(k)}; // TypeError​

4.6 跳出for...of

使用 ​break​来实现。

  1. ​for (let k of a){​
  2. ​    if(k>100)​
  3. ​        break;​
  4. ​    console.log(k);​
  5. ​}​

5. 生成器(详细介绍)

5.1 基本概念

Generator​​生成器函数是一种异步编程解决方案。
原理
执行 ​​Genenrator​​函数会返回一个遍历器对象,依次遍历 ​Generator​​函数内部的每一个状态。
​​Generator​函数是一个普通函数,有以下两个特征:

  • function​关键字与函数名之间有个星号;
  • 函数体内使用 ​yield​表达式,定义不同状态;

通过调用 ​next​​方法,将指针移向下一个状态,直到遇到下一个 ​yield​​表达式(或 ​return​​语句)为止。简单理解, ​Generator​​函数分段执行, ​yield​​表达式是暂停执行的标记,而 ​next​恢复执行。

  1. ​function * f (){​
  2. ​    yield 'hi';​
  3. ​    yield 'leo';​
  4. ​    return 'ending';​
  5. ​}​
  6. ​let a = f();​
  7. ​a.next();  // {value: 'hi', done : false}​
  8. ​a.next();  // {value: 'leo', done : false}​
  9. ​a.next();  // {value: 'ending', done : true}​
  10. ​a.next();  // {value: undefined, done : false}​

5.2 yield表达式

yield​​表达式是暂停标志,遍历器对象的 ​next​方法的运行逻辑如下:

  1. 遇到 ​yield​​就暂停执行,将这个 ​yield​后的表达式的值,作为返回对象的 ​value​属性值。
  2. 下次调用 ​next​​往下执行,直到遇到下一个 ​yield​。
  3. 直到函数结束或者 ​return​​为止,并返回 ​return​语句后面表达式的值,作为返回对象的 ​value​属性值。
  4. 如果该函数没有 ​return​​语句,则返回对象的 ​value​为 ​undefined​ 。

注意:

  • yield​​只能用在 ​Generator​函数里使用,其他地方使用会报错。
  1. ​// 错误1​
  2. ​(function(){​
  3. ​    yiled 1;  // SyntaxError: Unexpected number​
  4. ​})()​

  5. ​// 错误2  forEach参数是个普通函数​
  6. ​let a = [1, [[2, 3], 4], [5, 6]];​
  7. ​let f = function * (i){​
  8. ​    i.forEach(function(m){​
  9. ​        if(typeof m !== 'number'){​
  10. ​            yield * f (m);​
  11. ​        }else{​
  12. ​            yield m;​
  13. ​        }​
  14. ​    })​
  15. ​}​
  16. ​for (let k of f(a)){​
  17. ​    console.log(k)​
  18. ​}​
  • yield​表达式如果用于另一个表达式之中,必须放在圆括号内。
  1. ​function * a (){​
  2. ​    console.log('a' + yield);     //  SyntaxErro​
  3. ​    console.log('a' + yield 123); //  SyntaxErro​
  4. ​    console.log('a' + (yield));     //  ok​
  5. ​    console.log('a' + (yield 123)); //  ok​
  6. ​}​
  • yield​表达式用做函数参数或放在表达式右边,可以不加括号
  1. ​function * a (){​
  2. ​    f(yield 'a', yield 'b');    //  ok​
  3. ​    lei i = yield;              //  ok​
  4. ​}​

5.3 next方法

yield​​本身没有返回值,或者是总返回 ​undefined​​, ​next​​方法可带一个参数,作为上一个 ​yield​表达式的返回值。

  1. ​function * f (){​
  2. ​    for (let k = 0; true; k++){​
  3. ​        let a = yield k;​
  4. ​        if(a){k = -1};​
  5. ​    }​
  6. ​}​
  7. ​let g =f();​
  8. ​g.next();    // {value: 0, done: false}​
  9. ​g.next();    // {value: 1, done: false}​
  10. ​g.next(true);    // {value: 0, done: false}​

这一特点,可以让 ​Generator​函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。

  1. ​function * f(x){​
  2. ​    let y = 2 * (yield (x+1));​
  3. ​    let z = yield (y/3);​
  4. ​    return (x + y + z);​
  5. ​}​
  6. ​let a = f(5);​
  7. ​a.next();   // {value : 6 ,done : false}​
  8. ​a.next();   // {value : NaN ,done : false}  ​
  9. ​a.next();   // {value : NaN ,done : true}​
  10. ​// NaN因为yeild返回的是对象 和数字计算会NaN​

  11. ​let b = f(5);​
  12. ​b.next();     // {value : 6 ,done : false}​
  13. ​b.next(12);   // {value : 8 ,done : false}​
  14. ​b.next(13);   // {value : 42 ,done : false}​
  15. ​// x 5 y 24 z 13​

5.4 for...of循环

for...of​​循环会自动遍历,不用调用 ​next​​方法,需要注意的是, ​for...of​​遇到 ​next​​返回值的 ​done​​属性为 ​true​​就会终止, ​return​​返回的不包括在 ​for...of​循环中。

  1. ​function * f(){​
  2. ​    yield 1;​
  3. ​    yield 2;​
  4. ​    yield 3;​
  5. ​    yield 4;​
  6. ​    return 5;​
  7. ​}​
  8. ​for (let k of f()){​
  9. ​    console.log(k);​
  10. ​}​
  11. ​// 1 2 3 4  没有 5 ​

5.5 Generator.prototype.throw()

throw​方法用来向函数外抛出错误,并且在Generator函数体内捕获。

  1. ​let f = function * (){​
  2. ​    try { yield }​
  3. ​    catch (e) { console.log('内部捕获', e) }​
  4. ​}​

  5. ​let a = f();​
  6. ​a.next();​

  7. ​try{​
  8. ​    a.throw('a');​
  9. ​    a.throw('b');​
  10. ​}catch(e){​
  11. ​    console.log('外部捕获',e);​
  12. ​}​
  13. ​// 内部捕获 a​
  14. ​// 外部捕获 b​

5.6 Generator.prototype.return()

return​​方法用来返回给定的值,并结束遍历Generator函数,如果 ​return​​方法没有参数,则返回值的 ​value​​属性为 ​undefined​。

  1. ​function * f(){​
  2. ​    yield 1;​
  3. ​    yield 2;​
  4. ​    yield 3;​
  5. ​}​
  6. ​let g = f();​
  7. ​g.next();          // {value : 1, done : false}​
  8. ​g.return('leo');   // {value : 'leo', done " true}​
  9. ​g.next();          // {value : undefined, done : true}​

5.7 next()/throw()/return()共同点

相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换 ​yield​表达式。

  • next()​​将 ​yield​表达式替换成一个值。
  1. ​let f = function * (x,y){​
  2. ​    let r = yield x + y;​
  3. ​    return r;​
  4. ​}​
  5. ​let g = f(1, 2); ​
  6. ​g.next();   // {value : 3, done : false}​
  7. ​g.next(1);  // {value : 1, done : true}​
  8. ​// 相当于把 let r = yield x + y;​
  9. ​// 替换成 let r = 1;​
  • throw()​​将 ​yield​表达式替换成一个 ​throw​语句。
  1. ​g.throw(new Error('报错'));  // Uncaught Error:报错​
  2. ​// 相当于将 let r = yield x + y​
  3. ​// 替换成 let r = throw(new Error('报错'));​
  • next()​​将 ​yield​表达式替换成一个 ​return​语句。
  1. ​g.return(2); // {value: 2, done: true}​
  2. ​// 相当于将 let r = yield x + y​
  3. ​// 替换成 let r = return 2;​

5.8 yield* 表达式

用于在一个Generator中执行另一个Generator函数,如果没有使用 ​yield*​会没有效果。

  1. ​function * a(){​
  2. ​    yield 1;​
  3. ​    yield 2;​
  4. ​}​
  5. ​function * b(){​
  6. ​    yield 3;​
  7. ​    yield * a();​
  8. ​    yield 4;​
  9. ​}​
  10. ​// 等同于​
  11. ​function * b(){​
  12. ​    yield 3;​
  13. ​    yield 1;​
  14. ​    yield 2;​
  15. ​    yield 4;​
  16. ​}​
  17. ​for(let k of b()){console.log(k)}​
  18. ​// 3​
  19. ​// 1​
  20. ​// 2​
  21. ​// 4​

5.9 应用场景

  1. 控制流管理
    解决回调地狱:
  2. ​// 使用前​
  3. ​f1(function(v1){​
  4. ​    f2(function(v2){​
  5. ​        f3(function(v3){​
  6. ​            // ... more and more​
  7. ​        })​
  8. ​    })​
  9. ​})​

  10. ​// 使用Promise ​
  11. ​Promise.resolve(f1)​
  12. ​    .then(f2)​
  13. ​    .then(f3)​
  14. ​    .then(function(v4){​
  15. ​        // ...​
  16. ​    },function (err){​
  17. ​        // ...​
  18. ​    }).done();​

  19. ​// 使用Generator​
  20. ​function * f (v1){​
  21. ​    try{​
  22. ​        let v2 = yield f1(v1);​
  23. ​        let v3 = yield f1(v2);​
  24. ​        let v4 = yield f1(v3);​
  25. ​        // ...​
  26. ​    }catch(err){​
  27. ​        // console.log(err)​
  28. ​    }​
  29. ​}​
  30. ​function g (task){​
  31. ​    let obj = task.next(task.value);​
  32. ​  // 如果Generator函数未结束,就继续调用​
  33. ​  if(!obj.done){​
  34. ​      task.value = obj.value;​
  35. ​      g(task);​
  36. ​  }​
  37. ​}​
  38. ​g( f(initValue) );​
  39. 异步编程的使用 在真实的异步任务封装的情况:
  40. ​let fetch = require('node-fetch');​
  41. ​function * f(){​
  42. ​    let url = 'http://www.baidu.com';​
  43. ​    let res = yield fetch(url);​
  44. ​    console.log(res.bio);​
  45. ​}​
  46. ​// 执行该函数​
  47. ​let g = f();​
  48. ​let result = g.next();​
  49. ​// 由于fetch返回的是Promise对象,所以用then​
  50. ​result.value.then(function(data){​
  51. ​    return data.json();​
  52. ​}).then(function(data){​
  53. ​    g.next(data);​
  54. ​})​

参考资料

1.MDN 迭代器和生成器
2.ES6中的迭代器(Iterator)和生成器(Generator)

【JS】132-重温基础:语法和数据类型_迭代器_02



举报

相关推荐

0 条评论