var和let和const
by Prarthana S. Sannamani
通过Prarthana S.Sannamani
In this article, we will explore the history of var
in JavaScript, the need for let
and const
, and the differences between them.
在本文中,我们将探讨JavaScript中var
的历史, let
和const
的需求以及它们之间的区别。
This post consists of two sections: Fictional piece and Technical explanation.
这篇文章分为两部分:虚构作品和技术说明。
The fictional piece is intended to ease beginners into the concepts, but several parts are simplified and do not always present an accurate 1:1 analogy.
该虚构作品旨在使初学者更容易理解这些概念,但是简化了几个部分,并不一定总是提供准确的1:1类比。
Let’s start!
开始吧!
JavaScript town was a bustling town beside the sea with a commercial district filled with high rise buildings.
JavaScript镇是一个繁华的小镇,在海边,有一个充满高层建筑的商业区。
Since time immemorial, the residents of JavaScript town used Vary
boxes to store their valuables, especially their prized gold marbles. To do so, the residents had two options:
自远古时代起,JavaScript镇的居民就使用Vary
盒子存放他们的贵重物品,尤其是珍贵的黄金大理石。 为此,居民有两种选择:
Since the town prided itself on law and order, they set up several rules and procedures.
由于该镇以治安为荣,他们制定了一些规则和程序。
A shop could have “special offer” counters, such as “If
you are over 20 years old, buy a special box here.” And “For
(every) child of
your family, buy a kid’s box here” (other blocks such as if
and loops).
一家商店可能设有“特价”柜台,例如“ If
您超过20岁,请在这里购买一个特殊的盒子。” 还有“ For
您家的(每个)孩子来说of
在这里购买一个孩子的盒子”(其他街区,例如if
和loops)。
At the sea level or on any hill, residents could own ONLY a single colored Vary
box (duplicate identifiers not allowed).
在海平面或任何山丘上,居民只能拥有一个彩色的Vary
框(不允许使用重复的标识符)。
The Vary
box could never be empty from the moment it was created. It had to contain cotton (undefined
) or gold marbles at all times (effect of hoisting).
自创建之日起, Vary
框就永远不能为空。 它必须始终包含棉花( undefined
)或金色大理石(提升效果)。
We will follow the journey of a resident, John, in this article.
在本文中,我们将遵循居民约翰的旅程。
John enters the shop and declares what color of Vary
box he desires to buy at the “declaration-initialization” counter. The guard notes this in his registration book.
约翰进入商店,并在“申报初始化”柜台上宣布想要购买哪种颜色的Vary
盒子。 保安员在他的登记簿中记录了这一点。
The guard conjures the colored Vary
box, fills it with cotton and hands it to John.
守卫让人想起彩色的Vary
盒子,里面装满棉花,然后交给John。
Naturally, these rules brought along peculiar problems.
这些规则自然会带来特殊的问题。
You can imagine how frustrating this situation was. With the residents of JavaScript town losing their marbles, the Town Council decided to take action.
您可以想象这种情况多么令人沮丧。 随着JavaScript镇的居民失去大理石,镇议会决定采取行动。
In a grand Town Meeting in 2015, they proudly introduced two new boxes: Lety
and Consty
.
在2015年的一次大型城镇会议上,他们自豪地推出了两个新包装盒: Lety
和Consty
。
They also introduced the other major change: the removal of “special offer” counters from Lety
and Consty
shops. Instead, these counters were upgraded to inner shops, which were built on a hill inside the shop.
他们还介绍了另一个重大变化:从Lety
和Consty
商店中删除“特价”柜台。 相反,这些柜台被升级为内部商店,内部商店建在商店内部的小山上。
John gets a ticket for his turn. Since the box is not created at declaration, it is not available for use.
约翰轮到他了。 由于该框不是在声明时创建的,因此无法使用。
This is where Lety
and Consty
purchase rules diverge.
这是Lety
和Consty
购买规则不同的地方。
At the counter, John has the choice to buy an empty Lety
box, or buy a Lety
box and have his gold marbles placed inside it immediately.
在柜台上,John可以选择购买一个空的Lety
盒子,或购买一个Lety
盒子并立即将其金色大理石放在里面。
Depending on his choice, the shop assistant conjures the Lety
box, and fills it with cotton or hands it over to the “assignment” counter, where John’s gold marbles are placed inside it.
根据他的选择,店员会变出Lety
盒子,并用棉花填充它或将其移交给“分配”柜台,John的金色大理石放在里面。
Consty
boxes are extremely special. Lined with a layer of gold inside and sealed with a lock, these boxes are so dear to the shop assistants that they refuse to sell them without knowing what exactly will be placed in them.
Consty
盒子非常特别。 这些盒子内衬有一层黄金,并用锁密封,对售货员是如此珍爱,以至于他们拒绝出售它们而又不知道它们到底要放什么。
John is required to hand over his gold marbles to the shop assistant, who conjures the colored Consty
box, places the gold marbles inside, and locks the box forever.
约翰被要求将他的金弹珠移交给售货员,店员会变出彩色的Consty
盒子,将金弹珠放进去,并永远锁住盒子 。
If you remember, John could directly place his gold marbles in the box or place a special piece of paper which indicated the location of his gold marbles.
如果您还记得的话,John可以直接将他的金色大理石放在盒子里,也可以放一张特殊的纸来标明他的金色大理石的位置。
If he places his gold marbles inside the Consty
box, he cannot add or remove them anymore. They are locked forever.
如果他将自己的金色大理石放在Consty
框中,则无法再添加或删除它们。 他们永远被锁定。
However, if he places the special piece of paper, it is a little different. While he cannot replace the paper, he can add or remove his gold marbles at the location he has specified on the paper.
但是,如果他放特殊纸,则有些不同。 尽管他不能更换纸张,但是他可以在纸张上指定的位置添加或移除他的金弹珠。
Let’s go back to the peculiar problems that prompted the invention of Lety
and Consty
boxes, and decide if they are resolved.
让我们回到促使Lety
和Consty
盒子发明的特殊问题,并确定它们是否得到解决。
With long waiting times for the “assignment” counter, John would forget that he had not placed his gold marbles in his box yet. He would open it to brag to his friends and find only cotton. Bummer!
约翰在等待“分配”柜台的时间很长时,会忘记他还没有将金色大理石放在盒子里。 他会打开它向朋友吹嘘,只发现棉花。 mm!
Since Lety
and Consty
boxes are not created until John heads over to the “initialization” or ”initialization-assignment” counter, respectively, he knows he does not have the box, and thus, does not try to use it. Even if he does, loud alarms installed in the shops start ringing to alert him of the fact.
由于Lety
和Consty
框是在John分别转到“初始化”或“初始化分配”计数器之前才创建的,因此他知道自己没有该框,因此不会尝试使用它。 即使他这样做,商店中安装的响亮警报也会响起,以提醒他这一事实。
Often, John would forget that he had already bought a certain colored box in a shop and newly register for same colored box again. This would instantly result in the disappearance of his existing box (and gold marbles!!), followed by the guard conjuring a new box filled with cotton. No warning! This was especially prevalent at the “special offers” counters.
通常,约翰会忘记自己已经在商店里购买了某个彩盒,然后又重新注册了该彩盒。 这将立即导致他现有的盒子(和金色的大理石弹珠!)消失,接着是警卫人员召唤出一个装满棉花的新盒子。 没有警告! 这在“特别优惠”柜台上尤其普遍。
This is handled by the removal of the “special offers” counters and the introduction of the below rule:
这可以通过删除“特价”柜台和引入以下规则来解决:
Once a resident registers for a certain colored box at the “declaration”desk in the Lety
or Consty
shops, he cannot re-register for the same colored box anymore in that shop! If he does, loud alarms will start blaring.
一旦居民在Lety
或Consty
商店的“声明”柜台注册了某个彩色框,他就无法在该商店重新注册相同的彩色框! 如果他这样做,响亮的警报将开始刺耳。
These wonderful new boxes and rules bought peace and serenity to JavaScript Town once again, and everyone lived happily ever after.
这些奇妙的新盒子和规则再次为JavaScript Town带来了和平与安宁,从此以后每个人都过着幸福的生活。
Let’s go over the technical aspects of var
, let
and const
to understand the story.
让我们回顾一下var
, let
和const
的技术方面,以了解故事。
If you are unfamiliar with hoisting and scope (function-level and block-level), I recommend that you read my previous article here.
如果您不熟悉提升和范围(功能级别和块级别),建议您在此处阅读我的上一篇文章。
Here is an extract to understand the hills analogy I have used above:
这是理解我上面使用的山丘类比的摘录:
To increase our understanding of block level and function level scope, let us consider the analogy of hills. Assume that global scope is the land at sea level and local scopes are hills. If you stand on top of a hill, you can see (access) variables below your altitude. However, if you are sea level, you cannot see (access) variables at a higher altitude.
为了增加对块级别和功能级别范围的理解,让我们考虑一下希尔的类比。 假设全球范围是海平面上的土地,本地范围是山丘。 如果您站在山顶上,可以看到(进入)海拔以下的变量。 但是,如果您处于海平面,则无法在更高的高度看到(访问)变量。
In C++, every block
{}
results in the formation of a new hill (local scope), at an altitude one level higher than the one it is enclosed in. Nested blocks result in multi-level hills.在C ++中,每个块
{}
都会形成一个新的山(局部作用域),其高度比其所围成的山高1层。嵌套的块会形成多层山。
In JavaScript, only a function results in the formation of a new hill (local scope). Other blocks such as
if
blocks are present on the same altitude.在JavaScript中,只有函数会导致形成新的小山(本地范围)。 其他街区(例如,
if
街区位于同一高度)。
Therefore, if a variable is declared on a certain hill (block), it can be accessed from that hill (block) and all hills (blocks) above it.
因此,如果在某个山丘(块)上声明了变量,则可以从该山丘(块)及其上方的所有山丘(块)进行访问。
Declaration Phase: Registration of a variable in its scope, which can be global/function/block scope. In this phase, no memory is allocated yet.
声明阶段 :在变量范围内注册变量,该变量可以是全局/功能/块范围。 在此阶段,尚未分配内存。
Initialization Phase: Allocation of memory for the variable, where a binding is created, and the variable is initialized with undefined
.
初始化阶段 :为变量分配内存,在此创建绑定,并使用undefined
初始化变量。
Assignment Phase: Assignment of a value to the variable.
分配阶段 :将值分配给变量。
It is important to note that variable declaration and declaration phase are not the same!
重要的是要注意变量声明和声明阶段不一样!
A variable declaration is a statement such as var a
.
变量声明是诸如var a
类的语句。
The declaration phase is a step carried out by the JavaScript compiler. In this step, when the compiler encounters a variable declaration, it declares/registers it in its corresponding scope (if the declaration does not already exist). Later on, the code generated by the compiler is executed by the JavaScript engine.
声明阶段是JavaScript编译器执行的步骤。 在此步骤中,当编译器遇到变量声明时,它将在其相应的范围内声明/注册它(如果声明尚不存在)。 稍后,由编译器生成的代码由JavaScript引擎执行。
hoisted: registered in the scope, and initialized with undefined
吊起:在范围内注册,并用undefined
初始化
Below is a simple example where we initialize a variable, update its value, and re-declare it.
下面是一个简单的示例,其中我们初始化变量,更新其值并重新声明。
// Hoistedconsole.log(a); // undefined
var a = 10;console.log(a); // 10
a = 20; // value updated: OKconsole.log(a); // 20
var a = 30; // re-declared: OKconsole.log(a); // 30
At the top of the scope, all variables are declared in their corresponding scope and initialized with a value of undefined
. Registration and initialization are coupled. Thus, variable a
is available for use from the top of the scope. So when we try to access the value of a
before it is declared, it does not throw an error. Rather, undefined
is printed. This is known as variable hoisting.
在作用域的顶部,所有变量都在其对应的作用域中声明,并使用undefined
值初始化。 注册和初始化是耦合的。 因此,变量a
可从范围的顶部使用。 因此,当我们尝试在声明a
之前访问其值时,它不会引发错误。 而是打印undefined
。 这称为可变提升。
Below is an example that shows the function scope of var
.
下面的示例显示var
的功能范围。
function outerFunc() { var a = 10; if (a > 5) { var a = 20; console.log(a); // 20 } console.log(a); // 20}
Variable a
is initially declared in the scope of outerFunc
. Since the if
block does not create a new scope, when we re-declare variable a
, the earlier variable a
gets wiped away and a new variable a
gets created with a value of 20
.
变量a
最初在outerFunc
的范围内声明。 由于if
块不会创建新的作用域,因此当我们重新声明变量a
,较早的变量a
会消失,而创建的新变量a
的值为20
。
Accidental re-declaration of var
variables is a common mistake developers make due to silent re-declaration and confusion in understanding function scope.
意外重新声明var
变量是开发人员由于无声的重新声明和对函数范围的理解上的混乱而经常犯的错误。
Below is a simple example where we initialize a variable, update its value, and try to re-declare it.
下面是一个简单的示例,其中我们初始化变量,更新其值并尝试重新声明它。
console.log(a); // ReferenceError: a is not defined
let a = 10;console.log(a); // 10
a = 20;console.log(a); // 20
let a = 30; // SyntaxError: Identifier 'a' has already been declared
Updating a let
variable is allowed. However, if you try to re-declare it, you encounter a SyntaxError
. This protects developers from silent and accidental re-declaration of variables.
允许更新let
变量。 但是,如果尝试重新声明它,则会遇到SyntaxError
。 这样可以保护开发人员免于无声且无意间重新声明变量。
Are let
variables hoisted?
是否let
变量挂起?
This is a tricky question. The internet is divided on this: there are arguments for both sides. Some developers believe that let
(and const
) variables are not hoisted, because they cannot be accessed before their declaration statement is reached, unlike var
. However, this answer really depends on your definition of hoisting. If hoisting is the coupling of the declaration and initialization phases of a variable at the top of its corresponding scope, then let
and const
variables are not hoisted.
这是一个棘手的问题。 互联网对此有分歧:双方都有争论。 一些开发人员认为, let
(和const
)变量不会被悬挂,因为与var
不同,无法在到达声明语句之前访问它们。 但是,这个答案实际上取决于您对起重的定义。 如果提升是变量的声明和初始化阶段在其相应范围的顶部耦合,则let
和const
变量不会被提升。
However, after reading several opinions and not being any closer to the truth, I decided to go with MDN’s definition of hoisting.
但是,在阅读了几条意见之后,我并没有更接近事实,我决定采用MDN的起重定义。
let
bindings are created at the top of the (block) scope containing the declaration, commonly referred to as "hoisting". (MDN)
let
绑定在包含声明的(块)作用域的顶部创建,通常称为“提升”。 (MDN)
According to this definition, the answer to our question is yes. let
variables are hoisted, but they are not initialized with undefined
. Thus, they exist in a time period called the “Temporal Dead Zone” from the start of the block until their definition is evaluated. Trying to access them in TDZ throws a ReferenceError
, as seen in the example.
根据这个定义,我们问题的答案是肯定的。 悬挂let
变量,但不使用undefined
初始化它们。 因此,它们从块的开始一直存在到称为“临时死区”的时间段,直到评估其定义为止。 如示例所示,尝试在TDZ中访问它们会引发ReferenceError
。
Below is an example that shows block scope of let
.
下面是显示let
块范围的示例。
function outerFunc() { let a = 10; if (a > 5) { let a = 20; console.log(a); // 20 } console.log(a); // 10}
The first declaration of variable a
is in the scope of outerFunc
. The if
block creates a new scope, and when we make the second declaration of variable a
, it gets registered in the new scope. This is independent from the outerFunc
scope. Hence, a separate variable a
is created, and we can observe that changes to the inner variable a
do not affect the outer variable a
.
变量a
的第一个声明在outerFunc
的范围内。 if
块创建一个新的作用域,当我们对变量a
进行第二次声明时,它将在新作用域中注册。 这与outerFunc
范围无关。 因此,创建了一个单独的变量a
,我们可以观察到对内部变量a
更改不会影响外部变量a
。
This allows developers to easily create temporary variables inside condition and looping blocks, without having to search if the variable already exists in the function.
这使开发人员可以轻松地在条件和循环块内创建临时变量,而不必搜索该函数中是否已存在该变量。
Below is a simple example where we initialize a variable, try to update its value, and try to re-declare it.
下面是一个简单的示例,其中我们初始化变量,尝试更新其值,然后尝试重新声明它。
console.log(a); // ReferenceError: a is not defined
const a = 10;console.log(a); // 10
a = 20; // TypeError: Assignment to constant variable.
const a = 30; // SyntaxError: Identifier 'a' has already been declared
const b; // SyntaxError: Missing initializer in const declaration
Similar to let
variables, const
variables are hoisted, but not initialized with undefined
. Trying to access them in the Temporal Dead Zone throws a ReferenceError
.
与let
变量类似, const
变量是悬挂的,但不使用undefined
初始化。 尝试在临时死区中访问它们会引发ReferenceError
。
If we try to initialize a const
variable without an assignment, as in the example above for const b;
, we encounter a SyntaxError: Missing initializer in const declaration
. Similarly, we cannot re-declare const
variables. It leads to a SyntaxError
.
如果我们尝试在没有赋值的情况下初始化const
变量,如上面const b;
的示例所示const b;
,我们SyntaxError: Missing initializer in const declaration
遇到SyntaxError: Missing initializer in const declaration
。 同样,我们不能重新声明const
变量。 它导致一个SyntaxError
。
Let’s temporarily hold off our discussion of updating const
variables.
让我们暂时停止有关更新const
变量的讨论。
Below is an example of block level scope of const
variables:
以下是const
变量的块级范围的示例:
function outerFunc() { const a = 10; if (a > 5) { const a = 20; console.log(a); // 20 } console.log(a); // 10}
The above behavior is similar to let
variables, where a new scope is created for the if
block, and hence, changes to the inner variable a
do not affect the outer variable a
.
上面的行为类似于let
变量,其中为if
块创建了一个新作用域,因此,对内部变量a
更改不会影响外部变量a
。
Let’s return to the discussion of updating const
variables.
让我们回到更新const
变量的讨论。
There is a common misunderstanding that const
variables hold constant values, and cannot ever be updated. However, const
works differently.
常见的误解是const
变量具有常量值,并且永远无法更新。 但是, const
工作方式有所不同。
After the initial assignment, the binding of const
variables is immutable., and therefore, the reference to what is stored inside the const
variable cannot be modified. In the simplest terms, this means you cannot have a statement with just the const
variable on the left hand side, followed by an equal sign =
, and a new value on the right hand side.
初始分配后, const
变量的绑定是不可变的 ,因此,无法修改对const
变量内部存储内容的引用 。 用最简单的术语来说,这意味着您不能拥有仅在左侧具有const
变量,后跟等号=
以及在右侧具有新值的语句。
However, whether the value can be updated depends on what is stored in it. Let’s consider the two cases:
但是,该值是否可以更新取决于其中存储的内容。 让我们考虑两种情况:
If a variable is assigned a primitive data type, the data type gets passed by value. Hence, if we have a statement let x = 10
, we can visualize x
containing the Number 10
.
如果为变量分配了原始数据类型,则该数据类型将通过value传递。 因此,如果我们有一个let x = 10
的语句,我们可以可视化包含数字10
x
。
If a variable is assigned an object, the object is passed by reference. Hence, if we have a statement let x = [1,2,3]
, x
does not contain the array [1,2,3]
. Instead, it contains a reference (address) of where the array [1,2,3]
is stored in memory after its creation. Hence, we can visualize x
containing an address such as 5274621
.
如果为变量分配了对象,则该对象将通过reference传递。 因此,如果我们有一个语句let x = [1,2,3]
,则x
不包含数组[1,2,3]
。 相反,它包含数组创建后在内存中存储的数组[1,2,3]
的引用(地址)。 因此,我们可以可视化包含地址(例如5274621
x
。
Let’s see examples from primitive and object data types:
让我们看一下原始和对象数据类型的示例:
// Booleanconst a = true;a = false; // TypeError: Assignment to constant variable.
// Nullconst b = null;b = 10; // TypeError: Assignment to constant variable.
// Undefinedconst c = undefined;c = 10; // TypeError: Assignment to constant variable.
// Numberconst d = 50;d = 100; // TypeError: Assignment to constant variable.
// Stringconst e = 'hello';e = 'world'; // TypeError: Assignment to constant variable.
// Symbolconst f = Symbol('foo');f = 100; // TypeError: Assignment to constant variable.
As we can see above, trying to update the value of any primitive data type results in a TypeError
.
如上所示,尝试更新任何原始数据类型的值都会导致TypeError
。
/* Arrays are stored by reference.Hence, although the binding is immutable, the values are not. */
const c = [1,2,3];
c.push(10); // No errorconsole.log(c); // [1,2,3,10]
c.pop(); // No errorconsole.log(c); // [1,2,3]
c = [4,5,6]; // TypeError: Assignment to constant variable.
As we can see above, we can push and pop items from the array since this only modifies the contents of what the const
variable is pointing to, but does not try to overwrite the contents of the const
variable itself. However, if we try to update the binding of the const
variable by re-assigning it a completely new array c = [4,5,6]
, it throws a TypeError
.
正如我们在上面看到的,我们可以从数组中推送和弹出项目,因为这只会修改const
变量指向的内容,而不会尝试覆盖const
变量本身的内容。 但是,如果尝试通过为const
变量重新分配一个全新的数组c = [4,5,6]
来更新const
变量的绑定,则会抛出TypeError
。
/* Objects are stored by reference.Hence, although the binding is immutable, the values are not. */
const d = { name: 'John Doe', age: 35};
d.age = 40; // Modifying a property: No errorconsole.log(d); // { name: 'John Doe', age: 40};
d.zipCode = '52534'; // Adding a property: No errorconsole.log(d); // { age: 40, name: "John Doe", zipCode: '52534; }
d = { name: 'Mary Jane', age: 25}; // TypeError: Assignment to constant variable.
As we can see above, we can modify and add properties to the object since this only modifies the contents of what the const
variable is pointing to, but does not try to overwrite the contents of the const
variable itself. However, if we try to update the binding of the const
variable by re-assigning it a completely new object d = { name: 'Mary Jane', age: 25 };
, it throws a TypeError
.
正如我们在上面看到的,我们可以修改对象并向其添加属性,因为这只会修改const
变量指向的内容,而不会尝试覆盖const
变量本身的内容。 但是,如果我们尝试通过为const
变量的绑定重新分配一个全新的对象d = { name: 'Mary Jane', age: 25 };
来更新const
变量的绑定d = { name: 'Mary Jane', age: 25 };
,则抛出TypeError
。
JavaScript now has three kinds of variables, and a natural question is wondering when to use what.
JavaScript现在具有三种变量,一个自然的问题是想知道何时使用什么。
After the introduction of block-scoped let
, the usage of var
is generally discouraged to avoid confusion with function level scope, accidental re-declarations, and hoisting bugs with undefined
value. Unless you have a compelling reason to use function scope of var
, use let
.
在引入块作用域的let
,通常不建议使用var
以避免与函数级别范围,意外的重新声明以及undefined
值的错误的混淆。 除非您有充分的理由使用var
函数范围,否则请使用let
。
Use const
to hold values that are facts, such as const PI = 3.14
, or values that should strictly remain unmodified for the entire execution of the program.
使用const
可以保存事实值,例如const PI = 3.14
,或者在整个程序执行期间应严格保持不变的值。
A common programming approach consists of developers starting off by declaring all variables with const
, and progressively converting them to let
variables if the need arises. Personally, I start with let
variables, and convert them to const
variables if I see the need. There is no set approach, and you should use what works best for your code.
一种常见的编程方法是,开发人员首先使用const
声明所有变量,然后在需要时逐步将其转换为let
变量。 就个人而言,我先从let
变量开始,然后在需要时将它们转换为const
变量。 没有固定的方法,您应该使用最适合您的代码的方法。
If you have time, I strongly suggest that you read the fictional piece again as it will cement the connections in your mind with the additional technical knowledge.
如果您有时间,我强烈建议您再次阅读该虚构的文章,因为它会通过其他技术知识使您的思维联系更加牢固。
Thank you for reading! I hope you learned something new, and I would love to receive feedback.
感谢您的阅读! 希望您能学到新知识,也希望收到反馈。
Follow me on Twitter here, and LinkedIn here.
跟随我的Twitter 在这里 ,和LinkedIn 在这里 。
References:
参考文献:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/const
https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
https://dmitripavlutin.com/variables-lifecycle-and-why-let-is-not-hoisted/
var和let和const