JS中声明变量的几种方式
在讲let和var的区别前,我们先来了解一下JS中声明变量的几种方式
- 在ES6之前,常用的声明变量的方式
- var 如: var a = 12;
- function 如: function A(){...},function是定义一个函数,实际上也相当于是创建变量的一种方式
- 在ES6之后,又新增了几种声明变量的方式
- let let关键字声明的变量具有块级作用域,如: let b = 13;
- const const关键声明的变量,一旦已经关联值就不能再跟其它值关联,如: const m = ‘abc’;
- import 利用import关键字导入其它模块,如:import xx from './xx.js'
- class 定义一个类:实际也是声明变量的一种,如:class B{}
变量提升
变量提升:在当前上下文中,代码执行前,会把所有带"var/function"关键字的进行提前声明或定义
- 带“var”的只是提前声明
- 带“function”的则是声明并定义
上下文 && 作用域
全局上下文 函数执行形成的“私有上下文” 块级私有上下文(块级作用域)
- 除了对象、函数等的大括号以外的其它大括号(如:判断体、循环体、代码块...)都可能会产生块级上下文
let 与 const的区别
- let声明的变量,变量所存储的值可以被更改
let a = 12;
a = 13;
console.log(a);//输出13
- const声明的变量,一旦被赋值,就不能再跟其它值关联(不允许指针重新指向)。
const b = 12;
b = 13;// 此句代码会直接报错:Uncaught TypeError:Assignment to constant variable.
console.log(b);
const obj = {name: 'JS高级'};
obj.name = "JS web高级";
console.log(obj);//{name: JS web高级}
//这个时候发现obj对象的值被改了,所以不能说const声明的就是常量
let与var的区别
- var存在变量提升,而let不存在变量提升
console.log(n);//undefined
console.log(m);//Uncaught ReferenceError:Cannot access 'm' before initialization
var n = 12;
let m = 13;
//这是因为var存在变量提升,在代码执行前先声明了变量n但并未赋值,所以console.log(n)会输出undefined
//而let不存在变量提升,不会提前被声明,所以console.log(m)会直接报错
- 全局上下文中,基于var声明的变量,也相当于给GO(全局对象=>window)新增一个属性,并且任何一个发生值的改变,另外一个也会跟着变化(映射机制);但是基于let声明的变量就是全局变量,跟GO没有任何关系
//用var声明的变量
var n = 12;
console.log(n);//12
console.log(window.n);//12
window.n = 13;
console.log(n);//13
//用let声明的变量
let m = 10;
console.log(m);//10
console.log(window.m);// undefined
- 在相同上下文中,let声明的变量不允许重复声明(不管之前是基于何种方式声明的,只要声明过就不能再基于let重复声明了),而var可以重复声明不做限制,因为浏览器只会按照声明一次处理
var n = 10;
var n = 12;
console.log(n);//12
//在代码执行之前,浏览器会进行词法分析、变量提升....
//而在“词法分析”阶段,如果发现有基于let/const并且重复声明变量的操作,则直接报语法错误 Uncaught SyntaxError: ...,整个代码都不会做任何执行。
console.log('OK');//不会执行
let m = 14;
let m = 12;
- let/const/function会产生块级作用域,而var不会
//代码块形成的块级上下文
{
var n = 12;
console.log(n);//12
let m = 13;
console.log(m);//13
}
console.log(n);//12
console.log(m);//Uncaught ReferenceError:...
//在代码块内部不管是基于let还是var声明的变量都没有报错,但是在代码块之外,用var声明的变量没有报错,而用let声明的变量就报错了,这就很好的说明了基于let声明的变量是由块级作用域的,而var没有
关于let和var在for循环中的区别
在上篇文章“web前端高级 - 闭包的应用及循环事件绑定的N中解决办法”中,我们用for循环给每个按钮绑定点击事件,并在点击按钮时获取当前按钮的索引。但是发现当我们在for中用var时并没有实现我们期望的效果,在把var改为let后却实现了。就是一个简单的不同的变量声明方式却产生了不同的结果。这是因为var和let在for循环中的机制不同所导致,同时也跟let的块级作用域有关。接下来我们就来分析一下var和let在for循环中的区别。
- var
var buttons = document.querySelectorAll('button');
for(var i = 0; i < buttons.length; i++){
buttons[i].onclick = function(){
console.log(`当前按钮的索引是${{i}`);
}
}
具体原因在上文中已经详细剖析,这里要说的是由于var并没有块级作用域的概念,所以这里操作的始终都是全局上下文中的变量i,所以当按钮的具体事件执行时,发现当前私有上下文中并没有变量i,所以就会向上级上下文(也就是全局上下文)中查找,所以最后每个按钮找到的都是全局上下文中的同一个变量i。
- let
var buttons = document.querySelectorAll('button');
for(let i = 0; i < buttons.length; i++){
buttons[i].onclick = function(){
console.log(`当前按钮的索引是${{i}`);
}
}
上文中提到:let之所以能够实现,是因为let也是基于闭包的机制。那么具体的底层执行原理是什么呢,接下来我们来深挖一下:
- 1.首先for循环主体会产生一个独立的私有上下文用来控制循环,然后每一轮循环也会再形成一个独立的私有上下文,这样循环几次就会形成几+1次的私有上下文。
- 2.每循环一次会形成一个独立的私有级上下文,然后父级上下文(循环主体上下文)会把当前的变量i传递给该私有块级上下文
- 3.在当前私有块级上下文中,会把i作为私有变量保存,并在当前上下文中进行处理后,返回给父级上下文(循环主体形成的私有上下文),同时创建事件绑定函数,函数的作用域就是当前的私有上下文。
- 4.条件成立进行下一轮循环,然后重复以上2~3步,直到循环结束为止。
- 5.所以每次触发按钮点击事件,函数执行时都会到循环时所形成的私有块级上下文中查找变量i,从而实现我们所期望的效果
下面用一张图来演示一下for循环的执行过程:
关于变量的扩展
- 扩展一:如果一个变量没有基于任何关键字声明,则相当于给window设置一个属性
//扩展一
//如果一个变量没有基于任何关键字声明,则相当于给window设置一个属性,如:
a = 13;
console.log(a);//13 首先看是否为全局变量,如果不是则再看是否为window的一个属性
console.log(b);//报错 如果不是则直接报未定义的错误
- 扩展二:如果一个变量声明在一个私有上下文中(例如函数),并且也没有基于任何关键字声明并且是给变量赋值操作,则也相当于是给window添加一个属性
//扩展二
//如果一个变量声明在一个私有上下文中(例如函数),并且也没有基于任何关键字声明并且是给变量赋值操作,则也相当于是给window添加一个属性
function fn(){
x = 111;
console.log(x);//111 按照作用域链查找机制,先看变量在当前私有上下文中是否存在,如果不存在则向上级上下文中查找,直到找到全局上下文,如果全局上下文中也没有并且是给变量赋值操作,则也相当于给window添加属性
console.log(y);//如果是获取操作则直接报错
}
- 扩展三:暂时性死区(浏览器暂存bug)基于typeof检测一个未被声明的变量,不会报错,结果是undefined
console.log(n);//Uncaught ReferenceError: n is not defeined
//上面的代码未进行任何变量的声明和定义直接使用,发现报错了,这也很合乎情理,但是再看下面的代码
console.log(typeof n);//undefined 基于typeof检测一个未被声明的变量,不会报错,结果输出undefined
//但是如果我们再在这句代码下面加一个let声明一下,发现上面的代码又会报错了
console.log(typeof n);//Uncaught ReferenceError:...
let n = 10;