admin管理员组

文章数量:1415061

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 change Page 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
Add a ment  | 

3 Answers 3

Reset to default 4

The 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 class Page with new Page(), the constructor function is called with this referring to the object being created, not Page 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 with Page.prototype and in fact, all methods you define in the class Page 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 named nameInput to the object being created with new Page(), not the prototype, while the constructor itself (constructor) and any non-static methods you might add to Page will be added as properties of Page.prototype. The constructor is accessed as Page.prototype.constructor, by the way, as you'd naturally expect. And Page.prototype.constructor === Page evaluates to true.

  • An expression of the form like { nameInput: ... } creates an object which prototype is Object.prototype, in practice the most basic form of object (except for objects without a prototype altogether that can be created with Object.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 of Object.prototype. This is why you can do ({}).toString() or ({}).hasOwnProperty("foobar") without actually having toString or hasOwnProperty properties in your object -- toString and hasOwnProperty are properties of Object.prototype referring to two distinct methods called toString and hasOwnProperty, respectively, and JavaScript creates a special property on your object called __proto__ referring to Object.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 with foo.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

本文标签: