I've been working on end to end test in testcafe and in their documentation I found following solution for Page Model:
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
}
}
export default new Page();
I've been doing some research and I cannot get my head around why it is not resolved with an object literal:
export const Page = {
nameInput: Selector('#developer-name');
}
What are consequences of using each of them?
I've been working on end to end test in testcafe and in their documentation I found following solution for Page Model:
class Page {
constructor () {
this.nameInput = Selector('#developer-name');
}
}
export default new Page();
I've been doing some research and I cannot get my head around why it is not resolved with an object literal:
export const Page = {
nameInput: Selector('#developer-name');
}
What are consequences of using each of them?
Share Improve this question edited Sep 18, 2019 at 18:53 TylerH 21.1k79 gold badges79 silver badges114 bronze badges asked Sep 18, 2019 at 9:57 K. TysK. Tys 1791 silver badge9 bronze badges 3-
Since
Page
has no methods, a class doesn't make any sense. Even if Page did have methods, it would be easier to define them in the exported object literal than define a class, since the class is only referenced once – CertainPerformance Commented Sep 18, 2019 at 9:59 -
Well, you could do
variable instanceof Page
with the former but not the latter. However, given that you only need a single instance, I'm not sure how much use would that have. – VLAZ Commented Sep 18, 2019 at 10:03 -
The class will let you create multiple
Page
instances (although only one is created and exposed in this example), but the object literal only exposes a single instance. You can still have multiple instances with the object literal approach if you changePage
to a function that returns the object literal (like a DIY constructor):export const Page = () => ({nameInput: Selector('developer-name')});
– byxor Commented Sep 18, 2019 at 10:23
3 Answers
Reset to default 4The difference is significant but fundamentally both are JavaScript objects, albeit with different properties and values. Listing all differences on the level of the language would be a long story and you'd be wise to read and understand on JavaScript prototype-based inheritance, but the most important differences are:
Page
is a prototype object: whenever you create an object of classPage
withnew Page()
, the constructor function is called withthis
referring to the object being created, notPage
itself. Any property you access on the object is searched along the so-called "prototype chain", including the so-called prototype object. This prototype object can be accessed withPage.prototype
and in fact, all methods you define in the classPage
are also properties of this prototype object. Unlike own properties designed to refer to unique objects or primitives specific to an object, functions in JavaScript don't have to be bound to an object during object or function creation and can be shared between objects (belonging to the same class, for instance) but are called on the actual instance, not the prototype to which they may belong. In other words,this.nameInput
in your constructor actually adds a property namednameInput
to the object being created withnew Page()
, not the prototype, while the constructor itself (constructor
) and any non-static methods you might add toPage
will be added as properties ofPage.prototype
. The constructor is accessed asPage.prototype.constructor
, by the way, as you'd naturally expect. AndPage.prototype.constructor === Page
evaluates totrue
.An expression of the form like
{ nameInput: ... }
creates an object which prototype isObject.prototype
, in practice the most basic form of object (except for objects without a prototype altogether that can be created withObject.create(null)
) and thus no superclass or any traits beyond what the fundamental object prototype object could provide. Any properties any such{ ... }
object may seem to have through its prototype chain, including methods, are properties ofObject.prototype
. This is why you can do({}).toString()
or({}).hasOwnProperty("foobar")
without actually havingtoString
orhasOwnProperty
properties in your object --toString
andhasOwnProperty
are properties ofObject.prototype
referring to two distinct methods calledtoString
andhasOwnProperty
, respectively, and JavaScript creates a special property on your object called__proto__
referring toObject.prototype
. This is how it knows how to "walk the prototype chain". The names of functions themselves do not matter like that, by the way -- I may add a property on an object referring to an anonymous function:var foo = ({}); foo.bar = function() { };
and call said unnamed function withfoo.bar()
.
One mistake you appear to be making is confusing an object of a class with the class, otherwise you wouldn't pare export default class Page { ... }
to export const Page = { nameInput: Selector(...) }
-- the former creates a class accessible as Page
which is used as the prototype object whenever objects of the class are created, while the latter creates an object accessible as Page
which contains nameInput
referring to result of evaluating expression Selector("#developer-name")
(calling Selector
with the sole argument "#developer-name"
). Not the same thing at all, not to mention that former has Page
refer to a class (invariably a prototype in JavaScript), while latter has Page
refer to an object that does not seem to fit the pattern of a class.
The interesting things start when you realize that since a class is an object like any other in JavaScript, any object can be used as a class if you know how prototype-based inheritance works:
new (function() { this.nameInput = Selector("#developer-name"); })();
What happens here? You create a new object with an unnamed function as the object constructor. The effect is absolutely equivalent to otherwise creating the object with new Page
with Page
being your original ES6 class (ECMAScript 6 is the language specification that adds class
syntax to JavaScript).
You can also do this, again equivalent to if you defined Page
with class Page ...
:
function Page() {
this.nameInput = Selector("#developer-name");
}
var foo = new Page();
Page.prototype
will be the prototype object for foo
, accessible as foo.__proto__
and otherwise making it possible for you to call instance methods on foo
like foo.bar()
, provided you define bar
property on at least Page.prototype
:
function Page() {
this.nameInput = Selector("#developer-name");
}
Page.prototype.bar = function() {
console.log(this.nameInput);
}
var foo = new Page();
foo.bar();
In fact, the above is what browser would do internally if it had to interpret the following code:
class Page {
constructor() {
this.nameInput = Selector("#developer-name");
}
bar() {
console.log(this.nameInput);
}
}
It is beyond the scope of my answer to list differences between the two last approaches (isn't the same thing as the two approaches you proposed), but one difference is that with class Page ...
, Page
is not a property of window
in some user agents while with function Page ...
it is. It's partly historical reasons, but rest assured that so far defining constructors and prototype using either approach is pretty much the same, although I can imagine smarter JavaScript runtimes will be able to optimize the latter form better (because it's an atomic declaration, and not just a sequence of expressions and statements).
If you understand prototype-based inheritance at the heart of all of this, all your questions about this will fall away by themselves as very few fundamental mechanisms of JavaScript support 99% of its idiosyncrasies. You'll also be able to optimize your object design and access patterns, knowing when to choose ES6 classes, when not to, when using object literals ({ prop: value, ... }
) and when not to, and how to share fewer objects between properties.
Classes can be thought of as a blueprint, they both provide an object in the end. But as the object literals name implies, you literally create it there and then with this 'literal' syntax. A class however, we would use to instantiate new instances from 1 base blueprint.
let x = { myProp: undefined }
let y = { myProp: undefined }
x.myProp = "test";
y.myProp // undefined
Here we see we make two separate instances, but we will have to repeat code.
class X { }
let x = new X();
let y = new X();
A class does not need to repeat the code, as it is all encapsulated in the idea of what X should be, a blueprint.
Similar to above [in the literal] we have two separate instances but it's cleaner, more readable, and any change we wish to make to every instance of this 'X' object can now be changed simply in the class.
There's a plethora of other benefits and even a paradigm dedicated to Object-Oriented Programming, read here for more: https://www.internalpointers./post/object-literals-vs-constructors-javascript
To go further into the constructor question... In other languages we have fields. I believe when you assign a field in the constructor, it just creates an underthehood like field (I say underthehood like because JavaScript is prototype based, and the class syntax is syntactical sugar to help write prototypes easier for programmers familiar with class syntax in other languages).
Here is an example in C#.
public class X{
private int y;
X() {
this.y = 5;
}
}
It's more a convention to assign fields in the constructor in other languages, so I assume this has something to do with it in JavaScript.
Hope this helps.
By declaring it as a Class you can later identify what type of object it is with .constructor.name:
class Page {
constructor () {
this.nameInput = "something";
}
// No ma
anotherMethod() {
}
}
const pageClass = new Page();
const pageLiteral = {
nameInput: "something"
, // must have a ma
anotherMethod() {
}
}
console.log("Name of constructor for class: ", pageClass.constructor.name); // Page
console.log("Name of constructor for literal: ", pageLiteral.constructor.name); // Object