JavaScript 设计模式(五):迭代器模式

栏目: JavaScript · 发布时间: 6年前

内容简介:文章内容分两部分:上半部分开始...

JavaScript 设计模式(五):迭代器模式

文章内容分两部分:

  1. 前半部分为 “迭代器模式” 概念;
  2. 后半部分为 ES6 中 Iterator (迭代器)

上半部分开始...

迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

简单理解(白话理解):统一 “集合” 型数据结构的遍历接口,实现可循环遍历获取集合中各数据项(不关心数据项中的数据结构)。

生活小栗子:清单 TodoList。每日清单有学习类、生活类、工作类、运动类等项目,清单列表只管罗列,不管类别。

模式特点

  1. 为遍历不同数据结构的 “集合” 提供统一的接口;
  2. 能遍历访问 “集合” 数据中的项,不关心项的数据结构

模式实现

// 统一遍历接口实现
var each = function(arr, callBack) {
  for (let i = 0, len = arr.length; i < len; i++) {
    // 将值,索引返回给回调函数callBack处理
    if (callBack(i, arr[i]) === false) {
      break;  // 中止迭代器,跳出循环
    }
  }
}

// 外部调用
each([1, 2, 3, 4, 5], function(index, value) {
    if (value > 3) {
      return false; // 返回false中止each
    }
    console.log([index, value]);
})

// 输出:[0, 1]  [1, 2]  [2, 3]

“迭代器模式的核心,就是实现统一遍历接口。”

模式细分

  1. 内部迭代器 (jQuery 的 $.each / for...of)
  2. 外部迭代器 (ES6 的 yield)

内部迭代器

内部迭代器: 内部定义迭代规则,控制整个迭代过程,外部只需一次初始调用

// jQuery 的 $.each(跟上文each函数实现原理类似)
$.each(['Angular', 'React', 'Vue'], function(index, value) {
    console.log([index, value]);
});

// 输出:[0, Angular]  [1, React]  [2, Vue]

优点:调用方式简单,外部仅需一次调用

缺点:迭代规则预先设置,欠缺灵活性。无法实现复杂遍历需求(如: 同时迭代比对两个数组)

外部迭代器

外部迭代器: 外部显示(手动)地控制迭代下一个数据项

借助 ES6 新增的 Generator 函数中的 yield* 表达式来实现外部迭代器。

// ES6 的 yield 实现外部迭代器
function* generatorEach(arr) {
  for (let [index, value] of arr.entries()) {
    yield console.log([index, value]);
  }
}

let each = generatorEach(['Angular', 'React', 'Vue']);
each.next();
each.next();
each.next();

// 输出:[0, 'Angular']  [1, 'React']  [2, 'Vue']

优点:灵活性更佳,适用面广,能应对更加复杂的迭代需求

缺点:需显示调用迭代进行(手动控制迭代过程),外部调用方式较复杂

适用场景

不同数据结构类型的 “数据集合”,需要对外提供统一的遍历接口,而又不暴露或修改内部结构时,可应用迭代器模式实现。

下半部分开始...

ES6 的 Iterator 迭代器

“迭代器等同于遍历器。在某些文章中,可能会出现遍历器的字眼,其实两者的意思一致。”

JavaScript 中 原有表示 “集合” 的数据结构主要是 “数组(Array)” 和 “对象(Object)”,ES6又新增了 MapSet ,共四种数据集合,浏览器端还有 NodeList 类数组结构。为 “集合” 型数据寻求统一的遍历接口,正是 ES6 的 Iterator 诞生的背景。

ES6 中迭代器 Iterator 作为一个接口,作用就是为各种不同数据结构提供统一的访问机制。任何数据结构只要部署了 Iterator 接口,就可以完成遍历操作。

Iterator 作用:

for...of

Iterator只是一种接口,与遍历的数据结构是分开的。重温迭代器模式特点:我只要统一遍历数据项的接口,不关心其数据结构。

ES6 默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性上,该属性本身是一个函数,代表当前数据结构默认的遍历器生成函数。执行该函数 [Symbol.iterator]() ,会返回一个遍历器对象。只要数据结构拥有 Symbol.iterator 属性,那么它就是 “可遍历的” 。

遍历器对象的特征:

  1. 拥有 next 属性方法;
  2. 执行 next() ,会返回一个包含 valuedone 属性的对象

    value
    done
    

原生具备 Iterator 接口的数据结构:

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. 函数的 arguments 对象
  7. NodeList 对象
let arr = ['a', 'b', 'c'];
let iterator = arr[Symbol.iterator]();

iterator.next();  // { value: 'a', done: false }
iterator.next();  // { value: 'b', done: false }
iterator.next();  // { value: 'c', done: false }
iterator.next();  // { value: undefined, done: false }

原生部署 Iterator 接口的数据结构,无需手动执行遍历器生成函数,可使用 for...of 自动循环遍历。

for...of 运行原理:

  1. 首先调用遍历对象 [Symobo.iterator]() 方法,拿到遍历器对象;
  2. 每次循环,调用遍历器对象 next() 方法,得到 {value: ..., done: ... } 对象
// for...of 自动遍历拥有 Iterator 接口的数据结构
let arr = ['a', 'b', 'c'];
for (let item of arr) {
  console.log(item);
}

// 输出:a  b  c

类数组对象:存在数值键名和 length 属性的对象

类数组对象部署 Iterator 方法:

// 方法一:
NodeList.prototype[Symbol.iterator] = Array.prototype[Sybmol.iterator];

// 方法二:
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

// for...of 遍历类数组对象
let arrLike = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};

for (let item of arrLike) {
  console.log(item);
}

// 输出:a  b  c

对象(Object)没有默认 Iterator 接口,因为对象属性遍历顺序不确定,需开发者手动指定。

注意:

Symbol.iterator
Symbol.iterator
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj]; // TypeError: [] is not a function

for...of 遍历普通对象的解决方法:

Objet.keys
let person = {
  name: 'Ken',
  sex: 'Male'
}

// Object.keys
for (let key of Object.keys(person)) {
  console.log(`${key}: ${person[key]}`);
}

// Generator 包装对象
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for (let [key, value] of entries(person)) {
  console.log(`${key}: ${value}`);
}

// 输出:
// name: Ken 
// sex: Male

ES6 的 Iterator 应用场景

  1. 解构赋值
  2. 扩展运算符
  3. yield*
  4. 任何以数组为参数的遍历的场景:

    for...of
    Array.from()
    Map()/Set()/WeakMap()/WeakSet()
    Promise.all()/Promise.race()
    

for...of 对比 for / for...in / forEach

for 循环 :需定义索引变量,指定循环终结条件。

for (let i = 0, len = arr.length; i < len; i++) {
  console.log(arr[i]);
}

forEach: 无法中途跳出循环, break/return

forEach(arr, function(item, index) {
  console.log(item, index);
})

for...in:

  1. 只能获取键名,不能获取键值
  2. 以字符串为键名(但数组的键名为数值类型索引)
  3. 任意顺序遍历键名(???)
  4. 会遍历手动添加的其它键(原型链上的键)
  5. 为遍历对象设计,不适用数组
let triangle = {a: 1, b: 2, c: 3};

function ColoredTriangle() {
  this.color = 'red';
}

ColoredTriangle.prototype = triangle;

let obj = new ColoredTriangle();

for (let prop in obj) {
  // 需手动判断是否属于自身属性,而不是原型链属性
  if (obj.hasOwnProperty(prop)) {
    console.log(`obj.${prop} = ${obj[prop]}`);
  } 
}

// 输出:obj.color = red

for...of 较其它三者优点:

  1. for...in 一样简洁,但没有 for...in 的缺点;
  2. 不同于 forEach , 可使用 break/return/continue 退出循环;
  3. 提供了遍历所有数据的统一接口

缺点:遍历普通对象时,不能直接使用。

参考文章

本文首发Github,期待Star!

https://github.com/ZengLingYong/blog

作者:以乐之名


以上所述就是小编给大家介绍的《JavaScript 设计模式(五):迭代器模式》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Impractical Python Projects

Impractical Python Projects

Lee Vaughan / No Starch Press / 2018-11 / USD 29.95

Impractical Python Projects picks up where the complete beginner books leave off, expanding on existing concepts and introducing new tools that you’ll use every day. And to keep things interesting, ea......一起来看看 《Impractical Python Projects》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器