450 lines
13 KiB
JavaScript
450 lines
13 KiB
JavaScript
/** section: Language, related to: Array
|
|
* $A(iterable) -> Array
|
|
*
|
|
* Accepts an array-like collection (anything with numeric indices) and returns
|
|
* its equivalent as an actual Array object.
|
|
* This method is a convenience alias of [[Array.from]], but is the preferred way
|
|
* of casting to an Array.
|
|
**/
|
|
function $A(iterable) {
|
|
if (!iterable) return [];
|
|
// Safari <2.0.4 crashes when accessing property of a node list with property accessor.
|
|
// It nevertheless works fine with `in` operator, which is why we use it here
|
|
if ('toArray' in Object(iterable)) return iterable.toArray();
|
|
var length = iterable.length || 0, results = new Array(length);
|
|
while (length--) results[length] = iterable[length];
|
|
return results;
|
|
}
|
|
|
|
/** section: Language, related to: Array
|
|
* $w(string) -> Array
|
|
* - string (String): A string with zero or more spaces.
|
|
*
|
|
* Splits a string into an array, treating all whitespace as delimiters.
|
|
*
|
|
* Equivalent to Ruby's `%w{foo bar}` or Perl's `qw(foo bar)`.
|
|
**/
|
|
function $w(string) {
|
|
if (!Object.isString(string)) return [];
|
|
string = string.strip();
|
|
return string ? string.split(/\s+/) : [];
|
|
}
|
|
|
|
/** alias of: $A
|
|
* Array.from(iterable) -> Array
|
|
**/
|
|
Array.from = $A;
|
|
|
|
/** section: Language
|
|
* class Array
|
|
* includes Enumerable
|
|
*
|
|
* Prototype extends all native JavaScript arrays with quite a few powerful
|
|
* methods.
|
|
*
|
|
* This is done in two ways:
|
|
*
|
|
* * It mixes in the [[Enumerable]] module, which brings in a ton of methods.
|
|
* * It adds quite a few extra methods, which are documented in this section.
|
|
*
|
|
* With Prototype, arrays become much, much more than the trivial objects we
|
|
* used to manipulate, limiting ourselves to using their `length` property and
|
|
* their `[]` indexing operator. They become very powerful objects that
|
|
* greatly simplify the code for 99% of the common use cases involving them.
|
|
*
|
|
* <h5>Why you should stop using for...in to iterate</h5>
|
|
*
|
|
* Many JavaScript authors have been misled into using the `for...in` JavaScript
|
|
* construct to loop over array elements. This kind of code just won't work
|
|
* with Prototype.
|
|
*
|
|
* The ECMA 262 standard, which defines ECMAScript 3rd edition, supposedly
|
|
* implemented by all major browsers including MSIE, defines ten methods
|
|
* on Array (§15.4.4), including nice methods like `concat`, `join`, `pop`, and
|
|
* `push`.
|
|
*
|
|
* This same standard explicitly defines that the `for...in` construct (§12.6.4)
|
|
* exists to enumerate the properties of the object appearing on the right side
|
|
* of the `in` keyword. Only properties specifically marked as _non-enumerable_
|
|
* are ignored by such a loop. By default, the `prototype` and `length`
|
|
* properties are so marked, which prevents you from enumerating over array
|
|
* methods when using for...in. This comfort led developers to use `for...in` as a
|
|
* shortcut for indexing loops, when it is not its actual purpose.
|
|
*
|
|
* However, Prototype has no way to mark the methods it adds to
|
|
* `Array.prototype` as non-enumerable. Therefore, using `for...in` on arrays
|
|
* when using Prototype will enumerate all extended methods as well, such as
|
|
* those coming from the [[Enumerable]] module, and those Prototype puts in the
|
|
* Array namespace (listed further below).
|
|
*
|
|
* <h5>What you should use instead</h5>
|
|
*
|
|
* You can revert to vanilla loops:
|
|
*
|
|
* for (var index = 0; index < myArray.length; ++index) {
|
|
* var item = myArray[index];
|
|
* // Your code working on item here...
|
|
* }
|
|
*
|
|
* Or you can use iterators, such as [[Array#each]]:
|
|
*
|
|
* myArray.each(function(item) {
|
|
* // Your code working on item here...
|
|
* });
|
|
*
|
|
*
|
|
* The inability to use `for...in` on arrays is not much of a burden: as you'll
|
|
* see, most of what you used to loop over arrays for can be concisely done
|
|
* using the new methods provided by Array or the mixed-in [[Enumerable]]
|
|
* module. So manual loops should be fairly rare.
|
|
*
|
|
*
|
|
* <h5>A note on performance</h5>
|
|
*
|
|
* Should you have a very large array, using iterators with lexical closures
|
|
* (anonymous functions that you pass to the iterators and that get invoked at
|
|
* every loop iteration) in methods like [[Array#each]] — _or_ relying on
|
|
* repetitive array construction (such as uniq), may yield unsatisfactory
|
|
* performance. In such cases, you're better off writing manual indexing loops,
|
|
* but take care then to cache the length property and use the prefix `++`
|
|
* operator:
|
|
*
|
|
* // Custom loop with cached length property: maximum full-loop
|
|
* // performance on very large arrays!
|
|
* for (var index = 0, len = myArray.length; index < len; ++index) {
|
|
* var item = myArray[index];
|
|
* // Your code working on item here...
|
|
* }
|
|
*
|
|
**/
|
|
|
|
(function() {
|
|
var arrayProto = Array.prototype,
|
|
slice = arrayProto.slice,
|
|
_each = arrayProto.forEach; // use native browser JS 1.6 implementation if available
|
|
|
|
function each(iterator) {
|
|
for (var i = 0, length = this.length; i < length; i++)
|
|
iterator(this[i]);
|
|
}
|
|
if (!_each) _each = each;
|
|
|
|
/**
|
|
* Array#clear() -> Array
|
|
*
|
|
* Clears the array (makes it empty) and returns the array reference.
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* var guys = ['Sam', 'Justin', 'Andrew', 'Dan'];
|
|
* guys.clear();
|
|
* // -> []
|
|
* guys
|
|
* // -> []
|
|
**/
|
|
function clear() {
|
|
this.length = 0;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Array#first() -> ?
|
|
*
|
|
* Returns the array's first item (e.g., `array[0]`).
|
|
**/
|
|
function first() {
|
|
return this[0];
|
|
}
|
|
|
|
/**
|
|
* Array#last() -> ?
|
|
*
|
|
* Returns the array's last item (e.g., `array[array.length - 1]`).
|
|
**/
|
|
function last() {
|
|
return this[this.length - 1];
|
|
}
|
|
|
|
/**
|
|
* Array#compact() -> Array
|
|
*
|
|
* Returns a **copy** of the array without any `null` or `undefined` values.
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* var orig = [undefined, 'A', undefined, 'B', null, 'C'];
|
|
* var copy = orig.compact();
|
|
* // orig -> [undefined, 'A', undefined, 'B', null, 'C'];
|
|
* // copy -> ['A', 'B', 'C'];
|
|
**/
|
|
function compact() {
|
|
return this.select(function(value) {
|
|
return value != null;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Array#flatten() -> Array
|
|
*
|
|
* Returns a flattened (one-dimensional) copy of the array, leaving
|
|
* the original array unchanged.
|
|
*
|
|
* Nested arrays are recursively injected inline. This can prove very
|
|
* useful when handling the results of a recursive collection algorithm,
|
|
* for instance.
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* var a = ['frank', ['bob', 'lisa'], ['jill', ['tom', 'sally']]];
|
|
* var b = a.flatten();
|
|
* // a -> ['frank', ['bob', 'lisa'], ['jill', ['tom', 'sally']]]
|
|
* // b -> ['frank', 'bob', 'lisa', 'jill', 'tom', 'sally']
|
|
**/
|
|
function flatten() {
|
|
return this.inject([], function(array, value) {
|
|
if (Object.isArray(value))
|
|
return array.concat(value.flatten());
|
|
array.push(value);
|
|
return array;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Array#without(value[, value...]) -> Array
|
|
* - value (?): A value to exclude.
|
|
*
|
|
* Produces a new version of the array that does not contain any of the
|
|
* specified values, leaving the original array unchanged.
|
|
*
|
|
* <h5>Examples</h5>
|
|
*
|
|
* [3, 5, 6].without(3)
|
|
* // -> [5, 6]
|
|
*
|
|
* [3, 5, 6, 20].without(20, 6)
|
|
* // -> [3, 5]
|
|
**/
|
|
function without() {
|
|
var values = slice.call(arguments, 0);
|
|
return this.select(function(value) {
|
|
return !values.include(value);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Array#reverse([inline = true]) -> Array
|
|
* - inline (Boolean): Whether to modify the array in place. Defaults to `true`.
|
|
* Clones the original array when `false`.
|
|
*
|
|
* Reverses the array's contents, optionally cloning it first.
|
|
*
|
|
* <h5>Examples</h5>
|
|
*
|
|
* // Making a copy
|
|
* var nums = [3, 5, 6, 1, 20];
|
|
* var rev = nums.reverse(false);
|
|
* // nums -> [3, 5, 6, 1, 20]
|
|
* // rev -> [20, 1, 6, 5, 3]
|
|
*
|
|
* // Working inline
|
|
* var nums = [3, 5, 6, 1, 20];
|
|
* nums.reverse();
|
|
* // nums -> [20, 1, 6, 5, 3]
|
|
**/
|
|
function reverse(inline) {
|
|
return (inline === false ? this.toArray() : this)._reverse();
|
|
}
|
|
|
|
/**
|
|
* Array#uniq([sorted = false]) -> Array
|
|
* - sorted (Boolean): Whether the array has already been sorted. If `true`,
|
|
* a less-costly algorithm will be used.
|
|
*
|
|
* Produces a duplicate-free version of an array. If no duplicates are
|
|
* found, the original array is returned.
|
|
*
|
|
* On large arrays when `sorted` is `false`, this method has a potentially
|
|
* large performance cost.
|
|
*
|
|
* <h5>Examples</h5>
|
|
*
|
|
* [1, 3, 2, 1].uniq();
|
|
* // -> [1, 2, 3]
|
|
*
|
|
* ['A', 'a'].uniq();
|
|
* // -> ['A', 'a'] (because String comparison is case-sensitive)
|
|
**/
|
|
function uniq(sorted) {
|
|
return this.inject([], function(array, value, index) {
|
|
if (0 == index || (sorted ? array.last() != value : !array.include(value)))
|
|
array.push(value);
|
|
return array;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Array#intersect(array) -> Array
|
|
* - array (Array): A collection of values.
|
|
*
|
|
* Returns an array containing every item that is shared between the two
|
|
* given arrays.
|
|
**/
|
|
function intersect(array) {
|
|
return this.uniq().findAll(function(item) {
|
|
return array.detect(function(value) { return item === value });
|
|
});
|
|
}
|
|
|
|
/** alias of: Array#clone
|
|
* Array#toArray() -> Array
|
|
**/
|
|
|
|
/**
|
|
* Array#clone() -> Array
|
|
*
|
|
* Returns a duplicate of the array, leaving the original array intact.
|
|
**/
|
|
function clone() {
|
|
return slice.call(this, 0);
|
|
}
|
|
|
|
/** related to: Enumerable#size
|
|
* Array#size() -> Number
|
|
*
|
|
* Returns the size of the array (e.g., `array.length`).
|
|
*
|
|
* This is just a local optimization of the mixed-in [[Enumerable#size]]
|
|
* which avoids array cloning and uses the array's native length property.
|
|
**/
|
|
function size() {
|
|
return this.length;
|
|
}
|
|
|
|
/** related to: Object.inspect
|
|
* Array#inspect() -> String
|
|
*
|
|
* Returns the debug-oriented string representation of an array.
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* ['Apples', {good: 'yes', bad: 'no'}, 3, 34].inspect()
|
|
* // -> "['Apples', [object Object], 3, 34]"
|
|
**/
|
|
function inspect() {
|
|
return '[' + this.map(Object.inspect).join(', ') + ']';
|
|
}
|
|
|
|
/** related to: Object.toJSON
|
|
* Array#toJSON() -> String
|
|
*
|
|
* Returns a JSON string representation of the array.
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* ['a', {b: null}].toJSON();
|
|
* //-> '["a", {"b": null}]'
|
|
**/
|
|
function toJSON() {
|
|
var results = [];
|
|
this.each(function(object) {
|
|
var value = Object.toJSON(object);
|
|
if (!Object.isUndefined(value)) results.push(value);
|
|
});
|
|
return '[' + results.join(', ') + ']';
|
|
}
|
|
|
|
/**
|
|
* Array#indexOf(item[, offset = 0]) -> Number
|
|
* - item (?): A value that may or may not be in the array.
|
|
* - offset (Number): The number of initial items to skip before beginning
|
|
* the search.
|
|
*
|
|
* Returns the index of the first occurrence of `item` within the array,
|
|
* or `-1` if `item` doesn't exist in the array. `Array#indexOf` compares
|
|
* items using *strict equality* (`===`).
|
|
*
|
|
* <h5>Examples</h5>
|
|
*
|
|
* [3, 5, 6, 1, 20].indexOf(1)
|
|
* // -> 3
|
|
*
|
|
* [3, 5, 6, 1, 20].indexOf(90)
|
|
* // -> -1 (not found)
|
|
*
|
|
* ['1', '2', '3'].indexOf(1);
|
|
* // -> -1 (not found, 1 !== '1')
|
|
**/
|
|
function indexOf(item, i) {
|
|
i || (i = 0);
|
|
var length = this.length;
|
|
if (i < 0) i = length + i;
|
|
for (; i < length; i++)
|
|
if (this[i] === item) return i;
|
|
return -1;
|
|
}
|
|
|
|
/** related to: Array#indexOf
|
|
* Array#lastIndexOf(item[, offset]) -> Number
|
|
* - item (?): A value that may or may not be in the array.
|
|
* - offset (Number): The number of items at the end to skip before beginning
|
|
* the search.
|
|
*
|
|
* Returns the position of the last occurrence of `item` within the array — or
|
|
* `-1` if `item` doesn't exist in the array.
|
|
**/
|
|
function lastIndexOf(item, i) {
|
|
i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
|
|
var n = this.slice(0, i).reverse().indexOf(item);
|
|
return (n < 0) ? n : i - n - 1;
|
|
}
|
|
|
|
// Replaces a built-in function. No PDoc needed.
|
|
function concat() {
|
|
var array = slice.call(this, 0), item;
|
|
for (var i = 0, length = arguments.length; i < length; i++) {
|
|
item = arguments[i];
|
|
if (Object.isArray(item) && !('callee' in item)) {
|
|
for (var j = 0, arrayLength = item.length; j < arrayLength; j++)
|
|
array.push(item[j]);
|
|
} else {
|
|
array.push(item);
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
Object.extend(arrayProto, Enumerable);
|
|
|
|
if (!arrayProto._reverse)
|
|
arrayProto._reverse = arrayProto.reverse;
|
|
|
|
Object.extend(arrayProto, {
|
|
_each: _each,
|
|
clear: clear,
|
|
first: first,
|
|
last: last,
|
|
compact: compact,
|
|
flatten: flatten,
|
|
without: without,
|
|
reverse: reverse,
|
|
uniq: uniq,
|
|
intersect: intersect,
|
|
clone: clone,
|
|
toArray: clone,
|
|
size: size,
|
|
inspect: inspect,
|
|
toJSON: toJSON
|
|
});
|
|
|
|
// fix for opera
|
|
var CONCAT_ARGUMENTS_BUGGY = (function() {
|
|
return [].concat(arguments)[0][0] !== 1;
|
|
})(1,2)
|
|
|
|
if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat;
|
|
|
|
// use native browser JS 1.6 implementation if available
|
|
if (!arrayProto.indexOf) arrayProto.indexOf = indexOf;
|
|
if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf;
|
|
})();
|