Categories
JavaScript Best Practices

Better JavaScript — Arrays and Properties

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

Calling hasOwnProperty Safely

The hasOwnProperty is part of Object.prototype , which means that it can be overridden or removed.

So to make sure that it’s always available, we can call it directly from the Object.prototype .

We can write:

const hasOwnProperty = Object.prototype.hasOwnProperty;

or:

const hasOwnProperty = {}.hasOwnProperty;

Then we can call it by writing:

hasOwnProperty.call(dict, 'james');

given that we have:

const dict = {
  james: 33,
  bob: 22,
  mary: 41
};

We can abstract the logic for checking entries our of the object.

For instance, we can create a class with the items.

We can write:

class Dict {
  constructor(elements) {
    this.elements = elements;
  }

  has(key) {
    return {}.hasOwnProperty.call(this.elements, key);
  }

  set(key, val) {
    this.elements[key] = val;
  }

  get(key) {
    return this.elements[key];
  }

  remove(key) {
    delete this.elements[key];
  }
}

We created a Dict class to hold an elements instance variable that we can manipulate with methods for doing common dictionary operations.

has checks if the property exists.

set sets the key with the value.

get gets the value with the given key.

remove removes an entry from the object.

Now we can use it by writing:

const dict = new Dict({
  james: 33,
  bob: 22,
  mary: 41
});

Use Arrays for Ordered Collections

Arrays should be used for ordered collections.

Ordered collections have an index and they are iterated through with the given order.

We can create an array by writing:

const arr = [1, 2, 3];

Then we can use it by writing:

for (const a of arr) {
  console.log(a);
}

We looped through the arr array with the for-of loop.

It always iterates from the start to the end, so the order is predictable.

The for-of loop shouldn’t be mistaken for the for-in loop which loops through items in an unpredictable order.

Never Add Enumerable Properties to Object.prototype

We shouldn’t add enumerable properties to Object.prototype .

Object.prototype is a property that we don’t own so we shouldn’t change it since it’ll give results that most people don’t expect.

Also, the for-in loop will pick up the enumerable property of a prototype so it’ll be iterated through by it.

We don’t want that to happen.

If we want to add a property to Object.prototype , then it should be made non-enumerable.

For instance, we can write:

Object.defineProperty(Object.prototype, "allKeys", {
  value() {
    const result = [];
    for (const key in this) {
      result.push(key);
    }
    return result;
  },
  writable: true,
  enumerable: false,
  configurable: true
});

We set enumerable to false so that it won’t be picked up by the for-in loop.

Conclusion

We shouldn’t add enumerable properties to Object.prototype .

Also, to call hasOwnProperty safely, we shouldn’t call it directly from the object itself since it can be modified.

Arrays are good for ordered collections.

Categories
JavaScript Best Practices

Better JavaScript — Arguments and Parameters

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

Optional Arguments

Arguments of a function can be anything in JavaScript.

We can have optional arguments.

If they aren’t passed into a function, then they’re undefined .

We can provide a default value for parameters that may be undefined .

For example, we can write:

function f(x = 1) {  
  return x;  
}

Then x ‘s default value 1.

If no value is passed in, then it’s 1.

Accept Option Objects for Keyword Arguments

If we have a function that has many parameters, then it’s hard to remember what the parameters are and their type.

Therefore, the more arguments that it takes, the harder it is to work with the function.

If we have lots of parameters, then we should combine them into one object parameter.

Then we can destructure the object parameter into variables.

For instance, if we have:

const alert = new Alert(100, 75, 300, 200,  
  "Error", message,  
  "green", "white", "black",  
  "error", true);

Then that’s hard to remember.

Instead, we should put them into one object:

const alert = new Alert({  
  x: 100,  
  y: 75,  
  width: 300,  
  height: 200,  
  title: "Error",  
  message: message,  
  titleColor: "green",  
  bgColor: "white",  
  textColor: "black",  
  icon: "error",  
  modal: true  
});

Since we have the property keys, it’s easy to know what the parameters mean.

Then in the alert constructor, we can destructure the arguments:

function Alert({  
  x,  
  y,  
  width,  
  height,  
  title,  
  message,  
  titleColor,  
  bgColor,  
  textColor,  
  icon,  
  modal  
}) {  
  //...  
}

We get the same benefits of the parameters and get the property values as variables with destructuring.

The meaning of each property is clear with an object parameter.

And we don’t have to worry about the order.

Option objects consist of optional arguments, so we can omit everything in the object.

But if we need want something to be required, we can separate them out from the object parameter.

So we can write:

function Alert(  
  title,  
  message, {  
    x,  
    y,  
    width,  
    height,  
    titleColor,  
    bgColor,  
    textColor,  
    icon,  
    modal  
  }) {  
  //...  
}

If we want to provide default values for them, we can do that easily by assigning default values to the properties.

For instance, we can write:

function Alert({  
  x = 100,  
  y = 100,  
  width = 300,  
  height = 300,  
  title,  
  message,  
  titleColor,  
  bgColor,  
  textColor,  
  icon,  
  modal  
}) {  
  //...  
}

to set the default value of the parameters.

x , y , width , and height have default values.

We don’t have to check for undefined or use the || operator to provide default values.

Conclusion

If we have lots of parameters in our function, then we should use an object parameter instead.

We can assign values to them and destructure them into variables.

Optional arguments can be assigned to parameters.

Categories
JavaScript Best Practices

Better JavaScript — Things that Shouldn’t be Automatic

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

Problems with Coercions

Data coercions don’t change the data to the format we expect.

For instance, if we have:

const date = new Date("2020/12/31");
date == "2020/12/31"

Then the comparison would be false .

This is because date converted to a string it:

"Thu Dec 31 2020 00:00:00 GMT-0800 (Pacific Standard Time)"

The toString method converts to the format it wants.

To convert a date to YYYY/MM/DD format, we can write:

function toYMD(date) {
  const y = date.getFullYear(),
    m = date.getMonth() + 1,
    d = date.getDate();
    return `${y}/${(m < 10 ? "0" + m : m)}/${(d < 10 ? "0" + d : d)}`;
}

We get the year, month, and date.

Then we put them all in a string.

If the month or date has one digit, then we put a 0 before it.

Now we can compare them both as strings in the same format:

toYMD(date) === "2020/12/31";

Semicolon Insertion

JavaScript lets us drop the semicolon.

However, it just inserts it for us automatically.

This is called automatic semicolon insertion.

The JavaScript engine infers the semicolon into our program automatically.

The semicolon insertion mechanism is in the ES standard.

This means the mechanism is portable between JavaScript engines.

Semicolon insertion has its pitfalls.

We can’t avoid learning its rules if we skip the semicolon.

Semicolons are existing before a } character, after one or new lines, or at the end of program input.

We can only leave our semucolons at the end of a line, block or program.

So we can write:

function Point(x, y) {
  this.x = x || 0
  this.y = y || 0
}

But if we have:

function area(r) { r = +r return Math.PI * r ** 2 }

then we get an error.

Also, when the next input token can’t be parsed a semicolon is inserted.

So:

a = b
(foo());

is the same as:

a = b(foo());

But:

a = b
foo();

are parsed as 2 statements since:

a = b foo();

isn’t a valid JavaScript code.

We always have to pay attention to the start of the next statement to detect whether we can omit a semicolon.

We can’t leave off a semicolon if the next line’s first token can be interpreted as the continuation of the statement.

If we have:

a = b
["foo", "bar", "baz"].forEach((key) => {
  //...
});

Then it’s interpreted as:

a = b["foo", "bar", "baz"].forEach((key) => {
  //...
});

The bracket expression doesn’t make sense since we have multiple items inside it.

If we have:

a = b
/foo/i.test(str) && bar();

Then both lines are parsed as a single statement.

The first / is treated as the division operator.

Another problem is when we concatenate files without semicolons.

If we have:

file1.js

(function() {
  // ...
})()

file2.js

(function() {
  // ...
})()

then we may run into problems.

Both files contents may be treated as one statement when concatenated:

(function() {
  // ...
})()(function() {
  // ...
})()

We can concatenate the files after each file:

(function() {
  // ...
})();
(function() {
  // ...
})();

so we get won’t have to worry about the semicolons when concatenating.

Conclusion

There’re many issues with omitting semicolons.

We should just put them in ourselves rather than letting the JavaScript engine do it for us in the wrong place.

Likewise, we should avoid automatic coercions.

Categories
JavaScript Best Practices

Better JavaScript — Standard Library

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

Inheriting From Standard Classes

With the class syntax, we can create subclasses of standard classes easily.

For instance, we can create a subclass of the Array constructor by writing:

class MyArray extends Array {}

const myArray = new MyArray(1, 2, 3);

Then myArray.length is 3.

Since the Array constructor is called when we call the MyArray constructor, then length property is set properly.

The [[Class]] value of MyArray is set to Array so the length is set properly.

If we get the prototype of myArray :

Object.prototype.toString.call(myArray)

We get:

[object Array]

The following are the values of [[Class]] for various built-in constructors:

  • Array — [[Class]] is 'Array'
  • Boolean[[Class]] is 'Boolean'
  • Date[[Class]] is 'Date'
  • Error[[Class]] is 'Error'
  • Function[[Class]] is 'Function'
  • JSON[[Class]] is 'JSON'
  • Math[[Class]] is 'Math'
  • Number[[Class]] is 'Number'
  • Object[[Class]] is 'Object'
  • RegExp[[Class]] is 'RegExp'
  • String[[Class]] is 'String'

This type of inheritance can only be done with the class syntax.

The old constructor syntax wouldn’t set the [[Class]] property properly.

Prototypes are an Implementation Detail

Prototypes have the properties that an object inherit from.

Objects are the interfaces.

We shouldn’t inspect the prototype structure of objects that we don’t control.

And we shouldn’t inspect properties that implement the internals of objects we don’t control.

If it’s something that we don’t control, then we can’t change them.

So we’ve to deal with them with our own code that we control.

No Reckless Monkey-Patching

Monkey-patching is changing the code on the fly.

JavaScript doesn’t prevent us from changing the properties of any object, including built-in objects.

For instance, we can add an instance method to the Array constructor by writing:

Array.prototype.split = function(i) {
  return [this.slice(0, i), this.slice(i)];
};

But other people may do the same thing.

So we may get properties and methods that we don’t expect in built-in objects.

And they can be removed and break a lot of code that uses it.

So we’ll run into problems if we add things to built-in objects that we don’t expect.

The better way to add our own code is to keep them separate from built-in objects.

For instance, we can write:

function split(arr, i) {
  return [arr.slice(0, i), arr.slice(i)];
};

to create our own split function.

However, one acceptable thing we can add that changes existing built-in objects is polyfills.

They are libraries that add functionality that should be supported in JavaScript engines in the future.

Their behavior is standardized so that we can use them to add methods in a safe way.

Conclusion

With the class syntax, we can inherit from standard classes.

Also, we shouldn’t inspect code that we don’t control since we can’t change them.

And we shouldn’t monkey-patch native built-in objects with our own code.

Categories
JavaScript Best Practices

Better JavaScript — Prototypes

Like any kind of apps, JavaScript apps also have to be written well.

Otherwise, we run into all kinds of issues later on.

In this article, we’ll look at ways to improve our JavaScript code.

Object.getPrototypeOf or proto

Since ES6, __proto__ has become a standard property of an object.

We can get and set it like any other property.

For instance, we can write:

const obj = {};

console.log(obj.__proto__);

to get the prototype of obj .

We get the object’s prototype with the __proto__ property.

We can set it by writing:

const obj = {};
obj.__proto__ = {
  foo: 1
};

console.log(obj.__proto__);

and we get {foo: 1} as the prototype.

If we want to create an object with a prototype, we can also call Object.create with a prototype.

So we can write:

const obj = Object.create({
  foo: 1
})

and we get the same result.

We cal also use the Object.getPrototypeOf method to get the prototype of an object.

For instance, we can write:

Object.getPrototypeOf(obj)

and we get the same result as getting the __proto__ property.

The __proto__ property is standard, so we can safely use it to get and set the prototype.

Make Constructors new-Agnostic

If we’re creating a constructor function, then we may be able to call it as a function.

For instance, if we have:

function Person(name) {
  this.name = name;
}

const p = Person('james');
console.log(this.name)

Then this is the global object and the name property has value 'james' .

This is definitely not what we want.

If we have strict mode, then this would be undefined at the top level, so we can’t create global variables accidentally.

If we have:

function Person(name) {
  "use strict";
  this.name = name;
}

const p = Person('james');

Then we get the error ‘Uncaught TypeError: Cannot set property ‘name’ of undefined’.

This provides us with some protection from call constructors as a regular function.

To make the check more robust, we can add an instance check into the constructor:

function Person(name) {
  if (!(this instanceof Person)) {
    return new Person(name);
  }
  this.name = name;
}

This way, with or without new , we still get the Person instance returned.

The best way is to use the class syntax.

So we can rewrite the constructor to:

class Person {
  constructor(name) {
    this.name = name;
  }
}

This way, if we call Person without new , we get the error ‘Uncaught TypeError: Class constructor Person cannot be invoked without ‘new’’.

Conclusion

There’re various ways to get and set the prototype.

Also, the class syntax is the most robust way to create a constructor.