ES6 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

上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量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

上面代码中,变量ilet声明的,当前的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;

上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

简单理解变量提升就是在变量定义之前使用变量,浏览器不报错而显示undefined。let不存在变量提升,只要在定义前使用变量就会报错。

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用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 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法: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,内容安全策略),那么evalnew 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面向对象编程指南》,暂时未理解)

闭包的特性

csdn中的文章介绍了闭包的三个特性:

①函数嵌套函数 ②函数内部可以引用函数外部的参数和变量 ③参数和变量不会被垃圾回收机制回收

闭包的作用

闭包最常见的作用是在父函数中定义子函数,子函数返回父函数的某个变量,使其在父函数外也可以访问父函数的变量获取值。闭包最常见的使用时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("我是匿名函数")}() 
立即执行函数的作用:
  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
  3. 封装变量

总而言之:立即执行函数会形成一个单独的作用域,我们可以封装一些临时变量或者局部变量,避免污染全局变量

与立即执行函数相关的面试题:
<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>
立即执行函数使用的场景
  1. 你的代码在页面加载完成之后,不得不执行一些设置工作,比如时间处理器,创建对象等等。
  2. 所有的这些工作只需要执行一次,比如只需要显示一个时间。
  3. 但是这些代码也需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,如果将这些变量作为全局变量,不是一个好的注意,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中,不会让任何变量泄露成全局变量,看如下代码:
    在这里插入图片描述
    比如上面的代码,如果没有被包裹在立即执行函数中,那么临时变量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)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值