Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Function.prototype.call and tests. #345

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion es5-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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);
Expand Down
81 changes: 79 additions & 2 deletions tests/spec/s-function.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down
68 changes: 66 additions & 2 deletions tests/spec/s-object.js
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -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';

Expand Down Expand Up @@ -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 () {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test will actually cause problems in newer implementations, since in ES6, many of these prototype objects stop being instances of themselves, and instead would return '[object Object]'.

Please split these up such that array/boolean/object/function are always tested, but that the rest are skipped with an xit, much like https://github.com/es-shims/es5-shim/blob/master/tests/spec/s-object.js#L89-L90

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look and try to do that.

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]');
});
});
});