Skip to content

Commit 6c845ea

Browse files
author
Xotic750
committed
Patch Function#call and Function#apply together, more robust than single fix.
Ref: es-shims#304 Changed some code back as mentioned in comments. Changed more code as per comments Changed more code as per comments. Changed some variable names to better reflect comments. Fixed missed invocation. Added some comments and code changes as discussed. [tests] Remove unneeded jshint comment. [Tests] use the preferred it/skip pattern for this strict mode test. es-shims#345 (comment) And some other cleanup Added `arguments` expectations to tests. Add tests for `Object#toString` of typed arrays and Symbols, if they exist. Added note about typed array tests. Fix `hasToStringTagRegExpBug` Removed RegExp and Array bug detection as can not test, possible Opera 9. Fixed missing `force` on `defineProperties` that caused the patch to not be applied on IE<9. Fixed `Uint8ClampedArray` tests for Opera 11 and IE10 that don't have it. Removed offending test that was moved to detection, but forgotten. Avoid all possibilities of `call` calling `call`. Do not pass `undefined` argument, we know IE<9 has unfixable bug. Final cleanup (hopeully) Port over work from `apply` fix Move code so that it is specific to the fix. Robustness, move before bind. Remove `Array#slice` tests. Add notes about `eval` and `apply` avoidance.
1 parent 4b6dcad commit 6c845ea

File tree

4 files changed

+334
-21
lines changed

4 files changed

+334
-21
lines changed

es5-shim.js

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ var array_splice = ArrayPrototype.splice;
5555
var array_push = ArrayPrototype.push;
5656
var array_unshift = ArrayPrototype.unshift;
5757
var array_concat = ArrayPrototype.concat;
58+
var str_split = StringPrototype.split;
5859
var call = FunctionPrototype.call;
60+
var apply = FunctionPrototype.apply;
5961
var max = Math.max;
6062
var min = Math.min;
6163

@@ -163,7 +165,6 @@ var ES = {
163165
// http://es5.github.com/#x9.9
164166
/* replaceable with https://npmjs.com/package/es-abstract ES5.ToObject */
165167
ToObject: function (o) {
166-
/* jshint eqnull: true */
167168
if (o == null) { // this matches both null and undefined
168169
throw new TypeError("can't convert " + o + ' to object');
169170
}
@@ -176,11 +177,160 @@ var ES = {
176177
}
177178
};
178179

180+
// Check failure of by-index access of string characters (IE < 9)
181+
// and failure of `0 in boxedString` (Rhino)
182+
var boxedString = $Object('a');
183+
var splitString = boxedString[0] !== 'a' || !(0 in boxedString);
184+
179185
//
180186
// Function
181187
// ========
182188
//
183189

190+
// Tests for inconsistent or buggy `[[Class]]` strings.
191+
/* eslint-disable no-useless-call */
192+
var hasToStringTagBasicBug = to_string.call() !== '[object Undefined]' || to_string.call(null) !== '[object Null]';
193+
/* eslint-enable no-useless-call */
194+
var hasToStringTagLegacyArguments = to_string.call(arguments) !== '[object Arguments]';
195+
var hasToStringTagInconsistency = hasToStringTagBasicBug || hasToStringTagLegacyArguments;
196+
// Others that could be fixed:
197+
// Older ES3 native functions like `alert` return `[object Object]`.
198+
// Inconsistent `[[Class]]` strings for `window` or `global`.
199+
200+
var hasApplyArrayLikeDeficiency = (function () {
201+
var arrayLike = { length: 4, 0: 1, 2: 4, 3: true };
202+
var expectedArray = [1, undefined, 4, true];
203+
var actualArray;
204+
try {
205+
actualArray = (function () {
206+
// `array_slice` is safe to use here, no known issue at present.
207+
return array_slice.apply(arguments);
208+
}.apply(null, arrayLike));
209+
} catch (e) {
210+
if (to_string.call(actualArray) !== '[object Array]' || actualArray.length !== arrayLike.length) {
211+
return true;
212+
}
213+
while (expectedArray.length) {
214+
if (actualArray.pop() !== expectedArray.pop()) {
215+
return true;
216+
}
217+
}
218+
}
219+
return false;
220+
}());
221+
222+
var shouldPatchCallApply = hasToStringTagInconsistency || hasApplyArrayLikeDeficiency;
223+
224+
if (shouldPatchCallApply) {
225+
// Constant. ES3 maximum array length.
226+
var MAX_ARRAY_LENGTH = 4294967295;
227+
// To prevent recursion when `call` and `apply` are patched. Robustness.
228+
call.call = call;
229+
call.apply = apply;
230+
apply.call = call;
231+
apply.apply = apply;
232+
}
233+
234+
if (hasToStringTagLegacyArguments) {
235+
// This function is for use within `call` and `apply` only.
236+
// To avoid any possibility of `call` recursion we use original `hasOwnProperty`.
237+
var isDuckTypeArguments = (function (hasOwnProperty) {
238+
return function (value) {
239+
if (value != null) { // Checks `null` or `undefined`.
240+
if (typeof value === 'object' && call.call(hasOwnProperty, value, 'length')) {
241+
var length = value.length;
242+
if (length > -1 && length % 1 === 0 && length <= MAX_ARRAY_LENGTH) {
243+
return !call.call(hasOwnProperty, value, 'arguments') && call.call(hasOwnProperty, value, 'callee');
244+
}
245+
}
246+
}
247+
return false;
248+
};
249+
}(ObjectPrototype.hasOwnProperty));
250+
}
251+
252+
if (shouldPatchCallApply) {
253+
// For use with `call` and `apply` fixes.
254+
var toStringTag = function (value) {
255+
// Add whatever fixes for getting `[[Class]]` strings here.
256+
if (value === null) {
257+
return '[object Null]';
258+
}
259+
if (typeof value === 'undefined') {
260+
return '[object Undefined]';
261+
}
262+
if (hasToStringTagLegacyArguments && isDuckTypeArguments(value)) {
263+
return '[object Arguments]';
264+
}
265+
// `to_string` is safe to use here, no known issue at present.
266+
return call.call(to_string, value);
267+
};
268+
// For use with `apply` fix.
269+
var isArrayLikeObject = function (value) {
270+
if (value != null) { // Checks `null` or `undefined`.
271+
var type = typeof value;
272+
// `to_string` is safe to use here, no known issue at present.
273+
if (type === 'object' && type !== 'function' && call.call(to_string, value) !== '[object Function]') {
274+
var length = value.length;
275+
if (typeof length === 'number') {
276+
return length > -1 && length % 1 === 0 && length <= MAX_ARRAY_LENGTH;
277+
}
278+
}
279+
}
280+
return false;
281+
};
282+
}
283+
284+
defineProperties(FunctionPrototype, {
285+
// ES-5 15.3.4.3
286+
// http://es5.github.io/#x15.3.4.3
287+
// The apply() method calls a function with a given this value and arguments
288+
// provided as an array (or an array-like object).
289+
apply: function (thisArg) {
290+
var argsArray = arguments[1];
291+
if (arguments.length > 1) {
292+
// IE9 (though fix not needed) has a problem here for some reason!!!
293+
// Pretty much any function here causes error `SCRIPT5007: Object expected`.
294+
if (!isArrayLikeObject(argsArray)) {
295+
throw new TypeError('Function.prototype.apply: Arguments list has wrong type');
296+
}
297+
}
298+
// If `this` is `Object#toString`, captured or modified.
299+
if (this === to_string || this === Object.prototype.toString) {
300+
return toStringTag(thisArg);
301+
}
302+
// All other applys.
303+
if (arguments.length > 1) {
304+
// Boxed string access bug fix.
305+
if (splitString && to_string.call(thisArg) === '[object String]') {
306+
// `str_split` is safe to use here, no known issue at present.
307+
argsArray = call.call(str_split, argsArray, '');
308+
}
309+
// `array_slice` is safe to use here, no known issue at present.
310+
argsArray = call.call(array_slice, argsArray);
311+
} else {
312+
// `argsArray` was `undefined` (not present).
313+
argsArray = [];
314+
}
315+
316+
return apply.call(this, thisArg, argsArray);
317+
},
318+
319+
// ES-5 15.3.4.4
320+
// http://es5.github.io/#x15.3.4.4
321+
// The call() method calls a function with a given this value and arguments
322+
// provided individually.
323+
call: function (thisArg) {
324+
// If `this` is `Object#toString`, captured or modified.
325+
if (this === to_string || this === Object.prototype.toString) {
326+
return toStringTag(thisArg);
327+
}
328+
// All other calls.
329+
// `array_slice` is safe to use here, no known issue at present.
330+
return apply.call(this, thisArg, call.call(array_slice, arguments, 1));
331+
}
332+
}, shouldPatchCallApply);
333+
184334
// ES-5 15.3.4.5
185335
// http://es5.github.com/#x15.3.4.5
186336

@@ -321,7 +471,7 @@ defineProperties(FunctionPrototype, {
321471
});
322472

323473
// _Please note: Shortcuts are defined after `Function.prototype.bind` as we
324-
// us it in defining shortcuts.
474+
// use it in defining shortcuts.
325475
var owns = call.bind(ObjectPrototype.hasOwnProperty);
326476
var toStr = call.bind(ObjectPrototype.toString);
327477
var strSlice = call.bind(StringPrototype.slice);
@@ -372,11 +522,6 @@ defineProperties($Array, { isArray: isArray });
372522
// http://es5.github.com/#x15.4.4.18
373523
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach
374524

375-
// Check failure of by-index access of string characters (IE < 9)
376-
// and failure of `0 in boxedString` (Rhino)
377-
var boxedString = $Object('a');
378-
var splitString = boxedString[0] !== 'a' || !(0 in boxedString);
379-
380525
var properlyBoxesContext = function properlyBoxed(method) {
381526
// Check node 0.6.21 bug where third parameter is not boxed
382527
var properlyBoxesNonStrict = true;

tests/spec/s-array.js

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var hasStrictMode = (function () {
99

1010
return !this;
1111
}());
12+
var ifHasStrictIt = hasStrictMode ? it : xit;
1213

1314
describe('Array', function () {
1415
var testSubject;
@@ -123,17 +124,15 @@ describe('Array', function () {
123124
expect(toStr.call(listArg)).toBe('[object String]');
124125
});
125126

126-
if (hasStrictMode) {
127-
it('does not autobox the content in strict mode', function () {
128-
var context;
129-
[1].forEach(function () {
130-
'use strict';
127+
ifHasStrictIt('does not autobox the content in strict mode', function () {
128+
var context;
129+
[1].forEach(function () {
130+
'use strict';
131131

132-
context = this;
133-
}, 'x');
134-
expect(typeof context).toBe('string');
135-
});
136-
}
132+
context = this;
133+
}, 'x');
134+
expect(typeof context).toBe('string');
135+
});
137136
});
138137
describe('#some()', function () {
139138
var actual, expected, numberOfRuns;
@@ -1541,4 +1540,32 @@ describe('Array', function () {
15411540
expect(obj[2]).toBeUndefined();
15421541
});
15431542
});
1543+
1544+
describe('#slice()', function () {
1545+
it('works on arrays', function () {
1546+
var arr = [1, 2, 3, 4];
1547+
var result = arr.slice(1, 3);
1548+
expect(result).toEqual([2, 3]);
1549+
});
1550+
1551+
it('is generic', function () {
1552+
var obj = { 0: 1, 1: 2, 2: 3, 3: 4, length: 4 };
1553+
var result = Array.prototype.slice.call(obj, 1, 3);
1554+
expect(result).toEqual([2, 3]);
1555+
});
1556+
1557+
it('works with arguments', function () {
1558+
var obj = (function () {
1559+
return arguments;
1560+
}(1, 2, 3, 4));
1561+
var result = Array.prototype.slice.call(obj, 1, 3);
1562+
expect(result).toEqual([2, 3]);
1563+
});
1564+
1565+
it('boxed string access', function () {
1566+
var obj = '1234';
1567+
var result = Array.prototype.slice.call(obj, 1, 3);
1568+
expect(result).toEqual(['2', '3']);
1569+
});
1570+
});
15441571
});

tests/spec/s-function.js

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,84 @@
1-
/* global describe, it, expect, beforeEach */
1+
/* global describe, it, xit, expect, beforeEach */
2+
var hasStrictMode = (function () {
3+
'use strict';
4+
5+
return !this;
6+
}());
7+
var ifHasStrictIt = hasStrictMode ? it : xit;
8+
var global = Function('return this')();
29

310
describe('Function', function () {
4-
'use strict';
11+
describe('#call()', function () {
12+
it('should pass correct arguments', function () {
13+
// https://github.com/es-shims/es5-shim/pull/345#discussion_r44878754
14+
var result;
15+
var testFn = function () {
16+
return Array.prototype.slice.call(arguments);
17+
};
18+
var argsExpected = [null, '1', 1, true, testFn];
19+
/* eslint-disable no-useless-call */
20+
result = testFn.call(undefined, null, '1', 1, true, testFn);
21+
expect(result).toEqual(argsExpected);
22+
result = testFn.call(null, null, '1', 1, true, testFn);
23+
expect(result).toEqual(argsExpected);
24+
/* eslint-enable no-useless-call */
25+
result = testFn.call('a', null, '1', 1, true, testFn);
26+
expect(result).toEqual(argsExpected);
27+
result = testFn.call(1, null, '1', 1, true, testFn);
28+
expect(result).toEqual(argsExpected);
29+
result = testFn.call(true, null, '1', 1, true, testFn);
30+
expect(result).toEqual(argsExpected);
31+
result = testFn.call(testFn, null, '1', 1, true, testFn);
32+
expect(result).toEqual(argsExpected);
33+
result = testFn.call(new Date(), null, '1', 1, true, testFn);
34+
expect(result).toEqual(argsExpected);
35+
});
36+
// https://github.com/es-shims/es5-shim/pull/345#discussion_r44878771
37+
ifHasStrictIt('should have correct context in strict mode', function () {
38+
'use strict';
39+
40+
var subject;
41+
var testFn = function () {
42+
return this;
43+
};
44+
expect(testFn.call()).toBe(undefined);
45+
/* eslint-disable no-useless-call */
46+
expect(testFn.call(undefined)).toBe(undefined);
47+
expect(testFn.call(null)).toBe(null);
48+
/* eslint-enable no-useless-call */
49+
expect(testFn.call('a')).toBe('a');
50+
expect(testFn.call(1)).toBe(1);
51+
expect(testFn.call(true)).toBe(true);
52+
expect(testFn.call(testFn)).toBe(testFn);
53+
subject = new Date();
54+
expect(testFn.call(subject)).toBe(subject);
55+
});
56+
it('should have correct context in non-strict mode', function () {
57+
var result;
58+
var subject;
59+
var testFn = function () {
60+
return this;
61+
};
62+
63+
expect(testFn.call()).toBe(global);
64+
/* eslint-disable no-useless-call */
65+
expect(testFn.call(undefined)).toBe(global);
66+
expect(testFn.call(null)).toBe(global);
67+
/* eslint-enable no-useless-call */
68+
result = testFn.call('a');
69+
expect(typeof result).toBe('object');
70+
expect(String(result)).toBe('a');
71+
result = testFn.call(1);
72+
expect(typeof result).toBe('object');
73+
expect(Number(result)).toBe(1);
74+
result = testFn.call(true);
75+
expect(typeof result).toBe('object');
76+
expect(Boolean(result)).toBe(true);
77+
expect(testFn.call(testFn)).toBe(testFn);
78+
subject = new Date();
79+
expect(testFn.call(subject)).toBe(subject);
80+
});
81+
});
582

683
describe('#apply()', function () {
784
it('works with arraylike objects', function () {

0 commit comments

Comments
 (0)