ES6 声明与表达式

62 阅读4分钟

1. let 与 const

1.1 let

基本用法

{ let a = 0; 
  a // 0 
} 
a // 报错 ReferenceError: a is not defined

代码块内有效

let 是在代码块内有效,var 是在全局范围内有效:

{
    let a = 0;
    var b = 1;
}
a  // ReferenceError: a is not defined
b  // 1

不能重复声明

let 只能声明一次 var 可以声明多次:

let a = 1;
let a = 2;
var b = 3;
var b = 4;
a  // Identifier 'a' has already been declared
b  // 4

无变量提升

let 不存在变量提升,var 会变量提升:

console.log(a);  //ReferenceError: a is not defined
let a = "apple"

console.log(b);  //undefined
var b = "banana";

变量 b 用 var 声明存在变量提升,所以当脚本开始运行的时候,b 已经存在了,
但是还没有赋值,所以会输出 undefined。

变量 a 用 let 声明不存在变量提升,在声明变量 a 之前,a 不存在,所以会报错。

暂时性死区

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

var tmp = 123;

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

顶层对象的属性

使用var关键字声明的全局作用域变量属于window对象。

使用let关键字声明的全局作用域变量不属于window对象

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回undefined

迭代计数使用

for 循环计数器很适合用 let

for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(i);
    })
}
// 输出十个 10
for (let j = 0; j < 10; j++) {
    setTimeout(function () {
        console.log(j);
    })
}
// 输出 0123456789

变量 i 是用 var 声明的,在全局范围内有效,所以全局中只有一个变量 i, 
每次循环时,setTimeout 定时器里面的 i 指的是全局变量 i ,
而循环里的十个 setTimeout 是在循环结束后才执行,所以此时的 i 都是 10。

变量 j 是用 let 声明的,当前的 j 只在本轮循环中有效,
每次循环的 j 其实都是一个新的变量,所以 setTimeout 定时器里面的 j 其实是不同的变量,即最后输出 12345。
(若每次循环的变量 j 都是重新声明的,如何知道前一个循环的值?
这是因为 JavaScript 引擎内部会记住前一个循环的值)。

1.2 const

const 声明一个只读变量,声明之后不允许改变。意味着,一旦声明必须初始化,否则会报错。

基本用法

const PI = "3.1415926";
PI  // 3.141592

const MY_AGE;  // SyntaxError: Missing initializer in const declaration    

暂时性死区

ES6 明确规定,代码块内如果存在 let 或者 const,代码块会对这些命令声明的变量从块的开始就形成一个封闭作用域。代码块内,在声明变量 PI 之前使用它会报错。

var PI = "a";
if (true) {
    console.log(PI);  // ReferenceError: PI is not defined
    const PI = "3.1415926";
}

注意要点

const 如何做到变量在声明初始化之后不允许改变的?

其实 const 保证的不是变量的值不变,而是保证变量指向的内存地址所保存的数据不允许改动。此时,你可能已经想到,简单类型和复合类型保存值的方式是不同的。是的,对于简单类型(数值 number、字符串 string 、布尔值 boolean),值就保存在变量指向的那个内存地址,因此 const 声明的简单类型变量等同于常量。而复杂类型(对象 object,数组 array,函数 function),变量指向的内存地址其实是保存了一个指向实际数据的指针,所以 const 只能保证指针是固定的,至于指针指向的数据结构变不变就无法控制了,所以使用 const 声明复杂类型对象时要慎重

2. 解构赋值

概述

解构赋值是对赋值运算符的扩展。

他是一种针对数组或者对象进行模式匹配,然后对其中的变量进行赋值。

在代码书写上简洁且易读,语义更加清晰明了;也方便了复杂对象中数据字段获取。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

解构模型

在解构中,有下面两部分参与:

  • 解构的源,解构赋值表达式的右边部分。
  • 解构的目标,解构赋值表达式的左边部分。

数组模型解构(Array)

如果解构不成功,变量的值就等于undefined

基本

let [a, b, c] = [1, 2, 3]; 
// a = 1 
// b = 2 
// c = 3

可嵌套

let [a, [[b], c]] = [1, [[2], 3]]; 
// a = 1 
// b = 2 
// c = 3

可忽略

let [a, , b] = [1, 2, 3]; 
// a = 1 
// b = 3

不完全解构

不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

let [a = 1, b] = []; 
// a = 1, 
// b = undefined

let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

剩余运算符

let [a, ...b] = [1, 2, 3]; 
//a = 1 
//b = [2, 3]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

字符串等

在数组的解构中,解构的目标若为可遍历对象,皆可进行解构赋值。可遍历对象即实现 Iterator 接口的数据。

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

let [a, b, c, d, e] = 'hello'; 
// a = 'h' 
// b = 'e' 
// c = 'l' 
// d = 'l' 
// e = 'o'

解构默认值

当解构模式有匹配结果,且匹配结果是 undefined 时,会触发默认值作为返回结果。

let [a = 2] = [undefined]; 
// a = 2

let [a = 3, b = a] = []; 
// a = 3, b = 3 
let [a = 3, b = a] = [1]; 
// a = 1, b = 1 
let [a = 3, b = a] = [1, 2]; 
// a = 1, b = 2
a 与 b 匹配结果为 undefined ,触发默认值:a = 3; b = a =3
a 正常解构赋值,匹配结果:a = 1,b 匹配结果 undefined ,触发默认值:b = a =1
a 与 b 正常解构赋值,匹配结果:a = 1,b = 2

对象模型解构(Object)

变量必须与属性同名,才能取到正确的值

如果解构失败,变量的值等于undefined

基本

let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; 
// foo = 'aaa' 
// bar = 'bbb' 

let { baz : foo } = { baz : 'ddd' }; 
// foo = 'ddd'

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
// 如果变量名与属性名不一致,必须写成下面这样

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

可嵌套可忽略

let obj = {p: ['hello', {y: 'world'}] }; 
let {p: [x, { y }] } = obj; 
// x = 'hello' 
// y = 'world' 

let obj = {p: ['hello', {y: 'world'}] }; 
let {p: [x, { }] } = obj; 
// x = 'hello'

不完全解构

let obj = {p: [{y: 'world'}] }; 
let {p: [{ y }, x ] } = obj; 
// x = undefined 
// y = 'world'

剩余运算符

let {a, b, ...rest} = {a: 10, b: 20, c: 30, d: 40}; 
// a = 10 
// b = 20 
// rest = {c: 30, d: 40}

解构默认值

let {a = 10, b = 5} = {a: 3}; 
// a = 3; b = 5; 

let {a: aa = 10, b: bb = 5} = {a: 3};
// aa = 3; bb = 5;

字符串解构赋值

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

let {length : len} = 'hello';
len // 5

数值和布尔值解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

常见用法

  • 函数计算任意个参数之和:
function sum(...a){
    return a.reduce((x,y) => x + y)
}

sum(1,2,3)  
// 6
  • 提取 JSON 数据
// 解构赋值对提取 JSON 对象中的数据,尤其有用。
let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]
  • 遍历 Map 结构

Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world
// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

3. Symbol

概述

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值,最大的用法是用来定义对象的唯一属性名

ES6 数据类型除了 Number 、 String 、 Boolean 、 Object、 null 和 undefined ,还新增了 Symbol 。

基本用法

Symbol 函数栈不能用 new命令,因为 Symbol 是原始数据类型,不是对象。可以接受一个字符串作为参数,为新创建的 Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。

let sy = Symbol("KK"); 
console.log(sy); // Symbol(KK) 
typeof(sy); // "symbol" 

// 相同参数 Symbol() 返回的值不相等 
let sy1 = Symbol("kk"); 
sy === sy1; // false

使用场景

作为属性名

用法

由于每一个 Symbol 的值都是不相等的,所以 Symbol 作为对象的属性名,可以保证属性不重名

let sy = Symbol("key1"); 

// 写法1 
let syObject = {}; 
syObject[sy] = "kk"; 
console.log(syObject); // {Symbol(key1): "kk"} 

// 写法2 
let syObject = { [sy]: "kk" }; 
console.log(syObject); // {Symbol(key1): "kk"} 

// 写法3 
let syObject = {}; 
Object.defineProperty(syObject, sy, {value: "kk"}); 
console.log(syObject); // {Symbol(key1): "kk"}

Symbol 作为对象属性名时不能用.运算符,要用方括号。因为.运算符后面是字符串,所以取到的是字符串 sy 属性,而不是 Symbol 值 sy 属性。

let sy = Symbol("key1");
let syObject = {}; 
syObject[sy] = "kk"; 

syObject[sy]; // "kk" 
syObject.sy; // undefined

注意点

Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。

但是不会出现在 for...in 、 for...of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols()Reflect.ownKeys() 取到。

let sy = Symbol("key1");
let syObject = {}; 
syObject[sy] = "kk"; 
console.log(syObject); 

for (let i in syObject) { 
    console.log(i); 
} // 无输出 
Object.keys(syObject); // [] 
Object.getOwnPropertySymbols(syObject); // [Symbol(key1)] 
Reflect.ownKeys(syObject); // [Symbol(key1)]

定义常量

    const COLOR_RED = Symbol("red");
    const COLOR_YELLOW = Symbol("yellow");
    const COLOR_BLUE = Symbol("blue");

    function ColorException(message) {
      this.message = message;
      this.name = "ColorException";
    }
    function getConstantName(color) {
      switch (color) {
        case COLOR_RED:
          return "COLOR_RED";
        case COLOR_YELLOW:
          return "COLOR_YELLOW ";
        case COLOR_BLUE:
          return "COLOR_BLUE";
        default:
          throw new ColorException("Can't find this color");
      }
    }

    try {

      var color = "green"; // green 引发异常
      var colorName = getConstantName(color);
    } catch (e) {
      var colorName = "unknown";
      console.log(e.message, e.name); // 传递异常对象到错误处理
    }

Symbol.for()

Symbol.for() 类似单例模式,首先会在全局搜索被登记的 Symbol 中是否有该字符串参数作为名称的 Symbol 值,如果有即返回该 Symbol 值,若没有则新建并返回一个以该字符串参数为名称的 Symbol 值,并登记在全局环境中供搜索。

let yellow = Symbol("Yellow"); 
let yellow1 = Symbol.for("Yellow"); 
yellow === yellow1; // false 

let yellow2 = Symbol.for("Yellow"); 
yellow1 === yellow2; // true

Symbol.keyFor()

Symbol.keyFor() 返回一个已登记的 Symbol 类型值的 key ,用来检测该字符串参数作为名称的 Symbol 值是否已被登记。


let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

// 变量s2属于未登记的 Symbol 值,所以返回`undefined`