ES6 (一) 彻底搞懂 var let const

204 阅读5分钟

ECMAScript 6(ES6)是于2015年6月正式发布的javascript语言的标准规范,下面介绍ES6最基础的新增——声明命令 let 和 const 。 最近查阅了很多资料,终于彻底搞懂了var let const,还顺便捋顺了以下几个重要的概念:

  • 作用域:全局域 函数域
  • 函数的执行时机
  • 全局变量/局部变量的隐式声明
  • 变量提升
  • 立即执行函数
  • 块级作用域
  • 暂时死区TDZ

声明命令

/* ES3提出 */
a=1      
var a=1  
/* ES6提出 */
let a=1  
const a=1 

下面会逐个介绍这四种声明命令,但为防止混乱,需要先回顾一遍以下几点概念。

作用域

变量的调用范围:单向可访问的,在函数局部域可以访问并修改全局域内的变量

//这是全局域
function fn(){
  //这是局部域
  var x=10  //x是在fn域内的局部变量
}
fn()
console.log(x)  //报错  

函数的执行时机

没有调用,就没有执行:

var x=10
function fn(){
  x=20
}
console.log(x)  //10

调用了,才执行,才真正修改了这个变量的值:

下面代码中,x=20没有带任何声明符号,于是向上一级作用域去寻找它的声明符号,由局部=>全局,此时的 x 就是全局变量

var x=10
function fn(){
  x=20 
}
fn()           //调用 执行
console.log(x)  //20

下面代码中,如果在函数内重新声明了x,那么这个 x 就是函数内的局部变量

var x=10     //x是全局域的变量
function fn(){
  var x=20   //重新声明,x是fn域私有的变量
  console.log(x) //20  就近访问原则
}
fn() //20
console.log(x)  //10  x仍是全局变量

总结:在函数内使用x变量时,会在当前函数域寻找声明,找到了就使用这个局部变量x,如果没有找到声明,就说明 x 不是当前函数域的局部变量,向上一级域寻找 x 声明,一直找到全局域,发现也没有声明,那么JS引擎会在全局加上隐式声明,默认初始值是undefined。

如何验证一个变量是否是全局变量?打印window.变量名

//相当于这里 var x
function fn(){
  x=10  //一个变量不声明直接赋值,会在全局进行隐式声明
}
fn()
console.log(x)   //10
x===window.x     //true

var x相当于把变量 x 挂载到全局对象window上作为属性,此时 x 是全局变量。

总结

  • 作用域是单向可访问的:内部作用域(函数局部域)可以访问外部作用域(全局域)的变量,外部不可以访问内部的私有变量
  • 私有变量:在当前局部作用域下声明的变量,(没有声明符号的不算,在调用时,它会向上一级寻找声明,一直找到全局作用域为止,如果一直找不到,JS引擎会在全局隐式声明)。
  • 函数的形参相当于函数域内的局部变量:
var x=10
function fn(x){  //这是一个形参 函数的形参会作为函数的局部变量的隐式声明  
  //相当于var x
  x=20
  console.log(x)
}
fn(x)           //20  传入的是10,但是被函数内20覆盖
console.log(x)  //10  全局变量
  • 就近原则:变量的使用会优先访问自己作用域的局部变量,找不到才向上寻找。

明确了以上概念,再回到这四种声明命令来:


a=1

可以分一下四种情况讨论:

var a=10
function fn(){
  var a=1  //有声明符,fn域内的局部变量
  console.log(a)
}
fn()  //1
console.log(a)  //10
-----------------------------
var a=10
function fn(){
  a=1  //没有声明符,向上一级寻找,a是全局变量
  console.log(a)
}
fn()  //1
console.log(a)  //1
console.log(window.a)  //1
-----------------------------
var a=10
function fn(){
  var a=5
  fn2()
  function fn2(){
    a=1  //没有声明符号,向上一级寻找,a是fn域内的局部变量
  }
  console.log(a)
}
fn()   //1
console.log(a)  //10
-----------------------------
a=1  //隐式声明为全局变量
console.log(a)  //1
console.log(window.a)  //1

因此,判断一个a=1局部变量还是全局变量就这么麻烦,需要看它所处的函数作用域内是否有 var 声明,没有再向上一层作用域去寻找,直到找到全局域都没有声明,就隐式声明为全局的,写这样的代码很容易造成歧义,不推荐使用。


var a=1

(1)var 存在变量提升,在声明之前使用的值为undefined

console.log(x)  //undefined
var x=10
console.log(X)  //10

(2)在同一个作用域内,var可以重复声明同一个变量

(3)var的作用域:全局域 or 函数域。在ES6之前,只有全局域和函数的{ }内构成函数域这两种作用域,函数内部可以嵌套的其他函数构成了一层层的函数作用域,最外面的是全局域,由if for 里面{ }包起来的并不是局部域,仍然是全局域。它们关不住var声明的变量。

for(let i=0;i<5;i++){
  var x=10  //x是全局变量,for关不住它
}
console.log(x)  //10 

因此,建议把var都写在当前作用域的最开头。

不过这样也不可避免由于变量提升带来一个问题:var会暴露局部变量,无论你在作用域里写什么,var a的声明都会提升到这个域的最开头,在全局就会挂载到window对象上,变成了window.a,我们并不希望这样的事情发生。

var arr=[]
for(var i=0;i<5;i++){   //for循环不构成作用域,i是一个全局变量
  arr.push(function (){
    console.log(i)
  })
}
arr[1]()    //5  在调用的时候才执行打印i,i值早已是循环完的5
arr[2]()    //5 

一个解决办法是使用IIFE 匿名函数中的立即执行函数,希望在每次循环时,把当前i的值截留下来:立即执行函数立刻将i的值传给了j:立即执行函数执行完毕了,才能进入下一次循环。

var arr=[]
for(var i=0;i<5;i++){
  (function(j){  //接收的形参  局部变量  相当于var j 
    arr.push(function (){
      console.log(j) 
    })
  })(i)         //实参
}
arr[1]()  //1
arr[2]()  //2

这是2015年ES6发布之前,JS语言设计上的失误,大部分语言都存在块级作用域,而很遗憾的是,var声明的变量并没有这种作用域,只有全局域和函数域,在当时只能用立即执行函数来弥补这个失误。


let a=1

(1)let 没有变量提升

console.log(x)  //报错  
let x=10;
console.log(X)  //10

(2)在同一个作用域内,let不可以被重复声明。

{
  let a=1
  console.log(a)  //1
  a=2   //let一个变量只能一次  
  console.log(a)  //2
}

(3)let的作用域:块级作用域,在最近的代码块{ }之间 (函数块,for( ){ },if{ }只要是{ },JS就认为是块级作用域 )

{
  let a=1
}
console.log(a) //报错  a未定义

(4)在不同作用域内,let可以重复声明一个变量,但是如果在一段作用域内先调用再声明,会有暂时死区TDZ问题。因此在作用域内,不要先调用再声明!

let num=10       //全局变量
function fn(){
  console.log(num)  //暂时性死区  声明前调用了
  let num=20     //局部变量 因为let没有变量提升
}
fn()  //报错

魔法:for + let

一共有六个i,圆括号里的 i 和花括号里的i0 i1 i2 i3 i4,每一个 i 都是一个独立的变量

var arr=[]
for(let i=0;i<5;i++){   // i的作用域只在( )里  
   //块里面的i = 圆括号里面i 的值  js自动加了这句 
  arr.push(function (){
    console.log(i)
  })
    //圆括号里面的i = 块里面的i的值  js自动加了这句 
}
arr[1]()  //1
arr[2]()  //2

这样刚才的例子就迎刃而解!只需要把var 改成let,不再需要麻烦的立即执行函数~


const

(1)(2)(3)同let

(4)const只有一次赋值机会,而且必须在声明时马上赋值

{
  const a=1       //const a 报错  //不赋值也得赋值
  console.log(a)  //1
  //a=2           //报错  
}

对比

varletconst
作用域(1)在函数外:全局域,挂载到window对象上 (2)在函数内:整个函数域块级块级
变量提升有,提升到该作用域的最开头××
重复声明××
重复赋值×

下面这些例子帮助你更好的理解var 与 let

题目1

for(var i=0;i<6;i++){
  
}
console.log(i)  //打印6
for(var i=0;i<6;i++){
  function fn(){
    console.log(i)
  }
  fn()  //打印出0,1,2,3,4,5
}

执行的时机:代码一瞬间执行完了,在点击这个按钮之前,i早就是6了

<button id=x>按钮</button>
for(var i=0;i<6;i++){
  function fn(){
    console.log(i)
  }
  x.onclick=fn  //6
}

题目2

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>
var liTags=document.querySelectorAll('li')
for(var i=0;i<liTags.length;i++){
  liTags[i].onclick=function(){  //for循环里形成了闭包
    console.log(i)
  }
}
//鼠标还没动呢 i就已经是4了   var作用域是全局的

解决办法:(1)用let: j作用域只在函数块里

for循环每次执行都是一个全新的独立的块作用域

var liTags=document.querySelectorAll('li')  //每一个都是<li>2</li>元素
for(var i=0;i<liTags.length;i++){
  let j=i
  liTags[j].onclick=function(){
    console.log(j)
  }
}
相当于重新开辟了四块空间 j0 j1 j2 j3
{
  let j=0
  liTags[0].onclick=function(){
    console.log(0)
  }
}
​
{
  let j=1
  liTags[1].onclick=function(){
    console.log(1)
  }
}
​
{
  let j=2
  liTags[2].onclick=function(){
    console.log(2)
  }
}
​
{
  let j=3
  liTags[3].onclick=function(){
    console.log(3)
  }
}
最后i是走到了4,但是每个j还保留这个自己的值

(2)用立即执行函数

var liTags=document.querySelectorAll('li')  
for(var i=0;i<liTags.length;i++){
 (function(j){  //用j来接收
   liTags[j].onclick=function(){
     console.log(j)
  }
 }) (i) //传递一个i
}

(3)最简单的一种写法:魔法 一共七个i 圆括号里的i 和{}里的i i0 i1 i2 i3 i4 i5

var liTags=document.querySelectorAll('li')  
for(let i=0;i<liTags.length;i++){  //i的作用域只在( )里
  //块里面的i = 圆括号里面i 的值
  liTags[i].onclick=function(){   //js自动加了一句let _i=i  创建了一个同名的_i
    console.log(i)
  }
  //圆括号里面的i = 块里面的i 的值
}

题目3

for (var i = 0; i <10; i++) {  
  setTimeout(function() {  
    console.log(i);        
  }, 0);
}
// 10个10
for (let i = 0; i < 10; i++) { 
  setTimeout(function() {
    console.log(i);    // i 是循环体内局部作用域,不受外界影响。
  }, 0);
}
//0  1  2  3  4  5  6  7  8 9

以上就是let var、const的用法,希望以后不要再犯错。