Simulate the 'new' operator in JavaScript

后端 未结 4 698
别跟我提以往
别跟我提以往 2020-11-29 08:52

I tried to simulate the \'new\' operator in JavaScript in a code like this:

Function.method(\'new\', function ( ) {
    var objPrototype = Object.create(this         


        
相关标签:
4条回答
  • 2020-11-29 09:20

    From the specification:

    11.2.2 The new Operator # Ⓣ Ⓡ Ⓖ

    The production NewExpression : new NewExpression is evaluated as follows:

    1. Let ref be the result of evaluating NewExpression.
    2. Let constructor be GetValue(ref).
    3. If Type(constructor) is not Object, throw a TypeError exception.
    4. If constructor does not implement the [[Construct]] internal method, throw a TypeError exception.
    5. Return the result of calling the [[Construct]] internal method on constructor, providing no arguments (that is, an empty list of arguments).

    The production MemberExpression : new MemberExpression Arguments is evaluated as follows:

    1. Let ref be the result of evaluating MemberExpression.
    2. Let constructor be GetValue(ref).
    3. Let argList be the result of evaluating Arguments, producing an internal list of argument values (11.2.4).
    4. If Type(constructor) is not Object, throw a TypeError exception.
    5. If constructor does not implement the [[Construct]] internal method, throw a TypeError exception.
    6. Return the result of calling the [[Construct]] internal method on constructor, providing the list argList as the argument values.

    In either case, all steps are correctly followed:

    var objPrototype = Object.create(this.prototype);    // 1-4 1-5
    var instance = this.apply(objPrototype, arguments);  // 5   6
    

    The point of interest is 2.
    The specification for [[construct]] states:

    When the [[Construct]] internal method for a Function object F is called with a possibly empty list of arguments, the following steps are taken:

    • Let obj be a newly created native ECMAScript object.
      . . .
    • Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args.
    • If Type(result) is Object then return result.
    • Return obj.

    typeof obj returns "object" for null, while null is not an object. However, since null is a falsy value, your code also works as intended:

    return (typeof instance === 'object' && instance ) || objPrototype;
    
    0 讨论(0)
  • 2020-11-29 09:23

    Here is an alternative to using the __proto__ approach. Its in line with how the OP initially started out...

    function New(fn) {
        var newObj = Object.create(fn.prototype);
        return function() {
            fn.apply(newObj, arguments);
            return newObj;
        };
    }
    

    This is a much cleaner way of doing it, and it passes prototype chain tests too.

    0 讨论(0)
  • 2020-11-29 09:35

    The new operator takes a function F and arguments: new F(arguments...). It does three easy steps:

    1. Create the instance of the class. It is an empty object with its __proto__ property set to F.prototype. Initialize the instance.

    2. The function F is called with the arguments passed and this set to be the instance.

    3. Return the instance

    Now that we understand what the new operator does, we can implement it in Javascript.

        function New (f) {
    /*1*/  var n = { '__proto__': f.prototype };
           return function () {
    /*2*/    f.apply(n, arguments);
    /*3*/    return n;
           };
         }
    

    And just a small test to see that it works.

    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    Point.prototype = {
      print: function () { console.log(this.x, this.y); }
    };
    
    var p1 = new Point(10, 20);
    p1.print(); // 10 20
    console.log(p1 instanceof Point); // true
    
    var p2 = New (Point)(10, 20);
    p2.print(); // 10 20
    console.log(p2 instanceof Point); // true
    
    0 讨论(0)
  • 2020-11-29 09:37

    The answers here are all valid for standard ES5, which was all there when when they were written, but they are not general solutions that will work in all ES6 contexts, so I want to expand on them. The short answer is that the code from the question:

    Function.method('new', function ( ) {
      var objPrototype = Object.create(this.prototype);
      var instance = this.apply(objPrototype, arguments);
    
      return instance;
    });
    

    would in a standard ES6 environment be better implemented as

    Function.method('new', function ( ) {
      return Reflect.construct(this, arguments);
    });
    

    which definitely simplifies things.

    Reflect.construct was introduced in ES6 as part of the Proxy system, but it has general uses like this situation.

    The reason this is the preferred method now is the simple reason that .apply doesn't work on every type of function anymore. The previous answer explains that new calls the internal language function [[Construct]] to initialize the argument. The approach using .apply essentially replaces the automatic object-creation and calling logic handled in [[Construct]] by instead manually creating the object and then calling the function, which uses it's [[Call]] internal method instead of [[Construct]].

    The call to the function is part of what changed in ES6. In ES5 pretty much the only thing you'll be constructing is a normal function Foo(){} value, so you can make assumptions about that. In ES6, class Foo {} syntax was introduced, and the constructor functions created by class syntax have more restrictions in place, so the assumptions made about ES5 do not apply. Most importantly, ES6 classes are explicitly disallowed from using [[Call]]. Doing the following will throw an exception:

    class Foo {}
    Foo();
    

    This is the same issue as .call and .apply. They are not function-constructing functions, they are function-calling functions. So if you try to use them on an ES6 class, they will throw exceptions.

    Reflect.construct avoids these issues by actually calling [[Construct]] and not [[Call]], but exposing it via an API that can be used without new.

    0 讨论(0)
提交回复
热议问题