各自特性
var
- 使用var定义变量,可以保存任何类型的值,可以不进行变量值的初始化,此时变量会保存undefined。
var a;(注:使用const声明变量不进行赋值则会引发错误❌) - var关键字存在变量提升的特性,变量的声明会提升到所在作用域顶部。(只会提升声明,不会提升赋值,此时打印值为undifined)
- 使用var声明变量,在函数体内,为局部变量,在函数体外则是全局变量,所以var,存在函数作用域和全局作用域。(当在全局作用域通过var声明变量时,该变量会作为属性则会挂载到全局对象(一般是window)上,如已存在该属性,则更新属性值)
- 当var在语句块中
{}声明变量,会造成变量泄露和覆盖外层作用域变量。 - var在with语句块中的问题和性能忧患!!!(极坑,详解见文章末尾)。
let
- let存在块级作用域,即一个大括号
{}就是一个块级作用域(es6新增),语句块外部无法访问内部的变量。
防止内部变量覆盖外部作用域变量或者泄露为全局变量
// var 没有块级作用域的概念,使用var此时会泄露为全局变量。
if (true) {
let a = 10;
var b = 20;
}
console.log(a); // a is not defined
console.log(b) // 20
// 覆盖外部作用域变量
var num = 1;
(function () {
console.log('num',num)
// 期望打印num的值为1,实际打印为undifined,使用let则不会。
if(true) {
var num = 2;
}
})()
-
let不存在变量提升的特性,在声明行前使用或打印,则会引发错误。
-
let不允许在同一作用域内,重复声明同一个变量。
-
let声明的变量不会挂载到全局对象下。
const
- const基本特性和let一致,不存在变量提升,不允许重复声明,声明不会挂载到全局对象下,也存在块级作用域的限制。
- 使用const声明变量,必须马上进行初始化赋值,否则报错。
- const声明的可以认为是个常量,一旦赋值就不能改变,否则报错。(当值为引用类型时,比如对象或数组,可以通过属性或者方法push来修改内部数据,实际上,此时常量储存的是引用类型数据的地址值,只要不修改该地址值,则不会引发错误)
不使用关键词声明
非严格模式下,直接给变量赋值或初始化,则该变量为全局变量,且会挂载到全局对象 (一般是window) 下。
严格模式下(use strict),未声明直接赋值,则会直接抛错(ReferenceError: a is not defined)
a = 1;
(function() {
a = 2;
b = 3;
})()
console.log(a); // 2
console.log(b); // 3
One More Thing
关于var变量提升的命名冲突
以下例子会打印函数而不是undifined,因为在推入执行上下文栈时,首先会处理函数声明,其次会处理变量声明,当变量名称跟已经声明的形参名称或上下文函数名称相同的时候,变量声明不会覆盖或影响已存在的声明。
console.log(a); // function a(){}
var a = 1;
function a(){}
暂时性死区Temporal Dead Zone(TDZ)
在块级作用域内,变量一旦用let声明,则该变量就直接绑定该作用域。ES6明确规定,如果语句块存在let,const命令,从一开始就形成了封闭区域,那么在声明之前,该变量都不可使用,术语称为“暂时性死区”
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
var a = 1;
if (true) {
a = 2; // ReferenceError
let a = 3;
}
// 由于全局代码存在变量a,但是if语句块内又声明了一个变量a,a从一开始直接绑定这个语句块,无法读取全局作用域的变量a,在执行到声明a那一行之前,都无法获取a,所以直接赋值会报错。
for循环为何推荐使用let?
如果你已仔细阅读以上关于变量作用域和变量提升的,大概已经大致知道答案了。
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// 最终会打印 5 5 5 5 5
首先,你得了解,setTimeout是异步的宏任务,执行顺序是,先执行5次for循环 =》 再执行5次宏任务的回调打印i, 执行顺序设计到js的事件循环机制,不懂可以另行科普。
梳理一遍:
- 因为var没有块级作用域的限制,使用var声明变量的会泄露为全局变量,每次循环更改的i都是同一个i。
- 在执行定时器回调的时候,循环已结束,i的值最终更新为5,所以打印i的值都是5。
- 将var修改为let,因为let存在块级作用域的特性,所以每次循环的块级作用域内部都会创建新的独立的i,所以最终打印会有正确的索引值。
在with中使用var的问题⭐️⭐️⭐️⭐️⭐️
JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在 statement 中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。
// 普通使用with指定命名空间
var person = {
name: 'Alice',
age: 30
};
with (person) {
console.log(name); // 输出 'Alice'
console.log(age); // 输出 30
}
当在with中使用var
const o1 = {};
with(o1) {
var a = 1;
}
console.log(a); // 1
console.log(o1.a) // undifined
console.log(window.a) // 1
以上体现了一个问题,with语句块中变量a,在全局也可以访问到,说明变量泄露到了外层作用域,此时假如在with中使用变量a,js编译器需要顺着作用域链向上查找,存在性能问题,也无法在with语句下做局部缓存。
当o1对象已存在同名属性
const o1 = {a: 0};
with(o1) {
var a = 1;
}
console.log(a); // undifined
console.log(o1.a) // 1
console.log(window.a) // undifined
可以看出,当with语句指定的命名空间对象中,存在和var声明的变量同名属性时,则会污染该属性,值将会被更新。
当换成let,const
const o1 = {a: 10, b: 20};
with(o1) {
const a = 11;
let b = 20;
}
console.log(window.a) // undifined
console.log(window.b) // undifined
console.log(o1.a) // 10
console.log(o1.b) // 20
console.log(a); // ReferenceError: a is not defined
console.log(b) // ReferenceError: b is not defined
总结:
当在with语句块中使用var关键字声明变量时,首先会检查with指定的命名空间对象(with决定的词法环境)中是否有同名属性,如果有则更新该属性值,如果没有,则该变量将泄露到外层作用域 **(with语句也可能处于函数体中) **,写到对应的外层词法环境中。
此时会影响到代码的性能,因为每次访问变量,都要沿着作用域查找变量,而这个过程比直接访问变量更耗时,换成let,const则可以直接将变量写入with下的词法环境。