let和const命令
块级作用域
在说明let
和const
之前, 先做个铺垫, 我们知道, 在ES5中只存在全局作用域和函数作用域, 但就是因为只有这两个作用域导致某些情况比较尴尬
function foo() {
console.log(a); // 你会发现在这里会输出undefined
if (a === 100) { // 即使这个判断进不去也会导致判断中的代码跟console.log(a)一样处于一个函数作用域中
var a = 200; // 在旧版本的chrome中更恐怖的是如果这里你写一个函数声明,
// 那么上面打印的a就是一个函数, 这是一个很惊悚的事情
}
}
foo();
再来看个实例
for(var i = 0; i < 5; i++) {
console.log(i);// 会依次输出0 - 4, 但是因为for循环非函数作用域所以他声明的i会被放置在全局下
}
console.log(i); // 输出5, i成为了全局变量并且一直被保存着,这样也很不合理我们肯定希望i随着for循环一起消失
于是在ES6中规定了一个新的作用域块级作用域, 块级作用域表示一个代码块, 一对大括号中的代码就是一个代码块, 也是一个块级作用域, 而用let
和const
会直接开启块级作用域, let
和const
稍后介绍这里先混个脸熟
function foo() {
console.log(a); // 直接报错:ReferenceError: a is not defined
if (a === 100) { // 由于let的存在, 开启了块级作用域
let a = 200;
}
}
foo();
而第二种情况也获得了比较好的解决
for(let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i); // 报错: ReferenceError: i is not defined
小扩展:do表达式
由于特殊性导致块级作用域外是无法获得块级作用域中的值, 但是我们某些时刻有需求想要去拿块级作用域的值怎么办呢, 目前有一个提案可以让块级作用域外拿到块级作用域的返回值, 这就是do表达式, 不过目前还在提案阶段浏览器也都未实现, 所以我们暂时还不能使用, 了解了解就好
let
Es6新增了let
命令用来声明变量, 但是其声明的变量只在let
命令所在的代码块(块级作用域)内有效
来看个例子
let a = 10;
{
let b = 100;
var c = 200;
}
console.log(a); // 输出10
console.log(c); // 输出200
console.log(b); // 报错:Reference Error: b is not defined
用var
声明的c和用let
声明的b虽然在同一对大括号下, 但是必然是a和c都可以输出而b报错,因为ES5只有函数作用域和全局作用域并没有块级作用域这个概念, 所以var
定义的c得以输出
用let声明的变量特点
1. 不存在变量提升
我们在使用var
声明变量的时候会发生’变量声明提升的现象’, 变量声明提升会导致可以在变量声明之前使用该变量的值, 只是值为undefined
console.log(a); // undefined
var a = 100;
console.log(a); // 100
ES6认为这种情况是相当的不合理的, 一个变量怎么可以未经声明就能使用呢,所以ES6为了改变这个状况, 在ES6中推出了let
, let
声明的变量必须在声明后使用, 不存在变量声明提升的过程, 如果强制在声明前使用, 则报错
console.log(a); // 报错: ReferenceError: can't access 'a' before initialization
let a = 100;
2. 暂时性死区
在es6之前用var
声明变量的话会出现这样一种情况
var userName = 'loki';
if(userName){
console.log(userName); // 会输出loki
var userName = 'thor';
}
当我们在if判断中输出userName时候, 由于userName在全局作用域中已经得到声明, 所以在输出userName的时候会理所应当的输出loki, 在Es6推出块级作用域以后, 同时规定在块级作用域中一旦使用let
和const
命令, 则let
和const
会直接接管整个作用域, 任何在let
和const
声明变量之前使用该变量都会失败, 这个称之为暂时性死区
至于他为什么要这样做, 根据笔者的理解, 后面我们会介绍const
声明变量, const
声明的变量是常量无法改变的, 如果当我们在一个const
的作用域中能输入两次不同的userName值,势必违法了const
的准则, 大家先混个脸熟, const
稍后介绍
var userName = 'loki';
if(userName) {
console.log(userName); // 报错: ReferenceError: can't access 'userName' before initailization
let userName = loki;
}
上方就是我们在一个块级作用域中使用了let
, 他直接接管整个作用域, 即使全局有变量userName, 他也无法使用,所以以前的那些花里胡哨的操作统统失效,这样的情况就称之为’暂时性死区’
Tips: 暂时性死区的英文叫做temporal dead zone
, 简称TDZ, 所以以后有同事谈到TDZ要及时反应过来哦
总结一下暂时性死区也就是, 一旦进入某个作用域, 在该作用域中使用let
和const
声明的变量就已经被放在逻辑的最顶端, 但是不可以访问, 只有执行完声明变量那一行之后, 才可以对该变量进行操作
3. 不允许重复声明
在es5中, 我们是允许重复声明变量的, 重复声明的变量会根据执行的顺序进行依次覆盖
var userName = 'loki';
var userName = 'thor';
console.log(userName); // 输出thor
实际上这种写法是相当不好的, 如果变量可以重复声明并且覆盖的话, 那么某个程序有几千行, 我在第1000行的时候定义了一个变量, 在第3000行的时候同事接手开发不小心又定义了跟同一个名字的变量, 然后进行覆盖, 那么后面我再来操作该变量的时候可能会出一些错误, 变量可重复声明和覆盖导致bug率很高
所以ES6规定, 在在同一个作用域中let
和const
声明的变量不允许重复声明和赋值否则会报错
let userName = 'loki';
let userName = 'thor'; // 在执行到这一行的时候就会报错: SyntaxError: Identifier 'userName' has already been declared
console.log(userName);
const
const
也是es6推出的一种新的声明变量的方式, 跟let
有所区别的是, const
用于声明一个只读的常量, 也就是说, 使用const
声明一个变量, 这个变量的值将无法更改
用const声明变量的特点
1. 不存在变量提升
这个跟let是一样的
2. 暂时性死区
同let
3. 不允许重复声明
同let
4. 一旦使用const
声明变量, 则变量的值无法更改
啥意思呢, 当我们使用es5的var
和es6的let
的时候, 我们用这两关键字声明的变量是可以在后续代码块中重新赋值的
var str = 'hello';
let userName = 'loki';
str = 'world';
userName = 'thor';
console.log(str, userName); // world thor
而const
相对来说比较的霸道, 只要声明了就不能再对值进行任何写操作
const USER_NAME = 'loki';
USER_NAME = 'thor'; // 报错: TypeError: Assignment to constant varible
5. 由于第四点的存在, 导致第五点的到来, const
必须赋予初值
我们知道, 变量可以只声明, 稍后再赋值这是没问题的, 如下
var str = 'hello'; // 变量声明 + 赋值
var str2; // 变量声明
let userName = 'loki'; // 变量声明 + 赋值
let userName2; // 变量声明
上面两种操作方式都是ok的, 也不会报错, 但是如果你使用const
的话, 那就必须变量声明 + 赋值一起上, 否则报错
const password = '123';
const userName; // 报错: SyntaxError: Missing initializer in const declaration
这样本身也是合理的, const本来就不允许被更改值, 如果我们用const
只声明一个变量的话, 其实这个变量的值为undefined, 后续我们再赋值其实再将undefined更改为我们赋予的值, 这是有问题的
我们来看看const的本质
我们都说const是声明一个常量, 而且这个常量的值不可改动, 其实如果是深刻的理解的话, 并非如此
const声明的变量并不是值不能改动, 而是变量指向的内存地址无法改动, 所以如果是原始值的话当然是等同于值不能改动了, 但如果是引用值的话, 就并不一样
const obj = {
name: '小明',
age: 18
}
obj.name = '小李';
obj.age = 50;
const arr = [1, 2, 3];
arr[0] = 110;
console.log(obj ,arr); // 输出{name: "小李", age: 50 } [110, 2, 3];
所以说const
只能保证他所掌控的指针不变, 指针对应的数据结构如果为引用值的话那他无法控制引用至始终都无法变化, 所以用const
声明引用值不是可信任的
如果我们真的想让一个对象正儿八经成为不可变的, 我们可以使用Object.freeze
const obj = Object.freeze({
name: '小明',
age: 18
})
obj.name = '小李';
obj.age = 50;
console.log(obj); // 输出仍然是 {name: "小明", age: 18}
当然, 如果对象中嵌套对象的话, 我们可能需要递归冻结所有子对象
const obj = {
name: '小明',
age: 18,
address: {
province: '上海'
}
}
// 我们可以写一个深度冻结对象的方法, 以便于长期复用
function deepFreeze(obj) {
Object.freeze(obj);
for(let prop in obj) {
if(Object.prototype.toString.call(obj[prop]) === '[object Object]') {
deepFreeze(obj[prop]);
}
}
}
deepFreeze(obj);
obj.name = '小红';
obj.address.province = '湖北';
console.log(obj); // 不会产生任何变化
最后一个刨根问底的地方, let和const声明的变量是否和var声明的变量放在一起呢?
我们知道var
在全局声明的变量会放在GO(全局上下文)中, 在函数声明的变量会存放在函数声明周期的AO(函数执行期上下文)中, 而GO在浏览器中实际就是window对象, 所以我们在全局用var
声明的变量用window.xxx都可以随意访问到, 那么let
和const
是否依旧如此呢?
我们直接来看看好了
var userName = 'loki';
let age = 18;
const lastName = 'adam';
console.log(window.userName, window.age, window.lastName); //输出loki, undefined, undefined
实际上, let
和const
把全局也看成一个域, 跟函数作用域一样, let
和const
声明的变量会直接进入作用域链(也就是[[scopes]])中, 同时因为作用域链在window中无法查看, 我们直接执行一个函数
来访问它的执行期上下文, 看看能否看到[[scopes]]
function foo2() {
console.log(window.userName); // undefined
}
console.dir(foo2); // 注意这一行
foo();
var userName = 'loki';
let age = 18;
const lastName = 'adam';
console.log(window.userName); // loki
打开控制台我们直接查看输出的foo是啥,
console.dir(foo2)的结果中有一个[[scopes]]属性, 该属性如下,在其中我们可以看到我们在全局用let
和const
声明的变量的存在, 于此同时在foo2内部输出window.userName回应undefined, 也是因为当我们有一条作用域链存在可供查找的时候, window上的属性会被暂时屏蔽掉, 所以我们在最后一行输出window,此时已经不在任何作用域链条下, 所以可以轻松的输出userName, 但是由于let
和const
声明的变量只存在于[[scopes]]中, 所以只要没有作用域链, 我们就无法访问到他们,而作用域链伴随函数而诞生, 所以笔者上方使用了一个函数
如果对作用域链和作用域不是很清晰的话可以进我博客看作用域那一篇, 看完以后你会对上一段话理解可能会深入一点
解决令人头疼的一个经典问题
很多人都知道let可以避免产生闭包的问题, 但是不知道为啥
var list = document.querySelector('li');
// 假设我获取到页面上十个li并且给他们绑定事件
function foo() {
for(var i = 0; i < list.length; i ++) {
list.onclick = function() {
console.log(i);
}
}
}
foo();
毋庸置疑上方代码点击任何一个li都是输出10, 至于为什么如果你还不知道的话就回去补补闭包那块儿的知识, 在往常我们都会通过立即执行函数或者闭包工厂来解决这类问题, 在ES6中不需要了直接上个let
var list = document.querySelector('li');
// 假设我获取到页面上十个li并且给他们绑定事件
function foo() {
// var 改成let
for(let i = 0; i < list.length; i ++) {
list.onclick = function() {
console.log(i);
}
}
}
foo();
因为let
会控制块级作用域, 而for循环刚好是一个块级作用域, 所以当每次循环的完毕都会开启一个新的块级作用域进行下一轮循环, 而非跟var
一样每次都是全局作用域, 所以根据作用域链的话点击事件每次点击得到的值都是自己上方块级作用域的i值
关于const的一些传说
我之前听一个百度的大佬说能用const
尽量不要用let
, 至于var就基本不要使用了, 因为在浏览器底层 对const
的检索可能是要优于let
的, 因为const
根本不会改变 而let
可能会经常变化, 所以大家可以尽量用const
, 毕竟即使这个概念不存在, 用const
也会使得我们避免大多数的声明赋值方面的错误, 然后在const
无法使用的场景在使用let