🥷let & const
let命令
var声明的变量在全局范围内有效,let声明的变量只在它所在的代码块有效(适用于for循环的计数器)
{
let a = 10;
var b = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 1
for (let i = 0; i < 10; i++) { // 注意这里的i是一个父作用域
console.log(i);
}
var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。而let命令所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。这在语法上,称为“暂时性死区”。暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
🎯const命令
const声明一个只读的常量。一旦声明,常量的值就不能改变,且必须立即初始化。
const foo; // 'const' declarations must be initialized
const PI = 3.14;
PI = 5.14; // Inline Babel script: "PI" is read-only
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
ES6声明变量的6种方法:var命令、function命令、let命令、const命令、import命令和class命令
🥷变量的解构赋值
数组的解构赋值
从数组和对象中提取值,对变量进行赋值,这被称为解构
let [a, b, c] = [1, 2, 3];
let [x, y, ...z] = ['a']; // x="a" y=undefined z=[]
解构赋值允许指定默认值(只有当一个数组成员严格等于undefined,默认值才会生效)
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
对象的解构赋值
对象的解构中,对象的属性没有次序,变量必须与属性同名,才能取到正确的值。如果解构失败,变量的值等于undefined
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
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。如下代码,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
对象的解构也可以指定默认值,但是默认值生效的条件是对象的属性值严格等于undefined
var {x = 3} = {x: undefined}; // x=3
var {x = 3} = {x: null}; // x=null
字符串的解构赋值
let {length : len} = 'hello'; // len=5
数值布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象
let {toString: s} = 123;
s === Number.prototype.toString // true
函数参数的解构赋值
[[1, 2], [3, 4]].map(([a, b]) => a + b); // [ 3, 7 ]
undefined就会触发函数参数的默认值:
[1, undefined, 3].map((x = 'yes') => x); // [ 1, 'yes', 3 ]
变量的解构赋值用途
交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
🎯提取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]
函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
遍历Map结构
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
// ...
}
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
🎯输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
字符串的扩展
字符的Unicode表示法
ES6允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的Unicode码点
"\u0061" // "a"
"\u{41}\u{42}\u{43}" // "ABC"
🎯模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量
模板字符串中嵌入变量,需要将变量名写在 ${} 之中。
注意,所有模板字符串的空格和换行都是被保留的。如果不想要这个换行,可以使用trim() 方法消除它
// 普通字符串
`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 func = (name) => `Hello ${name}!`;
func('Jack') // "Hello Jack!"
字符串新增方法
- String.fromCodePoint()
- 此方法定义在String对象上,用于从Unicode码点返回对应字符
console.log(String.fromCharCode(0x20BB7)); // 错误:ஷ
console.log(String.fromCodePoint(0x20BB7)); // 正确:𠮷
- String.raw()
- 该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法
String.raw`Hi\n` === "Hi\\n" // true
- 实例方法:codePointAt()
- 正确处理4个字节储存的字符,返回一个字符的码点
- 实例方法:normalize()
- 将字符的不同表示方法统一为同样的形式,称为Unicode正规化
- 实例方法:includes(), startsWith(), endsWith()
- includes() :返回布尔值,表示是否找到了参数字符串
- startsWith() :返回布尔值,表示参数字符串是否在原字符串的头部
- endsWith() :返回布尔值,表示参数字符串是否在原字符串的尾部
- 这三个方法都支持第二个参数,表示开始搜索的位置(使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束)
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
- 实例方法:repeat()
- 返回一个新字符串,表示将原字符串重复n次
'x'.repeat(3) // "xxx"
- 实例方法:padStart(),padEnd()
- 如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
- 一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
- 实例方法:trimStart(),trimEnd()
- trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
const s = ' abc ';
s.trim() // "abc"
s.trimStart() // "abc "
s.trimEnd() // " abc"
- 实例方法:matchAll()
- matchAll()方法返回一个正则表达式在当前字符串的所有匹配
- 实例方法:replaceAll()
- replaceAll()的第二个参数replacement是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。
- $&:匹配的字符串。
- $` :匹配结果前面的文本。
- $':匹配结果后面的文本。
- $n:匹配成功的第n组内容,n是从1开始的自然数。这个参数生效的前提是,第一个参数必须是正则表达式。
- $$:指代美元符号$。
- replaceAll()的第二个参数replacement是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。
'aabbcc'.replaceAll('b', '_') // 'aa__cc'
- 实例方法:at()
- at()方法接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)
const str = 'hello';
str.at(1) // "e"
str.at(-1) // "o"
数值的扩展
- 二进制和八进制
- ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示
- 如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法
0b111110111 === 503 // true
0o767 === 503 // true
Number('0b111') // 7
Number('0o10') // 8
- 数值分隔符
- 不能放在数值的最前面(leading)或最后面(trailing)
- 不能两个或两个以上的分隔符连在一起
- 小数点的前后不能有分隔符
- 科学计数法里面,表示指数的e或E前后不能有分隔符
- 分隔符不能紧跟着进制的前缀0b、0B、0o、0O、0x、0X
let budget = 1_000_000_000_000;
- Number.isFinite(), Number.isNaN()
- Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。如果参数类型不是数值,Number.isFinite一律返回false
- Number.isNaN()用来检查一个值是否为NaN。如果参数类型不是NaN,Number.isNaN一律返回false
- Number.parseInt(), Number.parseFloat()
// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
- Number.isInteger()
- 用来判断一个数值是否为整数
- 如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数
Number.isInteger(25) // true
Number.isInteger(25.0) // true 25等同于25.0
Number.isInteger(25.1) // false
- Number.EPSILON
- 常量Number.EPSILON,表示1与大于1的最小浮点数之间的差
- 它是JavaScript能够表示的最小精度,可以用来设置“能够接受的误差范围”
function withinErrorMargin (left, right) {
return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3 // false
withinErrorMargin(0.1 + 0.2, 0.3) // true
1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true
-
安全整数和Number.isSafeInteger()
- ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限
- Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内
-
BigInt
- 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示
- BigInt类型的数据必须添加后缀n
1234 // 普通整数
1234n // BigInt
🥷函数的扩展
🎯函数参数的默认值
ES6可以为函数的参数设置默认值,直接写在参数定义的后面
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
参数默认值可以与解构赋值的默认值,结合起来使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {}) // "GET"
fetch('http://example.com') // 报错
// 双重默认值
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com') // "GET"
🎯rest参数
ES6 引入 rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。函数的length属性,不包括rest参数。
严格模式
只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
name属性
函数的name属性,返回该函数的函数名
function foo() {}
foo.name // "foo"
var f = function () {};
f.name // "f"
🎯箭头函数
ES6允许 使用“箭头”(=>)定义函数
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭头函数 不需要参数或需要多个参数,就使用一个圆括号代表参数部分
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多于一条语句,就要 使用大括号将它们括起来,并且使用 return 语句返回
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数 直接返回一个对象,必须在对象外面加上括号,否则会报错
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
箭头函数可以与变量解构结合使用
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
箭头函数使得表达更加简洁
const isEven = n => n % 2 === 0;
const square = n => n * n;
箭头函数的一个用处是 简化回调函数
// 普通函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
// 普通函数写法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);
rest参数与箭头函数结合
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5) // [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5) // [1,[2,3,4,5]]
箭头函数有几个使用注意点:
(1)箭头函数没有自己的this对象
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替
(4)不可以使用yield命令,因此箭头函数不能用作Generator函数
尾调用优化
尾调用是指某个函数的最后一步是调用另一个函数
function f(x){ return g(x); }
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。尾递归只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
// 采用函数默认值的写法:
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
函数参数的尾逗号
允许函数的最后一个参数有尾逗号
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
数组的扩展
🎯扩展运算符
扩展运算符(spread)是三个点 (...)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。该运算符主要用于函数调用。(注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错)
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
add(...[4, 38]) // 42
// 将arr2数组添加到arr1数组的尾部
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
扩展运算符的常用应用:
复制数组(返回原数组的克隆)
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
合并数组- 注意:如果数组里是对象,则是浅拷贝(合并而成的新数组,但是成员是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。)
[...arr1, ...arr2, ...arr3]
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = [...a1, ...a2];
a3[0] === a1[0] // true
- 与解构赋值结合
- 扩展运算符可以与解构赋值结合起来,用于生成数组。
[a, ...rest] = list
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
- 字符串
- 扩展运算符还可以将字符串转为真正的数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
- 实现了Iterator接口的对象
- 任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
类方法
Array.from()- 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6新增的数据结构Set和Map)
- Array.from()还可以接受一个函数作为第二个参数,作用类似于数组的map()方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES6 的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
// 返回一个具有三个成员的数组,每个位置的值都是undefined
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9]
Array.of()- 用于将一组值,转换为数组
- Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载,它的行为非常统一
- Array.of()总是返回参数值组成的数组。如果没有参数,就返回一个空数组
Array.of(3, 11, 8) // [3,11,8]
Array.of(undefined) // [undefined]
实例方法
- copyWithin()
- 在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
Array.prototype.copyWithin(target, start = 0, end = this.length)
【参数】:
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3) // [4, 5, 3, 4, 5]
- find()、findIndex()、findLast()、findLastIndex()
- 数组实例的find() 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
- find()方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
[1, 4, -5, 10].find((n) => n < 0) // -5
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
- 数组实例的findIndex() 方法,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
- find()和findIndex()都是从数组的0号位,依次向后检查。ES2022新增了两个方法findLast()和findLastIndex() ,从数组的最后一个成员开始,依次向前检查,其他都保持不变。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
- fill()
- fill() 方法使用给定值,填充一个数组,一般用于空数组的初始化
- 还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
- 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象
['a', 'b', 'c'].fill(7) // [7, 7, 7]
new Array(3).fill(7) // [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
- entries()、keys()、values()
- 用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历
- keys()是对键名的遍历、values()是对键值的遍历、entries()是对键值对的遍历
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
- includes()
- 该方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似
- 该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
- flat()、flatMap()
- 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响
[1, 2, [3, 4]].flat() // [1, 2, 3, 4]
[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
[1, [2, [3]]].flat(Infinity) // [1, 2, 3]
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
- at()
- 接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)
const arr = [5, 12, 8, 130, 44];
arr.at(2) // 8
arr.at(-2) // 130
-
toReversed()、toSorted()、toSpliced()、with()
- toReversed()对应reverse(),用来颠倒数组成员的位置
- toSorted()对应sort(),用来对数组成员排序
- toSpliced()对应splice(),用来在指定位置,删除指定数量的成员,并插入新成员
- with(index, value)对应splice(index, 1, value),用来将指定位置的成员替换为新的值
- 上面四个方法,允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝。
-
group()、groupToMap()
- group()的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组
- group()的返回值是一个对象,该对象的键名就是每一组的组名,即分组函数返回的每一个字符串(如下是even和odd);该对象的键值是一个数组,包括所有产生当前键名的原数组成员。
const array = [1, 2, 3, 4, 5];
array.group((num, index, array) => {
return num % 2 === 0 ? 'even': 'odd';
});
// { odd: [1, 3, 5], even: [2, 4] }
- groupToMap()的作用和用法与group()完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象
- 使用原则:按照字符串分组就使用group(),按照对象分组就使用groupToMap()
- 数组的空位
- 数组的空位指的是,数组的某一个位置没有任何值
- ES6 则是明确将空位转为undefined,扩展运算符(...)也会将空位转为undefined
Array(3) // [, , ,]
- Array.prototype.sort() 的排序稳定性
- 排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变
对象的扩展
🎯属性的简洁表示法
- 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法
- 注意,简写的对象方法不能用作构造函数,会报错
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
属性名表达式
- 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
🎯对象的扩展运算符(...)
- 解构赋值
- 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
- 解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x=1 y=2 z={ a: 3, b: 4 }
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用 x 和 y 参数进行操作
// 其余参数传给原始函数
return baseFunction(restConfig);
}
- 扩展运算符
- 对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
- 对象的扩展运算符,只会返回参数对象自身的、可枚举的属性(不会返回方法,因为方法定义在原型对象上)
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
对象的新增方法
- Object.is()
- 它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致
Object.is('foo', 'foo') // true
Object.is({}, {}) // false
Object.is(NaN, NaN) // true
- Object.assign()
- 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。第一个参数是目标对象,后面的参数都是源对象
- Object.getOwnPropertyDescriptors()
- 返回指定对象所有自身属性(非继承属性)的描述对象
- __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
- __proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象
- Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身
- 该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象
- Object.keys()
- 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
- Object.values()
- 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
const obj = { foo: 'bar', baz: 42 };
Object.values(obj) // ["bar", 42]
- Object.entries()
- 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
- Object.fromEntries()
- 是Object.entries()的逆操作,用于将一个键值对数组转为对象
- 该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将Map结构转为对象
- 还可以配合URLSearchParams对象,将查询字符串转为对象
// 例一
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
- Object.hasOwn()
- 判断是否为自身的属性。接受两个参数,第一个是所要判断的对象,第二个是属性名
运算符的扩展
指数运算符(**)
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2 // 512
let b = 4;
b **= 3; // 64
// 等同于 b = b * b * b;
🎯链判断运算符
“链判断运算符”(?.)
直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined。
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
链判断运算符有三种写法:
obj?.prop // 对象属性是否存在obj?.[expr] // 对象属性是否存在func?.(...args) // 函数或对象方法是否存在
a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
需要注意以下几点:
(1)短路机制:本质上,?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行。(链判断运算符一旦为真,右侧的表达式就不再求值)
a?.[++x]
// 等同于
a == null ? undefined : a[++x]
(2)括号的影响:如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响
(a?.b).c
// 等价于
(a == null ? undefined : a.b).c
(3)报错场合
以下都是错误场景
// 构造函数
new a?.()
new a?.b()
// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`
// 链判断运算符的左侧是 super
super?.()
super?.foo
// 链运算符用于赋值运算符左侧
a?.b = c
(4)右侧不得为十进制数值
Null判断运算符
Null判断运算符(??)。它的行为类似“||”,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。
const animationDuration = response.settings?.animationDuration ?? 300;
适合判断函数参数是否赋值
function Component(props) {
const enable = props.enabled ?? true;
// …
}
逻辑赋值运算符
这三个运算符 ||=、&&=、??= 相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。
// 或赋值运算符
x ||= y
// 等同于
x || (x = y)
// 与赋值运算符
x &&= y
// 等同于
x && (x = y)
// Null 赋值运算符
x ??= y
// 等同于
x ?? (x = y)
它们的一个用途是,为变量或属性设置默认值:
user.id ||= 1;
function example(opts) {
opts.foo ??= 'bar';
opts.baz ??= 'qux';
}
Symbol
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。
Symbol值通过Symbol() 函数生成(注意,Symbol()函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的)
let s1 = Symbol('foo');
s1 // Symbol(foo)
s1.toString() // "Symbol(foo)"
s1.description // "foo"
Symbol值作为标识符,用于对象的属性名,就能保证不会出现同名的属性
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
Set&Map数据结构
Set
Set成员值无重复的
const s = new Set();
const set = new Set([1, 2, 3, 4, 4]);
[...set] // [1, 2, 3, 4]
两个便捷用法:
// 去除数组的重复成员
// 方法1:
[...new Set(array)]
// 方法2:
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
// 去除字符串里面的重复字符
[...new Set('ababbc')].join('') // "abc"
Set实例的操作方法(用于操作数据):
- Set.prototype.add(value) :添加某个值,返回 Set 结构本身。
- Set.prototype.delete(value) :删除某个值,返回一个布尔值,表示删除是否成功。
- Set.prototype.has(value) :返回一个布尔值,表示该值是否为Set的成员。
- Set.prototype.clear() :清除所有成员,没有返回值。
Set实例的遍历方法(用于遍历成员):
- Set.prototype.keys() :返回键名的遍历器
- Set.prototype.values() :返回键值的遍历器
- Set.prototype.entries() :返回键值对的遍历器
- Set.prototype.forEach() :使用回调函数遍历每个成员
WeakSet
WeakSet结构与Set类似,也是不重复的值的集合。但是,WeakSet的成员只能是对象,而不能是其他类型的值;且WeakSet不可遍历
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]}
const b = [3, 4];
const ws = new WeakSet(b); // Uncaught TypeError: Invalid value used in weak set(…)
Map
Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
备注:Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
Map实例的属性和操作方法:
- size属性:返回Map结构的成员总数
- Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就新生成该键
- set方法返回的是当前的Map对象,因此可以采用链式写法:
let map = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
- Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined
- Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前Map对象之中
- Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false
- Map.prototype.clear():清除所有成员,没有返回值
Map实例的遍历方法:
- Map.prototype.keys():返回键名的遍历器
- Map.prototype.values():返回键值的遍历器
- Map.prototype.entries():返回所有成员的遍历器
- Map.prototype.forEach():遍历 Map 的所有成员
【与其他数据结构的互相转换】
- Map转为数组:最方便的方法,就是使用扩展运算符(...)
const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
- 数组转为Map:将数组传入Map构造函数,就可以转为Map
new Map([ [true, 7],
[{foo: 3}, ['abc']]
])
// Map { true => 7, Object {foo: 3} => ['abc'] }
- Map转为对象:如果所有Map的键都是字符串,它可以无损地转为对象;如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
- 对象转为Map:对象转为Map可以通过Object.entries()
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
- Map转为JSON
- 一种情况是,Map的键名都是字符串,这时可以选择转为对象JSON
- 另一种情况是,Map的键名有非字符串,这时可以选择转为数组JSON
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
- JSON转为Map
- 正常情况下,所有键名都是字符串
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
- 有一种特殊情况,整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为Map。这往往是Map转为数组JSON的逆操作
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap
WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制
const wm1 = new WeakMap();
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
WeakRef
用于直接创建对象的弱引用(一大用处就是作为缓存,未被清除时可以从缓存取值,一旦清除缓存就自动失效)
let target = {};
let wr = new WeakRef(target);
let obj = wr.deref();
if (obj) { // target 未被垃圾回收机制清除
// ...
}
🥷Promise对象
🎯含义和基本用法
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
- Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject
- resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从pending变为resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
- reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
- Promise实例生成以后,可以用
then方法分别指定resolved状态和rejected状态的回调函数- then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数
promise.then(function(value) {
// success
}, function(error) {
// failure
});
🎯Promise.prototype.then()
- 它的作用是为Promise实例添加状态改变时的回调函数
- then方法返回的是一个新的Promise实例(注意不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
);
Promise.prototype.catch()
- Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
- 一般来说,不要在then()方法里面定义Reject状态的回调函数(即then的第二个参数),总是使用catch方法。
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
Promise.prototype.finally()
- finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
Promise.all()
- Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例
const p = Promise.all([p1, p2, p3]);
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
Promise.race()
- Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例
const p = Promise.race([p1, p2, p3]);
Promise.allSettled()
- Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个Promise对象,并返回一个新的Promise对象
- 只有等到参数数组的所有Promise对象都发生状态变更(不管是fulfilled还是rejected),返回的Promise对象才会发生状态变更
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator(); // 只有等到这三个请求都结束了(不管请求成功还是失败),这里才会执行。
Promise.any()
- 该方法接受一组Promise实例作为参数,包装成一个新的Promise实例返回
- 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态
Promise.any([
fetch('https://v8.dev/').then(() => 'home'),
fetch('https://v8.dev/blog').then(() => 'blog'),
fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => { // 只要有一个 fetch() 请求成功
console.log(first);
}).catch((error) => { // 所有三个 fetch() 全部请求失败
console.log(error);
});
Promise.resolve()
- 用于将现有对象转为Promise对象
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
Promise.reject()
- 返回一个新的Promise实例,该实例的状态为rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
🎯Promise应用
- 加载图片
- 我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
- Generator函数与Promise的结合
- 使用Generator函数管理流程,遇到异步操作的时候,通常返回一个Promise对象
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
Promise.try()
让同步函数同步执行,异步函数异步执行:
- 第一种写法是用async函数来写
const f = () => console.log('now');
(async () => f())();
console.log('next');
(async () => f())()
.then(...)
.catch(...)
- 第二种写法是使用Promise.try
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
🥷Generator函数语法
🎯简介
形式上,Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号(一般是*紧跟在function后面);二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束
Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
for...of 循环
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
for (let v of numbers()) {
console.log(v);
}
// 1 2 注意3不输出
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
yield* 表达式
用来在一个Generator函数里面执行另一个Generator函数
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
🎯作为对象属性的Generator函数
如果一个对象的属性是Generator函数,可以简写成下面的形式
let obj = {
* myGeneratorMethod() {
···
}
};
// 等同于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator函数的异步应用
使用Generator函数,执行一个真实的异步任务:
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
🥷async函数
含义
async函数其实就是Generator函数的语法糖
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async函数就是将Generator函数的星号(*)替换成async,将yield替换成await。但是有以下四点的改进:
(1)内置执行器
async函数自带执行器,执行方式与普通函数一样,如:asyncReadFile();
(2)更好的语义
async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果
(3)更广的适用性
async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即resolved的Promise对象)
(4)返回值是Promise
async函数的返回值是Promise对象,可以用then方法指定下一步的操作
进一步说,async函数完全可以看作多个异步操作包装成的一个Promise对象,而await命令就是内部then命令的语法糖
🎯基本用法
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
🎯语法
返回Promise对象
async函数返回一个Promise对象。async函数内部return语句返回的值,会成为then方法回调函数的参数
async function f() {
return 'hello world';
}
f().then(v => console.log(v)) // "hello world"
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log('resolve', v),
e => console.log('reject', e)
)
//reject Error: 出错了
Promise对象的状态变化
只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)</title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// 只有三个操作全部完成,才会执行then方法里面的console.log。
await命令
正常情况下,await命令后面是一个Promise对象,返回该对象的结果。如果不是Promise对象,就直接返回对应的值
async function f() {
// 等同于: return 123;
return await 123;
}
f().then(v => console.log(v))// 123
错误处理
如果await后面的异步操作出错,那么等同于async函数返回的Promise对象被reject
防止出错的方法,最好是把await放在try...catch代码块之中
async function f() {
// 防止出错的方法,也是将其放在try...catch代码块之中
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
// hello world
Class
基本使用
类的数据类型就是函数,类本身就指向构造函数
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
私有属性,是在属性名之前使用#表示,只能在类的内部使用。也可以用来表示私有方法
#foo = 0;
#sum() {
return this.#a + this.#b;
}
类的继承
通过extends关键字实现继承,让子类继承父类的属性和方法(包括静态属性和静态方法,但是不包括私有属性和私有方法)
注意,静态属性是通过软拷贝实现继承的。如果父类的静态属性的值是一个对象,那么子类的静态属性也会指向这个对象,因为浅拷贝只会拷贝对象的内存地址。
class ColorPoint extends Point {}
🥷Module
概述与严格模式
ES6模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入
import { stat, exists, readFile } from 'fs';
ES6的模块自动采用严格模式,严格模式主要有以下限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用with语句
- 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
- eval不会在它的外层作用域引入变量
- eval和arguments不能被重新赋值
- arguments不会自动反映函数参数的变化
- 不能使用arguments.callee
- 不能使用arguments.caller
- 禁止this指向全局对象
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈
- 增加了保留字(比如protected、static和interface)
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
🎯export命令
如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量:
// profile.js
export var name = 'Michael';
export var year = 1958;
或者:(此方法推荐使用)
// profile.js
var name = 'Michael';
var year = 1958;
export { firstName, lastName, year };
export还可以输出函数或类:
export function multiply(x, y) {
return x * y;
};
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错
🎯import命令
使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
🎯import整体加载
可以使用整体加载,即用星号(*) 指定一个对象,所有输出值都加载在这个对象上面
// 逐一加载
import { area, circumference } from './circle';
// 整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
🎯export default 命令
export default 命令,为模块指定默认输出。 需要注意的是,这时import命令后面不使用大括号。
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
export default命令用于指定模块的默认输出。一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。
export与import的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。但是当前模块不能直接使用,只是相当于对外转发了
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
🎯跨模块常量
如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。然后将这些文件输出的常量,合并在index.js里面。使用的时候,直接加载index.js就可以了。
// constants/index.js
export {db} from './db';
export {users} from './users';
// script.js
import {db, users} from './constants/index';
import()
import()函数,支持动态加载模块。import()类似于 Node.js 的require()方法,区别主要是前者是异步加载,后者是同步加载。
async function renderWidget() {
const container = document.getElementById('widget');
if (container !== null) {
// 等同于
// import("./widget").then(widget => {
// widget.render(container);
// });
const widget = await import('./widget.js');
widget.render(container);
}
}
renderWidget();
主要使用场景:
(1)按需加载:在需要的时候,再加载某个模块。如下是点击按钮后再加载
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
(2)条件加载:放在if代码块,根据不同的情况,加载不同的模块
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
(3)动态的模块路径
// 根据函数f的返回结果,加载不同的模块
import(f()).then(...);
🥷编程风格
🎯块级作用域
let取代var- let完全可以取代var,因为两者语义相同,而且let没有副作用
- 且var命令存在变量提升效用,let命令没有这个问题
全局常量使用const- 在let和const之间,建议优先使用const
- 所有的函数都应该设置为常量
🎯字符串
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号
const a = 'foobar';
const b = `foo${a}bar`;
🎯解构赋值
使用数组成员对变量赋值时,优先使用解构赋值
const arr = [1, 2, 3, 4];
const [first, second] = arr;
函数的参数如果是对象的成员,优先使用解构赋值
function getFullName({ firstName, lastName }) {}
如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序
function processInput(input) {
return { left, right, top, bottom };
}
const { left, right } = processInput(input);
🎯对象
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾
const a = { k1: v1, k2: v2 };
const b = {
k1: v1,
k2: v2,
};
- 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法
- 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义
const obj = {
id: 5,
name: 'San Francisco',
[getKey('enabled')]: true,
};
- 对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写
🎯数组
使用扩展运算符(...)拷贝数组
const itemsCopy = [...items];
- 使用 Array.from 方法,将类似数组的对象转为数组
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
🎯函数
立即执行函数可以写成箭头函数的形式
(() => { console.log('Welcome to the Internet.');})();
- 那些
使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this
// good
[1, 2, 3].map((x) => {
return x * x;
});
// best
[1, 2, 3].map(x => x * x);
- 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法
使用默认值语法设置函数参数的默认值
function handleThings(opts = {}) {
// ...
}
Map结构
- 只有模拟现实世界的实体对象时,才使用Object。如果只是需要key: value的数据结构,使用Map结构
Class
- 用Class,取代需要prototype的操作
- 使用extends实现继承
🎯模块
使用import取代require()
import { func1, func2 } from 'moduleA';
使用export取代module.exports- 如果模块
只有一个输出值,就使用export default,如果模块有多个输出值,除非其中某个输出值特别重要,否则建议不要使用export default,即多个输出值如果是平等关系,export default与普通的export就不要同时使用
import React from 'react';
class Breadcrumbs extends React.Component {
render() {
return <nav />;
}
};
export default Breadcrumbs;
- 如果模块默认输出一个函数,函数名的首字母应该小写,表示这是一个工具方法
function makeStyleGuide() {}
export default makeStyleGuide;
- 如果模块默认输出一个对象,对象名的首字母应该大写,表示这是一个配置值对象
const StyleGuide = {
es6: {}
};
export default StyleGuide;
ESLint的使用
ESLint是一个语法规则和代码风格的检查工具
1)在项目的根目录安装 ESLint:
$ npm install --save-dev eslint
2)安装Airbnb语法规则,以及 import、a11y、react 插件
$ npm install --save-dev eslint-config-airbnb
$ npm install --save-dev eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
3)在项目的根目录下新建一个.eslintrc文件,配置ESLint
{
"extends": "eslint-config-airbnb"
}
参考文档:
官方镜像:wangdoc.com/es6/