TypeScript learning-02 variable declaration

Variable declaration

Variable declaration

Let and const are relatively new variable declarations in JavaScript. As we mentioned before, let is similar to var in many aspects, but it can help you avoid some common problems in JavaScript. Const is an enhancement to let, which can prevent the re assignment of a variable.

Because TypeScript is a superset of JavaScript, it itself supports let and const. We will elaborate on these new declarations and why they are recommended instead of var.

If you didn't pay special attention to using JavaScript before, this section will evoke your memories. If you already know the weirdness of var declaration, you can easily skip this section.

var declaration

We have always defined JavaScript variables through the var keyword.

var a = 10;

As you can understand, a variable named a with a value of 10 is defined here.

We can also define variables inside the function:

function f() {
    var message = "Hello, world!";

    return message;
}

And we can also access the same variables inside other functions.

function f() {
    var a = 10;
    return function g() {
        var b = a + 1;
        return b;
    }
}

var g = f();
g(); // returns 11;

In the above example, we can get the variable in g. Whenever g is called, it can access the a variable in F. Even when g is called after f has been executed, it can still access and modify a.

function f() {
    var a = 1;

    a = 2;
    var b = g();
    a = 3;

    return b;

    function g() {
        return a;
    }
}

f(); // returns 2

Scope rule

For those familiar with other languages, var declarations have some strange scope rules. Take the following example:

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

    return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'

Some readers may want to see this example several times. Variable x is defined in the if statement, but we can access it outside the statement. This is because the VaR declaration can be accessed anywhere within the function, module, namespace or global scope containing it (we will describe it in detail later), and the code block containing it has no impact on it. Some call this * var scope or function scope *. Function parameters also use function scope.

These scope rules may cause some errors. One of them is that declaring the same variable multiple times does not report 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;
}

It's easy to see some problems here. The for loop in the inner layer will override the variable i, because all i refer to variables within the scope of the same function. Experienced developers are well aware that these problems may be missed during code review and cause endless trouble.

Capture the oddity of variables

Quickly guess what the following code will return:

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

To introduce, setTimeout will execute a function after a delay of several milliseconds (waiting for other code to be executed).

Well, let's look at the results:

10
10
10
10
10
10
10
10
10
10

Many JavaScript programmers are familiar with this behavior, but if you're confused, you're not alone. Most people expect the output to be like this:

0
1
2
3
4
5
6
7
8
9

Remember the capture variables we mentioned above?

We pass it on to setTimeout Each function expression of actually refers to the same function in the same scope i. 

Let's take a moment to think about why. setTimeout executes a function after a few milliseconds, and after the end of the for loop. After the for loop ends, the value of i is 10. So when the function is called, it will print 10!

A common solution is to use an immediately executed 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);
}

This strange form has become commonplace to us. Parameter i will override i in the for loop, but because we have the same name, we don't need to change the code in the body of the for loop.

let declaration

Now you know that there are some problems with VaR, which just explains why we use let statement to declare variables. Except for different names, let is written in the same way as var.

let hello = "Hello!";

The main difference is not in grammar, but in semantics, which we will study in depth next.

Block scope

When a variable is declared with let, it uses lexical scope or block scope. Unlike variables declared with var, which can be accessed outside the function containing them, block scope variables cannot be accessed outside the block or for loop containing 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;
}

Here we define two variables A and b. The scope of a is in the f function body, while the scope of b is in the if statement block.

Variables declared in catch statements also have the same scope rules.

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// Error: 'e' doesn't exist here
console.log(e);

Another feature of variables with block level scope is that they cannot be read or written before being declared. Although these variables always "exist" in their scope, the area until the code that declares them is a temporary dead zone. It's just to say that we can't access let statements before them. Fortunately, TypeScript can tell us this information.

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

Note that we can still get a block scoped variable before it is declared. It's just that we can't call that function before variable declaration. If the generated code target is ES2015, the modern runtime will throw an error; Today, however, 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;

For more information about temporary deadbands, see Mozilla Developer Network

Redefinition and shielding

We mentioned that when using var declaration, it doesn't care how many times you declare it; You'll only get one.

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

    if (true) {
        var x;
    }
}

In the above example, all declarations of x actually refer to the same x, and this is completely valid code. This is often the source of bug s. Well, the let statement won't be so loose.

let x = 10;
let x = 20; // Error, cannot declare ` x multiple times in 1 Scope`

TypeScript does not require two declarations that are both block level scopes 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'
}

This is not to say that block level scope variables cannot be declared with function scope variables. Instead, block level scope variables need to be declared in significantly 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

The act of introducing a new name into a nested scope is called masking. It is a double-edged sword. It may inadvertently introduce new problems and solve some mistakes. 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;
}

This version of the loop can get the correct result, because the i of the inner loop can mask the i of the outer loop.

Generally speaking, shielding should be avoided because we need to write clear code. At the same time, there are some scenes suitable for using it, and you need to plan well.

Acquisition of block level scope variables

When we first talked about getting variables declared with var, we briefly explored how the variable behaves after getting it. Intuitively, each time you enter a scope, it creates a variable environment. Even if the code in the scope has been executed, the environment and its captured variables still exist.

function theCityThatAlwaysSleeps() {
    let getCity;

    if (true) {
        let city = "Seattle";
        getCity = function() {
            return city;
        }
    }

    return getCity();
}

Because we have obtained the city in the city environment, we can still access it even after the if statement is executed.

Recall the previous example of setTimeout. Finally, we need to use the immediately executed function expression to obtain the state in each iteration of the for loop. In fact, what we do is create a new variable environment for the obtained variables. This is painful, but fortunately, you don't have to do it in TypeScript.

When the let declaration appears in the loop body, it has a completely different behavior. Not only is a new variable environment introduced into the loop, but such a new scope will be created for each iteration. This is what we do when using immediate function expressions, so in the setTimeout example, we can only use the let declaration.

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

It will output results consistent with expectations:

0
1
2
3
4
5
6
7
8
9

const statement

const declaration is another way to declare variables.

const numLivesForCat = 9;

They are similar to let declarations, but as its name implies, they cannot be changed after being assigned. In other words, they have the same scope rules as lets, but they cannot be reassigned.

It's easy to understand that the values they reference 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--;

Unless you use special methods to avoid it, the internal state of const variable is actually modifiable. Fortunately, TypeScript allows you to make members of objects read-only. The interface is described in detail in the chapter.

let vs. const

Now that we have two declarations with similar scopes, we naturally ask which should be used. Like most general questions, the answer is: it depends.

Using the principle of least privilege, const should be used for all variables except those you plan to modify. The basic principle is that if a variable does not need to be written to it, others who use this code can't write them, and think about why you need to reassign these variables. Using const also makes it easier for us to speculate about the flow of data.

In your own judgment, if appropriate, consult with team members.

let declarations are used in most parts of this manual.

deconstruction

Another TypeScript can resolve other ECMAScript 2015 features. For a complete list, see the article on the Mozilla Developer Network. In this chapter, we will give a brief overview.

Deconstruct array

The simplest deconstruction is the deconstruction assignment of the array:

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

This creates two named variables, first and second. It is equivalent to using index, but it is more convenient:

first = input[0];
second = input[1];

Deconstruction works better on declared variables:

// swap variables
[first, second] = [second, first];

Function parameters:

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

You can create the remaining variables in the array using the... Syntax:

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

Of course, because it's JavaScript, you can ignore trailing elements you don't care about:

let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

Or other elements:

let [, second, , fourth] = [1, 2, 3, 4];

Object deconstruction

You can also deconstruct objects:

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;

This creates a and b through o.a and o.b. Note that if you don't need c, you can ignore it.

Like array deconstruction, you can use undeclared assignment:

({ a, b } = { a: "baz", b: 101 });

Note that we need to enclose it in parentheses, because Javascript usually parses the statement starting with {into a block.

You can create the remaining variables in the object using the... Syntax:

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

Property rename

You can also give attributes different names:

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

The grammar here is beginning to get confused. You can read a: newName1 as "a as newName1". The direction is from left to right, as if you wrote the following:

let newName1 = o.a;
let newName2 = o.b;

What's puzzling is that the colon here is not indicative. If you want to specify its type, you still need to write the complete pattern after it.

let {a, b}: {a: string, b: number} = o;

Default value

The default value allows you to use the default value when the attribute is undefined:

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

Now, even if b is undefined, the attributes a and b of the variable wholeObject of the keepWholeObject function will have values.

Function declaration

Deconstruction can also be used for function declarations. Look at the following simple situation:

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

However, in general, it is more to specify the default value, and deconstructing the default value is a little tricky. First, you need to format the default value before it.

function f({ a="", b=0 } = {}): void {
    // ...
}
f();
The above code is an example of type inference, which will be introduced later in this manual.

Second, you need to know to give a default or optional attribute on the deconstruction attribute to replace the main initialization list. You should know that the definition of C has an b optional attribute:

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

Use deconstruction with caution. As can be seen from the previous examples, even the simplest deconstruction expression is difficult to understand. Especially when there is deep nested deconstruction, even if there are no stacked renames, default values and type annotations, it is difficult to understand. Deconstruction expressions should be kept as small and simple as possible. You can also directly use the assignment expression that will be generated by deconstruction.

open

The expansion operator is the opposite of deconstruction. It allows you to expand an array into another array, or expand an object into another object. For example:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

This will make the value of bothPlus [0, 1, 2, 3, 4, 5]. The expand operation creates a shallow copy of first and second. They will not be changed by the deployment operation.

You can also expand objects:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

The value of search is {food: "rich", price: "$$", ambiance: "noisy"}. Object expansion is much more complex than array expansion. 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 overwrite the previous attributes. Therefore, if we modify the above example and expand it at the end:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

Then, the food attribute in defaults will rewrite food: "rich". Here, this is not the result we want.

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

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

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.

Keywords: Javascript Front-end TypeScript

Added by chadowch on Sat, 29 Jan 2022 22:32:43 +0200