之前遇到过一位后端大哥自告奋勇,参与前端一个vue3项目的功能实现。当功能完成时大哥体现出他对vue3框架正确的认知,但轮到我看代码时,满屏的var让我不禁汗颜。
“声明变量不就是用var吗?“
“let和var不都是声明变量,你直接把var替换成let不就行了......”
相信不只是这位大哥,也有不少同学在学习ES6的知识后,也是简单认为let和const就只是代替了var的新用法。
变量提升
ES6之前我们只学了var关键字进行变量的声明,同时我们可以复习下变量提升的概念。
var声明的变量具有变量提升的特性,在javascript的创建阶段过程中,会对声明的变量与函数进行收集,提升到作用域的顶部。
其中有两个细节:
1.只有声明被提升(变量会初始化为undefined)
2.当变量名和函数名冲突时,函数会被优先提升
具体看下变量提升的效果:
console.log(num) // undefined
console.log(fun) // function fun(){...}
var num = 1
var fun = 2
function fun(){
// ...
}
console.log(num) // 1
console.log(fun) // 2
一、let、const
1.let和const的具体使用
ES6新增了let
和const
关键字:
-
let
用于声明变量,用法与var
类似 -
const
用于声明常量:-
与变量不同,常量是一个恒定的值,只读,不可修改
-
常量在定义时必须进行初始化赋值
-
-
相同特性:
-
在相同作用域内,无法对同一个
变量/常量
进行重复声明 -
存在暂时性死区
-
会形成块级作用域
-
不会在全局声明时(在最顶层作用域)创建window对象的属性
-
我们通过代码,观察以上的特性:
- const声明的常量无法被修改。
let varData = 'variable';
const constData = 'constant';
varData = 1;
constData = true // Uncaught TypeError: Assignment to constant variable.
- const声明时必须进行初始化(let可以不进行初始化赋值)。
let varData;
const constData; // Uncaught SyntaxError: Missing initializer in const declaration
- const声明的注意点:
const只是限制变量绑定的值,不会限制引用数据类型内部的变动。
const a = {
num : 1
}
a.num = 2 // 对象的属性仍然可以被改动,不被影响
a = 2 // 报错
- 同一作用域内,无法对同一个变量/常量进行重复声明。
let varData;
let varData = true; // Uncaught SyntaxError: Identifier 'varData' has already been declared
const constData = 1;
const constData = 2 //Uncaught SyntaxError: Identifier 'constData' has already been declared
var varData
let varData = 2 //Uncaught SyntaxError: Identifier 'varData' has already been declared
- 重复声明的注意点:
在switch语句中,因为let或const声明会形成块级作用域,也会导致重复声明。switch (key) {
case 1:
let a = 1
break;
case 2:
let a = 2 //Identifier 'a' has already been declared
break;
}
2.暂时性死区
暂时性死区:Temporal dead zone——TDZ
先看一段代码,方便我们对暂时性死区的理解:
console.log(a) // undefined
var a = 1
在js阶段时,我们学过预解析,var声明的变量会在初始化赋值前,进行变量提升(hoisting),在进行代码的执行阶段时,a变量已经存在,且值为undefined
这种逻辑其实有些奇怪,我们对变量a进行声明和初始化赋值,是为了后面的逻辑和功能,但在进行初始化赋值前,变量a就已经可以进行访问,并且有值。
ES6中为了纠正这种现象,改变语法行为:对let和const声明的变量/常量,一定要在声明后使用,否则报错
console.log(a) // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 1
在'暂时性死区'的概念中,let和const声明的变量/常量都规避了变量提升(hoisting)特性带来的怪异逻辑,让前端程序员在编写代码时,以更严谨的角度去进行编程,防止多余的误操作。
3.块级作用域
在ES6之前,只有全局作用域和函数作用域,在ES6中,新增了块级作用域
块级作用域由一对大括号界定,在大括号内使用let和const进行声明,才会形成块级作用域
{
let a = 1;
console.log(a) // 1
}
console.log(a) // a is not defined
让我们来看一下,块级作用域存在前后的对比:
- if执行语句中,var声明的变量直接作用于if语句所在的作用域内。
if(false){
var a = 1
}
console.log(a) // undefined
- 可以通过下面这个例子,更直观地观察:没有块级作用域带来的场景误读。
var title = 1
function fun(){
if(title == undefined){
var title = 10
}
console.log(title) // 10
}
fun()fun函数中,使用var声明的title变量因变量提升(hoisting)特性,在执行前被提升到函数作用域顶部,且初始值为undefined,当进入执行阶段时,if判断为true,进入if执行语句,进行title的赋值操作。
这也是很多初学者一开始就会产生的疑问:
A:‘我明明是if判断成功,我才声明这个变量并初始化,那在这一步操作之前,title不应该是沿着作用域链去寻找全局作用域中的title吗?‘。
B:‘变量提升(hoisting)的特性’。
A:’那我肯定是要符合条件,才进入if执行语句,里面的代码才会执行,title这个变量才去初始化和赋值。现在变量提升不就导致了判断条件受到影响?'。
B:‘变量提升(hoisting)的特性’。
A: '......(按住自己的拳头)'。
这个对话可能有些绕,我们可以看图来解读这个同学的疑惑:
-
可以通过下面这个例子,更直观地观察:没有块级作用域带来的场景误读。
var title = 1
function fun(){
if(title == undefined){
let title = 10
}
console.log(title) // 1
}
fun()
- for循环中var声明的计数变量直接作用于for语句所在的作用域内。
function fun(){
for (var i = 0; i < 10; i++){
// ...
}
console.log(i); // 10
}
- 拥有块级作用域后,可以避免用来计数的循环变量泄露。
function fun(){
for (let i = 0; i < 10; i++){
// ...
}
console.log(i); // i is not defined
}
- 异步代码执行过程中,需要通过IIFE实现closure(闭包),达到打印目标。
function fun(){
for (var i = 0; i < 10; i++){
;(function(i){
setTimeout(()=>{
console.log(i); // 0 1 2 3 ...
})
})(i)
}
}
- 拥有块级作用域后,不需要再通过closure(闭包)主动进行词法环境收集。
function fun(){
for (let i = 0; i < 10; i++){
setTimeout(()=>{
console.log(i); // 0 1 2 3 ...
})
}
}
总结
总结和了解var、let和const的特点后,我们才能明白"直接将var替换成let"这种做法携带的安全隐患有多大。
分析var和let/const的区别,必然要理清变量提升(Hoisting)、暂时性死区(TDZ)和块级作用域(block)这几个概念分别产生的效果。
let、const也是为了解决使用var时形成怪异逻辑的问题。通过规范的语法行为,统一代码的正确解读,减少代码在编写及运行时的误操作,提高代码的安全性和可读性。