diff --git a/es5-shim.js b/es5-shim.js index fc6778fd..975a3cd9 100644 --- a/es5-shim.js +++ b/es5-shim.js @@ -55,6 +55,7 @@ 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; @@ -175,11 +176,143 @@ 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) { + // To prevent recursion when `call` and `apply` are patched. Robustness. + call.call = call; + call.apply = apply; + apply.call = call; + apply.apply = apply; +} + +// This function is for use within `call` and `apply` only. +// To avoid any possibility of `call` recursion we use original `hasOwnProperty`. +var isDuckTypeArguments = hasToStringTagLegacyArguments && (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; + // Constant. ES3 maximum array length. 2^32-1 + if (length > -1 && length % 1 === 0 && length <= 4294967295) { + return !call.call(hasOwnProperty, value, 'arguments') && call.call(hasOwnProperty, value, 'callee'); + } + } + } + return false; + }; +}(ObjectPrototype.hasOwnProperty)); + +// For use with `call` and `apply` fixes. +var toStringTag = shouldPatchCallApply && 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]; + var type = typeof argsArray; + 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`. + 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 && type === 'object' && argsArray && argsArray.length > 0) { + // Boxed string access bug fix. + if (splitString && to_string.call(argsArray) === '[object String]') { + // `str_split` is safe to use here, no known issue at present. + argsArray = call.call(str_split, argsArray, ''); + } else { + // `array_slice` is safe to use here, no known issue at present. + argsArray = call.call(array_slice, argsArray); + } + } else { + // `argsArray` was `undefined` (not present), `== null` or not an object. + 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 @@ -375,11 +508,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..69e50702 100644 --- a/tests/spec/s-function.js +++ b/tests/spec/s-function.js @@ -1,16 +1,404 @@ -/* 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 () { + describe('should pass correct arguments', function () { + var testFn; + var expected; + beforeEach(function () { + testFn = function () { + return Array.prototype.slice.call(arguments); + }; + expected = [null, '1', 1, true, testFn]; + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.call(undefined, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.call(null, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var actual = testFn.call('a', null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `1`', function () { + var actual = testFn.call(1, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `true`', function () { + var actual = testFn.call(true, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `testFn`', function () { + var actual = testFn.call(testFn, null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + + it('`this` is `date`', function () { + var actual = testFn.call(new Date(), null, '1', 1, true, testFn); + expect(actual).toEqual(expected); + }); + }); + + describe('context in strict mode', function () { + 'use strict'; + + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + ifHasStrictIt('`this` is not defined', function () { + expect(testFn.call()).toBe(undefined); + }); + + ifHasStrictIt('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(undefined)).toBe(undefined); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(null)).toBe(null); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `a`', function () { + expect(testFn.call('a')).toBe('a'); + }); + + ifHasStrictIt('`this` is `1`', function () { + expect(testFn.call(1)).toBe(1); + }); + + ifHasStrictIt('`this` is `true`', function () { + expect(testFn.call(true)).toBe(true); + }); + + ifHasStrictIt('`this` is `testFn`', function () { + expect(testFn.call(testFn)).toBe(testFn); + }); + + ifHasStrictIt('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.call(subject)).toBe(subject); + }); + }); + + describe('context in non-strict mode', function () { + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + it('`this` is not defined', function () { + expect(testFn.call()).toBe(global); + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(undefined)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.call(null)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var result = testFn.call('a'); + expect(typeof result).toBe('object'); + expect(String(result)).toBe('a'); + }); + + it('`this` is `1`', function () { + var result = testFn.call(1); + expect(typeof result).toBe('object'); + expect(Number(result)).toBe(1); + }); + + it('`this` is `true`', function () { + var result = testFn.call(true); + expect(typeof result).toBe('object'); + expect(Boolean(result)).toBe(true); + }); + + it('`this` is `testFn`', function () { + expect(testFn.call(testFn)).toBe(testFn); + }); + + it('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.call(subject)).toBe(subject); + }); + }); + }); describe('#apply()', function () { - it('works with arraylike objects', function () { - var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; - var expectedArray = [1, undefined, 4, true]; - var actualArray = (function () { - return Array.prototype.slice.apply(arguments); - }.apply(null, arrayLike)); - expect(actualArray).toEqual(expectedArray); + var expected; + var testFn; + + beforeEach(function () { + expected = []; + testFn = function () { + return Array.prototype.slice.call(arguments); + }; + }); + + describe('should not throw if argument is `undefined`, `null` or `object`', function () { + it('`argArray` is `null`', function () { + var actual = testFn.apply(undefined, null); + expect(actual).toEqual(expected, 'null'); + }); + + it('`argArray` is `undefined`', function () { + var actual = testFn.apply(undefined, undefined); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `function`', function () { + var actual = testFn.apply(undefined, function () {}); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `{}`', function () { + var actual = testFn.apply(undefined, {}); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `date`', function () { + var actual = testFn.apply(undefined, new Date()); + expect(actual).toEqual(expected); + }); + + it('`argArray` is `regexp`', function () { + var actual = testFn.apply(undefined, /pattern/); + expect(actual).toEqual(expected); + }); + }); + + describe('should throw if argument is any other primitive', function () { + var testFn; + beforeEach(function () { + testFn = function () {}; + }); + + it('argument is `1`', function () { + expect(function () { + testFn.apply(undefined, 1); + }).toThrow(); + }); + + it('argument is `true`', function () { + expect(function () { + testFn.apply(undefined, true); + }).toThrow(); + }); + + it('argument is `\'123\'`', function () { + expect(function () { + testFn.apply(undefined, '123'); + }).toThrow(); + }); + }); + + describe('should pass correct arguments', function () { + var testFn; + var expected; + var args; + beforeEach(function () { + testFn = function () { + return Array.prototype.slice.call(arguments); + }; + expected = [null, '1', 1, true, testFn]; + args = [null, '1', 1, true, testFn]; + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.apply(undefined, args); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + var actual = testFn.apply(null, args); + expect(actual).toEqual(expected); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var actual = testFn.apply('a', args); + expect(actual).toEqual(expected); + }); + + it('`this` is `1`', function () { + var actual = testFn.apply(1, args); + expect(actual).toEqual(expected); + }); + + it('`this` is `true`', function () { + var actual = testFn.apply(true, args); + expect(actual).toEqual(expected); + }); + + it('`this` is `testFn`', function () { + var actual = testFn.apply(testFn, args); + expect(actual).toEqual(expected); + }); + + it('`this` is `date`', function () { + var actual = testFn.apply(new Date(), args); + expect(actual).toEqual(expected); + }); + }); + + describe('context in strict mode', function () { + 'use strict'; + + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + ifHasStrictIt('`this` is not defined', function () { + expect(testFn.apply()).toBe(undefined); + }); + + ifHasStrictIt('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(undefined)).toBe(undefined); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(null)).toBe(null); + /* eslint-enable no-useless-call */ + }); + + ifHasStrictIt('`this` is `a`', function () { + expect(testFn.apply('a')).toBe('a'); + }); + + ifHasStrictIt('`this` is `1`', function () { + expect(testFn.apply(1)).toBe(1); + }); + + ifHasStrictIt('`this` is `true`', function () { + expect(testFn.apply(true)).toBe(true); + }); + + ifHasStrictIt('`this` is `testFn`', function () { + expect(testFn.apply(testFn)).toBe(testFn); + }); + + ifHasStrictIt('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.apply(subject)).toBe(subject); + }); + }); + + describe('context in non-strict mode', function () { + var testFn; + beforeEach(function () { + testFn = function () { + return this; + }; + }); + + it('`this` is not defined', function () { + expect(testFn.apply()).toBe(global); + }); + + it('`this` is `undefined`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(undefined)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `null`', function () { + /* eslint-disable no-useless-call */ + expect(testFn.apply(null)).toBe(global); + /* eslint-enable no-useless-call */ + }); + + it('`this` is `\'a\'`', function () { + var result = testFn.apply('a'); + expect(typeof result).toBe('object'); + expect(String(result)).toBe('a'); + }); + + it('`this` is `1`', function () { + var result = testFn.apply(1); + expect(typeof result).toBe('object'); + expect(Number(result)).toBe(1); + }); + + it('`this` is `true`', function () { + var result = testFn.apply(true); + expect(typeof result).toBe('object'); + expect(Boolean(result)).toBe(true); + }); + + it('`this` is `testFn`', function () { + expect(testFn.apply(testFn)).toBe(testFn); + }); + + it('`this` is `date`', function () { + var subject = new Date(); + expect(testFn.apply(subject)).toBe(subject); + }); + }); + + describe('works with arraylike objects', function () { + it('object like sparse array', function () { + var arrayLike = { length: 4, 0: 1, 2: 4, 3: true }; + var expectedArray = [1, undefined, 4, true]; + var actualArray = (function () { + return Array.prototype.slice.apply(arguments); + }.apply(null, arrayLike)); + expect(actualArray).toEqual(expectedArray); + }); + + it('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]); + }); }); }); diff --git a/tests/spec/s-object.js b/tests/spec/s-object.js index 3065c669..f7d9a5ed 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 supportsDescriptors = Object.defineProperty && (function () { try { @@ -32,7 +34,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'; @@ -365,4 +374,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]'); + }); + }); });