3.3:变量
ECMAScript是松散类型,意思就是变量可以用于保存任何类型的数据。每个变量只不过是一个用于保存任意值的命名占位符。有3个关键字可以声明变量:var、const 和 let。var在ECMAScript的所有版本都可以使用,而const和let只能在ECMAScript6及更晚版本中使用。
3.3.1: var 关键字
可以使用var操作符,后跟变量名,效果如下:
var message;
这行代码定义了一个名为message的变量,可用用它保存任何类型的值(不初始化的情况下,变量会保存一个特殊值undefined)
var message = "hi";
在这里,message只是被定义成为一个保存字符串值hi的变量,这个保存值是可以被改变,也可以改变值的类型:
var message = 'hi'
message = 100 //虽然合法,但不推荐
在上面这个例子中,变量message被定义为一个保存字符值hi的变量,之后哟又被重写为保存了数值100,虽然只是不推荐,但在ECMAScript中是完全有效的。 1.var声明作用域
关键的问题是,使用var操作符定义的变量会成为包含它的函数的局部变量。比如,使用var在一个函数内部一定一个变量,也就意味着该变量将在函数退出时被销毁。
function test() {
var message = "hi" //局部变量
}
test()
console.log(message)//出错
在这上面的代码中,message 变量是在函数内部使用var定义。函数叫做test(),调用它会创建这个变量并给它复制,调用之后变量随即会被销毁,因此上面的代码中最后一行出现错误,如果在函数内部不使用var操作符直接起一个变量,会是一个全局变量.
function test() {
message = "hi" //全局变量
}
test()
console.log(message) //"hi"
如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量.
var message = "hi",
found = false,
age = 29
这里定义初始化3个变量,ECMAScript在之前说过是松散类型的,所以使用不同数据类型初始化的变量可以用一条语句来声明。插入换行和空格缩进是不被必需的,但在严格模式下,不能定义名为eval和arguments的变量,会导致语法的错误。
3.3.2:let声明
let跟var其实作用差不多,区别在于,let声明的范围是块作用域,而var声明的范围是函数作用域。
if(true) {
var name = "Matt";
console.log(name) //Matt
}
console.log(name) //Matt
if(true) {
let age = 26;
console.log(age) //26
}
console.log(age)
// ReferenceError: age 没有定义
在这里,age变量之所以不能在if块外部被引用,是因为它的作用域仅限于该块内部。块作用域是函数作用域的子集,因此适用于var的作用域限制同样也适用于let。
补充:let 不允许同一个块作用域中出现冗余的声明,会导致报错;
var name;
var name;
let age;
let age; //SyntaxError;标识符age已经声明过了
JavaScript引擎会记录用于变量声明的标识符及所在的块作用域,因此嵌套使用相同的标识符不会报错,这是因为同一个块中没有重复声明.
var name = "Nicholas";
console.log(name);//"Nicholas"
if(true) {
var name = 'Matt'
console.log(name)//'Matt'
}
let age = 30;
console.log(age)
if(true) {
let age = 26;
console.log(age) //26
}
对声明冗余报错不会因混用let和var而受影响。这两个关键字声明并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。
var name;
let name; //SyntaxError
let age;
var gae; //SyntaxError
1.暂时性死区:
let 与 var 的另外一个重要的区别,就是let声明变量不会在作用域中被提升。
//name 会被提升
console.log(name) //undefined
var name = "Matt";
//age 不会被提升
console.log(age) // ReferenceError 没有定义
let age = 26;
解析代码时,JavaScript引擎也会注意出现在块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量,let声明之前的执行瞬间被称为“暂时性死区(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError”
2.全局声明:
与var关键字不同,使用let在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)
var name = 'Matt';
console.log(window.name); 'Matt'
let age =26;
console.log(window.age) //undefined
不过,let声明依旧是在全局作用域中发生的,相应变量会在页面的生命周期内存续,因此,为了避免SyntaxError,必须确保页面不会重复声明同一个变量。
3.条件声明:
在使用var声明变量时,由于声明会提升,JavaScript引擎会自动将多余的声明在作用域顶部合并为一个声明。因为let的作用域是块,所以不可能检查前面是否已经使用let声明过同名变量,同时也就不可能在没有声明的情况下声明它。
<script>
var name = "Nicholas";
let age = 26;
</script>
<script>
//假设脚本不确定页面中是否已经声明了同名变量
//那它可以假设还没有声明过
var name = "Matt";
//这里没有问题,因为可以作为一个提升声明来处理
//不需要检查之前是否声明过变量名
let age = 36;
//如果age之前声明过,这里会报错
</script>
即使使用try/catch语句或typeof操作符也不能解决,因为条件块中let声明的作用域仅限于该块。
<script>
let name = "Nicholas";
let age = 36;
</script>
<script>
//假设脚本不确定页面中是否已经声明了同名变量
//那它可以假设还没有声明过
if(typeof name === 'undefined') {
let name;
}
//name被限制在if {}块的作用域内
//因此这个赋值形同全局赋值
name = 'Matt';
try {
cosole.log(age) //如果没有声明过,则会报错
}
catch(error) {
let age;
}
//age被限制在catch{}块的作用域内
//因此这个赋值形同全局赋值
age = 26;
</script>
[注意] 对于 let 这个新的 ES6 声明关键字,不能依赖条件声明模式。
4.for 循环中的let 声明:
在let出现之前,for循环定义的迭代变量会渗透到循环体外部:
for (var i = 0; i < 5; ++i) {
//循环逻辑
}
console.log(i) //5
改成let之后,这个问题就消失了,因为迭代变量的作用域仅限于for循环块内部:
for (let i = 0; i < 5; ++i) {
//循环逻辑
}
console.log(i) //ReferenceError: i 没有定义
在使用var时候,最常见的问题就是对迭代变量的奇特声明和修改:
for (var i = 0; i <5; ++i) {
setTimeout(()=>console.log(i),0)
}
//实际输出是 5 5 5 5 5 而不是 0 1 2 3 4
原因:
- 退出循环的时候,迭代变量保存的是导致循环退出的值
- 在之后执行超时逻辑时,所有的i都是同一个便利那个,所以输出的都是同一个值
而在使用let声明迭代变量的时候,JavaScript引擎在后台会为每个迭代循环声明一个新的迭代变量。
每个setTimeout引用的都是不同的变量实例,所以console.log输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值
for (let i = 0; i < 5; ++i) {
setTimeout(()=>console.log(i),0)
}
//会输出 0 1 2 3 4
这种每次迭代声明一个独立变量实例的行为适用于所有风格的for循环,包括for-in 和 for-of 循环。
3.3.3: const 声明
const的行为与let基本相同,唯一一个区别就在与它声明变量必须同时初始化变量,切尝试修改const声明的变量会报错:
const age = 26;
age = 36; //TypeError; 给常量赋值
//const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; //SyntaxError
//const声明的作用域也是块
const name = 'Matt';
if(true) {
const name = 'Nicholas';
}
console.log(name) //Matt
const声明的限制只适用于它指向的变量的引用。换句话说,如果const 变量引用的是一个对象,那么修改这个对象内部的属性是不违反 const 的限制。
const person = {}
person.name = 'Matt'; ok
JavaScript引擎会为for循环中的let声明分别创建独立的变量实例,虽然const 变量跟let变量类似,但不能用const来声明迭代变量 (因为迭代变量会自增);
for (const i = 0 ; i < 10; ++i) {
} //TypeError:给常量赋值
不过,如果你只想用const声明一个不会被修改的for循环变量,那也是可以,也就是说每次迭代只是创建一个新变量.这对for-of和for-in循环特别有意义
let i = 0;
for (const j = 7; i <. 5; ++i){
console.log(j)
}
// 7 7 7 7 7
for (const key in {a:1,b:2}) {
console.log(key)
//a , b
}
for (const value of [1,2,3,4,5]) {
console.log(value);
}
// 1 2 3 4 5
3.3.4:声明风格及最佳实践
ES6增加了let 和 const从客观上来讲对着门语言有更加精准地声明作用域和语义提供了更好支持
注意⚠️事项:
- 不使用var,因为有了let 和 const ,大多数开发者都会不再需要var,限制自己只使用let 和 const 有助于提升代码质量
- const 优先,let次之,因为const声明可以让浏览器运行强制保持变量不改变,也可以让静态代码分析工具提前发现不合法的赋值操作,因此,很多开发者认为应该优先使用const来声明变量,然后再使用let 可以让开发者有信心推断某些变量的值不会改变,同时也能迅速发现因意外赋值导致的非预期行为。
小呼吁:在变量这一篇可以提取到很多有用的细节点,希望大家可以认真看,每次回味之前学过的知识肯定会有遗漏或为什么是这样,在变量这一小节中为什么要使用let和const多于var