(十二)var、let、const及块级作用域

109 阅读6分钟

var、let、const的区别

varletconst
重复定义可以不可以不可以
重复赋值可以可以不可以
作用域是否提升提升不提升不提升

关于let/const作用域提升

什么是作用域提升呢? 我的理解是在变量定义位置之前可以使用这个变量。

var定义的变量可以提升作用域,letconst定义的变量不可以提升作用域。实际上在code解析阶段变量就会被创建,只是letconst定义的变量在赋值前都不可以被使用。

var、let、const与window的关系

es早期

早期每个执行上下文关联一个变量对象(veriable object,VO),在源代码中的变量和函数声明会被作为属性添加到VO中。

对于函数来说,参数也会被作为属性被添加到变量对象中。而全局变量对象指向的是GO/window

早期只有var关键字,在全局作用域通过var声明变量都会在window对象里。

es6

而在es6重新定义每个执行上下文关联一个变量环境(veriable environment,VE)中。在执行代码中变量和函数声明会被作为环境记录(Enviroment Record)添加到变量环境中。

对于函数来说,参数也会被作为环境记录添加到变量环境中。

全局变量环境不是指向window对象了,而是指向叫variable_的对象,其类型为Variable Map;但是es6只是规范,具体实施是浏览器去实现,而由于早期历史遗留问题,浏览器实现时对于var声明的变量会同时出现在window对象和varibale_里,并且具有关联,修改一个另外一个也会修改。

es6新增了两个关键字letconst,所有声明的变量都会保存到variable_里。

var、const和let使用哪个?

var属于历史遗留问题,不太推荐使用。
优先考虑const,这样可以保证数据的安全性不会随意的篡改;
只有当明确知道一个变量后续要重新赋值时,这个时候在使用let

for循环每次变量是否会重新定义

对于var定义的变量

变量ifor循环的初始化部分通过var关键字声明,并且它只会在循环开始时被定义变量ifor循环的初始化部分通过var关键字声明,并且它只会在循环开始时被定义一次。然后,在每次循环迭代中,i的值会根据循环条件和增量表达式进行更新。。然后,在每次循环迭代中,i的值会根据循环条件和增量表达式进行更新。

for (var i = 0; i < 5; i++) {
  console.log(i);
}

console.log(i); // 输出 5

对于let定义的变量

注意,在ES6引入的let关键字中定义的变量具有块级作用域,所以如果在for循环中使用let声明变量,那么在每次循环迭代后,变量将被重新定义。

for (let i = 0; i < 5; i++) {
  console.log(i);
}

上方循环代码相当于下方伪代码

// 下方代码为伪代码

// 第一次循环

{
  let i = 0;
  console.log(i);
}

// 第二次循环,第三次循环和第四次循环都为下面代码

{
  let i = 上方的i++的结果;
  console.log(i);
}

对于const定义变量

for循环不可以使用const来定义初始变量,因为const不可以重新赋值,所以下方代码i++会报错

for (const i = 0; i < 5; i++) {
  console.log(i);
}

作用域

在es早期作用域只有两种全局作用域函数作用域; es6新增一种作用域:块级作用域

什么是块级作用域

块级作用域是指在代码块(一对花括号{}中的代码)中声明的变量或函数只在该代码块内部可见。在代码块外部,这些变量或函数是不可访问的。ifswitchfor均为块级作用域

在JavaScript中,变量和函数的作用域由它们被声明的位置决定。如果在全局作用域中声明一个变量或函数,它将在整个程序中都是可见的。而在函数内部声明的变量或函数,则只在该函数作用域内可见。

块级作用域针对于var、let、cosnt、function和class

块级作用域在浏览器中对于varfunction定义的变量是不生效的,关于function定义未生效是因为历史遗留问题。 案例一:

{

  let a = 1;
  var b = 2;
  const c = 3;
  function foo() {}
  class Bar {}
}

console.log(a); //报错
console.log(b); //输出2
console.log(c); //报错
console.log(foo); //输出 function foo(){}
console.log(Bar); //报错

案例二:

for (var i = 0; i < 10; i++) {}

console.log(i); // 10

for (let j = 0; j < 10; j++) {}

console.log(j); // 报错 ,j变量只能在{}里使用

经典的按钮点击事件

假如页面上有4个按钮,利用for循环给4个按钮添加点击事件,代码如下,请问点击按钮会输出什么?

var btns = document.getElementsByTagName("button");

for (var i = 0; i < btns.length; i++) {
  btns[i].addEventListener("click", function listener() {
    console.log(i);
  });
}

不论点击第几个按钮都会输出4,为什么呢?

在es早期没有块级作用域概念,在es6虽然有了块级作用域概念,但是块级作用域对于var定义的变量不生效,所以var定义的变量会提升到全局中,也就是i变量目前在全局;而点击按钮会先在listener函数作用域寻找i,找不到则去上层作用域寻找,因为es早期没有块级作用域得概念,上层作用域则是全局;当我们给每个按钮添加完点击事件后,全局的i值已经为4,所以这时候点击按钮会输出4

那该如何解决呢?

es早期可以利用闭包解决,方案如下:

var btns = document.getElementsByTagName("button");

for (var i = 0; i < btns.length; i++) {
  (function(n){
    btns[n].addEventListener("click", function listener(){
      console.log(n);
    });
  })(i)
}

每次循环都会调用tempFn函数,并将i值0,1,2,3分别传入。而点击按钮会先在listener函数作用域寻找i,找不到则去tempFn函数作用域内寻找,可以找到,分别是0,1,2,3;这样,每个按钮输出值会按我们的想法输出.

es6后可以利用let关键字解决,方案如下:

var btns = document.getElementsByTagName("button");

for (let i = 0; i < btns.length; i++) {
  btns[i].addEventListener("click", function listener(){
    console.log(i);
  });
}

因为有块级作用域概念,所以每次点击按钮会在for的块级作用域里{}里寻找i