Categories
Modern JavaScript

Best of Modern JavaScript — let and const

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at JavaScript variables.

const Creates Immutable Variables

We can create immutable variables with const .

For example, if we have:

`const` `foo` `=` `'abc';`
`foo` `=` `'baz';`

then we have a TypeError.

const does not Make the Value Immutable

Even though the variable can’t be reassigned, its value can still change.

For instance, if we have an object:

`const` `obj` `=` `{};`

We can add properties to it:

`const` `obj` `=` `{};`
`obj.prop` `=` 'foo'`;`
`console.log(obj.prop);`

obj.prop would be 'foo' .

If we want to make objects immutable, we can call the Object.freeze method:

const obj = Object.freeze({});

Object.freeze only prevents the top level from changing.

Objects stored in its properties are still mutable.

If we have:

`const` `obj` `=` `Object.freeze({` `foo:` `{}` `});`

Then we can write:

obj.foo.qux = 'baz';

and it would work.

const in Loop Bodies

We can use const in loop bodies.

For instance, we can write:

function logArgs(...args) {
  for (const [index, elem] of args.entries()) {
    console.log(index, elem);
  }
}

We call the entries method which returns entries with the index with the index of the entry and elem with the entry itself.

const prevents assignment of the array.

Lifecycle of var-Declared Variables

var variables don’t have temporal dead zones.

This means they’re available everywhere within their scope.

The declaration of it is hoisted, but the value isn’t.

Lifecycle of let-Declared Variables

let variables are only available after they’re declared.

This means within the block, the temporal dead zone is between the start of the block and when they’re declared.

This is the same with const .

If we try to access these variables before they’re declared, we’ll get a ReferenceError .

If there’s isn’t a value assigned to a let variable, it’ll be undefined .

For example, if we have:

let foo = true;
if (true) {
  console.log(foo);

  let foo;
  console.log(foo);

  foo = 123;
  console.log(foo);
}
console.log(foo)

Then the console.log(foo); will get us a ReferenceError .

And:

let foo;
console.log(foo);

will log undefined .

and:

foo = 123;
console.log(foo);

logs 123.

And foo is true .

typeof Throws a ReferenceError for a Variable in the Temporal Deal Zone

We can’t use typeof with let and const variables that haven’t been declared yet.

For example, we can write:

if (true) {
  console.log(typeof foo);
  let foo;
}

Then we’ll get a ReferenceError if we run the code.

If we want to stop that, we should move the typeof below the let declaration.

Having a temporal dead zone lets us catch programming errors.

JavaScript may also have guards in the future to do data type and content checks.

So we should make sure that data is available before doing the checks.

If they’re only available after they’re declared, then the checks can easily be done.

let and const in Loop Heads

We can use let and const in loop heads with the for, for-in, and for-of loops.

Conclusion

let and const provides many benefits that aren’t provided by var .

Categories
Modern JavaScript

Best of Modern JavaScript — Function Names

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at function names in JavaScript.

Methods in Class Definitions

We can define methods in class definitions.

For instance, we can write:

class C {
  foo() {}

  ['ba' + 'r']() {}

  static baz() {}
}

We can define class methods with computed property names.

Then we can get their name property values by writing:

console.log(C.prototype.foo.name);
console.log(C.prototype.bar.name);
console.log(C.baz.name);

As we can see, class instance methods are just methods of C ‘s prototype .

The class syntax is just syntactic sugar on top of the regular prototypical inheritance model.

Static methods are methods attached to the property.

Getters and setters also have names.

For instance, we can write:

class C {
  get foo() {}
  set bar(value) {}
}

Then we get the names of these 2 methods with:

const getter = Object.getOwnPropertyDescriptor(C.prototype, 'foo').get;
console.log(getter.name);

const setter = Object.getOwnPropertyDescriptor(C.prototype, 'bar').set;
console.log(setter.name);

We use the Object.getOwnPropertyDescriptor method to get the property descriptor of our class methods.

And then we can get the getter with the get property and the setter with the set property.

They all have the name property to get the name.

Then the 2 console logs get us:

get foo
set bar

Methods with Symbol Keys

We can create methods with symbol keys.

For example, we can write:

const foo = Symbol('foo');
const bar = Symbol();

let obj = {
  [foo]() {},
  [bar]() {},
};
console.log(obj[foo].name);
console.log(obj[bar].name);

They also have the name property.

The first console log logs ‘[foo]’ .

And the 2nd console log logs an empty string.

Class Definitions

Class definitions create functions, so they also has a name property.

For instance, we can write:

class Foo {}
console.log(Foo.name);

Then Foo.name is 'Foo' .

We can also define it by writing:

const Baz = class {};
console.log(Baz.name);

Then Baz.name is 'Baz' .

Default Exports

Default exports of functions have names set to default .

This includes the following:

`export` `default` `function` `()` `{}`
`export` `default` `(function` `()` `{});`
`export` `default` `class` `{}`
`export` `default` `(class` `{});`
`export` `default` `()` `=>` `{};`

Other Kinds of Functions

new Function() ‘s name property is 'anonymous' .

This is a bug.

func.bind() creates a function with a name value that starts with 'bound' .

For example, we can write:

function bar(x) {
  return x
}
const bound = bar.bind(undefined, 123);
console.log(bound.name);

Then bound.name is 'bound bar' .

Generator functions get the name the same way normal functions do.

Function Names Assignment

Function names are always assigned during creating and never changed later.

For instance, if we have:

function createFunction() {
  return function() {};
}
const bar = createFunction();
console.log(bar.name);

Then bar.name isn’t 'bar' , it’s an empty string.

The name property doesn’t update after it’s created.

Since it’s anonymous at the beginning, it stays anonymous.

Changing Names of Functions

We can’t change the name value of functions.

So we can’t write:

func.name = 'foo';
func.name

to change a function’s name to 'foo' .

However, we can change the name by redefining it.

For example, we can write:

function bar() {}

Object.defineProperty(bar, 'name', {
  value: 'foo',
  configurable: true
});

Then we get 'foo' logged if we log bar.name ‘s value.

Conclusion

We can get and set the name of a function in various ways.

Categories
Modern JavaScript

Best of Modern JavaScript — Function Names and Arrow Functions

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at function names and arrow functions in JavaScript.

Determine Whether a Function was Invoked via new

We can find out whether a function was invoked with new by using the new.target property.

For instance, we can write the following to let us stop the function from being called with new :

function foo() {
  if (new.target !== undefined) {
    throw new Error('cannot be called as constructor');
  }
  //...
}

If we call it with new foo() , we get the error ‘Uncaught Error: cannot be called as constructor’.

With ES5, we can check for the value of this :

function foo() {
  "use strict";
  if (this !== undefined) {
    throw new Error('cannot be called as constructor');
  }
  //...
}

If this is undefined in strict mode, then we know it’s not called with new .

Arrow Functions

Arrow functions are new kinds of functions introduced with ES6.

It solves the problem of bindings with this , arguments and other built-in objects.

Also, they can’t be used as constructors and they’re shorter.

This means they’re great for callbacks.

Traditional Functions are Bad Non-Method Functions

Traditional functions are bad non-method functions since they bind to their own value of this inside the function.

Therefore, the following example won’t work:

function Suffix(suffix) {
  this.suffix = suffix;
}

Suffix.prototype.addSuffix = function(arr) {
  'use strict';
  return arr.map(function(x) {
    return x + this.suffix;
  });
};

The callback is defined with a traditional function, so it has its own value of this .

Therefore, there’s no suffix property in the callback.

To fix this, we can assign the this outside to another value.

For instance, we can write:

function Suffix(suffix) {
  this.suffix = suffix;
}

Suffix.prototype.addSuffix = function(arr) {
  'use strict';
  var that = this;
  return arr.map(function(x) {
    return x + that.suffix;
  });
};

We assigned this outside the callback to that , so that we can use it in the callback.

We can also specify the value of this with the map method.

The value is passed into the 2nd argument:

function Suffix(suffix) {
  this.suffix = suffix;
}

Suffix.prototype.addSuffix = function(arr) {
  'use strict';
  return arr.map(function(x) {
    return x + this.suffix;
  }, this);
};

We pass this into the 2nd argument to set this in the callback to the Suffix constructor.

And we can also use bind(this) to return a new function with the this value we want:

function Suffix(suffix) {
  this.suffix = suffix;
}

Suffix.prototype.addSuffix = function(arr) {
  'use strict';
  return arr.map(function(x) {
    return x + this.suffix;
  }.bind(this));
};

With ES6, we don’t need to do any of this.

We just use arrow functions:

function Suffix(suffix) {
  this.suffix = suffix;
}

Suffix.prototype.addSuffix = function(arr) {
  return arr.map((x) => {
    return x + this.suffix;
  });
};

We replace our function with an arrow function so won’t have to worry about the value of this in the callback.

We can replace the constructor with the class syntax:

class Suffix {
  constructor(suffix) {
    this.suffix = suffix;
  }

  addSuffix(arr) {
    return arr.map((x) => {
      return x + this.suffix;
    });
  }
}

We put everything inside the class.

Conclusion

We can determine whether a function is called with new in various ways.

Also, arrow functions don’t bind to any special values like this inside it, so we can use them for callbacks easily.

Categories
Modern JavaScript

Best of Modern JavaScript — Default Parameters and Rest

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at working with parameters and the rest syntax.

Why does undefined trigger default values?

undefined indicates that something doesn’t exist.

This is different from null in that it indicates an empty value.

Therefore, only undefined will trigger default values to be set.

Referring to Other Parameters in Default Values

We can refer to other parameters in default values.

For example, if we have:

function foo(x = 13, y = x) {
  console.log(x, y);
}

If we call foo(); , then we get that x and y are both 13.

If we call foo(7); , then we get that x and y are both 7.

And if we call foo(17, 12); , then we get that x is 17 and y is 12.

Referring to Inner Variables in Default Values

If we have code like:

const x = 'outer';

function foo(a = x) {
  const x = 'inner';
  console.log(a);
}

foo()

where we assign the outer variable as a value of a parameter, then even if we define a variable with the same name inside it, it’ll refer to the outer one.

We assigned the default value of a to x , so even if we defined x again with a new value, we still get a is 'outer' .

If there’s no x above the function, we’ll get a ReferenceError.

This also applies to parameters when a parameter is a function.

For example, if we have:

const bar = 2;

function foo(callback = () => bar) {
  const bar = 3;
  callback();
}

foo();

The callback is assigned to the function that returns bar by default, so that’s what will be called if we call callback with no callback passed into it.

So callback returns 2.

Rest Parameters

Rest parameters let us capture arguments that aren’t set to any parameter.

For instance, if we have:

function foo(x, ...args) {
  console.log(x, args);
  //···
}
foo('a', 'b', 'c');

Then x is 'a' and args is ['b', 'c'] .

If there’re no remaining parameters, then args will be set to an empty array.

This is a great replacement to the arguments object.

With ES5 or earlier, the only way to get all the arguments of a function is with the arguments object:

function logArgs() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(arguments[i]);
  }
}

With ES6 or later, we can just use rest parameters to do the same thing.

For instance, we can write:

function logArgs(...args) {
  for (const arg of args) {
    console.log(arg);
  }
}

We log all the arguments with the for-of loop.

Combining Destructuring and Access to the Destructured Value

Since the rest operator gives us an array, we can destructure it.

For example, we can write:

function foo(...args) {
  let [x = 0, y = 0] = args;
  console.log(x, y);
}

We set the first 2 entries of args to x and y respectively.

And we set their default values to 0.

The destructuring also works for object parameters.

For example, we can write:

function bar(options = {}) {
  const {
    x,
    y
  } = options;
  console.log(x, y);
}

We have the bar function that has the options parameter.

We destructured the object to the x and y variables.

arguments is an iterable object.

We can use the spread operator with ES6 to convert it to an array.

For example, we can write:

function foo() {
  console.log([...arguments]);
}

Then the array will have all the arguments.

Conclusion

We can use rest parameters to get all arguments passed into a function.

And we can refer to other values as default values.

Categories
Modern JavaScript

Best of Modern JavaScript — Default Parameters

Since 2015, JavaScript has improved immensely.

It’s much more pleasant to use it now than ever.

In this article, we’ll look at working with parameters.

Default Parameter Values

We can set default parameter values in JavaScript functions since ES6.

If the value of the parameter is undefined , then the default value will be set.

For instance, if we have a function:

function foo(x, y = 0) {
  return [x, y];
}

Then if we call it by writing foo(1, 3); , then we get [1, 3] .

If we call it by writing foo(1); , then we get [1, 0] .

And if we call it by writing foo(); , then we get [undefined, 0] .

Rest Parameters

Rest parameters let us get the remaining parameters as an array.

For instance,e if we have:

function bar(pattern, ...args) {
  return {
    pattern,
    args
  };
}

Then if we call it by writing format(3, 2, 1); , we get:

{ pattern: 3, args: [2, 1] }

Destructuring Parameters

We can destructure object parameters into variables.

For instance, we can write:

function baz({
  a = 0,
  b = -1,
  c = 1
} = {}) {
  console.log(a, b, c);
  //...
}

Our baz function takes an object with the properties a , b , and c .

We set the default values for them to 0, -1, and 1 respectively.

If the argument isn’t passed in, then we set it to an object, which will then set those default values.

So if we write:

baz({
  a: 10,
  b: 30,
  c: 2
});

then a is 10, b is 30, and c is 2.

If we write:

baz({
  a: 3
});

then a is 3, b is -1 and c is 1.

And if we write:

baz({});

or

baz();

then a is 0, b is -1, and c is 1.

Whenever any value is absent, it’ll be replaced with the default value.

Spread Operator

The spread operator lets us spread iterable objects into arguments for function calls.

For instance:

Math.max(2, 3, 4, 6)

is the same as:

Math.max(...[2, 3, 4, 6])

We can also spread some arguments:

Math.max(2, ...[3, 4], 6)

In arrays, we can use the spread operator to turn iterable values in array elements.

For instance, we can write:

`[1,` `...[2, 3],` `4]`

and get:

`[1,` `2, 3,` `4]`

Parameter Default Values

We can specify default values for parameters.

For instance, we can write:

function foo(x, y = 0) {
  return [x, y];
}

Then if we call it without 2nd argument, then the default value will be set as the value of y .

If we call foo(1) , then we get [1, 0] .

And if we call foo() , we get [undefined, 0] .

The default value is computed only when it’s needed

We can see this with this example:

const log = console.log;

function foo(x = log('x'), y = log('y')) {
  return [x, y];
}

If we call foo(1) , then we get 'x' .

And if we call foo() , then we get 'x' and 'y' logged.

Conclusion

We can add default parameters values to functions so that they’re assigned as the value of them when they’re undefined.