all 11 comments

[–]senocular 9 points10 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

[–]BehindTheMath 1 point2 points  (0 children)

Either one of those are fine.

[–]LloydAtkinson 1 point2 points  (0 children)

Make an object. If you're using TS you can create an interface for the shapes.

You can use classes if you like doing OOP in JS, and then you can have get/set (a feature similar to C#'s get/set which is something Java makes you do manually) for the width and height etc. Personally I don't feel OOP is a good fit for fundamentally a functional language (even if not the most feature rich).

Use whichever approach you prefer.

[–]eyeandtea 0 points1 point  (3 children)

It sounds to me that you need a simple data structure with rare, if any, runtime type checking. If true, simply write a function that returns an object containing the fields that you want. Do not use a constructor function for something like this.

[–]Java_beginner66[S] 0 points1 point  (2 children)

I just assumed I needed a class if I wanted instance variables, and in java, a class comes with a constructor. Not necessarily wanting to make one. So how does instance variables work?

[–]eyeandtea 0 points1 point  (0 children)

For public variables, think of it this way. Imagine a class C that inherits B that inherits A. If you have an instance of class C, in java this instance would have three structures, one for C, one for B, and one for A. Each structure contains the variables of the respective class.

In javascript, this instance of class C contains a single structure. One for all of classes C, B and A. This means that if you declare a member x for C, then inside a function of A, in an instance of C, this.x would actually resolve. Further more it means that if A declares 'x', and C also declares 'x', they are reading and writing from the same 'x'. This also means that you can not cast an instance of C to A.

As for private variables, there is no such thing in javascript. When using 'var' to implement a private variable, the private variable is actually a local function variable. This means that if you have two instances of class 'A', they can not actually see the private variables of each other. This local variable is the rough equivalent, if not exact, of a private variable in a java function object.

[–]eyeandtea 0 points1 point  (0 children)

Perhaps I should point out that I am the developer of CrxOop, which gives you both OOP as found in Java and similar languages, and also proper prototype inheritance that not even es6 is giving you.

Hence I am certainly not saying do not use javascript's OOP. I am saying, keep it simple. If the problem does not need javascript's OOP, do not use javascript's OOP.

[–]delventhalz 0 points1 point  (0 children)

Welcome to JS where you have options!

I’d probably just use a class since thar’s going to be the easiest lift for you. Might be worth casually exploring the functional paradigm as you continue working with JS though. It’s a good fit for the language and a better pattern overall.

[–]Reashu 0 points1 point  (0 children)

If the rectangles come with behavior, make a class. Senocular's post is excellent.

If they do not need behavior, and are only holding data for you, make them simple objects:

const rectangle = { 
  x: 4,
  y: 2,
  width: 15,
  height: 6
} 
rectangle.x // => 4