理解JavaScript的变量,作用域和提升机制
前言
变量是编程中最基础的概念,也是我们学习编程中最早接触且最重要的知识点。在JavaScript中我们有三种方式声明一个变量,通过关键词var
, let
, const
。
在这篇文章中,我们将理解变量是什么?怎么去声明和命名变量?var
, let
, const
三者之间的区别?全局变量和局部变量的含义,区别和使用场景?
懒人包
ECMAScript 2015(ES6)引入的let
,const
是为了解决var
,只有函数作用域无块作用域,声明提升会产生异常,可被重新声明,不分常量变量的现实问题。所以现在我们尽量要使用let
,const
,摒弃var
的声明方式。尽可能多地使用const
,在循环和重新分配的情况下使用let
。 通常可以在处理遗留代码之外避免使用var
。
理解变量
抽象点说,变量是储存值的命名容器。我们将未来有可能多次引用的值存储在变量中。在JavaScript中变量包含的值不只是数字。它可以是任何JavaScript数据类型,例如对象和字符串。
因为在JavaScript基于ECMAScript 2015(ES6)语言规范之前,只有一种方法来声明变量-使用关键字var
,所以市面上很多陈旧的代码和资料还是会只将var
用作变量,但是在新关键词let
,const
引入的ES6时代下,如何理解和使用let
,const
十分必要。
我们可以通过var
演示变量本身的概念,下图列子,我们会声明一个变量,并为其分配一个值。
// Assign the string value Sammy to the username identifier
var username = 'sammy_shark'
该语句包含以下部分:
- 使用var关键字声明变量
- 变量名称(标识符)用户名
- 赋值操作,用 = 语法表示
- 分配值 sammy_shark
那现在我们可以在代码中使用username
,JavaScript会记住username
携带的String类型的值sammy_shark
// Check if variable is equal to value
if (username === 'sammy_shark') {
console.log(true)
}
true
变量可以被用来代表JavaScript中任何类型。在下面例子中我们可以把string, number, object, Boolean和 null 赋值到变量中。
// Assignment of various variables
var name = 'Sammy'
var spartans = 300
var kingdoms = ['mammals', 'birds', 'fish']
var poem = { roses: 'red', violets: 'blue' }
var success = true
var nothing = null
使用console.log
,我们可以特定变量中包含的值内容。
// Send spartans variable to the console
console.log(spartans)
300
变量在内存中存储数据,以后就可以直接访问和修改。变量也可以重新分配和赋予新值。在下面简化的例子中,我们将演示如何把password存储到一个变量中,并且更新。
// Assign value to password variable
var password = 'hunter2'
// Reassign variable value with a new value
password = 'hunter3'
console.log(password)
'hunter3'
在实际程序中,password大多被安全的存储在数据库中,但是某些情况下,我们可能需要更新变量值 ,如上面例子password
的原值是 hunter2
, 但是我们要把 hunter3
赋予它 。
命名变量
变量名称在JavaScript中被称为 标识符。我们在了解JavaScript的语法和代码结构中讨论了标识符命名的一些规则。以下是必须遵循的一些规则。
- 变量名可以由字母(a-z),数字(0-9),美元符号($)和下划线(_)组成。
- 变量名称不得包含空格(制表符或空格)
- 变量名称不能以数字开头
- 命名的变量不能包含任何保留关键字
- 变量名称区分大小写
JavaScript还具有使用驼峰大小写的约定(有时称为camelCase),这是写第一个单词为小写,随后所有单词都大写的惯例。除某些例外,大多数标识符将遵循此约定。
看上去要遵循的规则有点多,其实驼峰式写法就可以满足正常工作需求。
作用域
作用域 scope
在JavaScript中指当前代码的上下文,它决定了变量对JavaScript的访问性。在JavaScript中有两种作用域 局部 和 全局。
全局变量被声明在块block
外部。局部变量被声明在块中。在下面的例子中,我们将创建一个全局变量。
// Initialize a global variable
var creature = 'wolf'
我们学过变量可以被重新赋值。使用局部作用域,实际我们可以在不改变初始值情况下新建一个和外部作用域同样名称的变量。
在下面的示例中,我们将创建一个全局species
变量。 而在函数内部有个具有相同名称的局部species
变量。 在控制台打印它们,我们可以看到变量的值根据作用域而有所不同,并且原始值没有更改。
// Initialize a global variable
var species = 'human'
function transform() {
// Initialize a local, function-scoped variable
var species = 'werewolf'
console.log(species)
}
// Log the global and local variable
console.log(species)
transform()
console.log(species)
human
werewolf
human
在这个例子中局部变量是 函数作用域。用var
关键词声明的变量是函数作用域,意味着它们仅将函数识别为一个单独的作用域。局部作用域的变量不能被全局作用域变量访问。
新关键字 let
and const
是 块状作用域,这意味着不仅在函数块中创建了局部作用域,而且还从任何其他块创建作用域。JavaScript中其他类型的代码块由关键字组成,例如if
,for
和while
。
为了说明函数作用域变量和块作用域变量之间的区别,我们将使用let
在if
块中分配一个新变量。
var fullMoon = true
// Initialize a global variable
let species = 'human'
if (fullMoon) {
// Initialize a block scoped variable
let species = 'werewolf'
console.log(`It is a full moon. Lupin is currently a ${species}.`)
}
console.log(`It is not a full moon. Lupin is currently a ${species}.`)
It is a full moon. Lupin is currently a werewolf.
It is not a full moon. Lupin is currently a human.
在此示例中,species
变量在全局范围内具有一个值(human
),在局部具有另一个值(werewolf
)。但是如果我们使用var
,将会有不同的结果。
var fullMoon = true
// Use var to initialize a variable
var species = 'human'
if (fullMoon) {
// Attempt to create a new variable in a block
var species = 'werewolf'
console.log(`It is a full moon. Lupin is currently a ${species}.`)
}
console.log(`It is not a full moon. Lupin is currently a ${species}.`)
It is a full moon. Lupin is currently a werewolf.
It is not a full moon. Lupin is currently a werewolf.
在本示例的结果中,全局作用域变量和块作用域变量最终都具有相同的值werewolf
。 这是因为您没有在var
中创建新的局部变量,而是在同一作用域中重新分配了相同的变量。var
不理解if
是另一个新作用域的一部分。
总而言之,作用域是变量对JavaScript的可见性。 全局作用域是作用域的最外部上下文,而局部作用域是更具体的作用域。 局部作用域有两种类型-函数作用域和块作用域。var
仅限于函数作用域,这意味着只能在函数内部创建新作用域。let
和const
具有块作用域,这意味着任何块都会创建一个新的局部作用域,例如if
,for
和while
。 块作用域更安全,因为它产生的代码不太可能无意间覆盖变量值。
提升机制
到目前为止,在大多数示例中,我们都使用var
来声明一个变量,并使用一个值对其进行了初始化。 声明和初始化后,我们可以访问或重新分配变量。
如果我们在声明和初始化变量之前尝试使用它,将返回undefined
。
// Attempt to use a variable before declaring it
console.log(x)
// Variable assignment
var x = 100
undefined
但是,如果省略var
关键字,则不再声明该变量,而只是对其进行初始化。 它将返回ReferenceError
并停止脚本的执行。
// Attempt to use a variable before declaring it
console.log(x)
// Variable assignment without var
x = 100
ReferenceError: x is not defined
这样做的原因是由于提升,这是一种JavaScript操作,其中变量和函数声明移至其作用域的顶部。 由于只提升实际的声明,而不提升初始化,因此第一个示例中的值返回undefined
。
为了更清楚地展示,下面是我们编写的代码以及JavaScript实际如何解释它。
// The code we wrote
console.log(x)
var x = 100
// How JavaScript interpreted it
var x
console.log(x)
x = 100
在执行脚本之前,JavaScript将x
作为变量保存到内存中。 由于在定义之前调用了它,因此结果是undefined
而不是100
,但是它不会导致ReferenceError
并暂停脚本。
我们原来预估的最可能的x的期望输出为true,结果输出是undefined
。
// Initialize x in the global scope
var x = 100
function hoist() {
// A condition that should not affect the outcome of the code
if (false) {
var x = 200
}
console.log(x)
}
hoist()
undefined
在上面示例中,我们在全局中将x
声明为100。 在if
语句中,x
可能会更改为200
,但是由于条件为false
,它应该不会影响x
的值。 取而代之的是,将x提升到了hoist()函数的顶部,并且值变成了undefined。
这种不可预测的行为可能会导致程序中的错误。 因为let
和const
是块作用域的,所以它们不会以这种方式提升,如下所示。
// Initialize x in the global scope
let x = true
function hoist() {
// Initialize x in the function scope
if (3 === 4) {
let x = false
}
console.log(x)
}
hoist()
true
变量的重复声明,在var
中使用没有问题,但在在let
和const
中将引发错误。
// Attempt to overwrite a variable declared with var
var x = 1
var x = 2
console.log(x)
2
// Attempt to overwrite a variable declared with let
let y = 1
let y = 2
console.log(y)
Uncaught SyntaxError: Identifier 'y' has already been declared
总而言之,var
允许提升,即将变量声明保存到内存中。 这会导致代码中未定义变量的意外结果。 引入let
和const
解决了这个问题,方法是在声明变量之前尝试使用变量或多次声明变量时抛出错误。
常量
我们已经学习了使用var
创建变量的方法,并且了解了let
和const
如何解决与范围和提升有关的潜在问题。 因此,建议停止使用var
,而使用较新的let
和const
。 尽管let
可以做var
可以做的一切,但是const
还有一些其他规则可以遵循。
许多编程语言都有常量,它们是不能修改或更改的值。 const是根据常量建模的,分配给const的值不能重新分配。
// Assign value to const
const SPECIES = 'human'
// Attempt to reassign value
SPECIES = 'werewolf'
console.log(SPECIES)
Uncaught TypeError: Assignment to constant variable.
尝试重新分配SPECIES
将导致错误。
由于无法重新分配const
值,因此需要同时声明和初始化它们,否则还会引发错误。
// Declare but do not intialize a const
const TODO;
console.log(TODO);
Uncaught SyntaxError: Missing initializer in const declaration
通常将所有的const
标识符都写成大写。 这易于与其他变量值区分开。
在编程中无法更改的值称为“不可变”,而相反的值则称为“可变”。 虽然不能重新分配const,但它们不是不变的,因为可以修改对象的属性。
// Create a CAR object with two properties
const CAR = {
color: 'blue',
price: 15000,
}
// Modify a property of CAR
CAR.price = 20000
console.log(CAR)
{ color: 'blue', price: 20000 }
总而言之,不能重新分配const
值,必须将其及其声明一起初始化。
var, let和const区别
JavaScript具有三个不同的关键字来声明变量,这为语言增加了一层复杂性。 两者之间的差异基于范围,提升和重新分配。
关键词 | 作用域 | 提升 | 可以被重新赋值 | 可以被重新声明 |
---|---|---|---|---|
var |
函数作用域 | Yes | Yes | Yes |
let |
块作用域 | No | Yes | No |
const |
块作用域 | No | No | No |
您可能想知道应该在自己的程序中使用这三个关键词。 普遍接受的做法是,尽可能多地使用const
,在循环和重新分配的情况下使用let
。 通常可以在处理遗留代码之外避免使用var
。