[Translation] The problem of JavaScript constructors and three ways to solve it

Introduction


As you know, you can create a new object in JavaScript using the constructor function of the following form:

function Fubar (foo, bar) {
  this._foo = foo;
  this._bar = bar;
}
var snafu = new Fubar("Situation Normal", "All Fsked Up");


When we call the constructor function using the keyword new , we get a new object, and the context of its constructor is set to the object itself. If we do not explicitly return anything from the constructor, then we get the object itself as a result. Thus, the body of the constructor function is used to initialize the newly created object, the prototype of which will be the contents of the prototypeconstructor property , so you can write as follows:

Fubar.prototype.concatenated = function () {
  return this._foo + " " + this._bar;
}
snafu.concatenated()
  //=> 'Situation Normal All Fsked Up'


Using the operator, instanceofyou can verify that the object was created using a specific constructor:

snafu instanceof Fubar
  //=> true


(It is instanceofpossible to make it work “incorrectly” in cases with more advanced idioms, or if you are a harmful troll who collects exceptions of the programming language and enjoys by torturing them for job seekers for interviews. However, it instanceofworks quite well for our purposes .)

Problem


What happens if we call the constructor by accidentally missing a keyword new ?

var fubar = Fubar("Fsked Up", "Beyond All Recognition");
fubar
  //=> undefined


Charles Sigmund Juan !? We called a regular function that returns nothing, so it fubarwill be undefined. This is not what we need, even worse, because:

_foo
  //=> 'Fsked Up'


JavaScript sets the context to the global scope to execute a regular function, so we just got there. Well, this can somehow be corrected:

function Fubar (foo, bar) {
  "use strict"
  this._foo = foo;
  this._bar = bar;
}
Fubar("Situation Normal", "All Fsked Up");
  //=> TypeError: Cannot set property '_foo' of undefined


Although the use of “use strict” is often omitted in code and in books, its use in production can be called almost mandatory due to cases like the one described above. However, constructors that do not provide the ability to call themselves without a keyword neware a potential problem.

So what can we do with this?

Solution No. 1 - car inheritance


David Herman explains auto-inheritance in his book Effective JavaScript . When we call the constructor with new, the pseudo-variable thispoints to a new instance of our so-called “class”. This can be used to determine whether a constructor was called using a codeword new.

function Fubar (foo, bar) {
  "use strict"
  var obj,
      ret;
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else return new Fubar(foo, bar);
}
Fubar("Situation Normal", "All Fsked Up");
  //=>
    { _foo: 'Situation Normal',
      _bar: 'All Fsked Up' }


Why make it work without new? One of the problems this approach solves is the impossibility of calling new Fubar(...). Consider an example:

function logsArguments (fn) {
  return function () {
    console.log.apply(this, arguments);
    return fn.apply(this, arguments)
  }
}
function sum2 (a, b) {
  return a + b;
}
var logsSum = logsArguments(sum2);
logsSum(2, 2)
  //=>
    2 2
    4


logsArgumentsdecorates a function that logs its arguments, returning the result of its call. Let's try to do the same with Fubar:

function Fubar (foo, bar) {
  this._foo = foo;
  this._bar = bar;
}
Fubar.prototype.concatenated = function () {
  return this._foo + " " + this._bar;
}
var LoggingFubar = logsArguments(Fubar);
var snafu = new LoggingFubar("Situation Normal", "All Fsked Up");
  //=> Situation Normal All Fsked Up
snafu.concatenated()
  //=> TypeError: Object [object Object] has no method 'concatenated'


This does not work because it snafu is an instance LoggingFubar, not Fubar. But if you use auto inheritance in Fubar:

function Fubar (foo, bar) {
  "use strict"
  var obj,
      ret;
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else {
    obj = new Fubar();
    ret = Fubar.apply(obj, arguments);
    return ret === undefined
           ? obj
           : ret;
  }
}
Fubar.prototype.concatenated = function () {
  return this._foo + " " + this._bar;
}
var LoggingFubar = logsArguments(Fubar);
var snafu = new LoggingFubar("Situation Normal", "All Fsked Up");
  //=> Situation Normal All Fsked Up
snafu.concatenated()
  //=> 'Situation Normal All Fsked Up'


Now it works, although, of course, snafuis an instance Fubar, not LoggingFubar. It is impossible to say for sure whether this is what we were seeking. This method cannot be called more than a useful abstraction, not devoid of leaks, just as it cannot be said that it “just works”, although thanks to it some things become possible, which are much more difficult to implement with other approaches.

Solution # 2 - using an overloaded function


A function that checks if an object is an instance of a particular class can be very useful. If you are not afraid of the idea that one function can do two different things, then you can make the constructor check for its own instanceof .

function Fubar (foo, bar) {
  "use strict"
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else return arguments[0] instanceof Fubar;
}
var snafu = new Fubar("Situation Normal", "All Fsked Up");
snafu
  //=>
    { _foo: 'Situation Normal',
      _bar: 'All Fsked Up' }
Fubar({})
  //=> false
Fubar(snafu)
  //=> true


This makes it possible to use the constructor as a filter.

var arrayOfSevereProblems = problems.filter(Fubar);


Solution number 3 - burn out with fire


If there is no urgent need for auto-inheritance, and the use of overloaded functions for some reason is not suitable, we may still need a way to avoid accidentally calling the constructor without using a keyword new. Let
"use strict"it help, but this is not a panacea. In this mode, an error will not be generated if you do not try to write the value to the global scope, and if we try to do something before writing the value mentioned, this will happen no matter what.

Maybe it's much better to take matters into your own hands? Oliver Scherrer offers the following solution:

function Fubar (foo, bar) {
  "use strict"
  if (!(this instanceof Fubar)) {
      throw new Error("Fubar needs to be called with the new keyword");
  }
  this._foo = foo;
  this._bar = bar;
}
Fubar("Situation Normal", "All Fsked Up");
  //=> Error: Fubar needs to be called with the new keyword


Easier and safer than just relying on "use strict". If you need to make a check on your own instanceof , you can wrap it in the constructor as a function method:

Fubar.is = function (obj) {
  return obj instanceof Fubar;
}
var arrayOfSevereProblems = problems.filter(Fubar.is);


Conclusion


Constructors called without a keyword newcan be a potential threat. There are three ways to avoid this: auto-inherit, using overloaded functions, and forcibly throwing an error in case of an incorrect call.

The original article of the author can be found here .

The article is accompanied by a discussion on the reddit .

Also popular now: