From 07a7b3e4cf961e51bddcdb90cdca34f9126ed286 Mon Sep 17 00:00:00 2001 From: Rick Waldron Date: Mon, 10 Dec 2012 13:52:02 -0500 Subject: [PATCH] Fixes #13021. Normalization of core utility array like detection based on standard protocol by Richard Gibson . Closes gh-1064 --- src/core.js | 73 ++++++++----- test/unit/core.js | 265 +++++++++++++++++++++++++++++++++------------- 2 files changed, 238 insertions(+), 100 deletions(-) diff --git a/src/core.js b/src/core.js index d748d0d21..9832daa59 100644 --- a/src/core.js +++ b/src/core.js @@ -583,21 +583,25 @@ jQuery.extend({ // args is for internal usage only each: function( obj, callback, args ) { - var name, + var value, i = 0, length = obj.length, - isObj = length === undefined || jQuery.isFunction( obj ); + isArray = isArraylike( obj ); if ( args ) { - if ( isObj ) { - for ( name in obj ) { - if ( callback.apply( obj[ name ], args ) === false ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { break; } } } else { - for ( ; i < length; ) { - if ( callback.apply( obj[ i++ ], args ) === false ) { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { break; } } @@ -605,15 +609,19 @@ jQuery.extend({ // A special, fast, case for the most common use of each } else { - if ( isObj ) { - for ( name in obj ) { - if ( callback.call( obj[ name ], name, obj[ name ] ) === false ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { break; } } } else { - for ( ; i < length; ) { - if ( callback.call( obj[ i ], i, obj[ i++ ] ) === false ) { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { break; } } @@ -640,18 +648,16 @@ jQuery.extend({ // results is for internal usage only makeArray: function( arr, results ) { - var type, - ret = results || []; + var ret = results || []; if ( arr != null ) { - // The window, strings (and functions) also have 'length' - // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - type = jQuery.type( arr ); - - if ( arr.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( arr ) ) { - core_push.call( ret, arr ); + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); } else { - jQuery.merge( ret, arr ); + core_push.call( ret, arr ); } } @@ -689,7 +695,6 @@ jQuery.extend({ for ( ; j < l; j++ ) { first[ i++ ] = second[ j ]; } - } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; @@ -722,12 +727,11 @@ jQuery.extend({ // arg is for internal usage only map: function( elems, callback, arg ) { - var value, key, - ret = [], + var value, i = 0, length = elems.length, - // jquery objects are treated as arrays - isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + isArray = isArraylike( elems ), + ret = []; // Go through the array, translating each of the items to their if ( isArray ) { @@ -741,8 +745,8 @@ jQuery.extend({ // Go through every key on the object, } else { - for ( key in elems ) { - value = callback( elems[ key ], key, arg ); + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); if ( value != null ) { ret[ ret.length ] = value; @@ -907,5 +911,18 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli class2type[ "[object " + name + "]" ] = name.toLowerCase(); }); +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || type !== "function" && + ( length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj ); +} + // All jQuery objects should point back to these rootjQuery = jQuery(document); diff --git a/test/unit/core.js b/test/unit/core.js index a7f0911be..ba68afa75 100644 --- a/test/unit/core.js +++ b/test/unit/core.js @@ -752,79 +752,148 @@ test("first()/last()", function() { }); test("map()", function() { - expect(8); + expect( 2 ); deepEqual( - jQuery("#ap").map(function(){ - return jQuery(this).find("a").get(); + jQuery("#ap").map(function() { + return jQuery( this ).find("a").get(); }).get(), - q("google", "groups", "anchor1", "mark"), + q( "google", "groups", "anchor1", "mark" ), "Array Map" ); deepEqual( - jQuery("#ap > a").map(function(){ + jQuery("#ap > a").map(function() { return this.parentNode; }).get(), - q("ap","ap","ap"), + q( "ap","ap","ap" ), "Single Map" ); +}); + +test("jQuery.map", function() { + expect( 25 ); - var keys, values, scripts, nonsense, mapped, flat; - //for #2616 - keys = jQuery.map( {"a":1,"b":2}, function( v, k ){ + var i, label, result, callback; + + result = jQuery.map( [ 3, 4, 5 ], function( v, k ) { return k; }); - equal( keys.join(""), "ab", "Map the keys from a hash to an array" ); + equal( result.join(""), "012", "Map the keys from an array" ); - values = jQuery.map( {a:1,b:2}, function( v, k ){ + result = jQuery.map( [ 3, 4, 5 ], function( v, k ) { return v; }); - equal( values.join(""), "12", "Map the values from a hash to an array" ); + equal( result.join(""), "345", "Map the values from an array" ); + + result = jQuery.map( { a: 1, b: 2 }, function( v, k ) { + return k; + }); + equal( result.join(""), "ab", "Map the keys from an object" ); - // object with length prop - values = jQuery.map( {a:1,b:2, length:3}, function( v, k ){ + result = jQuery.map( { a: 1, b: 2 }, function( v, k ) { return v; }); - equal( values.join(""), "123", "Map the values from a hash with a length property to an array" ); + equal( result.join(""), "12", "Map the values from an object" ); - scripts = document.getElementsByTagName("script"); - mapped = jQuery.map( scripts, function( v, k ){ + result = jQuery.map( [ "a", undefined, null, "b" ], function( v, k ) { return v; }); - equal( mapped.length, scripts.length, "Map an array(-like) to a hash" ); + equal( result.join(""), "ab", "Array iteration does not include undefined/null results" ); - nonsense = document.getElementsByTagName("asdf"); - mapped = jQuery.map( nonsense, function( v, k ){ + result = jQuery.map( { a: "a", b: undefined, c: null, d: "b" }, function( v, k ) { return v; }); - equal( mapped.length, nonsense.length, "Map an empty array(-like) to a hash" ); + equal( result.join(""), "ab", "Object iteration does not include undefined/null results" ); + + result = { + Zero: function() {}, + One: function( a ) {}, + Two: function( a, b ) {} + }; + callback = function( v, k ) { + equal( k, "foo", label + "-argument function treated like object" ); + }; + for ( i in result ) { + label = i; + result[ i ].foo = "bar"; + jQuery.map( result[ i ], callback ); + } + + result = { + "undefined": undefined, + "null": null, + "false": false, + "true": true, + "empty string": "", + "nonempty string": "string", + "string \"0\"": "0", + "negative": -1, + "excess": 1 + }; + callback = function( v, k ) { + equal( k, "length", "Object with " + label + " length treated like object" ); + }; + for ( i in result ) { + label = i; + jQuery.map( { length: result[ i ] }, callback ); + } + + result = { + "sparse Array": Array( 4 ), + "length: 1 plain object": { length: 1, "0": true }, + "length: 2 plain object": { length: 2, "0": true, "1": true }, + NodeList: document.getElementsByTagName("html") + }; + callback = function( v, k ) { + if ( result[ label ] ) { + delete result[ label ]; + equal( k, "0", label + " treated like array" ); + } + }; + for ( i in result ) { + label = i; + jQuery.map( result[ i ], callback ); + } + + result = false; + jQuery.map( { length: 0 }, function( v, k ) { + result = true; + }); + ok( !result, "length: 0 plain object treated like array" ); + + result = false; + jQuery.map( document.getElementsByTagName("asdf"), function( v, k ) { + result = true; + }); + ok( !result, "empty NodeList treated like array" ); - flat = jQuery.map( Array(4), function( v, k ){ - return k % 2 ? k : [k,k,k];//try mixing array and regular returns + result = jQuery.map( Array(4), function( v, k ){ + return k % 2 ? k : [k,k,k]; }); - equal( flat.join(""), "00012223", "try the new flatten technique(#2616)" ); + equal( result.join(""), "00012223", "Array results flattened (#2616)" ); }); test("jQuery.merge()", function() { expect(8); - var parse = jQuery.merge; + deepEqual( jQuery.merge([],[]), [], "Empty arrays" ); - deepEqual( parse([],[]), [], "Empty arrays" ); + deepEqual( jQuery.merge([ 1 ],[ 2 ]), [ 1, 2 ], "Basic" ); + deepEqual( jQuery.merge([ 1, 2 ], [ 3, 4 ]), [ 1, 2, 3, 4 ], "Basic" ); - deepEqual( parse([1],[2]), [1,2], "Basic" ); - deepEqual( parse([1,2],[3,4]), [1,2,3,4], "Basic" ); - - deepEqual( parse([1,2],[]), [1,2], "Second empty" ); - deepEqual( parse([],[1,2]), [1,2], "First empty" ); + deepEqual( jQuery.merge([ 1, 2 ],[]), [ 1, 2 ], "Second empty" ); + deepEqual( jQuery.merge([],[ 1, 2 ]), [ 1, 2 ], "First empty" ); // Fixed at [5998], #3641 - deepEqual( parse([-2,-1], [0,1,2]), [-2,-1,0,1,2], "Second array including a zero (falsy)"); + deepEqual( jQuery.merge([ -2, -1 ], [ 0, 1, 2 ]), [ -2, -1 , 0, 1, 2 ], + "Second array including a zero (falsy)"); // After fixing #5527 - deepEqual( parse([], [null, undefined]), [null, undefined], "Second array including null and undefined values"); - deepEqual( parse({"length":0}, [1,2]), {length:2, 0:1, 1:2}, "First array like"); + deepEqual( jQuery.merge([], [ null, undefined ]), [ null, undefined ], + "Second array including null and undefined values"); + deepEqual( jQuery.merge({ length: 0 }, [ 1, 2 ] ), { length: 2, 0: 1, 1: 2}, + "First array like"); }); test("jQuery.extend(Object, Object)", function() { @@ -937,54 +1006,110 @@ test("jQuery.extend(Object, Object)", function() { }); test("jQuery.each(Object,Function)", function() { - expect(14); - jQuery.each( [0,1,2], function(i, n){ - equal( i, n, "Check array iteration" ); - }); + expect( 23 ); + + var i, label, seen, callback; - jQuery.each( [5,6,7], function(i, n){ - equal( i, n - 5, "Check array iteration" ); + seen = {}; + jQuery.each( [ 3, 4, 5 ], function( k, v ) { + seen[ k ] = v; }); + deepEqual( seen, { "0": 3, "1": 4, "2": 5 }, "Array iteration" ); - jQuery.each( { name: "name", lang: "lang" }, function(i, n){ - equal( i, n, "Check object iteration" ); + seen = {}; + jQuery.each( { name: "name", lang: "lang" }, function( k, v ) { + seen[ k ] = v; }); + deepEqual( seen, { name: "name", lang: "lang" }, "Object iteration" ); - var total = 0; - jQuery.each([1,2,3], function(i,v){ total += v; }); - equal( total, 6, "Looping over an array" ); - total = 0; - jQuery.each([1,2,3], function(i,v){ - total += v; - if ( i == 1 ) { + seen = []; + jQuery.each( [ 1, 2, 3 ], function( k, v ) { + seen.push( v ); + if ( k === 1 ) { return false; } }); - equal( total, 3, "Looping over an array, with break" ); - total = 0; - jQuery.each({"a":1,"b":2,"c":3}, function(i,v){ total += v; }); - equal( total, 6, "Looping over an object" ); - total = 0; - jQuery.each({"a":3,"b":3,"c":3}, function(i,v){ total += v; return false; }); - equal( total, 3, "Looping over an object, with break" ); - - var f = function(){}; - f.foo = "bar"; - jQuery.each(f, function(i){ - f[i] = "baz"; + deepEqual( seen, [ 1, 2 ] , "Broken array iteration" ); + + seen = []; + jQuery.each( {"a": 1, "b": 2,"c": 3 }, function( k, v ) { + seen.push( v ); + return false; + }); + deepEqual( seen, [ 1 ], "Broken object iteration" ); + + seen = { + Zero: function() {}, + One: function( a ) {}, + Two: function( a, b ) {} + }; + callback = function( k, v ) { + equal( k, "foo", label + "-argument function treated like object" ); + }; + for ( i in seen ) { + label = i; + seen[ i ].foo = "bar"; + jQuery.each( seen[ i ], callback ); + } + + seen = { + "undefined": undefined, + "null": null, + "false": false, + "true": true, + "empty string": "", + "nonempty string": "string", + "string \"0\"": "0", + "negative": -1, + "excess": 1 + }; + callback = function( k, v ) { + equal( k, "length", "Object with " + label + " length treated like object" ); + }; + for ( i in seen ) { + label = i; + jQuery.each( { length: seen[ i ] }, callback ); + } + + seen = { + "sparse Array": Array( 4 ), + "length: 1 plain object": { length: 1, "0": true }, + "length: 2 plain object": { length: 2, "0": true, "1": true }, + NodeList: document.getElementsByTagName("html") + }; + callback = function( k, v ) { + if ( seen[ label ] ) { + delete seen[ label ]; + equal( k, "0", label + " treated like array" ); + return false; + } + }; + for ( i in seen ) { + label = i; + jQuery.each( seen[ i ], callback ); + } + + seen = false; + jQuery.each( { length: 0 }, function( k, v ) { + seen = true; }); - equal( "baz", f.foo, "Loop over a function" ); + ok( !seen, "length: 0 plain object treated like array" ); - var stylesheet_count = 0; - jQuery.each(document.styleSheets, function(i){ - stylesheet_count++; + seen = false; + jQuery.each( document.getElementsByTagName("asdf"), function( k, v ) { + seen = true; }); - equal(stylesheet_count, 2, "should not throw an error in IE while looping over document.styleSheets and return proper amount"); + ok( !seen, "empty NodeList treated like array" ); + i = 0; + jQuery.each( document.styleSheets, function() { + i++; + }); + equal( i, 2, "Iteration over document.styleSheets" ); }); test("jQuery.makeArray", function(){ - expect(17); + expect(15); equal( jQuery.makeArray(jQuery("html>*"))[0].nodeName.toUpperCase(), "HEAD", "Pass makeArray a jQuery object" ); @@ -1017,10 +1142,6 @@ test("jQuery.makeArray", function(){ equal( jQuery.makeArray(/a/)[0].constructor, RegExp, "Pass makeArray a regex" ); ok( jQuery.makeArray(document.getElementById("form")).length >= 13, "Pass makeArray a form (treat as elements)" ); - - // For #5610 - deepEqual( jQuery.makeArray({length: "0"}), [], "Make sure object is coerced properly."); - deepEqual( jQuery.makeArray({length: "5"}), [], "Make sure object is coerced properly."); }); test("jQuery.inArray", function(){ @@ -1072,7 +1193,7 @@ test("jQuery.proxy", function(){ // Test old syntax var test4 = { "meth": function( a ){ equal( a, "boom", "Ensure old syntax works." ); } }; jQuery.proxy( test4, "meth" )( "boom" ); - + // jQuery 1.9 improved currying with `this` object var fn = function() { equal( Array.prototype.join.call( arguments, "," ), "arg1,arg2,arg3", "args passed" ); -- 2.39.5