Skip to content

JS高级特性(一) 迭代器、生成器、柯里化

🕒 Posted at: 2020-11-01 ( 3 years ago )
Javascript
Javascript高级特性:迭代器、生成器、柯里化

迭代器与生成器

为什么需要迭代器

  • 在ES6之前,我们要遍历一个对象,只能使用for循环或者forEach方法。但是这两种方法都有一个缺点,就是无法中途停止或者继续遍历。而迭代器则可以解决这个问题。

  • 原始的遍历方法,只能遍历数组或者类数组对象,需要预先知道对象的结构,并为每种结构编写不同的遍历方法(例如通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构)。而迭代器则可以遍历任意对象,只要该对象实现了迭代器接口。

迭代器

迭代器是一种特殊的对象,它包含一个next方法,每次调用next方法都会返回一个对象,该对象包含两个属性:value和done。value表示当前的值,done表示是否迭代完成。

javascript
function makeIterator(arr) {
  let nextIndex = 0;
  return {
    next: function() {
      return nextIndex < arr.length ?
        { value: arr[nextIndex++], done: false } :
        { done: true };
    }
  };
}

let it = makeIterator(['a', 'b']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().done); // true

实现迭代器接口的对象

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • arguments
  • NodeList
javascript
let arr = [1, 2, 3];
// 通过Symbol.iterator获取迭代器
let it = arr[Symbol.iterator]();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().done); // true

自定义迭代器

javascript
class Counter {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator]() {
    let count = 1;
    let limit = this.limit;
    return {
      next() {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
      // 退出迭代器 
      return() {
        console.log('Exiting early');
        return { done: true };
      },
    };
  }
}

let counter = new Counter(3);
let it = counter[Symbol.iterator]();
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().done); // true

for (let i of counter) {
    console.log(i);
    if (i === 2) {
        break; // 退出迭代器 log: Exiting early
    }
}

生成器

生成器是一种特殊的函数,它可以在函数内部暂停和恢复代码执行。生成器函数使用function*关键字定义,函数内部使用yield关键字暂停代码执行。

WARNING

箭头函数不能用来定义生成器函数。yield关键字只能在生成器函数内部使用, 并且不能嵌套在普通函数内部。

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

let it = gen();

console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().done); // true

// 遍历生成器
for (let i of gen()) {
  console.log(i);
}

生成器函数的执行过程

  1. 调用生成器函数时,函数内部的代码并不会立即执行,而是返回一个迭代器对象。
  2. 当调用迭代器的next方法时,生成器函数内部的代码开始执行,直到遇到yield关键字。
  3. 当遇到yield关键字时,生成器函数会暂停执行,并返回yield后面的值。
  4. 当再次调用迭代器的next方法时,生成器函数会从上次暂停的地方继续执行,直到遇到下一个yield关键字或者函数结束。
  5. 当生成器函数执行结束时,迭代器的next方法会返回{ done: true }
  6. 如果在生成器函数内部抛出异常,迭代器的next方法会抛出异常。
  7. 如果在生成器函数内部调用return方法,迭代器的next方法会返回{ done: true }

生成器函数的参数

生成器函数可以接收参数,参数可以通过next方法传递。

javascript
function* gen(init) {
    console.log(init); // start
    let a = yield 1;
    console.log(a); // b
    let b = yield 2;
    console.log(b); // c
    let c = yield 3;
    console.log(c); // d
}

let it = gen('start');
// 第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数
console.log(it.next('a').value); // 1
console.log(it.next('b').value); // 2
console.log(it.next('c').value); // 3
console.log(it.next('d').done); // true

生成器的应用

异步编程

初步了解生成器函数的执行过程后,我们可以使用生成器函数来实现异步编程。

javascript
const delay = (ms) => new Promise((resolve) => setTimeout(() => {
    resolve(ms);
}, ms));

function* gen() {
    // yeild后面的异步操作会将按顺序执行 
    let result = yield delay(1000);
    console.log(result); // 1000
    let result2 = yield delay(2000);
    console.log(result2); // 2000
    console.log(result + result2); // 3000
}

let it = gen();

// 调用next后,返回的是一个promise对象 p
let p = it.next().value;

/**
 * 对p进行then处理,将结果ms传递给it.next(),此时result = ms
 * 对it.next()返回的promise对象进行then处理,将结果ms2传递给it.next(),此时result2 = ms2
 * 最后打印result + result2 = ms + ms2 = 1000 + 2000 = 3000
 */
p.then((ms) => {
    it.next(ms).value.then((ms2) => {
        it.next(ms2);
    })
})

将上面的代码进行封装,可以得到一个co函数。

javascript

const delay = (ms) => new Promise((resolve) => setTimeout(() => {
    resolve(ms);
}, ms));

function co(gen) {
    let it = gen();
    function next(data) {
        let result = it.next(data);
        if (result.done) {
            return;
        }
        result.value.then((data) => {
            next(data);
        })
    }
    next();
}

co(function*(){
    let result = yield delay(1000);
    console.log(result); // 1000
    let result2 = yield delay(2000);
    console.log(result2); // 2000
    console.log(result + result2); // 3000
})

npm package: co

使用例子

javascript
const co = require('co');

// 定义一个生成器函数
function* myGenerator() {
    try {
        // 使用 yield 关键字等待 Promise 解决
        var result1 = yield Promise.resolve(1);
        var result2 = yield Promise.resolve(2);

        // 输出结果
        console.log(result1 + result2); // 输出 3
    } catch (err) {
        // 如果有错误发生,打印错误堆栈
        console.error(err.stack);
    }
}

// 使用 co 执行生成器函数
co(myGenerator).then(function() {
    console.log('Generator function has completed.');
}).catch(function(err) {
    console.error('An error occurred:', err.stack);
});

无限序列

javascript
function* fibonacci() {
    let a = 0;
    let b = 1;
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}

let it = fibonacci();
console.log(it.next().value); // 0
console.log(it.next().value); // 1
console.log(it.next().value); // 1
console.log(it.next().value); // 2
console.log(it.next().value); // 3
console.log(it.next().value); // 5

递归遍历树

javascript
function* traverseTree(node) {
    yield node.value;
    if (node.left) {
        yield* traverseTree(node.left);
    }
    if (node.right) {
        yield* traverseTree(node.right);
    }
}

let tree = {
    value: 1,
    left: {
        value: 2,
        left: {
            value: 4
        },
        right: {
            value: 5
        }
    },
    right: {
        value: 3
    }
};

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

柯里化

柯里化是一种将多参数函数转换为多个单参数函数的技术。

javascript
// curry 函数接受一个函数 fn 并返回一个新的函数 curried
function curry(fn) {
    // curried 函数接受一些参数...
    return function curried(...args) {
        // 如果提供的参数个数足够,则直接调用原始函数 fn
        if (args.length >= fn.length) {
            // 使用 apply 来调用 fn 并设置正确的 this 上下文以及参数
            return fn.apply(this, args);
        } else {
            // 如果参数个数不足,返回一个新的函数来接收剩余的参数
            return function(...args2) {
                // 新函数接收剩余参数 args2,并递归地调用 curried 函数
                // 这次调用合并了之前接收到的参数 args 和新的参数 args2
                return curried.apply(this, args.concat(args2));
            }
        }
    };
}

// 使用示例:
// 假设有一个三参数的 sum 函数
function sum(a, b, c) {
    return a + b + c;
}

// 使用 curry 函数将 sum 函数转换为柯里化函数
let curriedSum = curry(sum);

// 通过柯里化函数一次性传递所有参数
console.log(curriedSum(1, 2, 3)); // => 6

// 通过柯里化函数逐步传递参数
console.log(curriedSum(2)(4)(6)); // => 12
let add5 = curriedSum(2)(3); // 预先填充了前两个参数
console.log(add5(10)); // => 15,最后一个参数被传入,并执行原始的 sum 函数

柯里化的应用

参数复用

javascript
function add(a, b, c) {
    return a + b + c;
}

let add5 = curry(add)(5);

console.log(add5(2, 3)); // 10

let add5And6 = curry(add)(5, 6);    

console.log(add5And6(7)); // 18

延迟执行

javascript

function ajax(url, data) {
    console.log(url, data);

    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(data);
        }, 1000);
    });
}

let ajaxCurry = curry(ajax);

let post = ajaxCurry('http://www.example.com');

post({ name: '张三' }).then((data) => {
    console.log(data);
});

post({ name: '李四' }).then((data) => {
    console.log(data);
});

函数组合

javascript
function compose(...fns) {
    return function (value) {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

function toUpperCase(str) {
    return str.toUpperCase();
}

function reverse(str) {
    return str.split('').reverse().join('');
}

let reverseAndUpperCase = compose(reverse, toUpperCase);

console.log(reverseAndUpperCase('hello')); // OLLEH
Copyright © RyChen 2024