fauxNew: Recreating the "new" operator

node v5.12.0
version: 5.0.0
endpointsharetweet
The "new" keyword in JS does four things, according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new: 1. It allocates a new object in memory 2. It sets the new object's prototype to the `.prototype` property of the constructor function 3. It calls the constructor function, with the context ("this") set to the new object 4. If the constructor explicitly returned an object, the `new` operator will return that object. Otherwise, it returns the newly created object. What follows is an attempt to re-create that, using just the reflection tools available in JS. Corrections and improvements welcome. If correct, this is in effect userland implementation of the ES6 Reflect.construct function (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct)
// This is a re-implementation of new, using just the reflection tools available in JS. // // Usage: instead of // new SomeConstructor(foo, bar) // call // fauxNew(SomeConstructor, [foo, bar]) // function fauxNew (constructor, args) { // allocate a new object and set the prototype var newObject = Object.create(constructor.prototype) // call the constructoir with "this" bound to the new object var retVal = constructor.apply(newObject, args || []) // if the constructor returned an object, return it; // otherwise return the new object var constructorReturnedAnObject = !!retVal && ["object", "function"].indexOf(typeof retVal) !== -1 return constructorReturnedAnObject? retVal : newObject }
Here are some sample constructors we can play with
function RegularClass (foo) { this.foo = foo } function ReturnsAnObject (foo) { this.foo = foo return {bar: "i am not this"} } function ReturnsANumber (foo) { this.foo = foo return 5 } function ReturnsAString (foo) { this.foo = foo return "five" } // dummy function for use in ReturnsAFunction function noop () {} function ReturnsAFunction (foo) { this.foo = foo return noop } function HasAPrototype (foo) { this.foo = foo } HasAPrototype.prototype = { bar: "i come from the prototype" } function ChecksInstanceOf () { this.isInstance = this instanceof ChecksInstanceOf }
Some testing helpers. test() takes a constructor, a mapper, and expected value, and a message. It calls the constructor using new, and using fauxNew, passing in a test argument, runs the results through the mapper, and compares what comes out. If the results for the new operator, fauxNew, and the expected value are all equal, we get a success message. Otherwise we get a failure message.
function assert (predicate, message) { console.log((predicate? "SUCCESS: ":"FAIL: ") + message) } // doesn't matter what this is, we just need it to be a consistent // value that can be passed in and out var testArg = Symbol("I am a test argument") function test (constructor, mapper, expected, message) { var traditional = mapper(new constructor(testArg)) var faux = mapper(fauxNew(constructor, [testArg])) assert (faux == traditional && faux == expected, message) }
Now some tests
test(RegularClass, x => x.foo, testArg, "member is set correctly") test(ReturnsAnObject, x => x.bar, "i am not this", "object is returned correctly") test(ReturnsANumber, x => x.foo, testArg, "returned number is ignored")
test(ReturnsAString, x => x.foo, testArg, "returned string is ignored") test(ReturnsAFunction, x => x, noop, "function is returned correctly") test(HasAPrototype, x => x.bar, "i come from the prototype", "prototype is applied correctly")
test(ChecksInstanceOf, x => x.isInstance, true, "passes instanceof test")
Loading…

no comments

    sign in to comment