Welcome to wechat public account: Front Reading Room

Variable declarations

Let and const are relatively new variable declarations in JavaScript. Let is similar to var in many ways, but it can help you avoid some of the problems common in JavaScript. Const is an enhancement to let that prevents reassignment of a variable.

Because TypeScript is a superset of JavaScript, it has built-in support for let and const. We’ll detail these new declarations below and why they are recommended instead of var.

The var statement

For a long time we defined JavaScript variables using the var keyword, but using the var keyword has some disadvantages.

Scope rule

Var declares some strange scoping rules.

function f(shouldInitialize: boolean) {
    if (shouldInitialize) {
        var x = 10;
    }

    return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'
Copy the code

Some readers may want to look at this example a few more times. The variable x is defined inside the if statement, but we can access it outside the statement. This is because the VAR declaration can be accessed anywhere within the containing function, module, namespace, or global scope (more on that later), and the code block containing it has no effect on this. Some call this the var scope or function scope. Function parameters also use function scopes.

These scope rules can cause some errors. For one thing, declaring the same variable more than once does not give an error:

function sumMatrix(matrix: number[] []) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (var i = 0; i < currentRow.length; i++) { sum += currentRow[i]; }}return sum;
}
Copy the code

The inner for loop overrides variable I, because all I’s refer to variables in the same function scope. As experienced developers are well aware, these issues can be missed during code review and cause endless trouble.

Capture variable quirks

A quick guess at what the following code returns:

for (var i = 0; i < 10; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
Copy the code

Well, take a look at the results:

10 10 10 10 10 10 10 10 10 10 10 10 10 10 10Copy the code

Many JavaScript programmers are already familiar with this behavior, because every function expression we pass to setTimeout actually refers to the same I in the same scope.

But in case you’re wondering, most people expect this output:

0
1
2
3
4
5
6
7
8
9
Copy the code

A common workaround is to use an immediate function expression (IIFE) to capture the value of I at each iteration:

for (var i = 0; i < 10; i++) {
    // capture the current state of 'i'
    // by invoking a function with its current value
    (function(i) {
        setTimeout(function() { console.log(i); }, 100 * i);
    })(i);
}
Copy the code

Let the statement

Now that you know that var has some problems, it illustrates why variables are declared in let statements. Let and var are written the same except for their names.

let hello = "Hello!";
Copy the code

Block scope

When you declare a variable with let, it uses lexical or block scope. Unlike variables declared with VAR, which can be accessed outside the function that contains them, block-scoped variables are not accessible outside the block or for loop that contains them.

function f(input: boolean) {
    let a = 100;

    if (input) {
        // Still okay to reference 'a'
        let b = a + 1;
        return b;
    }

    // Error: 'b' doesn't exist here
    return b;
}
Copy the code

Another feature of variables that have block-level scope is that they cannot be read or written before they are declared. Although these variables always “live” in their scope, the region until the code that declares them is a temporary dead zone.

a++; // illegal to use 'a' before it's declared;
let a;
Copy the code

Note that we can still get an owning block scoped variable before it is declared. We just can’t call that function before the variable is declared. Modern runtimes throw an error if the generated code is aimed at ES2015; But TypeScript does not report errors.

function foo() {
    // okay to capture 'a'
    return a;
}

// 'foo' cannot be called before 'a' is declared
// The runtime should throw an error
foo();

let a;
Copy the code

Redefine and mask

We mentioned that when you use var declarations, it doesn’t care how many times you declare them; You’re only going to get 1.

function f(x) {
    var x;
    var x;

    if (true) {
        varx; }}Copy the code

This is often a source of bugs. The good news is that the LET statement is not so loose.

let x = 10;
let x = 20; // error, cannot declare 'x' more than once in a scope
Copy the code

TypeScript does not require two block-scoped declarations to give an error warning.

function f(x) {
    let x = 100; // error: interferes with parameter declaration
}

function g() {
    let x = 100;
    var x = 100; // error: can't have both declarations of 'x'
}
Copy the code

Of course, this is not to say that block-level scoped variables cannot be declared using function scoped variables. Rather, block-level scoped variables need to be declared in markedly different blocks.

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }

    return x;
}

f(false.0); // returns 0
f(true.0);  // returns 100
Copy the code

The act of introducing a new name into a nested scope is called masking. It’s a double-edged sword that can accidentally introduce new problems and fix bugs. For example, suppose we now rewrite the previous sumMatrix function with let.

function sumMatrix(matrix: number[] []) {
    let sum = 0;
    for (let i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (let i = 0; i < currentRow.length; i++) { sum += currentRow[i]; }}return sum;
}
Copy the code

This version of the loop gets the right result because the I of the inner loop can mask the I of the outer loop.

Masking should generally be avoided because we need to write clean code.

Get block-level scope variables

We took a quick look at how the following code behaves once the variable is retrieved. Intuitively, each time a scope is entered, it creates an environment for a variable. Even after the scoped code has been executed, the environment and its captured variables still exist.

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            returncity; }}return getCity();
}
Copy the code

Because we already got city in the city environment, we can still access it even after the if statement is finished.

Recall from the setTimeout example, we ended up using an immediately executed function expression to get the state in each iteration of the for loop. In effect, what we do is create a new variable environment for the fetched variables. This is painful, but fortunately, you don’t have to do this in TypeScript.

Let declarations have completely different behavior when they appear in the body of the loop. Not only is a new variable environment introduced into the loop, but a new scope is created for each iteration. This is what we do when we use function expressions that execute immediately, so in the setTimeout example we just use the let declaration.

for (let i = 0; i < 10 ; i++) {
    setTimeout(function() {console.log(i); }, 100 * i);
}
Copy the code

Outputs the same results as expected:

0
1
2
3
4
5
6
7
8
9
Copy the code

Const statement

Const declarations are another way of declaring variables.

const numLivesForCat = 9;
Copy the code

They are similar to the LET declaration, but as the name suggests, they cannot be changed once assigned. In other words, they have the same scoped rules as lets, but the values it references are immutable.

const numLivesForCat = 9;
const kitty = {
    name: "Aurora".numLives: numLivesForCat,
}

// Error
kitty = {
    name: "Danielle".numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
Copy the code

Unless you use special methods to avoid it, the internal state of const variables is actually modifiable. Fortunately, TypeScript allows you to make members of objects read-only. Interfaces are explained in detail in the chapter.

let vs. const

Now that we have two declarations with similar scope, it is natural to ask which should be used.

Using the principle of least privilege, all variables should be const except those you plan to modify. The basic rule is that if a variable doesn’t need to be written to, then anyone else using the code can’t write to it, and think about why it needs to be reassigned. Using const also makes it easier to predict the flow of data.

deconstruction

TypeScript supports deconstruction

Deconstruction array

The simplest way to deconstruct an array is to destruct an assignment:

let input = [1.2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
Copy the code

On function arguments:

function f([first, second]: [number.number]) {
    console.log(first);
    console.log(second);
}
f(input);
Copy the code

You can use… in arrays. Syntax creates remaining variables:

let [first, ...rest] = [1.2.3.4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
Copy the code

Object to deconstruct

You can also deconstruct objects:

let o = {
    a: "foo".b: 12.c: "bar"
};
let { a, b } = o;
Copy the code

You can use… in objects. Syntax creates remaining variables:

let{ a, ... passthrough } = o;let total = passthrough.b + passthrough.c.length;
Copy the code

Property renaming

You can also give attributes different names:

let { a: newName1, b: newName2 } = o;
Copy the code

The colon here does not indicate the type. If you want to specify its type, you still need to write the full schema after it.

let {a, b}: {a: string.b: number} = o;
Copy the code

The default value

Default allows you to use the default value when the property is undefined:

function keepWholeObject(wholeObject: { a: string, b? :number }) {
    let { a, b = 1001 } = wholeObject;
}
Copy the code

Function declaration

Destructuring can also be used for function declarations. Here’s a simple case:

type C = { a: string, b? :number }
function f({ a, b }: C) :void {
    // ...
}
Copy the code

However, it’s often more about specifying default values, which can be tricky to deconstruct. First, you need to format the default value before it.

function f({ a="", b=0 } = {}) :void {
    // ...
}
f();
Copy the code

Second, you need to know to give a default or optional attribute on the destruct attribute to replace the main initializer list. Remember that the definition of C has an optional property of B:

function f({ a, b = 0 } = { a: "" }) :void {
    // ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to {a: ""}, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument
Copy the code

Be careful with deconstruction. As you can see from the previous examples, even the simplest deconstructed expressions are difficult to understand. Especially when there is deep nested deconstruction, it is difficult to understand the default values and type annotations even when there is no stacked renaming. Keep deconstructing expressions small and simple. You can also directly use the assignment expressions that the deconstruction will generate.

an

The unfold operation is the opposite of deconstruction. It allows you to expand an array into another array, or an object into another object. Such as:

let first = [1.2];
let second = [3.4];
let bothPlus = [0. first, ... second,5];
Copy the code

This gives bothPlus a value of [0, 1, 2, 3, 4, 5]. The expansion creates a shallow copy of first and second. They are not changed by expansion operations.

You can also expand objects:

let defaults = { food: "spicy".price: "$$".ambiance: "noisy" };
letsearch = { ... defaults,food: "rich" };
Copy the code

Like array expansion, it is processed from left to right, but the result is still an object. This means that the attributes that appear after the expanded object override the previous attributes.

There are other unexpected limitations to object expansion. First, it contains only enumerable properties of the object itself. Basically, when you expand an object instance, you lose its methods:

class C {
  p = 12;
  m(){}}let c = new C();
letclone = { ... c }; clone.p;// ok
clone.m(); // error!
Copy the code

Second, the TypeScript compiler does not allow you to expand type parameters on generic functions. This feature will be considered for implementation in future versions of TypeScript.

Welcome to wechat public account: Front Reading Room