ES6学习笔记之let和const

719 阅读7分钟

let 和 const 命令

ES6 声明变量的六种方法

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

let 命令

基本用法

  1. let用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
  2. 定义的变量只在该作用区域内有效,所以不会影响父作用域
{
 let a = 1
 var b = 2
}
a  // 报错:Uncaught ReferenceError: a is not defined at <anonymous>:1:1
b // 2

let 定义的变量只在该作用区域内有效,所以不会影响父作用域
所以很适合在for 循环中使用

var arr = []
for(var i =0; i< 10; i++) {
  arr[i] = function () {
    console.log(i)
  }
}
arr[1]()  // 10
arr[5]()  // 10

以上循环出的arr内的方法 执行的都是console.log(i), 这里的i就是一个全局变量,输出始终输出全局的i所以arr的每一个成员输出结果都一样

要正确输出遍历的数字,应该使用let 来定义循环的变量

var arr = []
for(let i =0; i< 10; i++) {
  arr[i] = function () {
    console.log(i)
  }
}

不存在变量提升

使用var 声明变量后变量变为全局,如果在声明之前调用变量,他的值为undefined 而不报错。 使用let 声明变量后,在声明之前调用变量就会报错,使逻辑看起来更合理,并可以正确的找到错误

// var 的情况
console.log(foo) // 输出:undefined
var foo = 2

// let 的情况
console.log(bar) // 报错:Uncaught ReferenceError: bar is not defined at <anonymous>:1:13
let bar = 2

let的这一特性在 块级作用域内同样适用

{
  a = 1 //报错: Uncaught ReferenceError: a is not defined at <anonymous>:1:5
  let a 
}

这个功能会影响到typeof 的使用

typeof xxx  // 一个不会声明的变量: undefined

typeof a  // undefined
var a

typeof b //报错: Uncaught ReferenceError: b is not defined at <anonymous>:1:5
let b

类似以下代码也会报错

var x = x // undefined

let x = x // ReferenceError: x is not defined

不允许重复声明

let a = 1
let a = 2 // 报错:Uncaught SyntaxError: Identifier 'dsfsdf' has already been declared at <anonymous>:1:1

var a = 1
let a = 2 // 报错:Uncaught SyntaxError: Identifier 'dsfsdf' has already been declared at <anonymous>:1:1

块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

为什么需要块级作用域

如果不使用块级作用域 会出现以下情况

  1. 内层变量可能会覆盖外层变量
var tmp = new Date()

function f() {
  console.log(tmp)
  if (false) {
    var tmp = 'hello world'
  }
}

f() // undefined
  1. 用来计数的循环变量泄露为全局变量。
for (var i = 0; i < 10; i++) {
  console.log(i)
}

console.log(i) // 10

ES6 的块级作用域

let为 JavaScript 新增了块级作用域。

function f1() {
  let n = 5
  if (true) {
    let n = 10
  }
  console.log(n) // 5
}

块级作用域以下特点:

  1. 允许块级作用域的任意嵌套

  2. 外层作用域无法读取内层作用域的变量

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

{{{{
  {
  let insane = 'Hello World'
    {let insane = 'Hello World'}
  }
  console.log(insane) // 报错
}}}}

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

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

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

块级作用域与函数声明

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
但是为了兼容部分旧代码,所以在块级作用域声明函数不会报错,可以运行

// 情况一
if (true) {
  function f() {}
}

// 情况二
try {
  function f() {}
} catch(e) {
  // ...
}

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可被引用。 但是,实际运行结果并不是这样,为了兼容部分老代码。仅对于ES6环境的浏览器运行中块级作用域之中声明的函数行为类似于var,因此会存在变量提升。

function f() { console.log('I am outside!') }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!') }
  }

  f()
}())

像上面的代码理论上是是会执行外面的f()的,实际上并不会,而且还会因为变量提升报错。

在开发过程中,应该避免在块级作用域中声明函数,如果实在需要,最好写成函数表达式,而不是函数声明语句。

// 函数声明语句
{
  function f() {
    return 11
  }
}

// 函数表达式
{
  let f = function () {
    return 111
  }
}

const 命令

基本用法

const声明一个只读的常量。

  1. 一旦声明,常量的值就不能改变。
  2. 因为第一条的原因,const声明常量的时候就必须赋值。
  3. const的作用域与let命令相同:只在声明所在的块级作用域内有效。
  4. const声明的常量,也与let一样不可重复声明。
// 1
const a = 1
a = 111      // 报错:TypeError: Assignment to constant variable.

// 2
const b     // 报错: Uncaught SyntaxError: Missing initializer in const declaration

// 3
{
	const c = 33
}
c          // 报错: Uncaught ReferenceError: c is not defined

const 本质

const 保证的并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。 对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。

const obj = {}
obj.a = 1  //  obj = {a: 1}

obj = {a: 1}  // 报错:Uncaught TypeError: Assignment to constant variable.

const arr = []
arr[0] = 'a' // arr = ['a']

arr = ['a'] // 报错:Uncaught TypeError: Assignment to constant variable.

如果想要一个无法更改的对象,可以使用Object.freeze方法。

const obj = Object.freeze({a: 1})

// 常规模式时,下面一行不起作用
// 严格模式时,该行会报错
obj.a = 2

对象还有一些属性也应该冻结,下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj)
  for (let key of Object.keys(obj)) {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  }
}

顶层对象的属性

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

window.a = 1;
a // 1

a = 2;
window.a // 2

ES6 中:

  • var命令和function命令声明的全局变量,依旧是顶层对象的属性;
  • let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

global 对象

ES5 的顶层对象存在一些问题,各种环境下顶层对象不统一

  1. 浏览器里面,顶层对象是windowself
  2. Web Worker里面,顶层对象是self
  3. Node 里面,顶层对象是 global

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

  1. 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
  2. 函数里面的this,在严格模式下会返回undefined
  3. 不管是严格模式,还是普通模式,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');
};