3.3 变量
可以保存任意类型值
var, const, let 声明变量
const与let只能在es6以及更新的版本才能用
3.3.1 var
variable 的缩写
var message;
不进行初始化时默认保存undefined
var message;
console.log(message); // undefined
声明时进行初始化
var message = "hello world";
作用域:
函数作用域, 非块作用域
function myFunc(){
var message = "hello world";
}
myFunc();
console.log(message); // 报错
省略关键字直接定义变量, 会导致这个变量成为一个全局变量
function myFunc() {
message = "hello world"; // 全局变量
}
myFunc(); // 只要调用一次这个函数就可以在函数外访问到
console.log(message); // hello world
不推荐这样使用, 很难维护, 突然冒出来的一个变量都不知道是干啥的
严格模式下这样会报错
定义多个变量
var message1 = "hi",
message2 = "hello world",
message3;
换行与空格缩进非必须, 但是如同上面的形式便于阅读
var声明提升问题:
var声明的变量会提升为在函数作用域的顶部声明(不包括赋值, 也就说如果有赋值则在原来位置上赋值)
console.log(message); // undefined
var message = "hi";
console.log(message); // hi
上面这段代码相当于下面:
var message;
console.log(message); // undefined
message = "hi";
console.log(message); // hi
提升会将所有变量声明都提升到函数作用域的顶部
使用var反复声明同一个变量不会出错
function test() {
var message = 1;
var message = "hi";
var message;
var message = true;
console.log(message);
}
test(); // true
如上段代码可以理解为先提升, 然后有四个message变量的var声明, 合并为一个, 所以不会报错
在全局作用域声明的变量会成为window对象的属性
var message = 'hi';
console.log(window.message); // hi
3.3.2 let
let声明的变量是块作用域
{
let message = "hi";
console.log(message); // hi
}
console.log(message); // 报错
块作用域是函数作用域的子集, 所以let声明的变量离开他所在函数时也无法使用
function test() {
let message = "hi";
}
console.log(message); // 报错, message is not defined
let不允许在同一作用域出现冗余声明
{
let message;
let message; // 报错
}
// 嵌套不会受影响
{
let message;
{
let message; // 可以
}
}
// 不受关键词影响
{
var message;
let message; // 报错
}
暂时性死区
let 声明的变量不会在作用域中提升
{
console.log(message); // 报错
let message = "hi";
}
但是js引擎会注意到出现在块后面的let声明, 不过在let声明执行之前, 都无法引用未声明的变量, 这称为暂时性死区
可以理解为实际上有提升(目前大多数都这样认为), 但是由于无法在原let声明出现前引用, 没有形式上如同var一样的提升, 所以看作没有提升
全局声明
let在全局作用域中声明的变量不会成为window对象的属性
var message1 = "hi";
let message2 = "hello world";
console.log(window.message1); // hi
console.log(window.message2); // undefined
不过,let声明仍然是在全局作用域中发生的,
相应变量会在页面的生命周期内存续。
因此,为了避免SyntaxError,必须确保页面不会重复声明同一个变量。
** 条件声明 **
同一作用域用多次用var声明同一个变量不会出问题,
但是对于let你无法检查前面是否已经使用let声明过同名变量
不信你试试
所以let无法依赖于条件声明模式
for循环中的let声明
let解决了循环定义的迭代变量渗透到循环体外部的问题
// var
for(var i = 1; i <= 5; i++) {
// 循环逻辑
}
console.log(i); // 5, 循环中定义的迭代变量i渗透到循环体外
// let
for(let j = 1; j <= 5; j++){
// 循环逻辑
}
console.log(j); // 报错, j没有定义, 解决了var的渗透问题
var迭代变量问题:
for(var i = 0; i< 5; i++){
setTimeout(()=>console.log(i), 0);
}
// 实际输出的是5,5,5,5,5
这里注意, js是单线程的, 先执行i=0并判断了他比5小后, 开始执行第一次循环体中的内容, 但是这里发现是setTimeout, 而setTimeout是异步的, 于是将他放入一个任务队列, 等到当前的同步任务执行完后在执行这个队列里的任务, 这样就导致了将循环语句执行完了但是把五个setTimeout全加到队列里没有执行, 而最后i=5时循环体不再继续, 这时这个队列里有五个setTimeout函数, 但此时都是公用一个i=5, 就导致了输出了5个5, 这里虽然setTimeout设置的时间为0, 事件本身没有延迟,但是改变了任务执行的先后顺序,执行的先后顺序又导致了微小的延迟(执行其他语句的延迟而非专门等了多少毫秒才执行setTimeout的设置的延迟)
具体可以看看下面这个例子
// 1
alert(1);//第一个输出
// 2
setTimeout(function() {
alert(2); //待同步任务执行完毕之后才输出
},0); //虽然设置的是0
// 3
alert(3);//第二个输出
// 2本身没有延迟但是由于顺序改变了, 要先执行完3才能执行2, 这里有一段微小延迟, 是执行顺序引起的而非函数中延迟参数设置引起的
而let可以解决上述问题
使用let时, js引擎在后台会为每个迭代循环声明一个新的迭代变量(虽然都叫i但是相当于在不同的块中)
每个setTimeout引用的都是不同的变量实例
for(let i = 0; i < 5; i++){
setTimeout(()=>console.log(i), 0);
}
这种每次迭代声明一个独立变量实例的行为适用于所有风格的for循环,
包括for-in和for-of循环
3.3.3 const
const与let基本相同
区别是: 声明时必须同时初始化, 且后续再尝试修改const声明的变量会导致错误
const message1 = 'hi';
console.log(message1); // hi
// const声明的变量的作用域也是块
{
const message2 = 'hello';
console.log(message2); // hello
}
try{
console.log(message2);
}
catch(err){
console.log('something wrong about message2')
}
// something wrong about message2
// 必须声明时初始化
const message3; // 报错
const声明不可修改的限制只限定于你指定的变量的引用, 也就是你不可以再更改去引用另一个对象, 但是你引用的对象的引用其实是可以更改的
let person1 = {
name = "zhangsan",
age = 18
}
let person2 = {
name = "lisi",
age = 20
}
const student1 = person1;
student1 = person2; // wrong
person1.age+=1; // ok
注意你给你声明的const变量赋的是一个变量还是一个值
let person = {
name: 'zhangsan',
age: 18
}
const student1_age = person.age;
// 这里person.age其实是一个值而非一个变量, 相当于const student1_age = 18, 与person.age没有引用关系
// 所以修改person.age不会引起因为是const声明的变量所以无法修改的错误
person.age = 19; // ok
console.log(student1_age); // 18
console.log(person.age); // 19
使用const声明一个不会被修改的for循环变量, 也就是说每次迭代只是创建一个新的变量, 这对for-in和for-of循环特别有意义
// 1
for(const key in {a:1, b:2}) {
console.log(key);
}
// a, b
// 2
for(const value of [1,2,3,4,5]) {
console.log(value);
}
// 1,2,3,4,5
// 3
for(const i = 0; i < 5; i++){
//something
}
// 报错
其实看到这里疑惑大大的, 但是想了会, 这里说说我的看法
对于上述代码片段3, 执行了5次循环体, 这五次执行使用的作用域应该是一个块, 而对于每次执行, 都再使用另一个块执行逻辑体并传入当时i的值, 也就是一个()对应五个{},对于使用let的循环体参数i, 自然没有影响, 但是使用const会导致i自增, 而const声明的变量不可以再修改, 而报错
但是对于for-of与for-in我还是解释不通, 除非再将()分作几个作用域, 对片段1来说{a:1, b:2}相当于将()再分为两个块, 那么这俩虽然名字都叫i但是也不会引起冲突
3.3.4 声明风格及最佳实践
- 不使用var
使用let与const代替var
- const优先, let次之
使用const声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。
因此,很多开发者认为应该优先使用const来声明变量,只在提前知道未来会有修改时,再使用let。
这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为
当然typescript更好的解决了这些问题