Why study ES6

ES6 is a version of JavaScript released in 2015, called ECMAScript6. This version is a huge change in every aspect of the JavaScript language. As we all know, JavaScript has suffered numerous criticisms since its birth. At the beginning of the language’s design, the designers merely positioned it as a simple scripting language, without introducing very advanced design principles. But what didn’t happen was that the advent of the browser, and then NodeJS, pushed JavaScript forward. As the language is used in a wide range of contexts and with a wide range of users, it is natural that potential problems with the language surface one by one. ES6 is designed to address these issues and keep it backward compatible.

Learning ES6 is actually a great way to understand JavaScript. Know what problems the language had before, and why are they called problems? And how ES6 solves these problems.

In addition, I mainly read Understanding ECMAScript 6 for the knowledge of ES6. The author’s description is clear and concise, and his examples are easy to understand. This article is mainly excerpted from the book, which I think is more important.


Block binding

Before ES6, variables in JS had only two scopes, global and function. Before we talk about scope, we need to know that JS actually has a precompile-like operation before it runs. This operation will lift all declarations of variables or functions. Note that this is only the declaration of variables, not the value binding of variables, as shown in the following code:

function getValue(condition) {
    if (condition) {
        var value = "blue";
        // other code
        return value;
    } else {
        // value exists here with a value of undefined
        return null;
    }
    // value exists here with a value of undefined
}
Copy the code

If you don’t know JS, you might simply assume that the value declaration only happens when condition is true, but since condition is declared above, the above code is actually equivalent to the following:

function getValue(condition) {
    var value;
    if (condition) { 
        value = "blue";
        // other code
        return value;
    } else {
        return null; }}Copy the code

Based on this, there were several problems with variable declarations and value bindings prior to ES6:

  • Without block scope, confusion of variable names can easily cause problems.
  • Because variables are pulled up, they are allowed to be used before they appear. This can be confusing to users

To address both of these issues, ES6 introduced Block Scopes, which can also be called lexical Scopes. We also need to use let and const in order to be backward compatible with older versions and declare lexical scoped variables. The use of these two keywords is not too much to say, their emergence also solves the two problems mentioned above.

TDZ (temporal dead zone). Variables declared with let and const are in scope, but the area before the declaration is called TDZ. Accessing a variable in this area will result in an access error (this does not mean that the variable has not been declared, Instead, variables in the lexical environment are not accessible in TDZ.

Based on what has been discussed above, the recommended practice after ES6 is to generally declare a variable or function with const and use let if the variable is to change. Doing so can avoid some potential problems.


string

Character access

Before ES6, the JS string would default to a sequence of 16 bits, with each character represented by 16 bits (code unit). And all the built-in methods for strings are based on this. But the advent of Unicode makes this a problem, because Unicode representations can exceed 16 bits. Take this example:

let text = "𠮷";
console.log(text.length);  / / 2
console.log(text.charCodeAt(0));  / / 55362
console.log(text.charCodeAt(1));  / / 57271
Copy the code

The single character here is actually two characters in JS’s view, because the character is represented by two 16-bit units.

ES6 provides several methods for accessing characters in strings, such as codePointAt, codePointLength, and fromCodePoint:

let text = "𠮷 a.";
console.log(text.charCodeAt(0)); / / 55362
console.log(text.charCodeAt(1)); / / 57271
console.log(text.charCodeAt(2)); / / 97

console.log(text.codePointAt(0)); / / 134071
console.log(text.codePointAt(1)); / / 57271
console.log(text.codePointAt(2)); / / 97

console.log(codePointLength("abc")); / / 3
console.log(codePointLength("𠮷 BC")); / / 3

console.log(String.fromCodePoint(134071)); / / "𠮷"
Copy the code

CodePointAt can also be used to easily determine whether a 16-bit sequence can be represented

function is32Bit(c) {
    return c.codePointAt(0) > 0xFFFF;
}
console.log(is32Bit("𠮷")); // true
console.log(is32Bit("a")); // false
Copy the code

Built-in function

Checking for a substring in a string prior to ES6 can only be done with indexOf, which sometimes seems unnatural. ES6 provides includes, startsWith, and endsWith to do this. Their usage is more straightforward, as can be seen from the method name:

let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
console.log(msg.endsWith("!")); // true
console.log(msg.includes("o")); // true

console.log(msg.startsWith("o")); // false
console.log(msg.endsWith("world!")); // true
console.log(msg.includes("x")); // false

console.log(msg.startsWith("o".4)); // true
console.log(msg.endsWith("o".8)); // true
console.log(msg.includes("o".8)); // false
Copy the code

Another useful built-in method is repeat, which, as the name implies, repeats a string to form a new string:

console.log("x".repeat(3)); // "xxx"
console.log("hello".repeat(2)); // "hellohello"
console.log("abc".repeat(4)); // "abcabcabcabc"
Copy the code

The main purpose of this built-in method is to format the output of some text.

Template literals

Template literals provide DSLS, which are language or syntax generated for a specific task, with a more specific purpose than regular programming languages. Template literals were designed to solve some pre-ES6 string representation problems:

  • Strings are not well represented on multiple lines
  • String formatting problems
  • HTML translation problems

Template literals are also very simple to use: ‘instead of regular quotes:

let message = `Hello world! `;
console.log(message); // "Hello world!"
console.log(typeof message); // "string"
console.log(message.length); / / 12
Copy the code

Similarly, template literals can easily handle string formatting:

let count = 10,
    price = 0.25,
    message = `${count} items cost $${(count * price).toFixed(2)}. `; 
console.log(message); // "10 items cost $2.50."
Copy the code

It is also important to note that template literals are JS expressions and can therefore be nested with each other:

let name = "pyhhou",
    message = `Hello, The ${`my name is ${ pyhhou }`
    }. `;
console.log(message); // "Hello, my name is pyhhou."
Copy the code

Template literals also have a custom use for tag templates. Allows the user to customize the content and form of the final string based on the template literal information:

function passthru(literals, ... substitutions) {
    let result = "";
    // run the loop only for the substitution count
    for (let i = 0; i < substitutions.length; i++) {
        result += literals[i];
        result += substitutions[i];
    }
    // add the last literal
    result += literals[literals.length - 1];
    return result;
}

let count = 10,
    price = 0.25,
    message = passthru`${count} items cost $${(count * price).toFixed(2)}. `; 
console.log(message); // "10 items cost $2.50."
Copy the code

There are some built-in template functions, such as shring. raw, that allow strings to be rendered without special symbols:

let message1 = `Multiline\nstring`,
    message2 = String.raw`Multiline\nstring`;

console.log(message1); // "Multiline
                       // string"
console.log(message2); // "Multiline\\nstring"
Copy the code


function

The default parameters

Before ES6, JS functions did not support default arguments. This brings a lot of inconvenience to function parameter passing and parameter judgment. The ES6 starts to support default parameters. Default parameters change some of the original JS mechanics a bit.

In previous versions, the Arguments object held all the arguments to the current function. And in non-strict mode, arguments and arguments objects are also associated:

function mixArgs(first, second) {
    console.log(first === arguments[0]); // true
    console.log(second === arguments[1]); // true
    first = "c";
    second = "d";
    console.log(first === arguments[0]); // true
    console.log(second === arguments[1]); // true
}
mixArgs("a"."b");
Copy the code

However, the relationship between the default arguments and arguments objects in ES6 will behave like it did in strict mode before:

// not in strict mode
function mixArgs(first, second = "b") {
    console.log(arguments.length); / / 1
    console.log(first === arguments[0]); // true
    console.log(second === arguments[1]); // false
    first = "c";
    second = "d"
    console.log(first === arguments[0]); // false
    console.log(second === arguments[1]); // false
}
mixArgs("a");
Copy the code

Function argument passing is similar to variable declaration and value binding, such as the following is allowed:

function add(first, second = first) {
    return first + second;
}
console.log(add(1.1)); / / 2
console.log(add(1)); / / 2
Copy the code
function getValue(value) {
    return value + 5;
}

function add(first, second = getValue(first)) {
    return first + second;
}

console.log(add(1.1)); / / 2
console.log(add(1)); / / 7
Copy the code

Because of this, parameters are also subject to the TDZ phenomena we mentioned earlier, such as the following code:

function add(first = second, second) {
    return first + second;
}

console.log(add(1.1)); / / 2
console.log(add(undefined.1)); // throws an error
Copy the code

The unknown parameters

In real development, we often pass multiple arguments to a function, and the number of arguments is uncertain. Before ES6, though, Arguments seemed to solve this problem. However, this approach does not guarantee the brevity of the code, and such code is not easy to maintain and extend. ES6 introduces residual parameters to solve this problem:

function pick(object, ... keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}
Copy the code

There are two things to note about the remaining parameters:

  • The remaining arguments must exist at the end of the function argument list, otherwise a syntax error is reported
  • In literal assignment of an object, the remaining arguments cannot be used in setter functions

Clarify the dual role of functions

Prior to ES6, functions could be either called in general or used as object generation. When used to generate objects, we can prefix the function with new. The reason for this dual nature is that normal functions have two internal attributes (internal slots), which are [[Call]] and [[Constructor]]. [[Call]] is responsible for calling and executing functions. When a function call is preceded by new, [[Constructor]] is called. This internal slot is responsible for generating the new object, executing the function body, and binding this to the newly generated object.

Before ES6, we needed instanceof to determine if the function used new:

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // using new
    } else {
        throw new Error("You must use new with Person."); }}var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // throws an error
Copy the code

But this is not rigorous, and there are other ways to avoid such judgments:

function Person(name) {
   if (this instanceof Person) {
       this.name = name; }
   else {
       throw new Error("You must use new with Person."); }}var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // works!
Copy the code

ES6 added the new. Target meta-attribute, which only has a value if the function uses new:

function Person(name) {
    if (typeof new.target ! = ="undefined") {
        this.name = name;
    } else {
        throw new Error("You must use new with Person."); }}var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // error!
Copy the code

It is important to note that using new.target outside of a function will result in a syntax error.

Arrow function

This is one of the biggest functional innovations of ES6. The arrow function can be viewed as an anonymous JS function, and it has a concise syntax. In addition, arrow functions throw away many of the confusing designs of traditional functions, such as the fact that arrow functions don’t have arguments objects, that arrow functions can’t change the binding of this, and that they can’t be used as constructors to generate new objects.

Let’s take a look at the properties of arrow functions:

  • There are no bindings for this, super, arguments, and new.target

    The value of the above attribute in the arrow function is the value of the outer function

  • New cannot be added when called

    Arrow functions have no [[Constructor]] internal slot

  • There is no prototype

Arrow functions are much more concise than traditional functions:

let getTempItem = id= > ({ id: id, name: "Temp" });

// effectively equivalent to:
let getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};
Copy the code

The arrow function is used to solve the problem of binding this to traditional functions. For example, the following code fails because the inner function binds this by default:

let PageHandler = {
    id: "123456".init: function() {
        document.addEventListener("click".function(event) {
            this.doSomething(event.type); // error
        }, false);
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id); }};Copy the code

Replacing the inner function with the arrow function solves this problem nicely:

let PageHandler = {
    id: "123456".init: function() {
        document.addEventListener("click".event= > this.doSomething(event.type),
            false
        );
    },
    doSomething: function(type) {
        console.log("Handling " + type + " for " + this.id); }};Copy the code

Of course, although arrow functions have these differences compared to normal functions, you can still use operations like call(), apply(), and bind() on arrow functions, just to say that this will not be bound:

var sum = (num1, num2) = > num1 + num2;
console.log(sum.call(null.1.2)); / / 3
console.log(sum.apply(null[1.2])); / / 3

var boundSum = sum.bind(null.1.2);
console.log(boundSum()); / / 3
Copy the code

As you can see, arrow functions are designed to be lightweight, simple to use and less resource-intensive than traditional functions.

Function call optimization

When calling another function at the end of a function, it is possible to move the current function off the stack to save stack space, as in the following example:

function doSomething() {
    return doSomethingElse();   // tail call
}
Copy the code

ES6, in strict mode, provides this optimization:

"use strict";
function doSomething() {
    // optimized
    return doSomethingElse();
}
Copy the code

Note that there are calls at the end of a function that are not tail calls:

"use strict";
function doSomething() {
    // not optimized - no return
    doSomethingElse();
}
Copy the code
"use strict";
function doSomething() {
    // not optimized - must add after returning
    return 1 + doSomethingElse();
}
Copy the code
"use strict";
function doSomething() {
    var num = 1,
    func = () = > num;
    // not optimized - function is a closure
    return func();
}
Copy the code

The effect of tail-function optimization is not obvious in normal function calls, but it can be very different in recursive calls, as in the following example:

function factorial(n, p = 1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p;

        // optimized
        return factorial(n - 1, result); }}Copy the code

With tail-call optimization, the above functions do not cause stack overflow problems, even if the input parameters are extremely large.