newless icon indicating copy to clipboard operation
newless copied to clipboard

bind is faster

Open trusktr opened this issue 7 years ago • 15 comments

https://github.com/Mr0grog/newless/blob/3fe059f3f8ac26d945afb01c880dd68d2e61d631/newless.js#L50-L61

Right there, I visited that link, and it seems nowadays bind is a lot faster (in Chrome):

image

trusktr avatar Mar 28 '18 17:03 trusktr

Almost twice as fast.

trusktr avatar Mar 28 '18 17:03 trusktr

That’s great! It should be obvious I haven’t worked on this library since before that was the case, though, and I don’t think it has any serious consumers today. If you’d like to change this, I’ll happily accept a PR.

Mr0grog avatar Mar 28 '18 21:03 Mr0grog

Not sure I know enough to make the change yet. So even if a constructor function is bound, it can still be constructed and the binding doesn't affect the construction?

// seems to work:
class Foo {}
new (Foo.bind.apply(Foo, [null, 1, 2, 3]))

But what's the point? What does that achieve over just new (Foo.bind(null, 1, 2, 3)), or even just new Foo(1, 2, 3)?

trusktr avatar Mar 29 '18 01:03 trusktr

Oh, I see, because unknown number of args. Doh.

trusktr avatar Mar 29 '18 01:03 trusktr

On this part,

https://github.com/Mr0grog/newless/blob/3fe059f3f8ac26d945afb01c880dd68d2e61d631/newless.js#L41-L48

is there an advantage of using class there? Or would using spread with new constructor(...([].slice.call(args))) work just as fine?

trusktr avatar Mar 29 '18 01:03 trusktr

How about something like this one?

      return Function("constructor, args, target",
        "'use strict';" +

        // spec: TypeError if third arg is explicitily undefined
        "if (arguments.length === 3 && typeof target === 'undefined') throw new TypeError('undefined is not a constructor');" +

        "target = target || constructor;" +

        (supportsSpread ?

          "var value = new constructor(...([].slice.call(args)));"

        :

          "args = Array.prototype.slice.call(args);" +
          "args = [null].concat(args);" +
          "var value = new constructor.bind.apply(constructor, args);"

        ) +

        // fix up the prototype so it matches the intended one, not one who's
        // prototype is the intended one :P
        "Object.setPrototypeOf(value, target.prototype);" +
        "return value;"
      );

Am I missing something critical by not using class syntax there?

trusktr avatar Mar 29 '18 01:03 trusktr

Am I missing something critical by not using class syntax there?

Yes. That’s what makes the third argument (target) work. Here’s an example:

// An example class hierarchy
class Z { constructor() { console.log('Z'); } }
class Y extends Z { constructor() { super(); console.log('Y'); } }
class X extends Y { constructor() { super(); console.log('X'); } }

// For reference, let’s see what Reflect.construct actually does:
var v1 = Reflect.construct(Y, [], X);  // Logs: Z, Y
console.log(v1 instanceof X);          // true

// Here’s how we simulate that with the ad-hoc `class`
var TempClass = class instantiator extends X {};
Object.setPrototypeOf(TempClass, Y);
var v2 = new TempClass();              // Logs: Z, Y
console.log(v2 instanceof X);          // true

// Here is what you are proposing:
var v3 = new Y();                      // Logs: Z, Y
console.log(v3 instanceof X);          // false (not what we want)

Mr0grog avatar Mar 29 '18 07:03 Mr0grog

If supportsSpread is false, then it calls constructor directly, which is the same problem.

trusktr avatar Mar 29 '18 22:03 trusktr

Plus, later on, you have the lines

        // fix up the prototype so it matches the intended one, not one who's
        // prototype is the intended one :P
        "Object.setPrototypeOf(value, target.prototype);" +
        "return value;"

Isn't that what takes care of making sure value is isntanceof target? If so, then isn't the ad-hoc class just redundant?

trusktr avatar Mar 29 '18 22:03 trusktr

That's why I was copying the non-supportsSpread version (instantiating constructor directly), figuring it worked the same. With the class instantiator, those last two lines just remove the instantiator prototype, but in either case the instance will have the same constructor logic ran on it, and will be instanceof target.

trusktr avatar Mar 29 '18 22:03 trusktr

Seems to work, try this in Chrome console:

var supportsSpread = true;

var construct = Function("constructor, args, newTarget",
    "'use strict';" +

    "if (arguments.length === 3 && typeof newTarget === undefined)" +
      "throw new TypeError('undefined is not a constructor');" +

    "newTarget = newTarget || constructor;" +

    (supportsSpread ?

      "var value = new constructor(...([].slice.call(args)));"

    :

      "args = Array.prototype.slice.call(args);" +
      "args = [null].concat(args);" +
      "var value = new constructor.bind.apply(constructor, args);"

    ) +

    "Object.setPrototypeOf(value, newTarget.prototype);" +
    "return value;"
);

// An example class hierarchy
class Z { constructor() { console.log('Z'); } }
class Y extends Z { constructor() { super(); console.log('Y'); } }
class X extends Y { constructor() { super(); console.log('X'); } }

// let’s see what the polyfill does:
var v1 = construct(Y, [], X);          // Logs: Z, Y
console.log(v1 instanceof X);          // true

trusktr avatar Mar 29 '18 23:03 trusktr

Oops, I had Reflect.construct in that last example. I updated it to just construct which works.

trusktr avatar Mar 29 '18 23:03 trusktr

I have a question about your example on ES Discuss. How does that magic work?

trusktr avatar Mar 29 '18 23:03 trusktr

It still matters during construction. Consider this case:

class Z { constructor() { console.log('Z! this instanceof X?', this instanceof X); } }
class Y extends Z { constructor() { super(); console.log('Y! this instanceof X?', this instanceof X); } }
class X extends Y { constructor() { super(); console.log('X'); } }

// let’s see what the polyfill does:
var v1 = Reflect.construct(Y, [], X);          // Logs: Z! this instanceof X? true, Y! this instanceof X? true
console.log(v1 instanceof X);          // true

// Here’s how we simulate that with the ad-hoc `class`
var TempClass = class instantiator extends X {};
Object.setPrototypeOf(TempClass, Y);
var v2 = new TempClass();              // Logs: Z! this instanceof X? true, Y! this instanceof X? true
Object.setPrototypeOf(v2, X.prototype);
console.log(v2 instanceof X);          // true

// Here is what you are proposing:
var v3 = new Y();                      // Logs: Z! this instanceof X? false, Y! this instanceof X? false (not what we want)
Object.setPrototypeOf(v3, X.prototype);
console.log(v3 instanceof X);          // true

That’s especially important if you have method overrides that change behavior used during instantiation:

class Z {
  constructor() {
    console.log('Z! this instanceof X?', this instanceof X);
    console.log(`My name is ${this.name}. Watch me dance: ${this.dance()}`);
  }
  get name() { return 'Ziggy'; }
  dance() { return '💃'; }
}

class Y extends Z {
  constructor() { super(); console.log('Y! this instanceof X?', this instanceof X); }
}

class X extends Y {
  constructor() { super(); console.log('X'); }
  get name() { return 'Xavier'; }
  dance() { return '🎷'; }
}

I can imagine this being especially critical for custom elements, where some parsing logic or event logic or something might be different on a subclass.


The reason Object.setPrototypeOf() is there is to remove instantiator from the prototype chain, because it should just be a hidden implementation detail. It’s changing the resulting prototype chain from:

value → instantiator.prototype → X.prototype → Y.prototype → Z.prototype → Object.prototype

to:

value → X.prototype → Y.prototype → Z.prototype → Object.prototype

Apologies if the comment above that line in the code was a little too obscure/jokey to make that clear.

Mr0grog avatar Mar 30 '18 04:03 Mr0grog

Got it! What it boils down to is simple ES5-like behavior,

const self = Object.create(X.prototype)
Y.apply(self)
return self

, except not. I wish ES6 didn't introduce classes that make ES5-style extension so difficult to work with.

In the part of the polyfill when spread is not supported, there's no instantiator pattern in the snippet:

 "var argList = '';" + 
 "for (var i = 0, len = args.length; i < len; i++) {" + 
   "if (i > 0) argList += ',';" + 
   "argList += 'args[' + i + ']';" + 
 "}" + 
 "var constructCall = Function('constructor, args'," + 
   "'return new constructor(' + argList + ');'" + 
 ");" + 
 "var value = constructCall(constructor, args);" 

Doesn't it need it there too?

trusktr avatar Mar 31 '18 05:03 trusktr