stream.js 一个很小、完全独立的Javascript类库


Posted in Javascript onOctober 28, 2011

<script src='stream-min.js'></script>
下载 stream.js
2Kb minified

streams是什么?
Streams 是一个操作简单的数据结构,很像数组或链接表,但附加了一些非凡的能力。

它们有什么特别之处?
跟数组不一样,streams是一个有魔法的数据结构。它可以装载无穷多的元素。是的,你没听错。他的这种魔力来自于具有延后(lazily)执行的能力。这简单的术语完全能表明它们可以加载无穷多的元素。

入门
如果你愿意花10分钟的时间来阅读这篇文章,你对编程的认识有可能会被完全的改变(除非你有函数式编程的经验!)。请稍有耐心,让我来先介绍一下streams支持的跟数组或链接表很类似的基本功能操作。然后我会像你介绍一些它具有的非常有趣的特性。

Stream 是一种容器。它能容纳元素。你可以使用 Stream.make 来让一个stream加载一些元素。只需要把想要的元素当成参数传进去:

var s = Stream.make( 10, 20, 30 ); // s is now a stream containing 10, 20, and 30
足够简单吧,现在 s 是一个拥有3个元素的stream: 10, 20, and 30; 有顺序的。我们可以使用 s.length() 来查看这个stream的长度,用 s.item( i ) 通过索引取出里面的某个元素。你还可以通过调用 s.head() 来获得这个stream 的第一个元素。让我们实际操作一下:

var s = Stream.make( 10, 20, 30 ); 
console.log( s.length() ); // outputs 3 
console.log( s.head() ); // outputs 10 
console.log( s.item( 0 ) ); // exactly equivalent to the line above 
console.log( s.item( 1 ) ); // outputs 20 
console.log( s.item( 2 ) ); // outputs 30

本页面已经加载了这个 stream.js 类库。如果你想运行这些例子或自己写几句,打开你的浏览器的Javascript控制台直接运行就行了。

我们继续,我们也可以使用 new Stream() 或 直接使用 Stream.make() 来构造一个空的stream。你可以使用 s.tail() 方法来获取stream里除了头个元素外的余下所有元素。如果你在一个空stream上调用 s.head() 或 s.tail() 方法,会抛出一个异常。你可以使用 s.empty() 来检查一个stream是否为空,它返回 true 或 false。

var s = Stream.make( 10, 20, 30 ); 
var t = s.tail(); // returns the stream that contains two items: 20 and 30 
console.log( t.head() ); // outputs 20 
var u = t.tail(); // returns the stream that contains one item: 30 
console.log( u.head() ); // outputs 30 
var v = u.tail(); // returns the empty stream 
console.log( v.empty() ); // prints true

这样做可以打印出一个stream里的所有元素:
var s = Stream.make( 10, 20, 30 ); 
while ( !s.empty() ) { 
console.log( s.head() ); 
s = s.tail(); 
}

我们有个简单的方法来实现这个: s.print() 将会打印出stream里的所有元素。

用它们还能做什么?
另一个简便的功能是 Stream.range( min, max ) 函数。它会返回一个包含有从 min 到 max 的自然数的stream。

var s = Stream.range( 10, 20 ); 
s.print(); // prints the numbers from 10 to 20 
在这个stream上,你可以使用 map, filter, 和 walk 等功能。 s.map( f ) 接受一个参数 f,它是一个函数, stream里的所有元素都将会被f处理一遍;它的返回值是经过这个函数处理过的stream。所以,举个例子,你可以用它来完成让你的 stream 里的数字翻倍的功能: function doubleNumber( x ) { 
return 2 * x; 
} 
var numbers = Stream.range( 10, 15 ); 
numbers.print(); // prints 10, 11, 12, 13, 14, 15 
var doubles = numbers.map( doubleNumber ); 
doubles.print(); // prints 20, 22, 24, 26, 28, 30

很酷,不是吗?相似的, s.filter( f ) 也接受一个参数f,是一个函数,stream里的所有元素都将经过这个函数处理;它的返回值也是个stream,但只包含能让f函数返回true的元素。所以,你可以用它来过滤到你的stream里某些特定的元素。让我们来用这个方法在之前的stream基础上构建一个只包含奇数的新stream:
function checkIfOdd( x ) { 
if ( x % 2 == 0 ) { 
// even number 
return false; 
} 
else { 
// odd number 
return true; 
} 
} 
var numbers = Stream.range( 10, 15 ); 
numbers.print(); // prints 10, 11, 12, 13, 14, 15 
var onlyOdds = numbers.filter( checkIfOdd ); 
onlyOdds.print(); // prints 11, 13, 15

很有效,不是吗?最后的一个s.walk( f )方法,也是接受一个参数f,是一个函数,stream里的所有元素都要经过这个函数处理,但它并不会对这个stream做任何的影响。我们打印stream里所有元素的想法有了新的实现方法:
function printItem( x ) { 
console.log( 'The element is: ' + x ); 
} 
var numbers = Stream.range( 10, 12 ); 
// prints: 
// The element is: 10 
// The element is: 11 
// The element is: 12 
numbers.walk( printItem );

还有一个很有用的函数: s.take( n ),它返回的stream只包含原始stream里第前n个元素。当用来截取stream时,这很有用:
var numbers = Stream.range( 10, 100 ); // numbers 10...100 
var fewerNumbers = numbers.take( 10 ); // numbers 10...19 
fewerNumbers.print();

另外一些有用的东西:s.scale( factor ) 会用factor(因子)乘以stream里的所有元素; s.add( t ) 会让 stream s 每个元素和stream t里对应的元素相加,返回的是相加后的结果。让我们来看几个例子:
var numbers = Stream.range( 1, 3 ); 
var multiplesOfTen = numbers.scale( 10 ); 
multiplesOfTen.print(); // prints 10, 20, 30 
numbers.add( multiplesOfTen ).print(); // prints 11, 22, 33

尽管我们目前看到的都是对数字进行操作,但stream里可以装载任何的东西:字符串,布尔值,函数,对象;甚至其它的数组或stream。然而,请注意一定,stream里不能装载一些特殊的值:null 和 undefined。

想我展示你的魔力!
现在,让我们来处理无穷多。你不需要往stream添加无穷多的元素。例如,在Stream.range( low, high )这个方法中,你可以忽略掉它的第二个参数,写成 Stream.range( low ), 这种情况下,数据没有了上限,于是这个stream里就装载了所有从 low 到无穷大的自然数。你也可以把low参数也忽略掉,这个参数的缺省值是1。这种情况中,Stream.range()返回的是所有的自然数。

这需要用上你无穷多的内存/时间/处理能力吗?
不,不会。这是最精彩的部分。你可以运行这些代码,它们跑的非常快,就像一个普通的数组。下面是一个打印从 1 到 10 的例子:

var naturalNumbers = Stream.range(); // returns the stream containing all natural numbers from 1 and up 
var oneToTen = naturalNumbers.take( 10 ); // returns the stream containing the numbers 1...10 
oneToTen.print();

你在骗人
是的,我在骗人。关键是你可以把这些结构想成无穷大,这就引入了一种新的编程范式,一种致力于简洁的代码,让你的代码比通常的命令式编程更容易理解、更贴近自然数学的编程范式。这个Javascript类库本身就很短小;它是按照这种编程范式设计出来的。让我们来多用一用它;我们构造两个stream,分别装载所有的奇数和所有的偶数。
var naturalNumbers = Stream.range(); // naturalNumbers is now 1, 2, 3, ... 
var evenNumbers = naturalNumbers.map( function ( x ) { 
return 2 * x; 
} ); // evenNumbers is now 2, 4, 6, ... 
var oddNumbers = naturalNumbers.filter( function ( x ) { 
return x % 2 != 0; 
} ); // oddNumbers is now 1, 3, 5, ... 
evenNumbers.take( 3 ).print(); // prints 2, 4, 6 
oddNumbers.take( 3 ).print(); // prints 1, 3, 5

很酷,不是吗?我没说大话,stream比数组的功能更强大。现在,请容忍我几分钟,让我来多介绍一点关于stream的事情。你可以使用 new Stream() 来创建一个空的stream,用 new Stream( head, functionReturningTail ) 来创建一个非空的stream。对于这个非空的stream,你传入的第一个参数成为这个stream的头元素,而第二个参数是一个函数,它返回stream的尾部(一个包含有余下所有元素的stream),很可能是一个空的stream。困惑吗?让我们来看一个例子:
var s = new Stream( 10, function () { 
return new Stream(); 
} ); 
// the head of the s stream is 10; the tail of the s stream is the empty stream 
s.print(); // prints 10 
var t = new Stream( 10, function () { 
return new Stream( 20, function () { 
return new Stream( 30, function () { 
return new Stream(); 
} ); 
} ); 
} ); 
// the head of the t stream is 10; its tail has a head which is 20 and a tail which 
// has a head which is 30 and a tail which is the empty stream. 
t.print(); // prints 10, 20, 30

没事找事吗?直接用Stream.make( 10, 20, 30 )就可以做这个。但是,请注意,这种方式我们可以轻松的构建我们的无穷大stream。让我们来做一个能够无穷无尽的stream:
function ones() { 
return new Stream( 
// the first element of the stream of ones is 1... 
1, 
// and the rest of the elements of this stream are given by calling the function ones() (this same function!) 
ones 
); 
} var s = ones(); // now s contains 1, 1, 1, 1, ... 
s.take( 3 ).print(); // prints 1, 1, 1

请注意,如果你在一个无限大的stream上使用 s.print(),它会无休无止的打印下去,最终耗尽你的内存。所以,你最好在使用s.print()前先s.take( n )。在一个无穷大的stream上使用s.length()也是无意义的,所有,不要做这些操作;它会导致一个无尽的循环(试图到达一个无尽的stream的尽头)。但是对于无穷大stream,你可以使用s.map( f ) 和 s.filter( f )。然而,s.walk( f )对于无穷大stream也是不好用。所有,有些事情你要记住; 对于无穷大的stream,一定要使用s.take( n )取出有限的部分。

让我们看看能不能做一些更有趣的事情。还有一个有趣的能创建包含自然数的stream方式:

function ones() { 
return new Stream( 1, ones ); 
} 
function naturalNumbers() { 
return new Stream( 
// the natural numbers are the stream whose first element is 1... 
1, 
function () { 
// and the rest are the natural numbers all incremented by one 
// which is obtained by adding the stream of natural numbers... 
// 1, 2, 3, 4, 5, ... 
// to the infinite stream of ones... 
// 1, 1, 1, 1, 1, ... 
// yielding... 
// 2, 3, 4, 5, 6, ... 
// which indeed are the REST of the natural numbers after one 
return ones().add( naturalNumbers() ); 
} 
); 
} 
naturalNumbers().take( 5 ).print(); // prints 1, 2, 3, 4, 5

细心的读者会发现为什么新构造的stream的第二参数是一个返回尾部的函数、而不是尾部本身的原因了。这种方式可以通过延迟尾部截取的操作来防止进行进入无穷尽的执行周期。

让我们来看一个更复杂的例子。下面的是给读者留下的一个练习,请指出下面这段代码是做什么的?

function sieve( s ) { 
var h = s.head(); 
return new Stream( h, function () { 
return sieve( s.tail().filter( function( x ) { 
return x % h != 0; 
} ) ); 
} ); 
} 
sieve( Stream.range( 2 ) ).take( 10 ).print();

请一定要花些时间能清楚这段代码的用途。除非有函数式编程经验,大多数的程序员都会发现这段代码很难理解,所以,如果你不能立刻看出来,不要觉得沮丧。给你一点提示:找出被打印的stream的头元素是什么。然后找出第二个元素是什么(余下的元素的头元素);然后第三个元素,然后第四个。这个函数的名称也能给你一些提示。

如果你对这种难题感兴趣,这儿还有一些。

如果你真的想不出这段代码是做什么的,你就运行一下它,自己看一看!这样你就很容易理解它是怎么做的了。

致敬
Streams 实际上不是一个新的想法。很多的函数式的编程语言都支持这种特征。所谓‘stream'是Scheme语言里的叫法,Scheme是LISP语言的一种方言。Haskell语言也支持无限大列表(list)。这些'take', 'tail', 'head', 'map' 和 'filter' 名字都来自于Haskell语言。Python和其它很多中语言中也存在虽然不同但很相似的这种概念,它们都被称作"发生器(generators)"。

这些思想来函数式编程社区里已经流传了很久了。然而,对于大多数的Javascript程序员来说却是一个很新的概念,特别是那些没有函数式编程经验的人。

这里很多的例子和创意都是来自 Structure and Interpretation of Computer Programs 这本书。如果你喜欢这些想法,我高度推荐你读一读它;这本书可以在网上免费获得。它也是我开发这个Javascript类库的创意来源。

如果你喜欢其它语法形式的stream,你可以试一下linq.js,或者,如果你使用 node.js, node-lazy 也许更适合你。

如果你要是喜欢 CoffeeScript 的话, Michael Blume 正在把 stream.js 移植到 CoffeeScript 上,创造出 coffeestream。

感谢你的阅读!
我希望你能有所收获,并喜欢上 stream.js。这个类库是免费的,所以,如果你喜欢它,或它能在某方面提供了帮助,你可以考虑替我买一杯热巧克力饮料 (我不喝咖啡) 或者 写信给我。如果你打算这样做,请写清你是哪里人,做什么的。我很喜欢收集世界各地的图片,所以,信中请附上你在你的城市里拍的照片!

Javascript 相关文章推荐
jQuery 加上最后自己的验证
Nov 04 Javascript
IE与FireFox中的childNodes区别
Oct 20 Javascript
鼠标焦点离开文本框时验证的js代码
Jul 19 Javascript
使用GruntJS链接与压缩多个JavaScript文件过程详解
Aug 02 Javascript
JS简单实现元素复制示例附图
Nov 19 Javascript
js数值和和字符串进行转换时可以对不同进制进行操作
Mar 05 Javascript
再谈javascript原型继承
Nov 10 Javascript
JS实现一个按钮的方法
Feb 05 Javascript
浅谈jQuery中的eq()与DOM中element.[]的区别
Oct 28 Javascript
vue vuex vue-rouert后台项目——权限路由(适合初学)
Dec 29 Javascript
浅谈mvvm-simple双向绑定简单实现
Apr 18 Javascript
详解JavaScript函数callee、call、apply的区别
Mar 08 Javascript
能说明你的Javascript技术很烂的五个原因分析
Oct 28 #Javascript
基于jquery的滚动鼠标放大缩小图片效果
Oct 27 #Javascript
input 和 textarea 输入框最大文字限制的jquery插件
Oct 27 #Javascript
VBS通过WMI监视注册表变动的代码
Oct 27 #Javascript
JavaScript Memoization 让函数也有记忆功能
Oct 27 #Javascript
JavaScript 类型的包装对象(Typed Wrappers)
Oct 27 #Javascript
40款非常棒的jQuery 插件和制作教程(系列一)
Oct 26 #Javascript
You might like
PHP 命令行参数详解及应用
2011/05/18 PHP
php对二维数组进行相关操作(排序、转换、去空白等)
2015/11/04 PHP
PHP数据分析引擎计算余弦相似度算法示例
2017/08/08 PHP
基于laravel where的高级使用方法
2019/10/10 PHP
javascript[js]获取url参数的代码
2007/10/17 Javascript
精通Javascript系列之Javascript基础篇
2011/06/07 Javascript
NodeJS框架Express的模板视图机制分析
2011/07/19 NodeJs
$.getJSON在IE下失效的原因分析及解决方法
2013/06/16 Javascript
JavaScript使用ActiveXObject访问Access和SQL Server数据库
2015/04/02 Javascript
DEDECMS如何为文章添加HOT NEW标志图片
2015/08/14 Javascript
关于JS 预解释的相关理解
2016/06/28 Javascript
jQuery组件easyui对话框实现代码
2016/08/25 Javascript
Bootstrap CSS布局之图像
2016/12/17 Javascript
JS实现的随机排序功能算法示例
2017/06/09 Javascript
前端MVVM框架解析之双向绑定
2018/01/24 Javascript
浅谈Angular单元测试总结
2019/03/22 Javascript
Layui数据表格之单元格编辑方式
2019/10/26 Javascript
完美解决通过IP地址访问VUE项目的问题
2020/07/18 Javascript
微信小程序自定义底部弹出框动画
2020/11/18 Javascript
Python中的is和id用法分析
2015/01/26 Python
详谈Python高阶函数与函数装饰器(推荐)
2017/09/30 Python
Python内置函数delattr的具体用法
2017/11/23 Python
Python实现替换文件中指定内容的方法
2018/03/19 Python
windows、linux下打包Python3程序详细方法
2020/03/17 Python
Python实现Keras搭建神经网络训练分类模型教程
2020/06/12 Python
详解如何使用Pytest进行自动化测试
2021/01/14 Python
Qoo10马来西亚:全球时尚和引领潮流的购物市场
2016/08/25 全球购物
北美最大的手工艺品零售商之一:Michaels Stores
2019/02/27 全球购物
Douglas意大利官网:购买香水和化妆品
2020/05/27 全球购物
普天C++笔试题
2016/03/20 面试题
行政助理的职责
2013/11/14 职场文书
安全生产知识竞赛活动总结
2014/07/07 职场文书
2014年保洁员工作总结
2014/11/19 职场文书
文明倡议书
2015/01/19 职场文书
被委托人身份证明
2015/08/07 职场文书
鲲鹏 CentOS 7 安装Python3.7
2022/05/11 Servers