1. 本文的目的
我们都知道var声明变量会先提升变量然后再赋值, 并且可以重复声明,这些特性导致代码编写后会更加混乱。 我们现在大都使用let来声明变量,但是经常还有一些老项目使用var以及一些经常遇到的面试题, 例如:
// 题1: 两者输出了什么
for(var i = 0; i < 10; i++) {
setTimeout(console.log(i))
};
console.log(i);
for(let j = 0; j < 10; j++) {
setTimeout(console.log(j))
};
console.log(j);
// 题2:打印了什么
function fn() {
let x = 2;
{
var x = 1;
}
console.log(x);
}
复制代码
文章通过一个个简单的案例来分析var与let的区别以及它们直接和块级作用域的关系, 方便大家在以后工作或者面试中遇到这些问题不靠猜,而是利用两者特点分析答案。 现在开始吧~
2. 首先理解什么是块
2.1块的含义:
在mdn上对块语句的说明是: 块语句用于组合零个或多个语句。该块由一对大括号界定。
2.2语句是什么意思:
语句可以理解为表达式, 网上有很多文章对JS中的表达式有很多描述这里总结一下, 只要有明确语义的语句都可以称为表达式。
栗子:下面这些代码在一对大括号中,并且每条语句有明确的含义 null代表着空值, 1代表的这里有一个为1的值, 以及还有一个函数,它们共同组合成了一个块。
{
null;
1;
() => {};
}
复制代码
2.3块变量访问的特点:
内部变量可以访问外层变量, 但是外层变量不能访问内部变量
let y = 10;
console.log(x); // 不能访问
{
let x = 20;
console.log(y); // 可以访问
}
复制代码
3. let特点
在理解了什么是块后我们要记住let的三大特点,之后会根据这三个特点进行分析与var的不同
特点1: 同一个作用域下不能重复声明。
特点2: 会产生暂时性死区 (TDZ)。
特点3: 只在当前块有效。
4. 特点分析
4.1 同一个作用域下不能重复声明
涉及方面: var与let声明不同点以及两者相互混用可能会造成的问题。
栗子1:
let x = 1;
let x = 2;
console.log(x);
----------
var x = 1;
var x = 2;
console.log(x);
复制代码
这个很简单, 由于let不能重复声明而var能重复声明所以let会报错, 但是很多人忽略的了关键词同一作用域例如这个就不会产生错误。
let x = 1;
{
let x = 2;
console.log(x); // 2
}
复制代码
栗子2: var与let混用的情况下(经常遇到)
function fn() {
let x = 1;
{
var x = 2;
}
console.log(x);
}
fn();
复制代码
这里会报错, 因为var会忽略块进行变量提升,已经声明了x再去let x就会报重复声明的错误。
4.2. 会产生暂时性死区(TDZ)和只在当前块有效
这两个特点一起说明因为很多栗子中它们都是密不可分的。 涉及点: 暂时性死区的特点、 赋值操作时注意点、函数参数默认值、for与if中是否含有块级作用域、多层块作用域嵌套问题。这些都是工作中经常遇到的所以要重点进行分析。
栗子1: 最简单的暂时性死区
console.log(x); // 报错
let x = 2;
---------------
console.log(y); // undefined
var y = 1;
复制代码
这个不必多说,let必须先声明才能使用。
栗子2: 赋值操作符导致的暂时性死区
let x = x;
console.log(x); //报错
---------;
let y = y;
console.log(y); // undefined
复制代码
这里要抓住重点赋值操作符是从右往左赋值,先读取右边的x此时x还未声明所以会报错。
栗子3:if { } 大括号有块作用域吗?
function fn() {
var x = 1;
if (x) {
console.log(x);
let x = 2;
}
}
fn();
复制代码
这里就稍微复杂一些, 如果没有let x = 2 这句就不会报错,此时也报错说明没有声明就使用了变量x, 说明了在执行语句中已经发现了当前块作用域中有了x, 优先在自己块上找。但是这个声明在使用的后面所以会报错。
很多同学这里有些搞不清楚, 为什么不是先打印1然后在声明一个变量x, 其实这里和JS的编译执行有关系虽然说JS值解释性语言解释一行执行一行, 但是它也有编译性语言的特征例如变量提升AO、GO作用域(之后会写文章说明), 这里可以简单理解为当执行console.log(x)的时候JS已经知道这个块作用域中已经有了一个x并且使用的是let声明模式,因为有暂时性死区的特点所以这里会报错。
栗子4: 函数参数默认值情况
// 情况1
function fn(x = 1, y = x) {
console.log(x); // 1
console.log(y); // 1
}
// 情况2 - 报错Cannot access 'y' before initialization
function fn(x = y, y = 1) {
console.log(x);
console.log(y);
}
复制代码
从两个情况来分析一下, 第一个可以正常运行,但是第二个情况会产生报错说y还没有被初始化就已经使用。可以分析出函数参数默认值是从左到右进行解析的,并且使用的是es6中let规则可以看做为下面的代码:
// 情况1
let x = 1;
let y = x;
// 情况2;
let x = y; // 所以这里会报错
let y = 1;
复制代码
栗子5: 函数参数与内部声明相同的情况
// 情况1内部let声明 重复定义错误 Identifier 'x' has already been declared
function fn(x = 1) {
let x = 2;
console.log(x);
}
// 情况2正常
function fn(x = 1) {
var x = 2;
console.log(x); // 2
}
复制代码
这里感觉很疑惑, 因为从情况1中说明函数()与 {} 中属于同一个块,所以会提示重复声明。如果是同一个块情况下 var x = 2 也会产生重复声明, 但是情况2又完全正常。查阅了一些资料说这个情况JS内部有修正机制函数内部遇到var情况下优先使用var, 具体可以先这么记一下,到时候能更深刻理解回来再写分析!
栗子6: for循环中是否存在着块作用域(经典面试题)
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 输出5个5
}, 0);
}
console.log(i); // 5
for (let j = 0; j < 5; j++) {
setTimeout(() => {
console.log(j); // 0 1 2 3 4
}, 0);
}
console.log(j); // j is not defined
复制代码
从这个栗子中我们也看出了var 的缺陷, 我们指向在for循环内部上下文中使用i, 但是它会被绑定到了外部作用域,循环中所有函数使用的同一个i变量, 所以会打印出5个5。而使用let就不会产生这个问题, 因为for循环头部的let不仅将j绑定到了for循环的块中,而且它将其重新绑定到了循环的每一个迭代中,简单的说变量j在循环过程中不止被声明一次,每次迭代都会声明,随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
栗子7: 继续证明for循环中的块作用域
var n = {a: [1, 2]};
for (let n of n.a) { // Cannot access 'n' before initialization
console.log(n);
}
复制代码
因为要遍历出n.a这个数组, 所以要先知道n.a这个值, 正因为()会产生块作用域所以n优先访问内部发现n还没有声明就已经被使用了所以这里会报错, 也证明了for()中也会产生块作用域。如果使用的是var的话也需要重点分析一下
var n = {a: [1, 2]};
for (var n of n.a) {
console.log(n); // 1 2
}
console.log(n, 'last'); // 2
复制代码
首先执行到for循环的时候先得出n.a的值为 [1, 2], 然后因为var a会提升覆盖外层的n值所以当执行完毕后最后打印的n值为2。
栗子8: if()小括号有块级作用域吗?
if (let i = 1) {
console.log(i);
}
复制代码
此时会报出语法错误, 因为if的括号中必须是一个值,不能含有声明等其他语句所以if()中不会产生块作用域。
栗子9: 多层块级嵌套和var声明
{
{
let a = 1;
{
{
var a = 1;
}
}
}
}
复制代码
这里也会报错,重点抓住var 变量提升的特点,它类似于冒泡一样一层层的往上提升到全局或者函数顶部。 这里中间只有有一个层级有相同的let变量就会产生错误。
5. 针对这些知识我们可以做到的优化
能用let尽量使用let, 使用var容易导致作用域不明确。
利用块级作用域来进行最小授权,减少全局变量避免全局污染。
// window全局情况下函数声明和变量声明都会绑定到window上,任何其他的地方都可以访问它们
var bigData = {}; // 一个数据量庞大的对象
function fn() {
console.log(bigData);
};
fn();
// 优化利用块级作用域进行包裹以及函数表达式避免函数提升
{
let bigData = {};
let fn = () => {
console.log(bigData);
}
}
复制代码
6. 最后
这些就是我对于var与let以及块级作用域的一些总结,今儿周末好不容易休息一下就整理了出来也花费了一天时间。 如果哪里还有需要补充的可以留言或者私信给我, 到时候也会加进去, 希望能帮助到大家~