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 //报错
}
对比
| var | let | const | |
|---|---|---|---|
| 作用域 | (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的用法,希望以后不要再犯错。