ES6 学习笔记(上) | 青训营笔记

75 阅读3分钟

ES6学习笔记

letvarconst声明变量

var

具备变量提升特性(当在声明变量之前调用变量,变量的值为undefined)。

在代码块外用var关键词声明变量a,在代码块中用let关键词声明同名变量a,代码块内优先调用let声明变量。

var a = 2;
{
  let a = 3;
  console.log(a); // 3
}
console.log(a); // 2
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

let

不具备变量提升特性。

声明的变量只作用于所在的代码块内。

{
	let i = 0;
	console.log(i); // 0
}
console.log(i); // not defined

const

用于声明常量,但不代表值不可变。

如果声明的是简单类型(数值、字符串、布尔值),那么值不可变。如果声明的是复合类型(数组、对象),const只能保证变量指向的内存地址即指针不变,但对于内部具体数据是否可以修改无法保证。

如果想将某一个对象冻结,可使用Object.freeze方法。

声明变量名为大写,且声明时需赋值。

{
  const ARR = [5,6];
  ARR.push(7);
  console.log(ARR); // [5,6,7]
  ARR = 10; // TypeError
}

顶层对象

在浏览器环境下,顶层对象的前缀是window,Node环境下是global,顶层对象属性与全局变量是等价的。

window.a = 1;
a // 1
a = 2;
window.a // 2

只有varfunction声明的全局变量拥有顶层变量的属性,使用letconstclass则不属于。

var a = 1;
window.a // 1
let b = 1;
window.b // undefined

globalThis

ES6当中引入globalThis作为顶层对象,在任何环境下,globalThis都是存在的,指向全局环境下的This

变量的解构赋值

数组的解构赋值

在ES6以前,为变量赋值只能直接指定值。

let a = 1;
let b = 2;
let c = 3;

ES6新增如下。

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

这种方法被称为模式匹配。

只要等号两边的格式相同就可以直接赋值。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

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

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

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

如果解构不成功,那么变量值就返回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

格式是匹配的,依然可以解构。

如果等号右边不是数组,那么就会报错,或者说只要这个对象拥有IteratorAPI就可以解构。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

Set结构也可以成功解构。

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

fibs是一个Generator函数,拥有IteratorAPI,因此能够解构。

解构赋值允许指定默认值。

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x = 1] = [null]; // x = null
let [x = 1] = [undefined]; // x = 1

当解构赋值的值是null,默认值失效,可以把null当成某种具体值,但是如果换成undefined,那么默认值不会失效。

如果默认值是一个表达式,那么这个表达式是惰性求值的,只有真正需要的时候才会进行求值。

function f() {
  console.log('aaa');
}

let [x = f()] = [1];

如上面这段代码,控制台不会输出aaa,即f函数没有执行,因为x能取到值,就不需要默认值。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

最后一句报错是因为x在引用y的时候,y还没有声明。

数组属于特殊的对象,可以对数组进行对象属性的解构赋值。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

其中0(也可以写作[0])就是代表数组中下标是0的元素,[arr.length - 1]对应代表的是数组中最后一个元素。[arr.length - 1]是属性名表达式。

对象的解构赋值

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

对象的解构赋值没有特定顺序,即只要对象中拥有与变量同名的属性即可。

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

let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

这种写法将Math对象中的log,sin,cos方法分别赋给log,sin,cos三个变量,以及console对象中的log方法赋给log变量,如果需要在代码中大量使用某些方法,这种声明可以减少一些代码量。

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 { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面代码中,解构赋值匹配的模式是foo,而真正被赋值的变量是baz

嵌套对象同样支持解构赋值。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

// 报错
let {foo: {bar}} = {baz: 'baz'};

对象的结构赋值也支持默认值。

var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

默认值生效的条件是对象的属性值严格等于undefined

var {x = 3} = {x: undefined};
x // 3

var {x = 3} = {x: null};
x // null

与上面所说的“可以把null当成某种具体值”一致。

对于已经声明的变量进行解构赋值应该注意。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面的代码中,JS引擎会将{x}理解成一个代码块,解构赋值会发生语法错误。

// 正确的写法
let x;
({x} = {x: 1}); // 运用圆括号进行包裹

字符串的解构赋值

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

此时字符串被转换成了一个类似数组的对象。

作为一个类似对象的数组,同样拥有length属性。

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

数值和布尔值的解构赋值

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

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

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

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

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构赋值

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

add表面上是一个数组,但是在传入参数之后就会被解构成两个变量分别为xy,函数能够成功得到值。

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

函数参数解构也可以使用默认值

function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

用途

交换变量的值

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

从函数返回多个值

函数如果想返回多个值,只能通过放在数组里或者对象里,再通过解构赋值将值取出来。

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

遍历Map结构

任何拥有Iterator接口的对象都可以使用for...of循环遍历,Map本身自带Iterator接口,配合变量的解构赋值,获取keyvalue就很方便。

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) {
  // ...
}

输入模块的指定方法

解构赋值能够使得在加载模块的时候,语句更加清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

上面这行代码运用了解构赋值,引用了source-map这个包中的SourceMapConsumerSourceNode模块。

字符串的扩展

字符串的Unicode表示法

"\u0061"
// "a"

上面这种表示方法,只限于码点在\u0000~\uFFFF之间的字符,对于超出范围的字符,必须使用两个双字节形式表示,如下。

"\uD842\uDFB7"
// "𠮷"

"\u20BB7"
// " 7"

可以看到第二个输出的是空格加上一个7,是因为JS会将上面的代码理解成\u20BB+7,而\u20BB是一个无法打印的字符,所以输出一个空格,而7则正常输出。

解决方法如下,也就是将码点放入大括号当中。

"\u{20BB7}"
// "𠮷"

"\u{41}\u{42}\u{43}"
// "ABC"

let hello = 123;
hell\u{6F} // 123

'\u{1F680}' === '\uD83D\uDE80'
// true

大括号表示法与四字节的UTF-16编码是等价的。

'\z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

以上是五种字符表示法。

字符串的遍历器接口

ES6为字符串添加了Iterator接口,因此字符串可以使用for...of循环遍历。

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

for...of循环可以可以识别大于0xFFFF的码点,而传统的for循环则无法识别,如下。

let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "𠮷"

由于0x20BB7是比0xFFFF大的码点,因此传统的for循环会将其识别成两个字符并都无法打印,而for...of就可以。

直接输入U+2028和U+2029

JS的字符串允许直接输入字符,也允许输入字符的转义形式,如下。

'中' === '\u4e2d' // true

但有五个字符较为特殊,他们不允许直接输入字符,只能够使用字符的转义形式,如下。

  • U+005C:反斜杠(reverse solidus)
  • U+000D:回车(carriage return)
  • U+2028:行分隔符(line separator)
  • U+2029:段分隔符(paragraph separator)
  • U+000A:换行符(line feed)

就像我们在写C语言等代码的时候,字符串中无法直接包含``,JS也一样,一定要转义成为\或者\u005c

JSON格式本身不允许直接输入正则表达式。

JSON.stringify()的改造

JSON标准规定JSON数据必须使用 UTF-8 编码,但是JSON.stringify()方法不一定返回的是 UTF-8 标准的编码。

JSON.stringify()的问题在于,它可能返回0xD8000xDFFF之间的单个码点。

JSON.stringify('\u{D834}') // "\u{D834}"

为了确保返回的是合法的 UTF-8 字符,ES2019改变了JSON.stringify()的行为。如果遇到0xD8000xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('\u{D834}') // ""\uD834""
JSON.stringify('\uDF06\uD834') // ""\udf06\ud834""

模版字符串

传统的 JS 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);

上面这段代码中,使用了模版字符串,其中变量用${...}进行包裹,像basket.countbasket.onSale就是变量,模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = ``Yo` World!`;

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

上面代码中,所有模板字符串的空格和换行,都是被保留的,比如<ul>标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

模板字符串中嵌入变量,需要将变量名写在${}之中。

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      // 传统写法为
      // 'User '(4) // 'x   '