diff --git a/es5-shim.js b/es5-shim.js index d6563dd0..0bad5145 100644 --- a/es5-shim.js +++ b/es5-shim.js @@ -55,7 +55,9 @@ var array_splice = ArrayPrototype.splice; var array_push = ArrayPrototype.push; var array_unshift = ArrayPrototype.unshift; var array_concat = ArrayPrototype.concat; +var str_split = StringPrototype.split; var call = FunctionPrototype.call; +var apply = FunctionPrototype.apply; var max = Math.max; var min = Math.min; @@ -175,11 +177,147 @@ var ES = { } }; +// Check failure of by-index access of string characters (IE < 9) +// and failure of `0 in boxedString` (Rhino) +var boxedString = $Object('a'); +var splitString = boxedString[0] !== 'a' || !(0 in boxedString); + // // Function // ======== // +// Tests for inconsistent or buggy `[[Class]]` strings. +/* eslint-disable no-useless-call */ +var hasToStringTagBasicBug = to_string.call() !== '[object Undefined]' || to_string.call(null) !== '[object Null]'; +/* eslint-enable no-useless-call */ +var hasToStringTagLegacyArguments = to_string.call(arguments) !== '[object Arguments]'; +var hasToStringTagInconsistency = hasToStringTagBasicBug || hasToStringTagLegacyArguments; +// Others that could be fixed: +// Older ES3 native functions like `alert` return `[object Object]`. +// Inconsistent `[[Class]]` strings for `window` or `global`. + +var hasApplyArrayLikeDeficiency = (function () { + var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; + var expectedArray = [1, undefined, 4, true]; + var actualArray; + try { + actualArray = (function () { + // `array_slice` is safe to use here, no known issue at present. + return array_slice.apply(arguments); + }.apply(null, arrayLike)); + } catch (e) { + if (to_string.call(actualArray) !== '[object Array]' || actualArray.length !== arrayLike.length) { + return true; + } + while (expectedArray.length) { + if (actualArray.pop() !== expectedArray.pop()) { + return true; + } + } + } + return false; +}()); + +var shouldPatchCallApply = hasToStringTagInconsistency || hasApplyArrayLikeDeficiency; + +if (shouldPatchCallApply) { + // Constant. ES3 maximum array length. + var MAX_ARRAY_LENGTH = 4294967295; + // To prevent recursion when `call` and `apply` are patched. Robustness. + call.call = call; + call.apply = apply; + apply.call = call; + apply.apply = apply; +} + +if (hasToStringTagLegacyArguments) { + // This function is for use within `call` and `apply` only. + // To avoid any possibility of `call` recursion we use original `hasOwnProperty`. + var isDuckTypeArguments = (function (hasOwnProperty) { + return function (value) { + if (value != null) { // Checks `null` or `undefined`. + if (typeof value === 'object' && call.call(hasOwnProperty, value, 'length')) { + var length = value.length; + if (length > -1 && length % 1 === 0 && length <= MAX_ARRAY_LENGTH) { + return !call.call(hasOwnProperty, value, 'arguments') && call.call(hasOwnProperty, value, 'callee'); + } + } + } + return false; + }; + }(ObjectPrototype.hasOwnProperty)); +} + +if (shouldPatchCallApply) { + // For use with `call` and `apply` fixes. + var toStringTag = function (value) { + // Add whatever fixes for getting `[[Class]]` strings here. + if (value === null) { + return '[object Null]'; + } + if (typeof value === 'undefined') { + return '[object Undefined]'; + } + if (hasToStringTagLegacyArguments && isDuckTypeArguments(value)) { + return '[object Arguments]'; + } + // `to_string` is safe to use here, no known issue at present. + return call.call(to_string, value); + }; +} + +defineProperties(FunctionPrototype, { + // 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). + apply: function (thisArg) { + var argsArray = arguments[1]; + if (arguments.length > 1) { + // IE9 (though fix not needed) has a problem here for some reason!!! + // Pretty much any function here causes error `SCRIPT5007: Object expected`. + var type = typeof argsArray; + if (type !== 'undefined' && type !== 'object' && type !== 'function') { + throw new TypeError('Function.prototype.apply: Arguments list has wrong type'); + } + } + // If `this` is `Object#toString`, captured or modified. + if (this === to_string || this === Object.prototype.toString) { + return toStringTag(thisArg); + } + // All other applys. + if (arguments.length > 1) { + // Boxed string access bug fix. + if (splitString && to_string.call(thisArg) === '[object String]') { + // `str_split` is safe to use here, no known issue at present. + argsArray = call.call(str_split, argsArray, ''); + } + // `array_slice` is safe to use here, no known issue at present. + argsArray = call.call(array_slice, argsArray); + } else { + // `argsArray` was `undefined` (not present). + argsArray = []; + } + + return apply.call(this, thisArg, argsArray); + }, + + // 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. + call: function (thisArg) { + // If `this` is `Object#toString`, captured or modified. + if (this === to_string || this === Object.prototype.toString) { + return toStringTag(thisArg); + } + // All other calls. + // `array_slice` is safe to use here, no known issue at present. + return apply.call(this, thisArg, call.call(array_slice, arguments, 1)); + } +}, shouldPatchCallApply); + // ES-5 15.3.4.5 // http://es5.github.com/#x15.3.4.5 @@ -320,7 +458,7 @@ defineProperties(FunctionPrototype, { }); // _Please note: Shortcuts are defined after `Function.prototype.bind` as we -// us it in defining shortcuts. +// use it in defining shortcuts. var owns = call.bind(ObjectPrototype.hasOwnProperty); var toStr = call.bind(ObjectPrototype.toString); var strSlice = call.bind(StringPrototype.slice); @@ -371,11 +509,6 @@ defineProperties($Array, { isArray: isArray }); // http://es5.github.com/#x15.4.4.18 // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/forEach -// Check failure of by-index access of string characters (IE < 9) -// and failure of `0 in boxedString` (Rhino) -var boxedString = $Object('a'); -var splitString = boxedString[0] !== 'a' || !(0 in boxedString); - var properlyBoxesContext = function properlyBoxed(method) { // Check node 0.6.21 bug where third parameter is not boxed var properlyBoxesNonStrict = true; diff --git a/tests/spec/s-function.js b/tests/spec/s-function.js index 97237adb..c2a940db 100644 --- a/tests/spec/s-function.js +++ b/tests/spec/s-function.js @@ -1,9 +1,213 @@ -/* global describe, it, expect, beforeEach */ +/* global describe, it, xit, expect, beforeEach */ +var hasStrictMode = (function () { + 'use strict'; + + return !this; +}()); +var ifHasStrictIt = hasStrictMode ? it : xit; +var global = Function('return this')(); describe('Function', function () { - 'use strict'; + describe('#call()', function () { + it('should pass correct arguments', function () { + // https://github.com/es-shims/es5-shim/pull/345#discussion_r44878754 + var result; + var testFn = function () { + return Array.prototype.slice.call(arguments); + }; + var argsExpected = [null, '1', 1, true, testFn]; + /* eslint-disable no-useless-call */ + result = testFn.call(undefined, null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + result = testFn.call(null, null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + /* eslint-enable no-useless-call */ + result = testFn.call('a', null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + result = testFn.call(1, null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + result = testFn.call(true, null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + result = testFn.call(testFn, null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + result = testFn.call(new Date(), null, '1', 1, true, testFn); + expect(result).toEqual(argsExpected); + }); + + // https://github.com/es-shims/es5-shim/pull/345#discussion_r44878771 + ifHasStrictIt('should have correct context in strict mode', function () { + 'use strict'; + + var subject; + var testFn = function () { + return this; + }; + expect(testFn.call()).toBe(undefined); + /* eslint-disable no-useless-call */ + expect(testFn.call(undefined)).toBe(undefined); + expect(testFn.call(null)).toBe(null); + /* eslint-enable no-useless-call */ + expect(testFn.call('a')).toBe('a'); + expect(testFn.call(1)).toBe(1); + expect(testFn.call(true)).toBe(true); + expect(testFn.call(testFn)).toBe(testFn); + subject = new Date(); + expect(testFn.call(subject)).toBe(subject); + }); + + it('should have correct context in non-strict mode', function () { + var result; + var subject; + var testFn = function () { + return this; + }; + + expect(testFn.call()).toBe(global); + /* eslint-disable no-useless-call */ + expect(testFn.call(undefined)).toBe(global); + expect(testFn.call(null)).toBe(global); + /* eslint-enable no-useless-call */ + result = testFn.call('a'); + expect(typeof result).toBe('object'); + expect(String(result)).toBe('a'); + result = testFn.call(1); + expect(typeof result).toBe('object'); + expect(Number(result)).toBe(1); + result = testFn.call(true); + expect(typeof result).toBe('object'); + expect(Boolean(result)).toBe(true); + expect(testFn.call(testFn)).toBe(testFn); + subject = new Date(); + expect(testFn.call(subject)).toBe(subject); + }); + }); describe('#apply()', function () { + it('should not throw if argument is `undefined`, `null` or `object`', function () { + var actual; + var testFn = function () { + return Array.prototype.slice.call(arguments); + }; + + expect(function () { + actual = testFn.apply(undefined, null); + }).not.toThrow(); + expect(actual).toEqual([]); + + expect(function () { + actual = testFn.apply(undefined, undefined); + }).not.toThrow(); + expect(actual).toEqual([]); + + expect(function () { + actual = testFn.apply(undefined, function () {}); + }).not.toThrow(); + expect(actual).toEqual([]); + + expect(function () { + testFn.apply(undefined, {}); + }).not.toThrow(); + expect(actual).toEqual([]); + + expect(function () { + testFn.apply(undefined, new Date()); + }).not.toThrow(); + expect(actual).toEqual([]); + + expect(function () { + testFn.apply(undefined, /pattern/); + }).not.toThrow(); + expect(actual).toEqual([]); + }); + + it('should throw if argument is a primitive', function () { + var testFn = function () {}; + + expect(function () { + testFn.apply(undefined, 1); + }).toThrow(); + + expect(function () { + testFn.apply(undefined, true); + }).toThrow(); + + expect(function () { + testFn.apply(undefined, '123'); + }).toThrow(); + }); + + it('should pass correct arguments', function () { + // https://github.com/es-shims/es5-shim/pull/345#discussion_r44878754 + var result; + var testFn = function () { + return Array.prototype.slice.call(arguments); + }; + var argsExpected = [null, '1', 1, true, testFn]; + /* eslint-disable no-useless-call */ + result = testFn.apply(undefined, [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + result = testFn.apply(null, [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + /* eslint-enable no-useless-call */ + result = testFn.apply('a', [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + result = testFn.apply(1, [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + result = testFn.apply(true, [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + result = testFn.apply(testFn, [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + result = testFn.apply(new Date(), [null, '1', 1, true, testFn]); + expect(result).toEqual(argsExpected); + }); + + // https://github.com/es-shims/es5-shim/pull/345#discussion_r44878771 + ifHasStrictIt('should have correct context in strict mode', function () { + 'use strict'; + + var subject; + var testFn = function () { + return this; + }; + expect(testFn.apply()).toBe(undefined); + /* eslint-disable no-useless-call */ + expect(testFn.apply(undefined)).toBe(undefined); + expect(testFn.apply(null)).toBe(null); + /* eslint-enable no-useless-call */ + expect(testFn.apply('a')).toBe('a'); + expect(testFn.apply(1)).toBe(1); + expect(testFn.apply(true)).toBe(true); + expect(testFn.apply(testFn)).toBe(testFn); + subject = new Date(); + expect(testFn.apply(subject)).toBe(subject); + }); + + it('should have correct context in non-strict mode', function () { + var result; + var subject; + var testFn = function () { + return this; + }; + + expect(testFn.apply()).toBe(global); + /* eslint-disable no-useless-call */ + expect(testFn.apply(undefined)).toBe(global); + expect(testFn.apply(null)).toBe(global); + /* eslint-enable no-useless-call */ + result = testFn.apply('a'); + expect(typeof result).toBe('object'); + expect(String(result)).toBe('a'); + result = testFn.apply(1); + expect(typeof result).toBe('object'); + expect(Number(result)).toBe(1); + result = testFn.apply(true); + expect(typeof result).toBe('object'); + expect(Boolean(result)).toBe(true); + expect(testFn.apply(testFn)).toBe(testFn); + subject = new Date(); + expect(testFn.apply(subject)).toBe(subject); + }); + it('works with arraylike objects', function () { var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; var expectedArray = [1, undefined, 4, true]; @@ -12,6 +216,17 @@ describe('Function', function () { }.apply(null, arrayLike)); expect(actualArray).toEqual(expectedArray); }); + + it('works with arguments object', function () { + var args = function () { + return arguments; + }; + var testFn = function () { + return Array.prototype.slice.call(arguments); + }; + expect(testFn.apply(undefined, args(1, 2, 3, 4))).toEqual([1, 2, 3, 4]); + }); + }); describe('#bind()', function () { diff --git a/tests/spec/s-object.js b/tests/spec/s-object.js index c17f1bde..4fbac096 100644 --- a/tests/spec/s-object.js +++ b/tests/spec/s-object.js @@ -1,4 +1,6 @@ -/* global describe, it, xit, expect, beforeEach, jasmine, window */ +/* global describe, it, xit, expect, beforeEach, jasmine, window, + ArrayBuffer, Float32Array, Float64Array, Int8Array, Int16Array, + Int32Array, Uint8Array, Uint8ClampedArray, Uint16Array, Uint32Array */ var ifWindowIt = typeof window === 'undefined' ? xit : it; var extensionsPreventible = typeof Object.preventExtensions === 'function' && (function () { @@ -22,7 +24,14 @@ var canFreeze = typeof Object.freeze === 'function' && (function () { return obj.a !== 3; }()); var ifCanFreezeIt = canFreeze ? it : xit; - +var toStr = Object.prototype.toString; +var noop = function () {}; +var hasIteratorTag = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol'; +var ifHasIteratorTag = hasIteratorTag ? it : xit; +var hasArrayBuffer = typeof ArrayBuffer === 'function'; +var ifHasArrayBuffer = hasArrayBuffer ? it : xit; +var hasUint8ClampedArray = typeof Uint8ClampedArray === 'function'; +var ifHasUint8ClampedArray = hasUint8ClampedArray ? it : xit; describe('Object', function () { 'use strict'; @@ -356,4 +365,59 @@ describe('Object', function () { expect(obj instanceof Object).toBe(false); }); }); + + describe('#toString', function () { + it('basic', function () { + expect(toStr.call()).toBe('[object Undefined]'); + /* eslint-disable no-useless-call */ + expect(toStr.call(undefined)).toBe('[object Undefined]'); + expect(toStr.call(null)).toBe('[object Null]'); + /* eslint-enable no-useless-call */ + expect(toStr.call(1)).toBe('[object Number]'); + expect(toStr.call(true)).toBe('[object Boolean]'); + expect(toStr.call('x')).toBe('[object String]'); + expect(toStr.call([1, 2, 3])).toBe('[object Array]'); + expect(toStr.call(arguments)).toBe('[object Arguments]'); + expect(toStr.call({})).toBe('[object Object]'); + expect(toStr.call(noop)).toBe('[object Function]'); + expect(toStr.call(new RegExp('c'))).toBe('[object RegExp]'); + expect(toStr.call(new Date())).toBe('[object Date]'); + expect(toStr.call(new Error('x'))).toBe('[object Error]'); + }); + ifHasArrayBuffer('Typed Arrays', function () { + var buffer = new ArrayBuffer(8); + expect(toStr.call(buffer)).toBe('[object ArrayBuffer]'); + expect(toStr.call(new Float32Array(buffer))).toBe('[object Float32Array]'); + expect(toStr.call(new Float64Array(buffer))).toBe('[object Float64Array]'); + expect(toStr.call(new Int8Array(buffer))).toBe('[object Int8Array]'); + expect(toStr.call(new Int16Array(buffer))).toBe('[object Int16Array]'); + expect(toStr.call(new Int32Array(buffer))).toBe('[object Int32Array]'); + expect(toStr.call(new Uint8Array(buffer))).toBe('[object Uint8Array]'); + expect(toStr.call(new Uint16Array(buffer))).toBe('[object Uint16Array]'); + expect(toStr.call(new Uint32Array(buffer))).toBe('[object Uint32Array]'); + }); + ifHasUint8ClampedArray('Uint8ClampedArray', function () { + var buffer = new ArrayBuffer(8); + expect(toStr.call(new Uint32Array(buffer))).toBe('[object Uint32Array]'); + }); + ifHasIteratorTag('Symbol.iterator', function () { + expect(toStr.call(Symbol.iterator)).toBe('[object Symbol]'); + }); + // https://github.com/es-shims/es5-shim/pull/345#discussion_r44878834 + it('prototypes', function () { + expect(toStr.call(Object.prototype)).toBe('[object Object]'); + expect(toStr.call(Array.prototype)).toBe('[object Array]'); + expect(toStr.call(Boolean.prototype)).toBe('[object Boolean]'); + expect(toStr.call(Function.prototype)).toBe('[object Function]'); + }); + // In ES6, many prototype objects stop being instances of themselves, + // and instead would return '[object Object]'. + xit('prototypes', function () { + expect(toStr.call(Number.prototype)).toBe('[object Number]'); + expect(toStr.call(String.prototype)).toBe('[object String]'); + expect(toStr.call(Error.prototype)).toBe('[object Error]'); + expect(toStr.call(Date.prototype)).toBe('[object Date]'); + expect(toStr.call(RegExp.prototype)).toBe('[object RegExp]'); + }); + }); });