Skip to content

Commit

Permalink
Fix array-like objects for Function.prototype.apply
Browse files Browse the repository at this point in the history
  • Loading branch information
Xotic750 committed Nov 17, 2015
1 parent fe284ac commit 01eee7d
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 23 deletions.
138 changes: 115 additions & 23 deletions es5-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
* Required reading: http://javascriptweblog.wordpress.com/2011/12/05/extending-javascript-natives/
*/

// Constant from ES6 spec.
var MAX_SAFE_INTEGER = Math.pow(2, 53) - 1;

// Shortcut to an often accessed properties, in order to avoid multiple
// dereference that costs universally. This also holds a reference to known-good
// functions.
Expand All @@ -56,9 +59,17 @@ var array_push = ArrayPrototype.push;
var array_unshift = ArrayPrototype.unshift;
var array_concat = ArrayPrototype.concat;
var call = FunctionPrototype.call;
var apply = FunctionPrototype.apply;
var max = Math.max;
var min = Math.min;

// To prevent recursion if `Function#call` or `Function#apply` are patched.
// Also robust, so can be used to replace standard '.call' amd `.apply`.
call.apply = apply;
call.call = call;
apply.call = call;
apply.apply = apply;

// Having a toString local variable name breaks in Opera so use to_string.
var to_string = ObjectPrototype.toString;

Expand Down Expand Up @@ -328,53 +339,134 @@ var strSplit = call.bind(StringPrototype.split);
var strIndexOf = call.bind(StringPrototype.indexOf);
var push = call.bind(array_push);

// ES-5 15.3.4.4
// http://es5.github.io/#x15.3.4.4
// Tests for inconsistent or buggy `[[Class]]` strings.
var hasToStringTagBasicBug = toStr() !== '[object Undefined]' || toStr(null) !== '[object Null]';
var hasToStringTagLegacyArguments = toStr(arguments) !== '[object Arguments]';
var hasToStringTagInconsistency = hasToStringTagBasicBug || hasToStringTagLegacyArguments;

// Others to check, or could be fixed:
// PhantomJS 1.9 which returns 'function' for `NodeList` instances.
// Others that could be fixed:
// Older ES3 native functions like `alert` return `[object Object]`.
// Inconsistent `[[Class]]` strings for `window` or `global`.

if (hasToStringTagLegacyArguments) {
// This function is for use within `Function#call` only.
// To avoid any possibility of `call` recursion.
var hasOwnProperty = ObjectPrototype.hasOwnProperty;

// This function is for use within `toStringTag` only.
var isDuckTypeArguments = function (value) {
return value != null && // Checks `null` or `undefined`.
typeof value === 'object' &&
owns(value, 'length') &&
call.call(hasOwnProperty, value, 'length') &&
typeof value.length === 'number' &&
value.length >= 0 &&
!owns(value, 'arguments') &&
owns(value, 'callee');
!call.call(hasOwnProperty, value, 'arguments') &&
call.call(hasOwnProperty, value, 'callee');
// Using `isCallable` here is dangerous as it can cause recursion.
// Unless modified to use `call.call`.
// A typeof check would be ok if necessary.
};
}

// Add whatever fixes for getting `[[Class]]` strings here.
var toStringTag = function (value) {
if (value === null) {
return '[object Null]';
}
if (typeof value === 'undefined') {
return '[object Undefined]';
}
if (hasToStringTagLegacyArguments && isDuckTypeArguments(value)) {
return '[object Arguments]';
}
return call.call(to_string, value);
};

// ES-5 15.3.4.4
// http://es5.github.io/#x15.3.4.4
// The call() method calls a function with a given this value and arguments
// provided individually.
defineProperties(FunctionPrototype, {
call: function () {
var thisArg = arguments[0];
// If `this` is `Object#toString`.
if (this === to_string) {
if (thisArg === null) {
return '[object Null]';
}
if (typeof thisArg === 'undefined') {
return '[object Undefined]';
}
if (hasToStringTagLegacyArguments && isDuckTypeArguments(thisArg)) {
return '[object Arguments]';
return toStringTag(thisArg);
}
// All other calls.
return apply.call(this, thisArg, call.call(array_slice, arguments, 1));
}
}, hasToStringTagInconsistency);

// ES-5 15.3.4.3
// http://es5.github.io/#x15.3.4.3
// The apply() method calls a function with a given this value and arguments
// provided as an array (or an array-like object).
var hasApplyArrayLikeDeficiency = (function () {
var arrayLike = { length: 4, 0: 1, 2: 4, 3: true };
var expectedArray = [1, undefined, 4, true];
var actualArray;
try {
actualArray = (function () {
return array_slice.apply(arguments);
}.apply(null, arrayLike));
} catch (e) {
if (toStringTag(actualArray) !== '[object Array]' || actualArray.length !== arrayLike.length) {
return true;
}
while (expectedArray.length) {
if (actualArray.pop() !== expectedArray.pop()) {
return true;
}
}
}
return false;
}());

if (hasToStringTagInconsistency || hasApplyArrayLikeDeficiency) {
var isObject = function (value) {
// Avoid a V8 JIT bug in Chrome 19-20.
// See https://code.google.com/p/v8/issues/detail?id=2291 for more details.
var type = typeof value;
return !!value && (type === 'object' || type === 'function');
};
var isFunction = function (value) {
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 8 which returns 'object' for typed array constructors, and
// PhantomJS 1.9 which returns 'function' for `NodeList` instances.
var tag = isObject(value) ? call.call(to_string, value) : '';
return tag === '[object Function]' || tag === '[object GeneratorFunction]';
};
var isLength = function (value) {
return typeof value === 'number' && value > -1 && value % 1 === 0 && value <= MAX_SAFE_INTEGER;
};
var isArrayLike = function (value) {
return value != null && typeof value !== 'function' && !isFunction(value) && isLength(value.length);
};
var isObjectLike = function (value) {
return !!value && typeof value === 'object';
};
var isArrayLikeObject = function (value) {
return isObjectLike(value) && isArrayLike(value);
};
}

defineProperties(FunctionPrototype, {
apply: function () {
var argsArray = arguments[1];
if (arguments.length > 1) {
if (!isArrayLikeObject(argsArray)) {
throw new TypeError('Function.prototype.apply: Arguments list has wrong type');
}
}
var i = arguments.length;
var rest = i ? Array(--i) : [];
while (i) {
rest[i - 1] = arguments[i--];
var thisArg = arguments[0];
// If `this` is `Object#toString`.
if (this === to_string) {
return toStringTag(thisArg);
}
return this.apply(thisArg, rest);
// All other applys.
argsArray = arguments.length > 1 ? call.call(array_slice, argsArray) : argsArray;
return apply.call(this, thisArg, argsArray);
}
}, hasToStringTagInconsistency);
}, hasToStringTagInconsistency || hasApplyArrayLikeDeficiency);

//
// Array
Expand Down
4 changes: 4 additions & 0 deletions tests/spec/s-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -1558,6 +1558,10 @@ describe('Array', function () {
}(1, 2, 3, 4));
result = Array.prototype.slice.call(obj, 1, 3);
expect(result).toEqual([2, 3]);

obj = '1234';
result = Array.prototype.slice.call(obj, 1, 3);
expect(result).toEqual(['2', '3']);
});
});
});

0 comments on commit 01eee7d

Please sign in to comment.