Why is it necessary to set the prototype constructor?

前端 未结 14 1761
孤城傲影
孤城傲影 2020-11-22 02:54

In the section about inheritance in the MDN article Introduction to Object Oriented Javascript, I noticed they set the prototype.constructor:

// correct the          


        
相关标签:
14条回答
  • 2020-11-22 03:17

    TLDR; Not super necessary, but will probably help in the long run, and it is more accurate to do so.

    NOTE: Much edited as my previous answer was confusingly written and had some errors that I missed in my rush to answer. Thanks to those who pointed out some egregious errors.

    Basically, it's to wire subclassing up correctly in Javascript. When we subclass, we have to do some funky things to make sure that the prototypal delegation works correctly, including overwriting a prototype object. Overwriting a prototype object includes the constructor, so we then need to fix the reference.

    Let's quickly go through how 'classes' in ES5 work.

    Let's say you have a constructor function and its prototype:

    //Constructor Function
    var Person = function(name, age) {
      this.name = name;
      this.age = age;
    }
    
    //Prototype Object - shared between all instances of Person
    Person.prototype = {
      species: 'human',
    }
    

    When you call the constructor to instantiate, say Adam:

    // instantiate using the 'new' keyword
    var adam = new Person('Adam', 19);
    

    The new keyword invoked with 'Person' basically will run the Person constructor with a few additional lines of code:

    function Person (name, age) {
      // This additional line is automatically added by the keyword 'new'
      // it sets up the relationship between the instance and the prototype object
      // So that the instance will delegate to the Prototype object
      this = Object.create(Person.prototype);
    
      this.name = name;
      this.age = age;
    
      return this;
    }
    
    /* So 'adam' will be an object that looks like this:
     * {
     *   name: 'Adam',
     *   age: 19
     * }
     */
    

    If we console.log(adam.species), the lookup will fail at the adam instance, and look up the prototypal chain to its .prototype, which is Person.prototype - and Person.prototype has a .species property, so the lookup will succeed at Person.prototype. It will then log 'human'.

    Here, the Person.prototype.constructor will correctly point to Person.

    So now the interesting part, the so-called 'subclassing'. If we want to create a Student class, that is a subclass of the Person class with some additional changes, we'll need to make sure that the Student.prototype.constructor points to Student for accuracy.

    It doesn't do this by itself. When you subclass, the code looks like this:

    var Student = function(name, age, school) {
     // Calls the 'super' class, as every student is an instance of a Person
     Person.call(this, name, age);
     // This is what makes the Student instances different
     this.school = school
    }
    
    var eve = new Student('Eve', 20, 'UCSF');
    
    console.log(Student.prototype); // this will be an empty object: {}
    

    Calling new Student() here would return an object with all of the properties we want. Here, if we check eve instanceof Person, it would return false. If we try to access eve.species, it would return undefined.

    In other words, we need to wire up the delegation so that eve instanceof Person returns true and so that instances of Student delegate correctly to Student.prototype, and then Person.prototype.

    BUT since we're calling it with the new keyword, remember what that invocation adds? It would call Object.create(Student.prototype), which is how we set up that delegational relationship between Student and Student.prototype. Note that right now, Student.prototype is empty. So looking up .species an instance of Student would fail as it delegates to only Student.prototype, and the .species property doesn't exist on Student.prototype.

    When we do assign Student.prototype to Object.create(Person.prototype), Student.prototype itself then delegates to Person.prototype, and looking up eve.species will return human as we expect. Presumably we would want it to inherit from Student.prototype AND Person.prototype. So we need to fix all of that.

    /* This sets up the prototypal delegation correctly 
     *so that if a lookup fails on Student.prototype, it would delegate to Person's .prototype
     *This also allows us to add more things to Student.prototype 
     *that Person.prototype may not have
     *So now a failed lookup on an instance of Student 
     *will first look at Student.prototype, 
     *and failing that, go to Person.prototype (and failing /that/, where do we think it'll go?)
    */
    Student.prototype = Object.create(Person.prototype);
    

    Now the delegation works, but we're overwriting Student.prototype with an of Person.prototype. So if we call Student.prototype.constructor, it would point to Person instead of Student. This is why we need to fix it.

    // Now we fix what the .constructor property is pointing to    
    Student.prototype.constructor = Student
    
    // If we check instanceof here
    console.log(eve instanceof Person) // true
    

    In ES5, our constructor property is a reference that refers to a function that we've written with the intent to be a 'constructor'. Aside from what the new keyword gives us, the constructor is otherwise a 'plain' function.

    In ES6, the constructor is now built into the way we write classes - as in, it's provided as a method when we declare a class. This is simply syntactic sugar but it does accord us some conveniences like access to a super when we are extending an existing class. So we would write the above code like this:

    class Person {
      // constructor function here
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
      // static getter instead of a static property
      static get species() {
        return 'human';
      }
    }
    
    class Student extends Person {
       constructor(name, age, school) {
          // calling the superclass constructor
          super(name, age);
          this.school = school;
       }
    }
    
    0 讨论(0)
  • 2020-11-22 03:20

    I'd disagree. It isn't necessary to set the prototype. Take that exact same code but remove the prototype.constructor line. Does anything change? No. Now, make the following changes:

    Person = function () {
        this.favoriteColor = 'black';
    }
    
    Student = function () {
        Person.call(this);
        this.favoriteColor = 'blue';
    }
    

    and at the end of the test code...

    alert(student1.favoriteColor);
    

    The color will be blue.

    A change to the prototype.constructor, in my experience, doesn't do much unless you're doing very specific, very complicated things that probably aren't good practice anyway :)

    Edit: After poking around the web for a bit and doing some experimentation, it looks like people set the constructor so that it 'looks' like the thing that is being constructed with 'new'. I guess I would argue that the problem with this is that javascript is a prototype language - there is no such thing as inheritence. But most programmers come from a background of programming that pushes inheritence as 'the way'. So we come up with all sorts of things to try and make this prototypical language a 'classic' language.. such as extending 'classes'. Really, in the example they gave, a new student is a person - it isn't 'extending' from another student.. the student is all about the person, and whatever the person is the student is as well. Extend the student, and whatever you've extended is a student at heart, but is customized to fit your needs.

    Crockford is a bit crazy and overzealous, but do some serious reading on some of the stuff that he's written.. it'll make you look at this stuff very differently.

    0 讨论(0)
  • 2020-11-22 03:23

    It's not always necessary, but it does have its uses. Suppose we wanted to make a copy method on the base Person class. Like this:

    // define the Person Class  
    function Person(name) {
        this.name = name;
    }  
    
    Person.prototype.copy = function() {  
        // return new Person(this.name); // just as bad
        return new this.constructor(this.name);
    };  
    
    // define the Student class  
    function Student(name) {  
        Person.call(this, name);
    }  
    
    // inherit Person  
    Student.prototype = Object.create(Person.prototype);
    

    Now what happens when we create a new Student and copy it?

    var student1 = new Student("trinth");  
    console.log(student1.copy() instanceof Student); // => false
    

    The copy is not an instance of Student. This is because (without explicit checks), we'd have no way to return a Student copy from the "base" class. We can only return a Person. However, if we had reset the constructor:

    // correct the constructor pointer because it points to Person  
    Student.prototype.constructor = Student;
    

    ...then everything works as expected:

    var student1 = new Student("trinth");  
    console.log(student1.copy() instanceof Student); // => true
    
    0 讨论(0)
  • This has the huge pitfall that if you wrote

    Student.prototype.constructor = Student;
    

    but then if there was a Teacher whose prototype was also Person and you wrote

    Teacher.prototype.constructor = Teacher;
    

    then the Student constructor is now Teacher!

    Edit: You can avoid this by ensuring that you had set the Student and Teacher prototypes using new instances of the Person class created using Object.create, as in the Mozilla example.

    Student.prototype = Object.create(Person.prototype);
    Teacher.prototype = Object.create(Person.prototype);
    
    0 讨论(0)
  • 2020-11-22 03:29

    Given simple constructor function:

    function Person(){
        this.name = 'test';
    }
    
    
    console.log(Person.prototype.constructor) // function Person(){...}
    
    Person.prototype = { //constructor in this case is Object
        sayName: function(){
            return this.name;
        }
    }
    
    var person = new Person();
    console.log(person instanceof Person); //true
    console.log(person.sayName()); //test
    console.log(Person.prototype.constructor) // function Object(){...}
    

    By default (from the specification https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor), all prototypes automatically get a property called constructor that points back to the function on which it is a property. Depending on the constructor, other properties and methods might be added to the prototype which is not a very common practice but still it is allowed for extensions.

    So simply answering: we need make sure that the value in prototype.constructor is correctly set as it is supposed by the specification to be.

    Do we have to always set correctly this value? It helps with debugging and makes internal structure consistent against specification. We should definitely when our API is being used by the thirdparties, but not really when the code is finally executed in the runtime.

    0 讨论(0)
  • 2020-11-22 03:32

    It is necessary when you need an alternative to toString without monkeypatching:

    //Local
    foo = [];
    foo.toUpperCase = String(foo).toUpperCase;
    foo.push("a");
    foo.toUpperCase();
    
    //Global
    foo = [];
    window.toUpperCase = function (obj) {return String(obj).toUpperCase();}
    foo.push("a");
    toUpperCase(foo);
    
    //Prototype
    foo = [];
    Array.prototype.toUpperCase = String.prototype.toUpperCase;
    foo.push("a");
    foo.toUpperCase();
    
    //toString alternative via Prototype constructor
    foo = [];
    Array.prototype.constructor = String.prototype.toUpperCase;
    foo.push("a,b");
    foo.constructor();
    
    //toString override
    var foo = [];
    foo.push("a");
    var bar = String(foo);
    foo.toString = function() { return bar.toUpperCase(); }
    foo.toString();
    
    //Object prototype as a function
    Math.prototype = function(char){return Math.prototype[char]};
    Math.prototype.constructor = function() 
      {
      var i = 0, unicode = {}, zero_padding = "0000", max = 9999;
      
      while (i < max) 
        {
        Math.prototype[String.fromCharCode(parseInt(i, 16))] = ("u" + zero_padding + i).substr(-4);
    
        i = i + 1;
        }    
      }
    
    Math.prototype.constructor();
    console.log(Math.prototype("a") );
    console.log(Math.prototype["a"] );
    console.log(Math.prototype("a") === Math.prototype["a"]);

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