May 07, 2021 TypeScript
let
and
const
relatively new variable declarations in JavaScript. A
s we mentioned earlier,
let
is similar to
var
in many ways, but can help you avoid some of the common problems in JavaScript.
const
is
let
enhancement to let that prevents a variable from been assigned again.
Because TypeScript is a superset of JavaScript, it supports
let
const
Below we'll explain in detail how these new claims are made and why they are recommended instead
var
If you didn't really care when you used JavaScript before, this section will evoke your memories.
If you already
var
the weirdness of var claims, you can easily skip this section.
var
declaration
We've always
var
variables with the var keyword.
var a = 10;
As you can understand, a variable named
a
of
10
defined here.
We can also define variables within functions:
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 example above,
g
can get the
f
defined in
a
function. W
henever
g
is called, it can access the
f
in
a
Even if
g
f
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
For those familiar with other languages,
var
declares some strange scope rules.
Take a look at 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 read this example several more times. T
he
x
is defined in the
if
statement, but we can access it outside the statement. T
his
var
declaration can be accessed anywhere inside the function, module, namespace, or global scope that contains it (we'll cover it in more detail later), and the blocks of code that contain it have little effect on it. S
ome people call this the
var
scope" or
"function scope".
Function parameters also use function scopes.
These scope rules may cause some errors. One of them is that declaring the same variable multiple times does not result in 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, and
for
the middle
i
i
refers to variables within the same function scope.
Experienced developers are well aware that these issues can be missed during code reviews, causing endless hassles.
A quick guess at what the following code will return:
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
As you'll see,
setTimeout
executes a function after a few milliseconds of delay (waiting for the other code to execute).
Well, take a look at the results:
10
10
10
10
10
10
10
10
10
10
Many JavaScript programmers are already familiar with this behavior, but if you're upset, you're not alone. Most people expect the output to look like this:
0
1
2
3
4
5
6
7
8
9
Remember the variable acquisition we talked about above?
Whenever
g
is called, it can access thef
ina
Let's take a little time to consider the situation in this context.
setTimeout
a function after a few milliseconds and is at the
for
the for loop.
for
end of the
i
the value of i
10
So when the function is called, it prints
10
A common workaround is to use an immediately executed function expression (IIFE) to capture the value
i
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.
Parameter
i
for
in the
i
but because we have the same name, we don't have to
for
code in the loop body much.
let
declaration
Now that you know there are some problems with
var
this explains why you use
let
statement to declare variables.
In addition to the different names,
let
var
in the same way as var.
let hello = "Hello!";
The main difference is not in syntax, but in semantics, which we'll look into next.
When you declare a variable with
let
it uses
a syn word scope
or a block
scope.
Unlike
var
with var, which can be accessed outside the function that contains them, block scope variables cannot be
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;
}
Here we define two variables
a
and
b
a
scope of a is inside the
f
function, while the scope of
b
is in the
if
statement block.
catch
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 scopes is that they cannot be read or written before they are declared. A
lthough these variables are always "present" in their scope, the regions that were in the time dead zone until the code that
declared them were declared.
It's just to show that we
let
before let statements, and fortunately TypeScript can tell us that.
a++; // illegal to use 'a' before it's declared;
let a;
Note that we can still get a block scope variable before it is declared. I t's just that we can't call that function before the variable is declared. If the generated code target is ES2015, the modern runtime throws an error;
function foo() {
// okay to capture 'a'
return a;
}
// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();
let a;
For more information about Time Dead Zone, check out the Mozilla Developer Network here .
When we mention
var
declarations, it doesn't matter how many times you declare;
function f(x) {
var x;
var x;
if (true) {
var x;
}
}
In the example above, all
x
actually refer to the
same
x
this is fully valid code. T
his often becomes a source of bugs.
Well,
let
it won't be so loose.
let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`
TypeScript does not require both declarations, which are block-level scopes, to give a false 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 within the scope of a function. Instead, block-level scope variables need to be declared in unused 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.
I
t is a double-edged sword that may accidentally introduce new problems and also resolve some errors.
For example, let's say we now override
sumMatrix
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 gets the correct result because the i of the
i
can mask the i of the outer
i
In general, we should avoid masking because we need to write clear code. There are also some scenarios that are suitable for using it, and you need to plan it well.
When we first talked about getting variables declared with
var
we briefly explored how variables behave when they are acquired. I
ntuitively, each time it enters a scope, it creates a variable
environment.
Even if the scope code has been executed, the environment and the variables it captures remain.
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
Because
city
got city in
city
we can
if
statement even after it's executed.
Recalling the previous
setTimeout
up using a function expression that executes immediately to get the
for
loop iteration. I
n fact, what we're doing is creating a new variable environment for the variables we get.
It's painful, but fortunately you don't have to do it in TypeScript.
There
let
completely different behavior when the let declaration appears in the loop body. N
ot only is a new variable environment introduced into the loop, but such
a new scope is
created for each iteration.
That's what we do when we use function expressions that execute
setTimeout
example we'll just use the
let
declaration.
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
outputs results that are consistent with expectations:
0
1
2
3
4
5
6
7
8
9
const
declaration
const
are another way to declare variables.
const numLivesForCat = 9;
They are
let
declarations, but as their names say, they cannot be changed after being assigned.
In other words, they have the same scope rules as
let
but they cannot be re-assigned.
It's easy to understand that the values they reference are imm changed.
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 a special method to avoid it, the internal state of the
const
variable is actually modifiable. F
ortunately, TypeScript allows you to set members of an object to read-only.
Interface The
chapter is detailed.
let
vs.
const
Now that we have two scope-similar declarations, we naturally ask which one to use. As with most general questions, the answer is: it depends.
Using
the principle of least
privilege, all variables except those you plan to modify should use
const
T
he basic principle is that if a variable doesn't need to be written to it, then no one else who uses it can write them, and think about why they need to be re-assigned.
Using
const
it easier for us to speculate on the flow of data.
On the other hand, users like
let
of let.
Let is used in most parts of
let
Talk to your team members if appropriate, according to your own judgment. F
ortunately, TypeScript allows you to specify that members of an object are
readonly
.
The
chapter on Interfaces
has the details.
Another TypeScript is already able to parse other ECMAScript 2015 features. S ee the article on the Mozilla Developer Network for a full list. In this chapter, we'll give you a brief overview.
The simplest deconstruction is the deconstruction assignment of an array:
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
This creates two named
first
and
second
Equivalent to using an index, but more convenient:
first = input[0];
second = input[1];
Deconstruction is better for declared variables:
// swap variables
[first, second] = [second, first];
Acting on function parameters:
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);
You can use
...name
syntax creates a list of remaining variables:
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
Of course, since it's JavaScript, you can ignore the trailing elements that 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];
You can also deconstruct objects:
let o = {
a: "foo",
b: 12,
c: "bar"
}
let {a, b} = o;
This
o.a
o.b
a
b
Note that if you don't
c
you can ignore it.
Just like array deconstruction, you can use unsted assignments:
({a, b} = {a: "baz", b: 101});
Note that we need to enclose it in parentheses, because Javascript typically
{
to a block with the statement that starts with .
You can also give properties different names:
let {a: newName1, b: newName2} = o;
The grammar here is starting to get messy. Y
ou can
a: newName1
as
a
newName1
The direction is from left to right, as if you had written the following:
let newName1 = o.a;
let newName2 = o.b;
Confusingly, the colon here is not an indication type. If you want to specify its type, you still need to write the full pattern later.
let {a, b}: {a: string, b: number} = o;
The default value lets you use the default value when the property is undefined:
function keepWholeObject(wholeObject: {a: string, b?: number}) {
let {a, b = 1001} = wholeObject;
}
Now, even
b
is undefined, the properties a and b of
wholeObject
a
and
b
have values.
keepWholeObject
Deconstruction can also be used for function declarations. Here's a look at the simple case:
type C = {a: string, b?: number}
function f({a, b}: C): void {
// ...
}
However, it is often more about specifying default values, which can be tricky to deconstruct. First, you need to know to set the default value before setting its type.
function f({a, b} = {a: "", b: 0}): void {
// ...
}
f(); // ok, default to {a: "", b: 0}
Second, you need to know that a default or optional property is given on the deconstruction property to replace the main initialization list.
To know that the definition of
C
has a
b
optional property:
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
Be careful with deconstruction. A s you can see from the previous example, even the simplest deconstruction can have many problems. E specially when there is deep nesting deconstruction, the default values and type annotations are incomprehensible even if there are no stacked renames. D econstruct expressions should be kept as small and simple as possible. You can also use the assignment expression that will be generated directly by deconstructing yourself.