前端开发说说ES6核心基础中的let和const命令

Posted 李猫er

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端开发说说ES6核心基础中的let和const命令相关的知识,希望对你有一定的参考价值。

ECMAScript 6简介

在这里插入图片描述

ECMAScript 6.0(简称ES6)是javascript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。几年过去了,目前各大浏览器的最新版本对ES6的支持度已经越来越高了,ES6的大部分特性都实现了。

而我们都知道Node.js是JavaScript语言的服务器运行环境,Node.js对ES6的支持度比浏览器更高。通过Node,我们可以体验更多ES6的特性。使用版本管理工具nvm,来安装Node,优点是可以自由切换版本。

Babel 转换器

Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,也就是说你可以用ES6的方式去编程,又可以在你现有的环境执行又不用担心现有环境是否支持。实例:

// 转换前
input.map(number => number + 1);

// 转换后
input.map(function (number) {
  return number + 1;
});

原理:上面的原始ES6代码用了箭头函数,这个特性还没有目前得到广泛支持,Babel 转换器将其转为普通函数,就能在现有的JavaScript环境执行了。

配置文件 .babelrc

Babel的配置文件是.babelrc,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件。
该文件用来设置转码规则和插件,基本格式如下:

{
  "presets": [],
  "plugins": []
}

presets字段用来设定转码规则,官方提供以下的规则集,可以根据需要安装:

# ES2015转码规则
$ npm install --save-dev babel-preset-es2015

# react转码规则
$ npm install --save-dev babel-preset-react

# ES7不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3

然后将安装的规则加入到配置文件中:.babelrc:

  {
    "presets": [
      "es2015",
      "react",
      "stage-1"
    ],
    "plugins": []
  }

ES6 let 和 Const 命令

let 命令

ES6 新增了let 命令,用来声明变量。它的用法跟es5的var 类似,但是用它所声明的变量让只在let命令所在的代码有效,也就是块级作用域,这也弥补了es5中var的缺陷。超出的这个代码块的话会报错。
例如下面:
在这里插入图片描述
在这里插入图片描述

上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。

循环作用域

用 let

for (let i = 0; i < 10; i++) {}

console.log(i);  //报错: ReferenceError: i is not defined

计数器i只在for循环体内有效,在循环体外引用就会报错。

用 var

  for(var i = 0;i<10;i++){
    console.log(i);  // 0,1,2,3,4,5,6,7,8,9
  }

  console.log(i);  // 0,1,2,3,4,5,6,7,8,9,10

上面代码中,变量i是var声明的,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。

不存在变量提升

在JavaScript 中,var 关键字定义的变量可以在使用后声明,也就是变量可以先使用再声明,这就是所谓的“变量提升”。而let不像var那样会发生“变量提升”现象,不可以在使用后声明。所以,变量一定要在声明后使用,否则报错。

console.log(info);  // 输出undefined
var info = "var 关键字定义的变量可以在使用后声明";


console.log(info2);  //报错 Uncaught ReferenceError: Cannot access 'info2' before initialization
let info2 = "let 关键字定义的变量不可以在使用后声明";

在这里插入图片描述

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量:

  • 在相同的作用域或块级作用域中,不能使用 let 关键字来重置 var 关键字声明的变量;
// 报错
function(){
  var x = 10;
  let x= 20;
}

  • 在相同的作用域或块级作用域中,不能使用 let 关键字来重置 let 关键字声明的变量;
// 报错
function(){
  let x = 10;
  let x = 20;
}


  • 在相同的作用域或块级作用域中,不能使用 var 关键字来重置 let 关键字声明的变量。
// 报错
function(){
  let x = 10;
  var x = 20;
}

let 关键字在不同作用域,或不同块级作用域中是可以重新声明赋值的。

// 不报错
let x = 10;       

function(){
  let x = 20
}


function func(arg){
  {
    let arg
  }
}

因此,不能在函数内部重新声明参数。

块级作用域

关于块级作用域,为什么需要块级作用域?在 ES6 之前,是没有块级作用域的概念的。ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多场景应用缺陷。

  • 其中,第一种场景:内层变量可能会覆盖外层变量。
var date = new Date();

function d(){
  console.log(date);
  if (false){
    var date = "2020-11-21 00:15:34";
  }
}

d();  // 输出undefined 

上面代码中,函数d()执行后,输出结果为undefined,原因在于变量提升,导致内层的date变量覆盖了外层的date变量。

  • 第二种场景:用来计数的循环变量泄露为全局变量。
var str = "hello world!";

for (var i=0;i<str.length;i++){
  console.log(str[i]);
}

console.log(i);  // 12

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

ES6 的块级作用域

ES6新增的let为JavaScript新增了块级作用域。
let声明的变量只在 let命令所在的代码块 {}内有效,在 {} 之外不能访问。

let i = 1;

function f(){
  let i = 2;
  if (true){
    for(let i=1;i<10;i++){
      console.log(i);
    }
  }
}

console.log(i);  // 输出 1

上面的函数有三个代码块,都声明了变量i,运行后输出1。由于块级作用域的作用,外层代码块不受内层代码块的影响。

ES6 允许块级作用域的任意嵌套:

{{{{let username = "李子"}}}};

上面代码使用了一个四层的块级作用域。并且外层作用域无法读取内层作用域的变量。

{{{
{let username = "李子"}
  console.log(username);  // 报错:Uncaught ReferenceError: username is not defined
}}};

内层作用域可以定义外层作用域的同名变量。

{{{
let username = "李子";
{let username = "李子"}
}}};

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了,写法更加简洁。

// IIFE 写法
(function () {
  var info = ...;
  ...
}());

// 块级作用域写法
{
  let info = ...;
  ...
}

块级作用域与函数声明

  • 在ES5中,js函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。尤其是在“严格模式”下如果在块级作用域中声明是会报错的。
// ES5严格模式
'use strict';

// 报错
if (true) {
  function f() {}
}

// 报错
try {
  function f() {}
} catch(e) {
}
  • 而ES6新增了块级作用域,明确允许在块级作用域中声明函数:
// ES6严格模式
'use strict';
if (true) {
  function f() {}
}
// 不报错
  • ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
        function f(){
            console.log("我在块级作用域外部");
        }

         (function(){
             while(true){
                 function f(){
                     console.log("我在块级作用域内部");
                 }
             }
             f();
         }());  // 我在块级作用域内部

上面代码在 ES5 中运行,会得到“我在块级作用域内部”,因为在if内声明的函数f会被提升到函数头部。

而如果用ES6特新来运行的话,结果就完全不一样了,结果依旧是:“我在块级作用域外部”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响,实际运行的代码如下:

        function f(){
            console.log("我在块级作用域外部");
        }

         (function(){
             f();
         }());  
  • ES6的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。
    1. 没有使用{ }:
// 报错
'use strict';
if (true)
  function f() {}

在这里插入图片描述
2. 使用{}:

//不报错 注意是在es6 浏览器中,如果在es5浏览器中,严格模式下依然会报错
'use strict';
if (true) {
  function f() {}
}

块级作用域与函数声明总结:

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。
    以上只对支持ES6的浏览器实现有效,其他环境还是将块级作用域的函数声明当作let处理就可以了。

由于环境的差异,如果在块级作用域中声明函数可能会有很多错误,所以,我们尽可能的避免在块级作用域中声明函数,如果必要在块级作用域中声明函数,可以使用函数表达式的方式代替函数声明语句

         // 函数声明语句
        {
          let name = "李子";
          function info(){
              return name;
          }
        }
        
        // 函数表达式
        {
            let name = "李子";
            let info = function(){
                return name;
            };
        }

do 表达式

本质上,块级作用域是一个将多个操作封装在一起的语句,没有返回值。

{
  let num = f();
  num = num + 1;
}

上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到num的值,因为块级作用域不返回值,除非num是全局变量。

如果可以将块级作用域变为表达式,也就是说可以返回值,那么问题就解决了。那怎么实现?办法就是在块级作用域之前加上do,使它变为do表达式。

let x = do {
  let num = f();
  num = num + 1;
};

const 命令

const声明一个只读的常量。一旦声明,常量的值就不能改变。
const一旦声明变量,就必须立即初始化,不能留到以后赋值。

// 一旦声明,常量的值就不能改变:
const name = "李子"; 
name = "李猫er"   //报错:TypeError: Assignment to constant variable.

// const一旦声明变量,就必须立即初始化,不能留到以后赋值:
const name;  // 报错:Missing initializer in const declaration

对于const来说,一旦声明,常量的值就不能改变;而且只声明不赋值,就会报错。

  • const的作用域与let命令相同:只在声明所在的块级作用域内有效。
if (true) {
  const name = "李子";
}

console.log(name);  //  Uncaught ReferenceError: name is not defined
  • const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
    在常量name声明之前就调用,结果会报错,如下:
if (true) {
  console.log(name);  //  ReferenceError: Cannot access 'name' before initialization
  const name = "李子";
}
  • const声明的常量,也与let一样不可重复声明。
var name = "李子";
let age = 20;

const name = "李子";  // eferenceError: Cannot access 'name' before initialization
const age = 20;
  • 对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
const info = {};
info.prop = 678;

info = {}  // TypeError: Assignment to constant variable.

上面代码中,常量info储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把info指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性:

const info = [];
info.push('李子');
info.push(20);

info = ['李猫er'] // TypeError: Assignment to constant variable.

上面代码中,常量info是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给info,就会报错。

ES5只有两种声明变量的方法:var命令和function命令。ES6除了添加let和const命令,另外还有两种声明变量的方法:import命令和class命令。所以,ES6一共有6种声明变量的方法。

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在Node指的是global对象。ES5之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a  // 1

a = 2;
window.a // 2

顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。

global 对象

ES5的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是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');
};

现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象。

垫片库system.global模拟了这个提案,可以在所有环境拿到global

// CommonJS的写法
require('system.global/shim')();

// ES6模块的写法
import shim from 'system.global/shim'; shim();

上面代码可以保证各种环境里面,global对象都是存在的。

// CommonJS的写法
var global = require('system.global')();

// ES6模块的写法
import getGlobal from 'system.global';
const global = getGlobal();

上面代码将顶层对象放入变量global。

学习文档:http://caibaojian.com/es6/

以上是关于前端开发说说ES6核心基础中的let和const命令的主要内容,如果未能解决你的问题,请参考以下文章

关于var和ES6中的let,const的理解

var let及const

前端面试经典题之ES6新特性

ES6 常用总结(前端开发js技术进阶提升总结)

ES6 let和const命令异同

ES6中的let和const