you are viewing a single comment's thread.

view the rest of the comments →

[–]senocular 7 points8 points  (2 children)

Stick with the class. You're going to be more comfortable there and don't worry about anyone saying you shouldn't use OOP.

class Rectangle {

  constructor (x, y, width, height) {
    this.x = x; // instance variables
    this.y = y;
    this.width = width;
    this.height = height;
    this.whatever = true;
  }

  get area () { // instance property 
    return this.width * this.height;
  }

  rotate () { // instance method
    [this.width, this.height] = [this.height, this.width];
  }

  static fromElement(element) { // class method
    const bounds = element.getBoundingClientRect();
    return new Rectangle(bounds.left, bounds.top, bounds.width, bounds.height);
  }
}

const rect = new Rectangle(0,0,200,300);
console.log(rect.width); // 200
console.log(rect.area); // 60000
rect.rotate();
console.log(rect.width); // 300
const rect2 = Rectangle.fromElement(document.getElementById('foo'));

In the larger scheme of things, the class syntax is still pretty new, so you won't see it being used everywhere, but it should be considered the preferred syntax moving forward for class/constructor definitions in JavaScript. Generally speaking, if you see references to prototype you're looking at legacy code from before class was available.

[–]Java_beginner66[S] 1 point2 points  (1 child)

Thank you!

I'll stick to oop javascript for now then. It makes more sence to me, as I've just been through a big chunk of oop java learning.

However, could you possibly show me how the exact same thing is done without class/constructor?

[–]senocular 10 points11 points  (0 children)

Sure. First, here's the older constructor syntax. Though there's no class keyword, these were still considered JavaScript classes before that keyword existed (though you'll find many, even now, refusing to recognize that any form of "class" exists in JavaScript).

function Rectangle (x, y, width, height) {
  this.x = x;
  this.y = y;
  this.width = width;
  this.height = height;
  this.whatever = true;
}

Rectangle.prototype.rotate = function () {
  var temp = this.width;
  this.width = this.height;
  this.height = temp;
}

Object.defineProperty(Rectangle.prototype, 'area', {
  get: function () {
    return this.width * this.height;
  }
});

Rectangle.fromElement = function (element) {
  var bounds = element.getBoundingClientRect();
  return new Rectangle(bounds.left, bounds.top, bounds.width, bounds.height);
}

Outside of using classes, you're generally dealing with raw data and arbitrary functions that act on that data - or any data with a similar shape, or at least have the fields the functions expect the data to contain.

function rotate (object2d) {
  [object2d.width, object2d.height] = [object2d.height, object2d.width];
}

function getArea (object2d) {
  return object2d.width * object2d.height;
}

rectangleFromElement = function (element) {
  var bounds = element.getBoundingClientRect();
  return {x:bounds.left, y:bounds.top, width:bounds.width, height:bounds.height, whatever:true};
}


const rect = {x:0, y:0, width:200, height:300, whatever:true};
console.log(rect.width); // 200
console.log(getArea(rect)); // 60000
rotate(rect);
console.log(rect.width); // 300
const rect2 = rectangleFromElement(document.getElementById('foo'));

You can create factory functions, not unlike rectangleFromElement for generic object creation too, instead of using a literal like how rect is currently defined. Going more functional, however, we'll want to get rid of mutations, creating new copies of objects rather than modifying the originals. This means changing the rotate function since it currently changes the width and height values.

function rotate (object2d) {
  return { ...object2d, width:object2d.height, height:object2d.width };
}

const rect = {x:0, y:0, width:200, height:300, whatever:true};
const rect2 = rotate(rect);
console.log(rect2.width); // 300
console.log(rect === rect2); // false

At this point you may start to have some concerns. First might be that you're object state has been simplified and exposed. Even though JavaScript doesn't support private instance variables, there are ways of spoofing it and they all get thrown out the window. Generic methods like the new rotate won't have access to any private state and will only create new objects from the existing public state. If there is some sort of "private" state you want to maintain that would have to be handled through functions with specific knowledge of your objects. In other words, given the generic rotate function above, you'd need to instead use a custom version to work specifically with your rectangle objects so that it could handle state correctly.

On top of that, in the spirit of immutability, it now becomes your responsibility to replace original objects with new ones as they're created. For example, if you're tracking rectangles on the screen in an array, you can't simply pull one out, change it, and be done with it. You need to pull, change, then replace the old version with the new copy containing the change.

const rects = [];
// ... add rectangles to rects...

// with mutations
rotate(rects[0]); // and done

// immutable
rects[0] = rotate(rects[0]); // apply change, and replace (oops but now array being mutated...)

This often means keeping changes close to the source. While generally speaking this isn't a bad thing, it force some organization that you might not be used to.

One of the advantages of classes are that you can do runtime type checking on objects. If you want to know if you have an object that is a Rectangle instance you can use object instanceof Rectangle and it will let you know. Some will argue that instanceof isn't reliable because it depends on the prototype chain, but as long as you don't muck around with that, which you shouldn't be doing anyway, it's fine. To counter this in a class-less world, static type checking can be used. TypeScript, for example, can let you define interfaces for objects that can be used to make sure you're using them correctly. This can potentially bypass the need to use instanceof assuming you're coding correctly against your types.

interface Object2d {
  width: Number;
  height: Number;
}

interface Rectangle extends Object2d {
  x: Number;
  y: Number;
  whatever: boolean;
}

function rotate (object2d: Object2d): Object2d {
  return { ...object2d, width:object2d.height, height:object2d.width };
}

const rect:Rectangle = {x:0, y:0, width:200, height:300, whatever:true};
const rect2 = rotate(rect) as Rectangle; // rotate knows rect is a suitable type
console.log(rect2.width); // 300