在JavaScript中實現LINQ——一次“失敗”的嘗試

發表於2017-02-06

這篇文章的起因是我在知乎上對JavaScript 函數語言程式設計存在效能問題麼?這個問題的回答。其實在這個問題之前挺久我就想做相關的嘗試,但懶癌無藥醫,挖坑如山倒,填坑如抽絲。

廢話不多說,走你。

C# 3.0引入了引以為豪的LINQ(Language INtergrated Query),可以用類函式式的方式操作集合(C#中的IEnumerable介面)。

在JS中,陣列也有類似的filtermapreduce一類方法,但存在重複遍歷問題,利用C#中LINQ的思路,給JS實現一套LINQ是否可行呢?

C#中的LINQ

C#中的LINQ是通過yield來避免重複遍歷的,抽象的說,Where(對應filter)、Select(對應map)這類的方法呼叫的時候,都只會把操作“暫存”起來,直到呼叫了ToArrayAggregate(對應reduce)之類的方法,才會“驅動”它去進行遍歷。

舉一個簡單的例子

上面是一個最基本的filter/map/reduce的過程(下文也會繼續用這個例子),只有在Aggregate呼叫的時候,才會對陣列進行遍歷,而WhereSelect只是一些型別為IQueryable<T>的中間過程。

C#中的LINQ得益於C#的yield關鍵字,配合First-Class-Function可以不費吹灰之力地構建IEnumerable<T>,而C#中的foreach提供了對IEnumerable<T>的語法糖,這樣就可以很自然的對LINQ的中間結果進行二次加工,而不需要繁瑣地手工呼叫.Next()

JS中的filter/map/reduce

JS中的原生陣列就自帶了filter/map/reduce等一系列函式化的集合操作方法,但使用中有一個隱患就是,每次呼叫它們都會進行一次完整的遍歷,這樣當用這樣連寫的風格,就會造成重複遍歷

上面的程式碼,在filtermap被呼叫的時候,都會遍歷一次陣列,reduce的時候再遍歷一次,這樣總共就被遍歷了三次,當集合比較大的時候,這估計不是大家所想見發生的事情。

如果在filter/map/reduce的回撥函式裡列印一些除錯資訊,我們會發現呼叫的次序大概會是這樣的

JS中的LINQ

yield/generator

ES6中有了yield和Generator Function(不熟悉的可以先回顧一下我幾百年前寫的這篇這篇文章),並且,由於Symbol.iteratorfor of語法的引入,能用生成器構造集合了,並且還能和for of無縫銜接。

也就是說,ES6已經有了C#那樣優雅地實現LINQ的基礎設施,我們就來實現一個簡單的試試。

IQueryable

首先我們像C#那樣實現一個IQueryable類,並且它通過Symbol.iterator能夠支援被for of遍歷

由於我們是“面向介面程式設計”的,這裡我們並不關心new Queryable(xxx)傳入的是一個Array、一個Generator還是一個Queryable,反正它們都可以被for of遍歷。

然後為了方便,在Array.prototype上掛了一個方法,別嫌髒,娛樂而已。

嘗試一下

filter/map/reduce

我們的Queryable類已經可以享受for of語法糖的便利了,然後我們就可以基於這個給它愚快地新增各種集合操作方法了

這裡注意,filtermap分別呼叫了_filter_map方法,它們返回的結果都是Generator,我們知道一個Generator只定義了集合如何“被遍歷”,而事實上它沒有真正發生操作,需要呼叫next()或者for of(也就是next()的語法糖)來“驅動”它進行遍歷。

reduce當中呼叫了for of,也就是它真正發生了遍歷。

趕緊爽一爽

如果在filter/map/reduce的回撥函式裡列印一些除錯資訊,我們會發現呼叫的次序大概會是這樣的

只遍歷了一遍

擴充套件LINQ

有了上面三個方法我們可以順便構造一下lengthtoArray這類的方法,比如

當然其實map/reduce都是foldl/foldr的具象(吃我一發安利,參考我寫的使用JavaScript實現“真·函數語言程式設計”,所以上面的那些方法其實都可以寫得更“函式式”,但既然這篇文章只是為了實驗,就不搞那麼多么蛾子了。

效能測試

我們用benchmark模組對上述程式碼進行效能測試,並且引入兩個對照組,不多說了,直接看程式碼吧

用長度為100的陣列進行測試,結果

可以看出,我們的LINQ效能非常非常的廢柴,主要原因:

  • JS對Generator的優化非常廢柴
  • JS對for of的優化非常廢柴——因為它就是Generator#next()的語法糖

結論

雖然我們通過ES6的一系列新特性給JS實現了lazy的LINQ,避免重複遍歷,是實現了,但想象中的效能提高卻是化為泡影。

當然,通過不斷優化,減少for of的使用,改為手工.next()遍歷,也許效能還會高一些,但一來我不太相信它會有很明顯的變化。二來更重要的是,不用for of的話,我們就不能實現“無痛”的集合操作程式碼編寫了,既然已經不能“無痛”,那麼同樣“痛”的方法自然有效能更優的,而且根本不需要Symbol.iterator、Generator等等這一大堆新特性。

所以這是一個成功的嘗試,也是一個失敗的嘗試。成功之處在於很開心能看到ES6有如此強大的基礎設施用於編寫優雅程式碼,發揮創造力。失敗之處麼,自然是由於現階段的JS引擎並沒有對這些新引入的特性進行值得稱道的優化,這也提醒我對於這些新特性——至少是說,需要runtime支援的新特性——不要盲目的追新。

相關文章