基于 ES6 新特性的 Iterator 的 Linq

831 阅读7分钟
原文链接: zhuanlan.zhihu.com

我是个刚入行前端的新人,这是我工作之余写的库,Linq.js(Github)源代码兼容ES5,主流的浏览器都可用!他主要提供两个类,Iterator和Iterable,有一系列对数据集合进行操作的方法,传统Linq能做到的都能做到。


什么是Iterator?

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

ES6的Iterator对象,有一个基本的方法next,每一次调用该方法,会得到一个对象。{value:any,done:bool}

value迭代器当前的值,done是表示迭代器的结束状态,true结束,false未结束。

在ES6,可以这样得到一个迭代器。

var iter = [2, 4, 6, 8][Symbol.iterator]();
iter.next();    //{value: 2, done: false}
iter.next();    //{value: 4, done: false}
iter.next();    //{value: 6, done: false}
iter.next();    //{value: 8, done: false}
iter.next();    //{value: undefined, done: true}
//...
iter.next();    //{value: undefined, done: true}

也可以自己构造一个迭代器。

//这是个自然数迭代器,无穷无尽
var iter = {
  next: function () {
    var index = 0;
    this.next = function () {
      return { value: index++, done: false };
    }
    return this.next();
  }
};
iter.next();    //{value: 0, done: false}
iter.next();    //{value: 1, done: false}
iter.next();    //{value: 2, done: false}
//...

ES6有专门的访问迭代器的语法:

for (var x of [2, 4, 6, 8]) {
  console.log(x);
}

这里的结果依次是打印2 4 6 8。

结合本文的第一个例子,可以发现,如果令一个对象的Symbol.iterator属性为一个迭代器的构造方法,同样可以用for…of…来遍历该对象。

var myIterable = {};
myIterable[Symbol.iterator] = function () { return Natural; };
for (var x of myIterable) {
  console.log(x);
  if (x === 10) break;
}

这里的结果依次是打印 3 4 5 6 7 8 9 10

这里从3开始,从句子意义上来说,是不对的。不过也合情合理,因为我没有在该构造器里返回一个新的迭代器。所以由于第二个例子对Natural进行了三次next,这时的Natural输出就从3开始了。


如果我把if (x === 10) break; 去掉,就会进入死循环,因为for..of…要得到一个属性done为true的对象才会自动停止。

于是可以将for…of..的循环过程视为,调用迭代器的next方法,对返回的对象进行判断。如果done为true时就退出;done为false时就返回value的值。

此时没有退出的话,将对语句体进行处理,然后重复一开始的调用及判断行为。


以上就是对ES6的Iterator的介绍。更加详细的介绍可以参考下阮一峰的文档。

参考:ECMAScript 6入门


什么是Linq?

LINQ,语言集成查询(Language Integrated Query)。

网上已经有好多关于Linq的资料,但没有一个比较统一的说法,我就根据自身经验来介绍吧。

Linq就是访问集合的范式。这种范式,能让我们以同样的形式去访问各种各样的数据集合。

在C#就有Linq to Objects,Linq to XMl,Linq to EF……

通俗来说,就是可以用“同样”的方法对不同的数据进行访问。

Linq的操作有投影,过滤,排序,联接,分组,聚合等等。


而且在C#linq还有强大的查询表达式(Query Expression),能用简便的类SQL的语法去表示些复杂的操作。

如:

var m = from n in arr where n < 5 orderby n descending select n;	//选出数组中小于5的元素,并且倒叙排列。

如果不习惯这种表达式,可以使用一般的方法语法(Fluent Syntax)。(注:查询表达式执行时也是转化成方法语法。)

如:

var m = arr.Where(n => n < 5).OrderByDescending(n => n);		//选出数组中小于5的元素,并且倒叙排列。

遗憾的是,我在JS所实现的Linq,没有做查询表达式这个功能。


在C#,只要一个对象实现了IEnumerator接口,该对象就能成为供Linq访问的迭代器。只要一个对象实现了IEnumerable接口,就能用Linq对其操作。

必须说到的是,IEnumerbale只需要实现一个GetEnumerator方法,该方法的作用是返回一个IEnumerator对象。其余的Linq方法,包括上文出现的whrere,OrderByDescending都是默认实现了的方法,不需要用户去实现。

也就是说,只要给出一个集合的迭代方法,就能使用Linq集成的所有访问集合的方法,非常方便。


Linq还有个很重要的特性,每个元素都是在访问的时候才被迭代器产生出来。使用得当,对一个集合进行多步操作,对数据源的迭代操作只会进行一次,性能上并不比自己调度的代码差。

以上就是对Linq的简单介绍。


如何使用ES6的Iterator实现Linq?

接下来介绍的是,我基于ES6的Iterator实现的Linq。

联系上文,可以知道IEnumerable等价于第一篇Iterator介绍里的拥有迭代器构造方法的对象。

在JS实现Linq也是基于这个观念。


我用Iterable取代IEnumerable,用Iterator取代IEnumerator。

Iterable.prototype实现各种Linq方法,所有的linq方法都是对Iterable对象的Symbol.iterator属性所构造的迭代器进行操作。


Linq方法在操作层次上分为两种,一种是返回Iterable对象的延迟操作,一种是触发迭代器next的生产操作

//这是一个表示自然数的Iterable对象,他每次产生值都会打印这个值。
var natural = Iterable(function () {
  var index = 0;
  return {
    next: function () {
      console.log(index);
      return { value: index++, done: false };
    }
  };
});

//自然数里前10个是既是2的倍数又不是3的倍数的数字。
var foo = natural.Where(function (v) { return v % 2 === 0; })
  .Where(function (v) { return v % 3 !== 0; })
  .Take(10);

此时foo并没有打印任何数据。这是上文所述的延迟操作

再次使用Linq的ToArray方法。

foo.ToArray();  //[2, 4, 8, 10, 14, 16, 20, 22, 26, 28]

会在后台发现他从0打印到了32。这是上文所述的生产操作


JS跟C#还是存在不少差异的,于是在JS上实现LInq也要做一些本地化的处理。

以下是一些与C#上的Linq不同的地方:

① 为了提高自由度,不主动抛出异常。

② 部分方法没有实现:Single,Cast,AsEnumerable,ToList。

③ 用ToMap方法代替ToDictionary方法。ES6的Map很好地代替了C#的Dictionary呢。


使用示例

简单地举两个使用场景。

var data = {
  "users": [
    {
      "id": 6287,
      "slug": "5SqsuF",
      "nickname": "刘淼",
      "avatar_source": "http://upload.jianshu.io/users/upload_avatars/6287/06c537002583.png",
      "total_wordage": "307.3K",
      "total_likes_count": "16.9K",
      "subscription_id": 6183
    },
    {
      "id": 326721,
      "slug": "5cfa376301c5",
      "nickname": "傅踢踢",
      "avatar_source": "http://upload.jianshu.io/users/upload_avatars/326721/fbb53af4d452.jpg",
      "total_wordage": "409.5K",
      "total_likes_count": "11.1K",
      "subscription_id": 334010
    },
    {
      "id": 186093,
      "slug": "0782ca5a0f41",
      "nickname": "philren",
      "avatar_source": "http://upload.jianshu.io/users/upload_avatars/186093/8f9c1cb18375.jpg",
      "total_wordage": "251.0K",
      "total_likes_count": "4.2K",
      "subscription_id": 183924
    },
    {
      "id": 1933412,
      "slug": "fd0599061897",
      "nickname": "一棵花白",
      "avatar_source": "http://upload.jianshu.io/users/upload_avatars/1933412/2a8a65019896.jpg",
      "total_wordage": "223.9K",
      "total_likes_count": "11.8K",
      "subscription_id": 1923632
    },
    {
      "id": 1211570,
      "slug": "4b9ff86a7af4",
      "nickname": "毒舌电影",
      "avatar_source": "http://upload.jianshu.io/users/upload_avatars/1211570/ef87d476-c24a-4645-91df-49c729f01b78.png",
      "total_wordage": "3354.3K",
      "total_likes_count": "79.2K",
      "subscription_id": 1176787
    },
    {
      "id": 233,
      "nickname": "LF2",
      "total_wordage": "223.9K",
      "total_likes_count": "0K",
    }
  ],
  "total_page": 39,
  "current_page": 1
};//简书上随手弄来的数据

var src = Iterable(data.users); //Iterable可以接受各种参数。构造迭代器的function,数组甚至是普通的object。
var f1 = src.OrderByDescending(function (item) { return Number(item.total_wordage.slice(0, -1)); })
  .ThenBy(function (item) { return item.id;})
  .Select(function (v) { return { nickname: v.nickname, total_wordage: v.total_wordage }; })
  .ToArray(); //按写作字数按降序排序,写作字数一样时按id升序排序,并且返回由名字跟写作字数组成的对象的数组。

var f2 = src.Max(function (item) { return Number(item.total_likes_count.slice(0, -1)); });  //返回最大的粉丝数;返回79.2

var obj1 = {
  id: 233,
  name: 'LF2',
  price: 6300,
  count: 1
};
var obj2 = {
  price: 6300,
  count: 1,
  id: 666,
  name: 'MM'
}
var f3 = Iterable(obj1)
  .Except(Iterable(obj2), function (x, y) { return x.key === y.key; })
  .Any();  //求两个对象属性是否存在差集;返回false。(可以用来判断是否一种对象。)

var opt1 = {
  url: '',
  data: {},
  success:Function()
}
var opt2 = {
  beforeSend: Function(),
  complete:Function()
}
var f4 = Iterable(opt1)
  .Union(Iterable(opt2), function (x, y) { return x.key === y.key; })
  .ToArray(); //这是求两个对象属性的合集。(啊咧咧,其实应该能返回Object,我应该开始做C#上没有的东西么。)

在ES6还能用for…of…的语法对Iterable对象进行遍历操作。

for (var x of Iterable("Hello World!").SkipWhile(function (v) { return v !== ' '; }).Reverse()) {
  console.log(x);
}//直到遇到W之前都跳过,然后倒序;结果:! d l r o W

总结

这个玩意其实还有进一步的改进,不局限于Linq to Objects里已有的方法,可以根据JS的特性新增几个方法。譬如说Max可以返回对象而不止于数值,可以ToObject等等。感谢大家的阅读,欢迎指出bug以及建议。