diff --git a/es5-shim.js b/es5-shim.js index d6563dd0..d92fc282 100644 --- a/es5-shim.js +++ b/es5-shim.js @@ -56,6 +56,7 @@ 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; @@ -180,6 +181,68 @@ var ES = { // ======== // +// 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`. + +if (hasToStringTagInconsistency) { + // To prevent recursion when `Function#call` is patched. Robustness. + call.call = call; +} + +if (hasToStringTagLegacyArguments) { + // This function is for use within `call` only. + // To avoid any possibility of `call` recursion we use original `hasOwnProperty`. + var isDuckTypeArguments = (function (hasOwnProperty) { + return function (value) { + return value != null && // Checks `null` or `undefined`. + typeof value === 'object' && + call.call(hasOwnProperty, value, 'length') && + typeof value.length === 'number' && + value.length >= 0 && + !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. + }; + }(ObjectPrototype.hasOwnProperty)); +} + +// 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 (thisArg) { + // If `this` is `Object#toString`. + if (this === to_string) { + // Add whatever fixes for getting `[[Class]]` strings here. + if (thisArg === null) { + return '[object Null]'; + } + if (typeof thisArg === 'undefined') { + return '[object Undefined]'; + } + if (hasToStringTagLegacyArguments && isDuckTypeArguments(thisArg)) { + return '[object Arguments]'; + } + return call.call(to_string, thisArg); + } + // All other calls. + // `array_slice` is safe to use here, no known issue at present. + // Could probably use eval/Function here to spread the arguments, then use + // `call.call` to avoid `apply` altogether. + return this.apply(thisArg, call.call(array_slice, arguments, 1)); + } +}, hasToStringTagInconsistency); + // ES-5 15.3.4.5 // http://es5.github.com/#x15.3.4.5 @@ -320,7 +383,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); diff --git a/tests/spec/s-function.js b/tests/spec/s-function.js index 97237adb..5fa4bfe1 100644 --- a/tests/spec/s-function.js +++ b/tests/spec/s-function.js @@ -1,7 +1,84 @@ -/* 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('works with arraylike objects', 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]'); + }); + }); });