1

I am confused should I declare methods inside or outside the constructor method in a class definition?

How does it help to declare to methods inside the constructor function in a class definition?

What is the disadvantage of declaring methods outside the constructor function?

class animal {
    constructor(name, type, color){
        this.name = name;
        this.type = type;
        this.color = color;
        this.sound = () => {console.log(this.name,this.type,this.color)}
    }
    
}

class animal {
    constructor(name, type, color){
        this.name = name;
        this.type = type;
        this.color = color;
    }
    sound = () => {console.log(this.name,this.type,this.color)}
}

Creating objects in either way, results in the similar objects. What am I missing?

KawaiKx
  • 9,558
  • 19
  • 72
  • 111
  • 1
    Having multiple ways of doing something does not necessarily make one worse than the other. Think about static and private methods as well. – JavaScript Jun 27 '22 at 06:50
  • 1
    Why would you declare methods of a class like that? Why not use the intended methods? – Ouroborus Jun 27 '22 at 06:51

1 Answers1

2

The definition of functions inside the constructor was mostly used to store and access private variables in calls like this:

function MyClass() {
  let privateVar = { something: "usefull" };
  this.getPrivateVar = function getPrivateVar() { return privateVar; };
}
new MyClass().getPrivateVar(); // { something: "usefull" }

Just because calls produce a similar result, doesn't mean that they have an equal behavior. They can act completly differently depending on how they get called or how they where defined.

This counts especially for arrow functions as they don't have a this binding. They access the closest this providing environment, when defined.

class MyClass {}
MyClass.prototype.arrow = () => this;
console.log(new MyClass().arrow() == globalThis); // true

The answers from How does JavaScript .prototype work? explain pretty well how prototype is working and for classes every property and function defined inside it, will be stored on constructor.prototype, with some exceptions like the static and #private properties.

  • Private properties are only available and accessible inside the class and not in an inherited or an extended class and can't be accessed by a classic prototype.
    class MyClass {
      #private = "example";
      doSomethingWithMyPrivate() { return this.#private }
    }
    MyClass.prototype.getThePrivate = function() { return this.#private };
    new MyClass().doSomethingWithMyPrivate(); // example
    // Both produce the same SyntaxError:
    // Private field '#private' must be declared in an enclosing class
    new MyClass().#private;
    new MyClass().getThePrivate();
    
    
  • Static properties will be placed on the constructor directly.
    class MyClass {
      static doSomething() { console.log("Okay!") }
    }
    MyClass.doSomething(); // Okay!
    new MyClass().doSomething(); // TypeError: doSomething is not a function
    

Every instance of a class will access the prototype references or the inheriting objects prototype until any object has a match with the property-name, if not it returns undefined or throw a more specific error depending on the situation.

From this answer:

Javascript has a mechanism when looking up properties on Objects which is called 'prototypal inheritance', here is what is basically does:

  • First is checked if the property is located on the Object itself. If so this property is returned.
  • If the property is not located on the object itself it will 'climb up the protochain'. It basically looks at the object referred to by the proto property. There it checks if the property is available on the object referred to by proto
  • If the property isn't located on the proto object it will climb up the proto chain all the way up to Object object.
  • If it cannot find the property nowhere on the object and its prototype chain it will return undefined.

So if you define the functions inside the constructor, each instance gets a new function assigned (they won't have the same reference). Which means, depending on the class (new animal).sound == (new animal).sound, the statement will return true (your 2nd example) or false (your 1st example) and the prototypal inheritance will take place or not.

// the old way
function Foo() {
  if (this instanceof Foo) {
    this.fx = () => {};
  } else throw new TypeError(`Constructor Foo cannot be invoked without 'new'`);
}
Foo.prototype.pfx = function() {};

console.log((new Foo).fx, (new Foo).fx == (new Foo).fx); // arrow ƒ, false
console.log((new Foo).pfx, (new Foo).pfx == (new Foo).pfx); // ƒ, true

// the class way
class Bar {
  constructor() { // similar too "ƒ Foo() {}" with the addition of super ƒ
    this.fx = () => {};
  }
  // this is almost like Bar.prototype.pfx = ...
  pfx() {}
}
// works like before
Bar.prototype.moo = function moo() {}; 

console.log((new Bar).fx, (new Bar).fx == (new Bar).fx); // arrow ƒ, false
console.log((new Bar).pfx, (new Bar).pfx == (new Bar).pfx); // ƒ, true
console.log((new Bar).moo, (new Bar).moo == (new Bar).moo); // ƒ, true

If you define properties as mentioned in your example #1, you can "override" other methods and this may cause trouble with private properties as mentioned above.

class Foo {
  constructor() {
    // just for the example
    this.fx = Foo.prototype.fx;
  }
  fx() { console.log("I am broken"); };
}
class Bar extends Foo {
  fx() { console.log("I fix something"); }
}
class Moo extends Bar {
  constructor() {
    super();
    delete this.fx;
  }
}
/*
 * If instance has no own property called fx the default 
 * behavior will take place:
 *   Test the construtor.prototype if it has one and execute this.
 *   If not, look at the inheriting classes in descending order
 *     (Moo > Bar > Foo > Object)
 * But bar.fx() => has an own property called fx which takes place
 */

let bar = new Bar();
bar.fx(); // I am broken

// none of the inheriting classes has a toJSON ƒ,
// so Object.prototype.toJSON will be called.
console.log(bar.toJSON == Object.prototype.toJSON) // true

// the Moo constructor deletes the own property after the Foo constructor was evaluated, so Bar.prototype.fx takes place
new Moo().fx() // I fix something
Christopher
  • 3,124
  • 2
  • 12
  • 29