ES6学习笔记
let、var、const声明变量
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
只有var和function声明的全局变量拥有顶层变量的属性,使用let 、const、class则不属于。
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
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
add表面上是一个数组,但是在传入参数之后就会被解构成两个变量分别为x、y,函数能够成功得到值。
[[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接口,配合变量的解构赋值,获取key、value就很方便。
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这个包中的SourceMapConsumer、SourceNode模块。
字符串的扩展
字符串的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()的问题在于,它可能返回0xD800到0xDFFF之间的单个码点。
JSON.stringify('\u{D834}') // "\u{D834}"
为了确保返回的是合法的 UTF-8 字符,ES2019改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。
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.count、basket.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 '