From: Lady Date: Sun, 17 Jul 2022 17:52:45 +0000 (-0700) Subject: Remove dependencies of function.js; add tests X-Git-Tag: 0.1.0~7 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/29307967cb084aceaa9265fe22af4348c9e4376a?ds=inline;hp=7c3a2eda590637af9463e5a59ab798f802d274a9 Remove dependencies of function.js; add tests - No longer depends on mutable runtime bindings or other modules - Unit tests for all function exports --- diff --git a/function.js b/function.js index 0b9ebcc..46cab86 100644 --- a/function.js +++ b/function.js @@ -7,22 +7,71 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { defineOwnProperty, isObject } from "./object.js"; +export const { + /** + * Creates a bound function from the provided function using the + * provided this value and arguments list. + * + * ☡ As with call and construct, the arguments must be passed as an + * array. + */ + bind, -/** - * Creates a bound function from the provided function using the - * provided this value and arguments list. - * - * ☡ As with call and construct, the arguments must be passed as an - * array. - */ -export const bind = (() => { - const callBind = Function.prototype.call.bind( - Function.prototype.bind, - ); - const bind = ($, boundThis, boundArgs) => - callBind($, boundThis, ...boundArgs); - return bind; + /** + * Returns a new function which calls the provided function with its + * first argument as the `this` value and the remaining arguments + * passed through. + * + * ※ This is effectively an alias for Function.prototype.call.bind. + */ + makeCallable, +} = (() => { + // ☡ Because these functions are used to initialize module constants, + // they can’t depend on imports from elsewhere. + const { + bind: functionBind, + call: functionCall, + } = Function.prototype; + const callBind = Reflect.apply(functionBind, functionCall, [ + functionBind, + ]); + const { + create: objectCreate, + defineProperty: defineOwnProperty, + getPrototypeOf: getPrototype, + } = Object; + const { iterator: iteratorSymbol } = Symbol; + const { [iteratorSymbol]: arrayIterator } = Array.prototype; + const { + next: arrayIteratorNext, + } = getPrototype([][iteratorSymbol]()); + const argumentIterablePrototype = { + [iteratorSymbol]() { + return { + next: callBind( + arrayIteratorNext, + call(arrayIterator, this.args, []), + ), + }; + }, + }; + return { + bind: ($, boundThis, boundArgs) => + callBind( + $, + boundThis, + ...objectCreate( + argumentIterablePrototype, + { args: { value: boundArgs } }, + ), + ), + makeCallable: ($) => + defineOwnProperty( + callBind(functionCall, $), + "length", + { value: $.length + 1 }, + ), + }; })(); /** @@ -43,47 +92,40 @@ export const call = Reflect.apply; */ export const construct = Reflect.construct; +/** + * Returns the provided value. + * + * ※ This function can be called as a constructor. When used in an + * `extends` clause and called via `super`, it will set the value of + * `this` to the provided value, enabling it to be extended with + * private class features. + */ +export const identity = function ($) { + return $; +}; + +/** Returns whether the provided value is callable. */ +export const isCallable = ($) => typeof $ === "function"; + /** Returns whether the provided value is a constructor. */ export const isConstructor = ($) => { - if (!isObject($)) { - // The provided value is not an object. + // The provided value is an object. + try { + construct( + function () {}, + [], + $, + ); // will throw if $ is not a constructor + return true; + } catch { return false; - } else { - // The provided value is an object. - try { - construct( - function () {}, - [], - $, - ); // will throw if $ is not a constructor - return true; - } catch { - return false; - } } }; -/** - * Returns a new function which calls the provided function with its - * first argument as the `this` value and the remaining arguments - * passed through. - * - * ※ This is effectively an alias for Function.prototype.call.bind. - */ -export const makeCallable = (() => { - const functionCall = Function.prototype.call; - const callable = ($) => defineOwnProperty( - bind(functionCall, $, []), - "length", - { value: $.length + 1 }, - ); - return callable; -})(); - /** * Returns whether the provided object inherits from the prototype of * the provided function. */ export const ordinaryHasInstance = makeCallable( - Function.prototype[Symbol.hasInstance], + Function.prototype[Symbol.hasInstance], ); diff --git a/function.test.js b/function.test.js new file mode 100644 index 0000000..4cfa85f --- /dev/null +++ b/function.test.js @@ -0,0 +1,292 @@ +// ♓🌟 Piscēs ∷ function.test.js +// ==================================================================== +// +// Copyright © 2022 Lady [@ Lady’s Computer]. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at . + +import { + assert, + assertEquals, + assertStrictEquals, + describe, + it, +} from "./dev-deps.js"; +import { + bind, + call, + construct, + identity, + isCallable, + isConstructor, + makeCallable, + ordinaryHasInstance, +} from "./function.js"; + +describe("bind", () => { + it("[[Call]] binds this", () => { + assertStrictEquals( + bind( + function () { + return this; + }, + "pass", + [], + ).call("fail"), + "pass", + ); + }); + + it("[[Call]] binds arguments", () => { + assertEquals( + bind( + function (...args) { + return [this, ...args]; + }, + "etaoin", + ["shrdlu"], + ).call("failure", "cmfwyp"), + ["etaoin", "shrdlu", "cmfwyp"], + ); + }); + + it("[[Call]] works with any arraylike third argument", () => { + assertEquals( + bind( + function (...args) { + return [this, ...args]; + }, + "etaoin", + { + 1: "shrdlu", + 3: "failure", + length: 3, + }, + ).call("failure", "cmfwyp"), + ["etaoin", undefined, "shrdlu", undefined, "cmfwyp"], + ); + }); +}); + +describe("call", () => { + it("[[Call]] calls with the provided this value", () => { + assertStrictEquals( + call( + function () { + return this; + }, + "pass", + [], + ), + "pass", + ); + }); + + it("[[Call]] calls with the provided arguments", () => { + assertEquals( + call( + function (...args) { + return [this, ...args]; + }, + "etaoin", + ["shrdlu", "cmfwyp"], + ), + ["etaoin", "shrdlu", "cmfwyp"], + ); + }); + + it("[[Call]] works with any arraylike third argument", () => { + assertEquals( + call( + function (...args) { + return [this, ...args]; + }, + "etaoin", + { + 1: "shrdlu", + 3: "cmfwyp", + 4: "failure", + length: 4, + }, + ), + ["etaoin", undefined, "shrdlu", undefined, "cmfwyp"], + ); + }); +}); + +describe("construct", () => { + it("[[Call]] defaults to the constructor as the target", () => { + const constructor = class {}; + assertStrictEquals( + Object.getPrototypeOf(construct( + constructor, + [], + )), + constructor.prototype, + ); + }); + + it("[[Call]] constructs with the provided new target", () => { + const target = function () {}; + assertStrictEquals( + construct( + function () { + return new.target; + }, + [], + target, + ), + target, + ); + }); + + it("[[Call]] constructs with the provided arguments", () => { + assertEquals( + construct( + function (...args) { + return [new.target.value, ...args]; + }, + ["shrdlu", "cmfwyp"], + Object.assign(function () {}, { value: "etaoin" }), + ), + ["etaoin", "shrdlu", "cmfwyp"], + ); + }); + + it("[[Call]] works with any arraylike third argument", () => { + assertEquals( + construct( + function (...args) { + return [new.target.value, ...args]; + }, + { + 1: "shrdlu", + 3: "cmfwyp", + 4: "failure", + length: 4, + }, + Object.assign(function () {}, { value: "etaoin" }), + ), + ["etaoin", undefined, "shrdlu", undefined, "cmfwyp"], + ); + }); +}); + +describe("identity", () => { + it("[[Call]] returns what it is given", () => { + const value = {}; + assertStrictEquals(identity(value), value); + }); + + it("[[Construct]] is constructable", () => { + const value = {}; + assertStrictEquals(new identity(value), value); + }); + + it("[[Construct]] is subclassable", () => { + const value = {}; + assertStrictEquals(new class extends identity {}(value), value); + }); +}); + +describe("isCallable", () => { + it("[[Call]] returns true for ordinary functions", () => { + assert(isCallable(function () {})); + }); + + it("[[Call]] returns true for arrow functions", () => { + assert(isCallable(() => {})); + }); + + it("[[Call]] returns true for generator functions", () => { + assert(isCallable(function* () {})); + }); + + it("[[Call]] returns true for classes", () => { + assert(isCallable(class {})); + }); + + it("[[Call]] returns true for builtin functions", () => { + assert(isCallable(Math.ceil)); + }); + + it("[[Call]] returns false for null", () => { + assert(!isCallable(null)); + }); + + it("[[Call]] returns false for objects", () => { + assert(!isCallable({})); + }); +}); + +describe("isConstructor", () => { + it("[[Call]] returns true for ordinary functions", () => { + assert(isConstructor(function () {})); + }); + + it("[[Call]] returns false for arrow functions", () => { + assert(!isConstructor(() => {})); + }); + + it("[[Call]] returns false for generator functions", () => { + assert(!isConstructor(function* () {})); + }); + + it("[[Call]] returns true for classes", () => { + assert(isConstructor(class {})); + }); + + it("[[Call]] returns false for builtin functions", () => { + assert(!isConstructor(Math.ceil)); + }); + + it("[[Call]] returns false for null", () => { + assert(!isConstructor(null)); + }); + + it("[[Call]] returns false for objects", () => { + assert(!isConstructor({})); + }); +}); + +describe("makeCallable", () => { + it("[[Call]] transfers the first argument to this", () => { + assertStrictEquals( + makeCallable( + function () { + return this; + }, + ).call("fail", "pass"), + "pass", + ); + }); + + it("[[Call]] transfers the remaining arguments", () => { + assertEquals( + makeCallable( + function (...args) { + return [this, ...args]; + }, + ).call("failure", "etaoin", "shrdlu", "cmfwyp"), + ["etaoin", "shrdlu", "cmfwyp"], + ); + }); +}); + +describe("ordinaryHasInstance", () => { + it("[[Call]] walks the prototype chain", () => { + const constructor = class { + [Symbol.hasInstance]() { + return false; + } + }; + assert( + ordinaryHasInstance( + constructor, + new class extends constructor {}(), + ), + ); + }); +});