📜 ⬆️ ⬇️

[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 following constructor function:

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


When we call a constructor function using the new keyword, we get a new object, and the context of its constructor is set on the object itself. If we explicitly do not 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 is the contents of the prototype property of the constructor, so you can write as follows:
')
 Fubar.prototype.concatenated = function () { return this._foo + " " + this._bar; } snafu.concatenated() //=> 'Situation Normal All Fsked Up' 


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

 snafu instanceof Fubar //=> true 


(It is possible to make instanceof “wrong” possible in cases with more advanced idioms, or if you are a harmful troll collecting exception programming language and enjoying it, torturing job seekers with them. However, for our purposes, instanceof works quite well.)

Problem


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

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


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

 _foo //=> 'Fsked Up' 


JavaScript sets the context to the global scope to perform a normal function, so we just got there to trash. Well, it 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 the code and in the books, in production it 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 the new keyword are a potential problem.

So what can we do about it?

Solution # 1 - auto-inheritance


David Herman explains auto-inheritance in his book Effective JavaScript . When we call the constructor with new , the pseudo-variable this points to a new instance of our so-called “class”. This can be used to determine whether the constructor was called with the code word 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 that 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 


logsArguments decorates the 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 snafu is an instance of 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, snafu is an instance of Fubar , not LoggingFubar . It is impossible to say for sure whether this is what we wanted. This method cannot be called a more than useful abstraction, not devoid of leaks, just as one cannot say that it “just works”, although thanks to it some things become possible which are much more difficult to implement with other approaches.

Solution # 2 - use overloaded function


A function that checks whether 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 perform a check on 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); 


Decision number 3 - burn out fire


If there is no urgent need for auto-inheritance, and the use of overloaded functions for some reason does not fit, we may still need a way to avoid accidentally invoking the constructor without using the new keyword. Let him
"use strict" helps, 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 same value, it will happen no matter what.

Could it be much better to take matters into your own hands? Oliver Scherrer offers this 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 a constructor as a function method:

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


Conclusion


Constructors called without the new keyword can pose a potential threat. This can be avoided in three ways: by auto-inheritance, 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 attached to the discussion on reddit .

Source: https://habr.com/ru/post/237959/


All Articles