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.

Categories
JavaScript Best Practices

Better JavaScript — Prototypes and Functions

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.

Closures are Better than Strings for Encapsulating Code

We should never have code in strings.

If we need to run code, we should put them in a function.

For instance, if we have the repeat function, we should never write something like:

function repeat(n, action) {
  for (let i = 0; i < n; i++) {
    eval(action);
  }
}

eval is always bad because of security, performance, and scoping issues.

Instead, we should write:

function repeat(n, action) {
  for (let i = 0; i < n; i++) {
    action();
  }
}

Then we can use it by writing:

const start = [];
repeat(1000, () => {
  start.push(Date.now());
});

We passed in a callback so that we can run action in our function.

The code should never be in a string.

Instead, it should take a callback function and run it.

Don’t Rely on toString Method of Functions

The toString method doesn’t always return the source code of a function.

It does return the source code most of the time for functions we create.

So if we have:

(function(x) {
  return x + 2;
}).toString();

Then we get:

"function(x) {
  return x + 2;
}"

as a result.

If we use the toString method on built-in native objects, then we won’t see the function’s source code.

For instance, if we have:

Math.max.toString()

Then we get:

"function max() { [native code] }"

If we do something like calling bind to a function:

(function(x) {
  return x + 2;
}).bind(16).toString();

Then we get:

"function(){return n.apply(e,t||arguments)}"

We don’t get back anything meaningful in the string.

Therefore, we can’t rely on toString to always return the source code of a function.

Avoid Nonstandard Stack Inspection Properties

We should never use properties like arguments.callee to get the function that calls another function.

And we shouldn’t use caller property on the property to get the function that called the function even if the property exists.

These aren’t standard and they should be avoided.

So we should write code like:

function revealCaller() {
  return revealCaller.caller;
}

These are all disallowed in ES5 strict mode, so this is a benefit of enabling it.

If we have:

function foo() {
  "use strict";
  return foo.caller;
}

We get ‘Uncaught TypeError: ‘caller’, ‘callee’, and ‘arguments’ properties may not be accessed on strict mode functions or the arguments objects for calls to them’.

Get the Difference Between prototype, getPrototypeOf, and proto

These objects are separate from each other.

prototype and __proto__ all get the prototype of an object and set it.

And getPrototypeOf is a method to get the prototype of an object.

__proto__ has become a standard so we can use it like prototype .

prototype is usually used for instance methods.

For instance, we can create a constructor by writing:

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

Person.prototype.toString = function() {
  return `Person ${this.name}`;
};

toString is an instance method of the constructor.

Then we can write:

const person = new Person("james");

to create a new Person instance.

The __proto__ property can be used like a regular property, so we can write:

person.__proto__

to get the prototype of the object.

We can also assign a value of it and use it as a property of an object.

Object.getPrototypeOf can be used to get the prototype of an object, so we can write:

Object.getPrototypeOf(person)

to get the prototype.

A prototype is a JavaScript object that an object inherits from.

Conclusion

We should never run code in strings in our code.

Also, we shouldn’t use caller and callee in our code.

And we can get and set object prototypes in various ways.