leebyron / iterall
- четверг, 30 июня 2016 г. в 03:13:02
JavaScript
Minimal zero-dependency utilities for using Iterables in all JavaScript environments.
iterall
provides a few crucial utilities for implementing and working with
Iterables and Array-likes in all JavaScript
environments, even old versions of Internet Explorer, in a tiny library weighing
well under 1KB when minified and gzipped.
This is a library for libraries. If your library takes Arrays as input, accept Iterables instead. If your library implements a new data-structure, make it Iterable.
When installed via npm
, iterall
comes complete with flow and
TypeScript definition files. Don't want to take the dependency? Feel free to
copy code directly from this repository.
// Limited to only Arrays 😥
if (Array.isArray(thing)) {
thing.forEach(function (item, i) {
console.log('Index: ' + i, item)
})
}
// Accepts all Iterables and Array-likes, in any JavaScript environment! 🎉
var isCollection = require('iterall').isCollection
var forEach = require('iterall').forEach
if (isCollection(thing)) {
forEach(thing, function (item, i) {
console.log('Index: ' + i, item)
})
}
For most of JavaScript's history it has provided two collection data-structures:
the Object
and the Array
. These collections can conceptually describe nearly
all data and so it's no suprise that libraries expecting lists of
things standardized on expecting and checking for an Array. This pattern even
resulted in the addition of a new method in ES5: Array.isArray()
.
As JavaScript applications grew in complexity, moved to the server where CPU is a constrained resource, faced new problems and implemented new algorithms, new data-structures are often required. With options from linked lists to HAMTs developers can use what is most efficient and provides the right properties for their program.
However none of these new data-structures can be used in libraries where an
Array
is expected, which means developers are often stuck between abandoning
their favorite libraries or limiting their data-structure choices at the cost of
efficiency or usefulness.
To enable many related data-structures to be used interchangably we need a
protocol, and luckily for us ES2015 introduced the
Iteration Protocols to describe all list-like data-structures which
can be iterated. That includes not just the new-to-ES2015 Map and Set
collections but also existing ones like arguments, NodeList and the
various TypedArray, all of which return false
for Array.isArray()
and in ES2015 implement the Iterator protocol.
While Iterators are defined in ES2015, they do not require ES2015 to work
correctly. In fact, Iterators were first introduced in 2012 in Firefox v17. Rather than using Symbol.iterator
, they used the property name "@@iterator"
(in fact, the ECMAScript
spec still refers to well-known Symbols
using this @@
shorthand). By falling
back to use "@@iterator"
when Symbol.iterator
is not defined, Iterators can
be both safely defined and used by any version of JavaScript.
Not only were Iterables defined in ES2015, they were also implemented by the
built-in data-structures including Array. Older JavaScript
environments do not implement Array.prototype[@@iterator]()
, however this is
only a minor problem. JavaScript has another related and much older protocol:
Array-like. An value is "Array-like" if it has a numeric length
property and
indexed access, but does not necessarily have methods like .push()
or .forEach()
.
Much like Array.from
, iterall
's forEach()
and
createIterator()
methods also accept collections which are not Iterable but
are Array-like. This means that iterall
can be used with Array,
arguments, NodeList, TypedArray and other Array-like collections
regardless of the JavaScript environment.
When libraries only accept Arrays as input, they stick developers with a tough
choice: limit which data-structures can be used or limit the ability to use that
library. Accepting Iterables removes this false dichotomy, and allows libraries
to be more generally useful. There's no need to limit to ES2015 environments and
bleeding-edge browsers to accept Iterable
.
Only using Arrays can limit the efficiency and usefulness of your application
code, but custom data-structures can often feel like a fish out of water in
JavaScript programs, only working with code written specifically for it.
Protocols like Iterable
helps these new data-structures work with more
libraries and built-in JavaScript behavior. There's no need to limit to ES2015
environments and bleeding-edge browsers to implement Iterable
.
Aren't Iterables slower than Arrays? I want the highest performance possible.
Arrays are Iterables. Iterable is a protocol that Array's adhere to in ES2015.
It's true that creating an Iterator and stepping through it can present some
overhead compared to a simple for-loop or array.forEach
. However iterall
's
forEach
will delegate directly to array.forEach
and will use a for-loop for
Array-like objects, ensuring the best performance for Arrays while still
maintaining support for all Iterables.
Should my library functions also return Iterables instead of Arrays? Won't that be limiting?
That could definitely be limiting if you return some generic Iterable where you could have returned an Array, and (depending on context) I wouldn't recommend you stop returning Arrays from functions if that's what you're doing today. However if your functions are returning some collection data-structure that is not an Array, you should certainly consider having them implement the Iterable protocol so they can be more widely useful.
Here are a few examples:
In React, render functions are expected to return view trees, where any
node (e.g. a <ul>
) can have many children (e.g. many <li>
). While it could
expect those children to always be represented as an Array, that would limit
React's usefulness - other data-structures couldn't be used. Instead, React
expects those children to be represented as an Iterable. That allows it to
continue to accept Arrays, but also accept many other data-structures.
Immutable.js implements many new kinds of data-structures (including HAMT)
all of which implement Iterable, which allows them to be used in many of
JavaScript's built-in functions, but also allows them to be used by many
libraries which accept Iterables, including React. Also, similar to
Array.from
, Immutable.js's constructors accept not only Arrays,
by any Iterable, allowing you to build any of these new data-structures from
any other data-structure.
Where are all the other functions like
map
,filter
, andreduce
?
Those "higher order" collection functions are awesome, but they don't belong in
this library. Instead this library should be used as a basis for building such
a library (as it should be used for many other libraries). The forEach
function provided by iterall
can be used as the underpinning for these.
As an example:
function reduce (collection, reducer, initial) {
var reduced = initial
forEach(collection, function (item) {
reduced = reducer(reduced, item)
})
return reduced
}
A property name to be used as the name of an Iterable's method responsible
for producing an Iterator. Typically represents the value Symbol.iterator
.
Symbol
is defined in ES2015 environments, however some transitioning
JavaScript environments, such as older versions of Node define Symbol
, but
do not define Symbol.iterator
. Older versions of Mozilla Firefox,
which originally introduced the Iterable protocol, used the string
value "@@iterator"
. This string value is used when Symbol.iterator
is
not defined.
Use $$ITERATOR
for defining new Iterables instead of Symbol.iterator
,
but do not use it for accessing existing Iterables, instead use
getIterator()
or isIterable()
.
Examples
var $$ITERATOR = require('iterall').$$ITERATOR
function Counter (to) {
this.to = to
}
Counter.prototype[$$ITERATOR] = function () {
return {
to: this.to,
num: 0,
next () {
if (this.num >= this.to) {
return { value: undefined, done: true }
}
return { value: this.num++, done: false }
}
}
}
var counter = new Counter(3)
for (var number of counter) {
console.log(number) // 0 ... 1 ... 2
}
Returns true if the provided object implements the Iterator protocol via
either implementing a Symbol.iterator
or "@@iterator"
method.
Parameters
obj
A value which might implement the Iterable protocol.Examples
var isIterable = require('iterall').isIterable
isIterable([ 1, 2, 3 ]) // true
isIterable('ABC') // true
isIterable({ length: 1, 0: 'Alpha' }) // false
isIterable({ key: 'value' }) // false
isIterable(new Map()) // true
Returns boolean true if Iterable.
Returns true if the provided object implements the Array-like protocol via
defining a positive-integer length
property.
Parameters
obj
A value which might implement the Array-like protocol.Examples
var isArrayLike = require('iterall').isArrayLike
isArrayLike([ 1, 2, 3 ]) // true
isArrayLike('ABC') // true
isArrayLike({ length: 1, 0: 'Alpha' }) // true
isArrayLike({ key: 'value' }) // false
isArrayLike(new Map()) // false
Returns boolean true if Array-like.
Returns true if the provided object is an Object (i.e. not a string literal) and is either Iterable or Array-like.
This may be used in place of Array.isArray() to determine if an object should be iterated-over. It always excludes string literals and includes Arrays (regardless of if it is Iterable). It also includes other Array-like objects such as NodeList, TypedArray, and Buffer.
Parameters
obj
An Object value which might implement the Iterable or Array-like protocols.Examples
var isCollection = require('iterall').isCollection
isCollection([ 1, 2, 3 ]) // true
isCollection('ABC') // false
isCollection({ length: 1, 0: 'Alpha' }) // true
isCollection({ key: 'value' }) // false
isCollection(new Map()) // true
var forEach = require('iterall').forEach
if (isCollection(obj)) {
forEach(obj, function (value) {
console.log(value)
})
}
Returns boolean true if Iterable or Array-like Object.
If the provided object implements the Iterator protocol, its Iterator object is returned. Otherwise returns undefined.
Parameters
iterable
Iterable<T> An Iterable object which is the source of an Iterator.Examples
var getIterator = require('iterall').getIterator
var iterator = getIterator([ 1, 2, 3 ])
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: undefined, done: true }
Returns Iterator<T> new Iterator instance.
If the provided object implements the Iterator protocol, the method responsible for producing its Iterator object is returned.
This is used in rare cases for performance tuning. This method must be called with obj as the contextual this-argument.
Parameters
iterable
Iterable<T> An Iterable object which defines an @@iterator
method.Examples
var getIteratorMethod = require('iterall').getIteratorMethod
var myArray = [ 1, 2, 3 ]
var method = getIteratorMethod(myArray)
if (method) {
var iterator = method.call(myArray)
}
Returns function (): Iterator<T> @@iterator
method.
Given an object which either implements the Iterable protocol or is
Array-like, iterate over it, calling the callback
at each iteration.
Use forEach
where you would expect to use a for ... of
loop in ES6.
However forEach
adheres to the behavior of Array#forEach described in
the ECMAScript specification, skipping over "holes" in Array-likes. It will
also delegate to a forEach
method on collection
if one is defined,
ensuring native performance for Arrays
.
Similar to Array#forEach, the callback
function accepts three
arguments, and is provided with thisArg
as the calling context.
Note: providing an infinite Iterator to forEach will produce an error.
Parameters
collection
(Iterable<T> | {length: number}) The Iterable or array to iterate over.callback
function (T, number, object) Function to execute for each iteration, taking up to three argumentsthisArg
Optional. Value to use as this
when executing callback
.Examples
var forEach = require('iterall').forEach
forEach(myIterable, function (value, index, iterable) {
console.log(value, index, iterable === myIterable)
})
// ES6:
for (let value of myIterable) {
console.log(value)
}
// Any JavaScript environment:
forEach(myIterable, function (value) {
console.log(value)
})
Similar to getIterator()
, this method returns a new Iterator given an
Iterable. However it will also create an Iterator for a non-Iterable
Array-like collection, such as Array in a non-ES2015 environment.
createIterator
is complimentary to forEach
, but allows a "pull"-based
iteration as opposed to forEach
's "push"-based iteration.
createIterator
produces an Iterator for Array-likes with the same behavior
as ArrayIteratorPrototype described in the ECMAScript specification, and
does not skip over "holes".
Parameters
collection
(Iterable<T> | {length: number}) An Iterable or Array-like object to produce an Iterator.Examples
var createIterator = require('iterall').createIterator
var myArraylike = { length: 3, 0: 'Alpha', 1: 'Bravo', 2: 'Charlie' }
var iterator = createIterator(myArraylike)
iterator.next() // { value: 'Alpha', done: false }
iterator.next() // { value: 'Bravo', done: false }
iterator.next() // { value: 'Charlie', done: false }
iterator.next() // { value: undefined, done: true }
Returns Iterator<T> new Iterator instance.
Contributions are welcome and encouraged!
Remember that this library is designed to be small, straight-forward, and well-tested. The value of new additional features will be weighed against their size. This library also seeks to leverage and mirror the ECMAScript specification in its behavior as much as possible and reasonable.
This repository has far more documentation and explanation than code, and it is expected that the majority of contributions will come in the form of improving these.