let和const命令及引发的知识补充
ES6 let和const命令
1、let命令
基本用法
ES6 新增了
let
命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
for(let i = 0;i < 10; i++){
//...
}
console.log(i);// ReferenceError: i is not defined
下面的代码如果使用var,最后输出的是10。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量
i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是10。
上面的代码块涉及到闭包,见下方闭包。
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量
i
是let
声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为
JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。
2019/09/10理解: 每个i的作用域都只在当前循环内生效,比如i=0
只在
a[i] = function(){
console.log(i);
};(i=0)
这个范围内生效,当i=1
时又是一个新的作用域,不能作用于i=0这个作用域内,所以a[0]()
的值为0,同理可得a[6]()
的值为6.
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次
abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域。
不存在变量提升
var
命令会发生变量提升现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
上面代码中,变量
foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,所以会输出undefined
。变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
简单理解变量提升就是在变量定义之前使用变量,浏览器不报错而显示undefined
。let不存在变量提升,只要在定义前使用变量就会报错。
暂时性死区
只要块级作用域内存在
let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量
tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。ES6明确规定,如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用
let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
还有一些暂时性死区的细节说明可见阮一峰ECMAScript6入门:let暂时性死区说明
2、块级作用域
没有块级作用域带来的不合理场景
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
上面代码的原意是,
if
代码块的外部使用外层的tmp
变量,内部使用内层的tmp
变量。但是,函数f执行后,输出结果为undefined
,原因在于变量提升,导致内层的tmp
变量覆盖了外层的tmp
变量。第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量
i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
ES6块级作用域
let
实际上为 JavaScript 新增了块级作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函数有两个代码块,都声明了变量
n
,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var
定义变量n
,最后输出的值才是 10。ES6 允许块级作用域的任意嵌套。
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};
上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。
内层作用域可以定义外层作用域的同名变量。
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
3、const
基本用法
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上面代码表明改变常量的值会报错。
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代码表示,对于const来说,只声明不赋值,就会报错。
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
上面代码在常量MAX声明之前就调用,结果报错。
const声明的常量,也与let一样不可重复声明。
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量
foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。下面是另一个例子。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给
a
,就会报错。如果真的想将对象冻结,应该使用
Object.freeze
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量
foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,后面章节还会提到,另外两种声明变量的方法:import
命令和class
命令。所以,ES6 一共有 6 种声明变量的方法。
4、顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6为了改变这一点,一方面规定,为了保持兼容性,
var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从
ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。
5、globalThis 对象
JavaScript语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
- 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
- 浏览器和 Web Worker里面,self也指向顶层对象,但是 Node 没有self。
- Node 里面,顶层对象是global,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用
this
变量,但是有局限性。
- 全局环境中,
this
会返回顶层对象。但是,Node 模块和 ES6 模块中,this
返回的是当前模块。- 函数里面的
this
,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this
会指向顶层对象。但是,严格模式下,这时this
会返回undefined
。- 不管是严格模式,还是普通模式,
new Function('return this')()
,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval
、new Function
这些方法都可能无法使用。
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
// 方法一
(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
// 方法二
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
现在有一个提案,在语言标准的层面,引入
globalThis
作为顶层对象。也就是说,任何环境下,globalThis
都是存在的,都可以从它拿到顶层对象,指向全局环境下的this。垫片库
global-this
模拟了这个提案,可以在所有环境拿到globalThis
。
6、知识补充
闭包
什么是闭包?
闭包的官方概念:闭包是指有权访问另一个函数作用域中的变量的函数。
菜鸟教程对闭包的理解:闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。直观的说就是形成一个不销毁的栈环境。
百度经验简单明了的闭包理解:闭包就是能够读取其他函数内部变量的函数。由于在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
我的理解:闭包在javaScript中的含义是能访问另一个作用域中变量的函数,表现形式是所有函数内部定义的函数,因为只要是函数内部定义的函数,就可以访问他父作用域内的变量,但是函数内部定义的函数虽可以访问父作用域内的值,但是不会将其返回,因此现在使用闭包通常是在函数内部定义函数,子函数将父函数的某个值返回,使得在全局范围或者祖范围内可以得到原本无法获取的父函数的值。
举个例子:
function nv(){
var n=10;
function subNv(){return n;}
return subNv;
}
var subNv=nv();
subNv();//10
当有个函数nv
的内部有个局部变量n
,这个变量无法在外面直接得到时,我们通过在这个函数里面在定义一个函数subNv
,并且用这个定义的函数去得到这个局部变量,最后我们在返回这个子函数subNv
,就能通过返回的这个函数得到局部变量n。这里的subNv
就是闭包。
事实上,每个函数都可以被理解成是一个闭包,因为每个函数都在其所在域中维护了某种私有联系。但在大多数时候,该作用域在函数体执行完之后就自行销毁了,除非遇到闭包,导致作用域被保持。(出自《Javascript面向对象编程指南》,暂时未理解)
闭包的特性
①函数嵌套函数 ②函数内部可以引用函数外部的参数和变量 ③参数和变量不会被垃圾回收机制回收
闭包的作用
闭包最常见的作用是在父函数中定义子函数,子函数返回父函数的某个变量,使其在父函数外也可以访问父函数的变量获取值。闭包最常见的使用时getter和setter。如下代码段所示:
var getValue,setValue;
(function(){
var secret = 0;
getValue = function(){
return secret;
};
setValue = function(v){
secret = v;
};
}());
在代码段中定义了一个立即执行函数,在其中定义了全局函数getValue()和setValue(),这两个函数有权访问父作用域也就是立即执行函数中的secret,因此这两个函数是闭包。getValue()返回secret的值,setValue设置secret的值,以此来确保局部变量secret的不可直接访问性。
闭包的坑
循环中的闭包是闭包中最常见的坑,参考以下代码:
function F(){
var arr = [],i;
for(i = 0;i < 3; i++){
arr[i] = function(){
return i;
};
}
return arr;
}
var arr = F();
arr[0]();//3
arr[1]();//3
arr[2]();//3
为什么输出都是3呢?因为arr[i] = function(){return i;};
嵌套在函数F()内,因此循环的3个arr[i] = function(){return i;};
是三个闭包,他们的i
取的是父组件中的i
,又由于创建函数var arr = F();
时,子函数不会被立即执行,因此这三个闭包并不会记录当时i
的值,只会将它们指向一个共同的局部变量i
。当通过arr[0]()
取值时,这时的i
在F()函数中已经是固定值3了,因此三个闭包返回的值都是3。
怎么避免这种情况的发生呢?在ES6中的let出来之前,使用立即执行函数,换一种闭包形式,代码如下:
function F(){
var arr = [],i;
for(i = 0;i < 3; i++){
arr[i] = (function(x){
return function(){
return x;
};
};)(i)
}
return arr;
}
var arr = F();
arr[0]();//0
arr[1]();//1
arr[2]();//2
使用立即执行函数,给立即执行函数传入当前的i
,这样全局变量i
就被赋值给了局部变量x
,x
被保护在立即执行函数内,也只作用于立即执行函数内,因此每次调用arr[i]()
就可以得到不同的值。
在let出来之后,只需将for循环内的i
使用let声明就可以:
function F(){
var arr = [];
for(let i = 0;i < 3; i++){
arr[i] = function(){
return i;
};
}
return arr;
}
var arr = F();
arr[0]();//3
arr[1]();//3
arr[2]();//3
因为let只作用于当前作用域内,当i=0
时,i的作用域为for循环中的判断条件,arr[i] = function(){return i;};
是一个闭包,取到了父函数for中i
的值。这个i
只作用于当前作用域,等价于
{
let i=0;
arr[i] = function(){
return i;
}
}
{
let i=1;
arr[i] = function(){
return i;
}
}
{
let i=2;
arr[i] = function(){
return i;
}
}
立即执行函数
立即执行函数的写法
有时,我们定义函数之后,立即调用该函数,这时不能在函数的定义后面直接加圆括号,这会产生语法错误。产生语法错误的原因是,function
这个关键字,既可以当做语句,也可以当做表达式,比如下边:
//语句
function fn() {};
//表达式
var fn = function (){};
为了避免解析上的歧义,JS引擎规定,如果function
出现在行首,一律解析成语句。因此JS引擎看到行首是function
关键字以后,认为这一段都是函数定义,不应该以原括号结尾,所以就报错了。
解决方法就是不要让function
出现在行首,让JS引擎将其理解为一个表达式,最简单的处理就是将其放在一个圆括号里,比如下边:
(function(){
//code
}())
(function (){
//code
})()
上边的两种写法,都是以圆括号开头,引擎会意味后面跟的是表达式,而不是一个函数定义语句,所以就避免了错误,这就叫做"立即调用的函数表达式"。
立即执行函数,还有一些其他的写法(加一些小东西,不让解析成语句就可以),比如下边:
(function () {alert("我是匿名函数")}()) //用括号把整个表达式包起来
(function () {alert("我是匿名函数")})() //用括号把函数包起来
!function () {alert("我是匿名函数")}() //求反,我们不在意值是多少,只想通过语法检查
+function () {alert("我是匿名函数")}()
-function () {alert("我是匿名函数")}()
~function () {alert("我是匿名函数")}()
void function () {alert("我是匿名函数")}()
new function () {alert("我是匿名函数")}()
立即执行函数的作用:
- 不必为函数命名,避免了污染全局变量
- 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
- 封装变量
总而言之:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量
与立即执行函数相关的面试题:
<body>
<ul id="list">
<li>公司简介</li>
<li>联系我们</li>
<li>营销网络</li>
</ul>
<script>
var list = document.getElementById("list");
var li = list.children;
for(var i = 0 ;i<li.length;i++){
li[i].onclick=function(){
alert(i); // 结果总是3.而不是0,1,2
}
}
</script>
</body>
为什么alert
总是3? 通过for
循环给每个元素绑定alert
事件,循环结束,i
值为3. onclick
指向一个匿名函数,当事件发生,显示弹框,i
是最后的 i
,而不是绑定时的值。原因是在函数内定义函数,会形成闭包,内部函数可以访问包含函数内的变量(这里是i
),但是内部函数访问的外部函数执行结束后的i
值,而不是绑定那一刻的值,因为那一刻,事件还未发生。
那么怎么解决这个问题呢,可以用立即执行函数,给每个li
创建一个独立的作用域,在立即执行函数执行的时候,i
的值从0到2,对应三个立即执行函数,这3个立即执行函数里边的j
分别是0,1,2所以就能正常输出了,看下边例子:
<body>
<ul id="list">
<li>公司简介</li>
<li>联系我们</li>
<li>营销网络</li>
</ul>
<script>
var list = document.getElementById("list");
var li = list.children;
for(var i = 0 ;i<li.length;i++){
(function(j){
li[j].onclick = function(){
alert(j);
})(i); 把实参i赋值给形参j
}
}
</script>
</body>
也可以使用ES6的块级作用域解决整个问题
<body>
<ul id="list">
<li>公司简介</li>
<li>联系我们</li>
<li>营销网络</li>
</ul>
<script>
var list = document.getElementById("list");
var li = list.children;
for(let i = 0 ;i<li.length;i++){
li[i].onclick=function(){
alert(i); // 结果是0,1,2
}
}
</script>
</body>
立即执行函数使用的场景
- 你的代码在页面加载完成之后,不得不执行一些设置工作,比如时间处理器,创建对象等等。
- 所有的这些工作只需要执行一次,比如只需要显示一个时间。
- 但是这些代码也需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,如果将这些变量作为全局变量,不是一个好的注意,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中,不会让任何变量泄露成全局变量,看如下代码:
比如上面的代码,如果没有被包裹在立即执行函数中,那么临时变量todaydom,days,today,year,month,date,day,msg都将成为全局变量(初始化代码遗留的产物)。用立即执行函数之后,这些变量都不会在全局变量中存在,以后也不会其他地方使用,有效的避免了污染全局变量。
立即执行函数的参数
(function(j){
//代码中可以使用j
})(i)
如果立即执行函数中需要全局变量,全局变量会被作为一个参数传递给立即执行函数(上例中的i就是一个全局变量,i代表的是实参,j是i在立即执行函数中的形参)。
立即执行函数的返回值
像其他函数一样,立即执行函数也可以有返回值。除了可以返回基本类型值以外,立即执行函数也能返回任何类型的值,比如对象,函数。
上例中立即执行函数的返回值被赋值给了一个变量result,这个函数简单的返回了res的值,这个值事先被计算并被存储在立即执行函数的闭包中。
在五中,如果在以后的代码中我需要msg这个值,我也可以返回一个包含msg的对象,方便在以后代码中使用(这样五中的一些临时变量也没有暴露在外面)。
总结立即执行函数有哪些作用?
1、改变变量的作用域(创建一个独立的作用域)
<body>
<ul id="list">
<li>公司简介</li>
<li>联系我们</li>
<li>营销网络</li>
</ul>
<script>
var list = document.getElementById("list");
var li = list.children;
for(var i = 0 ;i<li.length;i++){
(function(j){
li[j].onclick = function(){
alert(j);
})(i); 把实参i赋值给形参j
}
}
</script>
</body>
改变变量i的作用域,把全局变量i以参数的形式传递到立即执行函数中,在立即执行函数中定义变量i的形参变量j,变量j就是在立即执行函数的作用域中。(给每个li创建了一个作用域块,点击的时候寻找自由变量j,在立即执行块中找到)
2、封装临时变量
在上面的代码中,可以封装临时变量,避免全局变量的污染。也可以返回一个在全局中需要的变量(用return)。