prototype: Enhance the Enumerable and Array APIs to more closely match those of JavaScript 1.6 as implemented in Firefox 1.5. Closes #6650, #8409.

This commit is contained in:
Sam Stephenson 2007-07-09 18:55:58 +00:00
parent 29cd62ebeb
commit 1c17b6381a
7 changed files with 162 additions and 82 deletions

View File

@ -1,5 +1,12 @@
*SVN*
* Enhance the Enumerable and Array APIs to more closely match those of JavaScript 1.6 as implemented in Firefox 1.5. Closes #6650, #8409. [Mislav Marohnić, Sylvain Zimmer]
- Add Array#lastIndexOf, and change Array#indexOf not to overwrite the native method.
- Make Enumerable use Array.prototype.forEach instead of _each when possible (slight speed increase).
- Add "filter", "entries", "every", and "some" Array aliases.
- All Enumerable methods now have an additional parameter, "context", which, if present, specifies the object to which the iterators' "this" is bound.
- Function#bind and #curry now return the receiving function if the binding object is undefined.
* Temporary workaround for Prototype.BrowserFeatures.SpecificElementExtensions incorrectly evaluating to true on iPhone. (needs further investigation) [sam]
* The action for Form#request defaults to the current URL if the "action" attribute is empty. (This is what most of the major browsers do.) Fixes #8483. [Tomas, Mislav Marohnić]

View File

@ -1,8 +1,7 @@
function $A(iterable) {
if (!iterable) return [];
if (iterable.toArray) {
return iterable.toArray();
} else {
if (iterable.toArray) return iterable.toArray();
else {
var results = [];
for (var i = 0, length = iterable.length; i < length; i++)
results.push(iterable[i]);
@ -29,8 +28,7 @@ Array.from = $A;
Object.extend(Array.prototype, Enumerable);
if (!Array.prototype._reverse)
Array.prototype._reverse = Array.prototype.reverse;
if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;
Object.extend(Array.prototype, {
_each: function(iterator) {
@ -71,12 +69,6 @@ Object.extend(Array.prototype, {
});
},
indexOf: function(object) {
for (var i = 0, length = this.length; i < length; i++)
if (this[i] == object) return i;
return -1;
},
reverse: function(inline) {
return (inline !== false ? this : this.toArray())._reverse();
},
@ -115,6 +107,25 @@ Object.extend(Array.prototype, {
}
});
// use native browser JS 1.6 implementation if available
if (typeof Array.prototype.forEach == 'function')
Array.prototype._each = Array.prototype.forEach;
if (!Array.prototype.indexOf) Array.prototype.indexOf = function(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;
}
if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(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;
}
Array.prototype.toArray = Array.prototype.clone;
function $w(string) {

View File

@ -68,6 +68,7 @@ Object.extend(Object, {
Object.extend(Function.prototype, {
bind: function() {
if (arguments.length < 2 && arguments[0] === undefined) return this;
var __method = this, args = $A(arguments), object = args.shift();
return function() {
return __method.apply(object, args.concat($A(arguments)));
@ -82,6 +83,7 @@ Object.extend(Function.prototype, {
},
curry: function() {
if (!arguments.length) return this;
var __method = this, args = $A(arguments);
return function() {
return __method.apply(this, args.concat($A(arguments)));

View File

@ -1,8 +1,9 @@
var $break = {};
var Enumerable = {
each: function(iterator) {
each: function(iterator, context) {
var index = 0;
iterator = iterator.bind(context);
try {
this._each(function(value) {
iterator(value, index++);
@ -13,40 +14,45 @@ var Enumerable = {
return this;
},
eachSlice: function(number, iterator) {
eachSlice: function(number, iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var index = -number, slices = [], array = this.toArray();
while ((index += number) < array.length)
slices.push(array.slice(index, index+number));
return slices.map(iterator);
return slices.collect(iterator, context);
},
all: function(iterator) {
all: function(iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var result = true;
this.each(function(value, index) {
result = result && !!(iterator || Prototype.K)(value, index);
result = result && !!iterator(value, index);
if (!result) throw $break;
});
return result;
},
any: function(iterator) {
any: function(iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var result = false;
this.each(function(value, index) {
if (result = !!(iterator || Prototype.K)(value, index))
if (result = !!iterator(value, index))
throw $break;
});
return result;
},
collect: function(iterator) {
collect: function(iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var results = [];
this.each(function(value, index) {
results.push((iterator || Prototype.K)(value, index));
results.push(iterator(value, index));
});
return results;
},
detect: function(iterator) {
detect: function(iterator, context) {
iterator = iterator.bind(context);
var result;
this.each(function(value, index) {
if (iterator(value, index)) {
@ -57,7 +63,8 @@ var Enumerable = {
return result;
},
findAll: function(iterator) {
findAll: function(iterator, context) {
iterator = iterator.bind(context);
var results = [];
this.each(function(value, index) {
if (iterator(value, index))
@ -66,20 +73,24 @@ var Enumerable = {
return results;
},
grep: function(pattern, iterator) {
grep: function(pattern, iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var results = [];
this.each(function(value, index) {
var stringValue = value.toString();
if (stringValue.match(pattern))
results.push((iterator || Prototype.K)(value, index));
})
results.push(iterator(value, index));
});
return results;
},
include: function(object) {
if (typeof this.indexOf == 'function')
return this.indexOf(object) != -1;
var found = false;
this.each(function(value) {
if (value == object) {
if (value === object) {
found = true;
throw $break;
}
@ -95,7 +106,8 @@ var Enumerable = {
});
},
inject: function(memo, iterator) {
inject: function(memo, iterator, context) {
iterator = iterator.bind(context);
this.each(function(value, index) {
memo = iterator(memo, value, index);
});
@ -109,30 +121,33 @@ var Enumerable = {
});
},
max: function(iterator) {
max: function(iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var result;
this.each(function(value, index) {
value = (iterator || Prototype.K)(value, index);
value = iterator(value, index);
if (result == undefined || value >= result)
result = value;
});
return result;
},
min: function(iterator) {
min: function(iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var result;
this.each(function(value, index) {
value = (iterator || Prototype.K)(value, index);
value = iterator(value, index);
if (result == undefined || value < result)
result = value;
});
return result;
},
partition: function(iterator) {
partition: function(iterator, context) {
iterator = iterator ? iterator.bind(context) : Prototype.K;
var trues = [], falses = [];
this.each(function(value, index) {
((iterator || Prototype.K)(value, index) ?
(iterator(value, index) ?
trues : falses).push(value);
});
return [trues, falses];
@ -140,13 +155,14 @@ var Enumerable = {
pluck: function(property) {
var results = [];
this.each(function(value, index) {
this.each(function(value) {
results.push(value[property]);
});
return results;
},
reject: function(iterator) {
reject: function(iterator, context) {
iterator = iterator.bind(context);
var results = [];
this.each(function(value, index) {
if (!iterator(value, index))
@ -155,7 +171,8 @@ var Enumerable = {
return results;
},
sortBy: function(iterator) {
sortBy: function(iterator, context) {
iterator = iterator.bind(context);
return this.map(function(value, index) {
return {value: value, criteria: iterator(value, index)};
}).sort(function(left, right) {
@ -192,6 +209,9 @@ Object.extend(Enumerable, {
map: Enumerable.collect,
find: Enumerable.detect,
select: Enumerable.findAll,
filter: Enumerable.findAll,
member: Enumerable.include,
entries: Enumerable.toArray
entries: Enumerable.toArray,
every: Enumerable.all,
some: Enumerable.any
});

View File

@ -111,6 +111,33 @@
assertEqual(-1, [0].indexOf(1));
assertEqual(0, [1].indexOf(1));
assertEqual(1, [0,1,2].indexOf(1));
assertEqual(0, [1,2,1].indexOf(1));
assertEqual(2, [1,2,1].indexOf(1, -1));
assertEqual(1, [undefined,null].indexOf(null));
}},
testLastIndexOf: function(){ with(this) {
assertEqual(-1,[].lastIndexOf(1));
assertEqual(-1, [0].lastIndexOf(1));
assertEqual(0, [1].lastIndexOf(1));
assertEqual(2, [0,2,4,6].lastIndexOf(4));
assertEqual(3, [4,4,2,4,6].lastIndexOf(4));
assertEqual(3, [0,2,4,6].lastIndexOf(6,3));
assertEqual(-1, [0,2,4,6].lastIndexOf(6,2));
assertEqual(0, [6,2,4,6].lastIndexOf(6,2));
var fixture = [1,2,3,4,3];
assertEqual(4, fixture.lastIndexOf(3));
assertEnumEqual([1,2,3,4,3],fixture);
//tests from http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Objects:Array:lastIndexOf
var array = [2, 5, 9, 2];
assertEqual(3,array.lastIndexOf(2));
assertEqual(-1,array.lastIndexOf(7));
assertEqual(3,array.lastIndexOf(2,3));
assertEqual(0,array.lastIndexOf(2,2));
assertEqual(0,array.lastIndexOf(2,-2));
assertEqual(3,array.lastIndexOf(2,-1));
}},
testInspect: function(){ with(this) {

View File

@ -54,41 +54,31 @@
assert3(a3);
}
var globalBindTest = null;
new Test.Unit.Runner({
testFunctionBind: function() { with(this) {
function methodWithoutArguments(){
globalBindTest = this.hi;
}
function methodWithArguments(){
globalBindTest = this.hi + ',' + $A(arguments).join(',');
}
function methodWithBindArguments(){
globalBindTest = this.hi + ',' + $A(arguments).join(',');
}
function methodWithBindArgumentsAndArguments(){
globalBindTest = this.hi + ',' + $A(arguments).join(',');
}
methodWithoutArguments.bind({hi:'without'})();
assertEqual('without', globalBindTest);
methodWithArguments.bind({hi:'with'})('arg1','arg2');
assertEqual('with,arg1,arg2', globalBindTest);
methodWithBindArguments.bind({hi:'withBindArgs'},'arg1','arg2')();
assertEqual('withBindArgs,arg1,arg2', globalBindTest);
methodWithBindArgumentsAndArguments.bind({hi:'withBindArgsAndArgs'},'arg1','arg2')('arg3','arg4');
assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4', globalBindTest);
function methodWithoutArguments() { return this.hi };
function methodWithArguments() { return this.hi + ',' + $A(arguments).join(',') };
var func = Prototype.emptyFunction;
assertIdentical(func, func.bind());
assertIdentical(func, func.bind(undefined));
assertNotIdentical(func, func.bind(null));
assertEqual('without', methodWithoutArguments.bind({ hi: 'without' })());
assertEqual('with,arg1,arg2', methodWithArguments.bind({ hi: 'with' })('arg1','arg2'));
assertEqual('withBindArgs,arg1,arg2',
methodWithArguments.bind({ hi: 'withBindArgs' }, 'arg1', 'arg2')());
assertEqual('withBindArgsAndArgs,arg1,arg2,arg3,arg4',
methodWithArguments.bind({ hi: 'withBindArgsAndArgs' }, 'arg1', 'arg2')('arg3', 'arg4'));
}},
testFunctionCurry: function() { with(this) {
var split = function(delimiter, string) { return string.split(delimiter); };
var splitOnColons = split.curry(":");
assertNotIdentical(split, splitOnColons);
assertEnumEqual(split(":", "0:1:2:3:4:5"), splitOnColons("0:1:2:3:4:5"));
assertIdentical(split, split.curry());
}},
testFunctionDelay: function() { with(this) {

View File

@ -36,6 +36,8 @@
Nicknames: $w('sam- noradio htonl Ulysses'),
Basic: [1, 2, 3],
Primes: [
1, 2, 3, 5, 7, 11, 13, 17, 19, 23,
29, 31, 37, 41, 43, 47, 53, 59, 61, 67,
@ -57,7 +59,7 @@
new Test.Unit.Runner({
testEachBreak: function() {with(this) {
var result = 0;
[1, 2, 3].each(function(value) {
Fixtures.Basic.each(function(value) {
if ((result = value) == 2) throw $break;
});
@ -66,7 +68,7 @@
testEachReturnActsAsContinue: function() {with(this) {
var results = [];
[1, 2, 3].each(function(value) {
Fixtures.Basic.each(function(value) {
if (value == 2) return;
results.push(value);
});
@ -76,7 +78,26 @@
testEachChaining: function() {with(this) {
assertEqual(Fixtures.Primes, Fixtures.Primes.each(Prototype.emptyFunction));
assertEqual(3, [1, 2, 3].each(Prototype.emptyFunction).length);
assertEqual(3, Fixtures.Basic.each(Prototype.emptyFunction).length);
}},
testEnumContext: function() {with(this) {
var results = [];
Fixtures.Basic.each(function(value) {
results.push(value * this.i);
}, { i: 2 });
assertEqual('2 4 6', results.join(' '));
assert(Fixtures.Basic.all(function(value){
return value >= this.min && value <= this.max;
}, { min: 1, max: 3 }));
assert(!Fixtures.Basic.all(function(value){
return value >= this.min && value <= this.max;
}));
assert(Fixtures.Basic.any(function(value){
return value == this.target_value;
}, { target_value: 2 }));
}},
testAny: function() {with(this) {
@ -86,11 +107,11 @@
assert([true, false, false].any());
assert(![false, false, false].any());
assert([1, 2, 3, 4, 5].any(function(value) {
return value > 3;
assert(Fixtures.Basic.any(function(value) {
return value > 2;
}));
assert(![1, 2, 3, 4, 5].any(function(value) {
return value > 10;
assert(!Fixtures.Basic.any(function(value) {
return value > 5;
}));
}},
@ -101,11 +122,11 @@
assert(![true, false, false].all());
assert(![false, false, false].all());
assert([1, 2, 3, 4, 5].all(function(value) {
assert(Fixtures.Basic.all(function(value) {
return value > 0;
}));
assert(![1, 2, 3, 4, 5].all(function(value) {
return value > 3;
assert(!Fixtures.Basic.all(function(value) {
return value > 1;
}));
}},
@ -129,7 +150,7 @@
assertEnumEqual([], [].eachSlice(2));
assertEqual(1, [1].eachSlice(1).length);
assertEnumEqual([1], [1].eachSlice(1)[0]);
assertEqual(2, [1,2,3].eachSlice(2).length);
assertEqual(2, Fixtures.Basic.eachSlice(2).length);
assertEnumEqual(
[3, 2, 1, 11, 7, 5, 19, 17, 13, 31, 29, 23, 43, 41, 37, 59, 53, 47, 71, 67, 61, 83, 79, 73, 97, 89],
Fixtures.Primes.eachSlice( 3, function(slice){ return slice.reverse() }).flatten()
@ -184,18 +205,20 @@
assertEnumEqual([1, 2, 3, 4], arr[0]);
assertEnumEqual([5, 6, null, null], arr[1]);
arr = [1, 2, 3].inGroupsOf(4,'x');
var basic = Fixtures.Basic
arr = basic.inGroupsOf(4,'x');
assertEqual(1, arr.length);
assertEnumEqual([1, 2, 3, 'x'], arr[0]);
assertEnumEqual([1,2,3,'a'], [1,2,3].inGroupsOf(2, 'a').flatten());
assertEnumEqual([1,2,3,'a'], basic.inGroupsOf(2, 'a').flatten());
arr = [1, 2, 3].inGroupsOf(5, '');
arr = basic.inGroupsOf(5, '');
assertEqual(1, arr.length);
assertEnumEqual([1, 2, 3, '', ''], arr[0]);
assertEnumEqual([1,2,3,0], [1,2,3].inGroupsOf(2, 0).flatten());
assertEnumEqual([1,2,3,false], [1,2,3].inGroupsOf(2, false).flatten());
assertEnumEqual([1,2,3,0], basic.inGroupsOf(2, 0).flatten());
assertEnumEqual([1,2,3,false], basic.inGroupsOf(2, false).flatten());
}},
testInject: function() {with(this) {