ES6面试真题 (10题)
1. 说说var、let、const之间的区别

1.1.var
在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变量注意:顶层对象,在浏览器环境指的是window对象,在Node指的是global对象
1 var a = 10;
2 console.log(window.a) // 10
使用var声明的变量存在变量提升的情况
1 console.log(a) // undefined
2 var a = 20
在编译阶段,编译器会将其变成以下执行
1 var a
2 console.log(a)
3 a = 20
使用 var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
1 var a = 20
2 var a = 30
3 console.log(a) // 30
在函数中使用使用 var 声明变量时候,该变量是局部的
1 var a = 20
2 function change()
3 var a = 30
4 }
5 change()
6 console.log(a) // 20
而如果在函数内不使用 var,该变量是全局的
1 var a = 20
2 function change()
3 a = 30
4 }
5 change()
6 console.log(a) // 30
1.2. let
let 是 ES6 新增的命令,用来声明变量
用法类似于 var,但是所声明的变量,只在 let 命令所在的代码块内有效
1 {
2 let a = 20
3 }
4 console.log(a) // ReferenceError: a is not defined.
不存在变量提升
1 console.log(a) // 报错ReferenceError
2 let a = 2
这表示在声明它之前,变量a是不存在的,这时如果用到它,就会抛出一个错误
只要块级作用域内存在 let 命令,这个区域就不再受外部影响
1 var a = 123
2 if (true) {
3 a = 'abc' // ReferenceError
4 let a;
5 }
使用 let 声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”
最后,let 不允许在相同作用域中重复声明
1 let a = 20
2 let a = 30
3 // Uncaught SyntaxError: Identifier 'a' has already been declared
注意的是相同作用域,下面这种情况是不会报错的
1 let a = 20
2 {
3 let a = 30
4 }
因此,我们不能在函数内部重新声明参数
1 function func(arg) {
2 let arg;
3 }
4 func()
5 // Uncaught SyntaxError: Identifier 'arg' has already been declared
1.3. const
const 声明一个只读的常量,一旦声明,常量的值就不能改变
1 const a = 1
2 a = 3
3 // TypeError: Assignment to constant variable.
这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值
1 const a;
2 // SyntaxError: Missing initializer in const declaration
如果之前用 var 或 let 声明过变量,再用 const 声明同样会报错
1 var a = 20
2 let b = 20
3 const a = 30
4 const b = 30
5 // 都会报错
const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的,并不能确保改变量的结构不变
1 const foo = {};
2 // 为 foo 添加一个属性,可以成功
3 foo_prop = 123;
4 foo_prop // 123
5 // 将 foo 指向另一个对象,就会报错
6 foo = {}; // TypeError: "foo" is read-only
其它情况,const与let一致
1.4.区别
var、let、const三者区别可以围绕下面五点展开:
- 变量提升
暂时性死区
-
块级作用域
-
重复声明
修改声明的变量
- 使用
1.4.1. 变量提升
var 声明的变量存在变量提升,即变量可以在声明之前调用,值为 undefined
let 和 const 不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
1 // var
2 console.log(a) // undefined
3 var a = 10
4 // let
5 console.log(b) // Cannot access 'b' before initialization
6 let b = 10
7 // const
8 console.log(c) // Cannot access 'c' before initialization
9 const c = 10
1.4.2.暂时性死区
var 不存在暂时性死区
let 和 const 存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
1 // var
2 console.log(a) // undefined
3 var a = 10
4 // let
5 console.log(b) // Cannot access 'b' before initialization
6 let b = 10
7 // const
8 console.log(c) // Cannot access 'c' before initialization
9 const c = 10
1.4.3. 块级作用域
var 不存在块级作用域
1 // var
2 {
3 var a = 20
4 }
5 console.log(a) // 20
6 // let
7 {
8 let b = 20
9 }
10 console.log(b) // Uncaught ReferenceError: b is not defined
11 // const
12 {
13 const c = 20
14 }
15 console.log(c) // Uncaught ReferenceError: c is not defined
1.4.4. 重复声明
var 允许重复声明变量
let 和 const 在同一作用域不允许重复声明变量
1 // var
2 var a = 10
3 var a = 20 // 20
4 // let
5 let b = 10
6 let b = 20 // Identifier 'b' has already been declared
7 // const
8 const c = 10
9 const c = 20 // Identifier 'c' has already been declared
1.4.5. 修改声明的变量
var和let可以
const 声明一个只读的常量。一旦声明,常量的值就不能改变
1 // var
2 var a = 10
3 a = 20
4 console.log(a) // 20
5 //let
6 let b = 10
7 b = 20
8 console.log(b) // 20
9 // const
10 const c = 10
11 c = 20
12 console.log(c) // Uncaught TypeError: Assignment to constant variable
1.4.6. 使用
能用 const 的情况尽量使用 const,其他情况下大多数使用 let,避免使用 var
2. ES6中数组新增了哪些扩展?

2.1. 扩展运算符的应用
ES6通过扩展元素符...,好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列
1 console.log(...[1,2,3])
2 //123
3 console.log(1,...[2,3,4],5)
4 //12345
5 [...document(querySelectorAll('div')]
6 //[<div>, <div>, <div>]
主要用于函数调用的时候,将一个数组变为参数序列
1 function push(array, ...items) {
2 array.push(...items);
3 }
4 function add(x, y) {
5 return x + y;
6 }
7 const numbers = [4, 38];
8 add(...numbers) // 42
可以将某些数据结构转为数组
1 [.document(querySelectorAll('div'])]
能够更简单实现数组复制
1 const a1 = [1, 2];
2 const [...a2] = a1;
3 // [1,2]
数组的合并也更为简洁了
1 const arr1 = ['a', 'b'];
2 const arr2 = ['c'];
3 const arr3 = ['d', 'e'];
4 [...arr1, ...arr2, ...arr3]
5 // ['a', 'b', 'c', 'd', 'e']
注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组
下面看个例子就清楚多了
1 const arr1 = ['a', 'b', [1,2]];
2 const arr2 = ['c'];
3 const arr3 = [...arr1, ...arr2]
4 arr[1][0] = 9999 // 修改arr1里面数组成员值
5 console.log(arr[3]) // 影响到arr3, ['a', 'b', [9999,2], 'c']
扩展运算符可以与解构赋值结合起来,用于生成数组
1 const [first, ...rest] = [1, 2, 3, 4, 5];
2 first // 1
3 rest // [2, 3, 4, 5]
4 const [first, ...rest] = [];
5 first // undefined
6 rest // []
7 const [first, ...rest] = ["foo"];
8 first // "foo"
9 rest // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
1 const [...butLast, last] = [1, 2, 3, 4, 5];
2 // 报错
3 const [first, ...middle, last] = [1, 2, 3, 4, 5];
4 // 报错
可以将字符串转为真正的数组
1 [...]hello'
2 //['h",e",ll",ll",o"]
定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
1 let nodeList = document(selectorAll('div');
2 let array = [...nodeList];
3 let map = new Map([
4 [1, 'one'],
5 [2, 'two'],
6 [3, 'three'],
7]);
8 let arr = [...map.keys(); // [1, 2, 3]
如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错
1 const obj = {a: 1, b: 2};
2 let arr = [...obj]; // TypeError: Cannot spread non-iterable object
2.2.构造函数新增的方法
关于构造函数,数组新增的方法有如下:
-
Array.from()
-
Array.of()
2.2.1. Array.from()
将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)
1 let arrayLike = {
2 '0': 'a',
3 '1': 'b',
4 '2': 'c',
5 length: 3
6 };
7 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组
1 Array.from([1, 2, 3], (x) => x * x)
2 // [1, 4, 9]
2.2.2. Array.of()
用于将一组值,转换为数组
1 Array.of(3, 11, 8) // [3,11,8]
没有参数的时候,返回一个空数组
当参数只有一个的时候,实际上是指定数组的长度
参数个数不少于 2 个时,Array() 才会返回由参数组成的新数组
1 Array() // []
2 Array(3) // [,,,]
3 Array(3,11,8)//[3,11,8]
2.2.3. 实例对象新增的方法
关于数组实例对象新增的方法有如下:
-
copyWithin()
-
find()、findIndex()
fill()
-
entries(), keys(), values()
-
includes()
-
flat(),-flatMap()
2.2.4. copyWithin()
将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
参数如下:
-
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
-
start(可选):从该位置开始读取数据,默认为0。如果为负值,表示从末尾开始计算。
-
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
1 [1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
2 // [4, 5, 3, 4, 5]
2.2.5. find()、findIndex()
find() 用于找出第一个符合条件的数组成员
参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组
1 [1, 5, 10, 15].find(function(value, index, arr) {
2 return value > 9;
3 }) // 10
findIndex 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1
1 [1, 5, 10, 15].findIndex(function(value, index, arr) {
2 return value > 9;
3 }) // 2
这两个方法都可以接受第二个参数,用来绑定回调函数的 this 对象。
1 function f(v){
2 return v > this.age;
3 }
4 let person = {name: 'John', age: 20};
5 [10, 12, 26, 15].find(f, person); // 26
2.2.6. fill()
使用给定值,填充一个数组
1 ['a', 'b', 'c'].fill(7)
2 // [7, 7, 7]
3 new Array(3).fill(7)
4 // [7, 7, 7]
还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
1 ['a', 'b', 'c'].fill(7, 1, 2)
2 // ['a', 7, 'c']
注意,如果填充的类型为对象,则是浅拷贝
2.2.7. entries(), keys(), values()
1 or (let index of ['a', 'b'].keys()) {
2 console.log(index);
3 }
4 // 0
5 // 1
6 for (let elem of ['a', 'b'].values())
7 console.log(elem);
8 }
9 // 'a'
10 // 'b'
11 for (let [index, elem] of ['a', 'b'].entries())
12 console.log(index, elem);
13 }
14 // 0 "a"
2.2.8. includes()
用于判断数组是否包含给定的值
1 [1, 2, 3].includes(2) // true
2 [1, 2, 3].includes(4) // false
3 [1, 2, NaN].includes(NaN) // true
方法的第二个参数表示搜索的起始位置,默认为 $\odot$
参数为负数则表示倒数的位置
1 [1, 2, 3].includes(3, 3); // false
2 [1, 2, 3].includes(3, -1); // true
2.2.9. flat(),-flatMap()
将数组扁平化处理,返回一个新数组,对原数据没有影响
1 [1, 2, [3, 4]].flat()
2 // [1, 2, 3, 4]
flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为1
1 [1, 2, [3, [4, 5]]].flat()
2 // [1, 2, 3, [4, 5]]
3 [1, 2, [3, [4, 5]]].flat(2)
4 // [1, 2, 3, 4, 5]
flatMap() 方法对原数组的每个成员执行一个函数相当于执行 Array.prototype.map(), 然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组, 不改变原数组
1 // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
2 [2, 3, 4].flatMap((x) => [x, x * 2])
3 // [2, 4, 3, 6, 4, 8]
flatMap() 方法还可以有第二个参数,用来绑定遍历函数里面的 this
2.2.10. 数组的空位
数组的空位指,数组的某一个位置没有任何值
ES6则是明确将空位转为undefined,包括Array.from、扩展运算符、copyWithin()、fill()、entries()、keys()、values()、find()和findIndex()
建议大家在日常书写中,避免出现空位
2.2.11. 排序稳定性
将 sort() 默认设置为稳定的排序算法
1 const arr = [
2 'peach',
3 'straw',
4 'apple',
5 'spork'
6 ];
7 const stableSorting = (s1, s2) => {
8 if (s1[0] < s2[0]) return -1;
9 return 1;
10 };
11 arr.sort(stableSorting)
12 // ["apple", "peach", "straw", "spork"]
3. 函数新增了哪些扩展?

3.1. 参数
ES6 允许为函数的参数设置默认值
1 function log(x, y = 'World') {
2 console.log(x, y);
3 }
4 console.log('Hello') // Hello World
5 console.log('Hello', 'China') // Hello China
6 console.log('Hello', '') // Hello
函数的形参是默认声明的,不能使用 let 或 const 再次声明
1 function foo(x = 5) {
2 let x = 1; // error
3 const x = 2; // error
4 }
参数默认值可以与解构赋值的默认值结合起来使用
1 function foo({x, y = 5}) {
2 console.log(x, y);
3 }
4 foo({
5) // undefined 5
5 foo({x: 1}) // 1 5
6 foo({x: 1, y: 2}) // 1 2
7 foo() // TypeError: Cannot read property 'x' of undefined
上面的 foo 函数,当参数为对象的时候才能进行解构,如果没有提供参数的时候,变量 x 和 y 就不会生成,从而报错,这里设置默认值避免
1 function foo({x, y = 5} = {}){
2 console.log(x, y);
3 }
4 foo() // undefined 5
参数默认值应该是函数的尾参数,如果不是非尾部的参数设置默认值,实际上这个参数是没发省略的
1 function $f(x = 1,y)$ {
2 return [x,y];
3 }
4 f() // [1, undefined]
5 f(2)//[2,undefined]
6 f(,1)//报错
7 f(undefined,1)//[1,1]
3.2.属性
3.2.1. 函数的length属性
length 将返回没有指定默认值的参数个数
1 (function (a) {}.length // 1
2 (function (a = 5) {}.length // 0
3 (function (a, b, c = 5) {}.length // 2
rest 参数也不会计入 length 属性
```javascript
1 (function(...args) {}.length // $\theta$
如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了
1 (function $(a = 0,b,c)$ {{}).length // 0
2 (function $(a,b = 1,c)$ {{}).length // 1
3.2.2. name属性
返回该函数的函数名
1 var f = function() {};
2 // ES5
3 f.name // ""
4 // ES6
5 f.name // "f"
如果将一个具名函数赋值给一个变量,则name属性都返回这个具名函数原本的名字
1 const bar = function baz() {};
2 bar.name // "baz"
Function 构造函数返回的函数实例,name 属性的值为 anonymous
1 (new Function).name // "anonymous"
bind返回的函数,name属性值会加上bound前缀
1 function foo(){};
2 foo.bind({}).name // "bound foo"
3 (function(){{}).bind({}).name // "bound "
3.3. 作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域
等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的下面例子中, $y = x$ 会形成一个单独作用域, $x$ 没有被定义,所以指向全局变量 $x$
1 let $x = 1$
2 function $\mathsf{f}(\mathsf{y} = \mathsf{x})$ {
3 //等同于let $y = x$
4
5 let $x = 2$
6 console.log(y);
7 }
8 f()//1
3.4. 严格模式
只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
1 // 报错
2 function doSomething(a, b = a) {
3 'use strict';
4 // code
5 }
6 // 报错
7 const doSomething = function (\{a, b\}) {
8 'use strict';
9 // code
10 };
11 // 报错
12 const doSomething = (...a) => {
13 'use strict';
14 // code
15 };
16 const obj = {
17 // 报错
18 doSomething({a, b}) {
19 'use strict';
20 // code
21 };
22 };
3.5. 箭头函数
使用“箭头”( $= >$ )定义函数
1 var f = v => v;
2 // 等同于
3 var f = function(v) {
4 return v;
5 };
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
1 var f = () => 5;
2 // 等同于
3 var f = function() { return 5 };
4 var sum = (num1, num2) => num1 + num2;
5 // 等同于
6 var sum = function(num1, num2) {
7 return num1 + num2;
8 };
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回
1 var sum = (num1, num2) => { return num1 + num2; }
如果返回对象,需要加括号将对象包裹
1 let getTempItem = id => ({id: id, name:"Temp"]);
注意点:
-
函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
-
不可以当作构造函数,也就是说,不可以使用 new 命令,否则会抛出一个错误
-
不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
-
不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
4. 对象新增了哪些扩展?

4.1. 属性的简写
ES6中,当对象键名与对应值名相等的时候,可以进行简写
1 const baz = {foo:foo}
2 // 等同于
3 const baz = {foo}
方法也能够进行简写
1 const o = {
2 method() {
3 return "Hello!";
4 }
5 };
6 // 等同于
7 const o = {
8 method: function() {
9 return "Hello!";
10 }
11 }
在函数内作为返回值,也会变得方便很多
1 function getPoint() {
2 const x = 1;
3 const y = 10;
4 return {x, y};
5 }
6 getPoint()
7 // {x:1, y:10}
注意:简写的对象方法不能用作构造函数,否则会报错
1 const obj = {
2 f() {
3 this.foo = 'bar';
4 }
5 };
6 new obj.f() // 报错
4.2. 属性名表达式
ES6 允许字面量定义对象时,将表达式放在括号内
1 let lastWord = 'last word';
2 const a = {
3 'first word': 'hello',
4 [lastWord]: 'world'
5 };
6 a['first word'] // "hello"
7 a[lastname] // "world"
8 a['last word'] // "world"
表达式还可以用于定义方法名
1 let obj = {
2 ['h' + 'ello']() {
3 return 'hi';
4 }
5 };
6 obj.hello() // hi
注意,属性名表达式与简洁表示法,不能同时使用,会报错
1 // 报错
2 const foo = 'bar';
3 const bar = 'abc';
4 const baz = { [foo] };
5 // 正确
6 const foo = 'bar';
7 const baz = { [foo]: 'abc'};
注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串 [object Object]
1 const keyA = {a: 1};
2 const keyB = {b: 2};
3 const myObject = {
4 [keyA]: 'valueA',
5 [keyB]: 'valueB'
6 };
7 myObject // Object {{[object Object]: "valueB"}}
4.3. super关键字
this 关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字 super,指向当前对象的原型对象
1 const proto = {
2 foo: 'hello'
3 };
4 const obj = {
5 foo: 'world',
6 find() {
7 return super.foo;
8 }
9 };
10 Object.setPrototypeOf(obj, proto); // 为obj设置原型对象
11 obj.find() // "hello"
4.4.扩展运算符的应用
在解构赋值中,未被读取的可遍历的属性,分配到指定的对象上面
1 let { x, y, ...z} = { x: 1, y: 2, a: 3, b: 4 };
2 x // 1
3 y // 2
4 z // { a: 3, b: 4 }
注意:解构赋值必须是最后一个参数,否则会报错
解构赋值是浅拷贝
1 let obj = { a: { b: 1 } };
2 let { ...x } = obj;
3 obj.a.b = 2; // 修改obj里面a属性中键值
4 x.a.b // 2,影响到了结构出来x的值
对象的扩展运算符等同于使用 Object.assign() 方法
4.5. 属性的遍历
ES6一共有5种方法可以遍历对象的属性。
-
for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
-
Object_keys(obj): 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名
-
Object.getOwnProperties(obj): 回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名
-
Object.getOwnPropertySymbols(obj): 返回一个数组,包含对象自身的所有 Symbol 属性的键名
-
Reflect.ownKeys(obj): 返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举
上述遍历,都遵守同样的属性遍历的次序规则:
-
首先遍历所有数值键,按照数值升序排列
-
其次遍历所有字符串键,按照加入时间升序排列
-
最后遍历所有 Symbol 键,按照加入时间升序排序
1 Reflect.ownKeys({[Symbol()]:0,b:0,10:0,2:0,a:0})
2 // ['2', '10', 'b', 'a', Symbol())]
4.6. 对象新增的方法
关于对象新增的方法,分别有以下:
-
Object.is()
-
Object.assign()
-
Object.getOwnPropertyDescriptors()
-
Object.setPrototypeOf(), Object.getPrototypeOf()
-
Object.keys(), Object.values(), Object.entries()
-
Object.fromEntries()
4.6.1. Object.is()
严格判断两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是 $+0$ 不等于 $-0$ ,二是 NaN 等于自身
1 +0 == -0 //true
2 NaN $= = =$ NaN // false
3 Object.is(+0,-0)// false
4 Object.is(NaN, NaN) // true
4.6.2. Object.assign()
Object.assign() 方法用于对象的合并,将源对象 source 的所有可枚举属性,复制到目标对象 target
1 const target = { a: 1, b: 1 };
2 const source1 = { b: 2, c: 2 };
3 const source2 = { c: 3 };
4 Object.assign(target, source1, source2);
5 target // {a:1, b:2, c:3}
注意:Object.assign() 方法是浅拷贝,遇到同名属性会进行替换
4.6.3. Object.getOwnProperties()
返回指定对象所有自身属性(非继承属性)的描述对象
1 const obj = {
2 foo: 123,
3 get bar() { return 'abc' }
4 };
5 Object.getOwnPropertyDescriptors(obj)
6 // { foo:
7 // { value: 123,
8 // writeable: true,
9 // enumerable: true,
10 // configurable: true },
11 // bar:
12 // { get: [Function: get bar],
13 // set: undefined,
14 // enumerable: true,
15 // configurable: true }
4.6.4. Object.setPrototypeOf()
Object.setPrototypeOf 方法用来设置一个对象的原型对象
1 Object.setPrototypeOf(object,prototype)
2 //用法
3 const o = Object.setPrototypeOf({},null);
4.6.5. Object.getPrototype()
用于读取一个对象的原型对象
1 Object.getPrototypeOf(obj);
4.6.6. Object_keys()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组
1 var obj = { foo: 'bar', baz: 42 };
2 Object.keys(obj)
3 // ["foo", "baz"]
4.6.7. Object.values()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组
1 const obj = { foo: 'bar', baz: 42 };
2 Object.values(obj)
3 // ["bar", 42]
4.6.8. Object_entries()
返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数组
1 const obj = { foo: 'bar', baz: 42 };
2 Object.entries(obj)
3 // [ ["foo", "bar"], ["baz", 42] ]
4.6.9. Object.fromEntries()
用于将一个键值对数组转为对象
1 Object.fromEntries([
2 ['foo','bar'],
3 ['baz',42]
4 ])
5 // { foo:"bar", baz:42}
- 你是怎么理解ES6中 Promise的?使用场景?

5.1. 介绍
Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大
在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码
1 doSomething(function(result) {
2 doSomethingElse(result, function(newResult) {
3 doThirdThing(newResult, function(finalResult) {
4 console.log('得到最终结果:' + finalResult);
5 }, failureCallback);
6 }, failureCallback);
7 },failureCallback);
阅读上面代码,是不是很难受,上述形成了经典的回调地狱
现在通过 Promise 的改写上面的代码
1 doSomething().then(function(result) {
2 return doSomethingElse(result);
3 }
4 .then(function(newResult) {
5 return doThirdThing(newResult);
6 }
7 .then(function(finalResult) {
8 console.log('得到最终结果:' + finalResult);
9 }
10 .catch(failureCallback);
瞬间感受到 promise 解决异步操作的优点:
链式操作减低了编码难度
代码可读性明显增强
下面我们正式来认识promise:
5.1.1. 状态
promise 对象仅有三种状态
-
pending (进行中)
-
fulfilled (已成功)
-
[rejected] (已失败)
5.1.2. 特点
-
对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
-
一旦状态改变(从 pending 变为 fulfilled 和从 pending 变为 rejected),就不会再变,任何时候都可以得到这个结果
5.1.3. 流程
认真阅读下图,我们能够轻松了解promise整个流程

5.2. 用法
Promise 对象是一个构造函数,用来生成 Promise 实例
1 const promise = new Promise(function(resolve, reject) {});
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject
-
resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”
-
reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”
5.2.1. 实例方法
Promise 构建出来的实例存在以下方法:
then()
-
catch()
-
finally()
5.2.1.1. then()
then 是实例状态发生改变时的回调函数,第一个参数是 resolved 状态的回调函数,第二个参数是 rejected 状态的回调函数
then 方法返回的是一个新的 Promise 实例,也就是 promise 能链式书写的原因
1 getJSON("/posts.json").then(functionjson){
2 return json.post;
3 }).then(function(post) {
4 //...
5 });
5.2.1.2. catch
catch() 方法是 .then(null, rejection) 或 .then(undefined, rejection) 的别名,用于指定发生错误时的回调函数
1 getJSON('/posts.json').then(function Posts) {
2 // ...
3 }); catch(function(error) {
4 // 处理 getJSON 和前一个回调函数运行时发生的错误
5 console.log('发生错误! ', error);
6 });
- Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止
1 getJSON('/post/1.json').then(function(post) {
2 return getJSON(post/commentURL);
3 }) .then(function/comments) {
4 //somecode
5}).catch(function(error){
6 //处理前面三个Promise产生的错误
7});
一般来说,使用catch方法代替then()第二个参数
Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应
1 const someAsyncThing = function() {
2 return new Promise(function(resolve, reject) {
3 // 下面一行会报错,因为x没有声明
4 resolve(x + 2);
5 });
6 };
浏览器运行到这一行,会打印出错误提示 ReferenceError: x is not defined,但是不会退出进程
catch() 方法之中,还能再抛出错误,通过后面 catch 方法捕获到
5.2.1.3. finally()
finally() 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
1 promise
2 .then(result => {……})
3 .catch(error => {……})
4 .finally(( ) => {……});
5.2.2.构造函数方法
Promise 构造函数存在以下方法:
- all()
- race()
- allSettled()
- resolve()
- reject()
- try()
5.2.3. all()
- Promise.all()
- 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
1 const p = Promise.all([p1, p2, p3]);
接受一个数组(迭代对象)作为参数,数组成员都应为 Promise 实例
实例 p 的状态由 p1、p2、p3 决定,分为两种:
-
只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数
-
只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数
注意,如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all() 的 catch 方法
1 const p1 = new Promise((resolve, reject) => {
2 resolve('hello');
3 })
4 .then(result => result)
5 .catch(e => e);
6 const p2 = new Promise((resolve, reject) => {
7 throw new Error('报错了');
8 })
9 .then(result => result)
10 .catch(e => e);
11 Promise.all([p1, p2])
12 .then(result => console.log(result))
13 .catch(e => console.log(e));
14 // ["hello", Error: 报错了]
如果 p2 没有自己的 catch 方法,就会调用 Promise.all() 的 catch 方法
1 const p1 = new Promise((resolve, reject) => {
2 resolve('hello');
3 })
4 .then(result => result);
5 const p2 = new Promise((resolve, reject) => {
6 throw new Error('报错了');
7 })
8 .then(result => result);
9 Promise.all([p1, p2])
10 .then(result => console.log(result))
11 .catch(e => console.log(e));
12 // Error: 报错了
5.2.4. race()
- Promise raced() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例
1 const p = Promise.race([p1, p2, p3]);
只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变率先改变的 Promise 实例的返回值则传递给 p 的回调函数
1 const p = Promise raced([
2 fetch('/resource-that-may-take-a-while'),
3 new Promise(function (resolve, reject) {
4 setTimeout(( => reject(new Error('request timeout')), 5000)
5 })
6 ]);
7 p
8 .then console.log)
9 .catch console.error);
5.2.5. allSettled()
- Promise.allSettled() 方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例
只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束
1 const promises = [
2 fetch('/api-1'),
3 fetch('/api-2'),
4 fetch('/api-3'),
5 ];
6 await Promise.allSettled(promises);
7 removeLoadingIndicator();
5.2.5.1. resolve()
1 Promise.resolve('foo')
2//等价于
3new Promise(resolve $\Rightarrow$ resolve('foo'))
参数可以分成四种情况,分别如下:
-
参数是一个 Promise 实例,promise.resolve 将不做任何修改、原封不动地返回这个实例
-
参数是一个 thenable 对象,promise.resolve 会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then() 方法
-
参数不是具有 then() 方法的对象,或根本就不是对象, Promise.resolve() 会返回一个新的 Promise 对象,状态为 resolved
-
没有参数时,直接返回一个 resolved 状态的 Promise 对象
5.2.5.2. reject()
- Promisereject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected
1 const p = Promisereject('出错了');
2 // 等同于
3 const p = new Promise((resolve, reject) => reject('出错了'))
4 p.then(null, function (s) {
5 console.log(s)
6 });
7 // 出错了
- Promisereject() 方法的参数,会原封不动地变成后续方法的参数
1 Promisereject('出错了')
2 .catch(e $\Rightarrow$ {
3 console.log(e $= = =$ '出错了')
4})
5//true
5.3. 使用场景
将图片的加载写成一个 Promise,一旦加载完成,Promise 的状态就发生变化
1 const preloadImage = function (path) {
2 return new Promise(function (resolve, reject) {
3 const image = new Image();
4 image.onload = resolve;
5 image.onerror = reject;
6 image.src = path;
7 },);
8 };
通过链式操作,将多个渲染数据分别给个 then,让其各司其职。或当下个异步请求依赖上个请求结果的时候,我们也能够通过链式操作友好解决问题
1 //各司其职
2 getInfo().then(res => {
3 let { bannerList} = res
4 //渲染轮播图
5 console.log(bannerList)
6 return res
7 }) . then (res => {
8
9 let { storeList} = res
10 //渲染店铺列表
11 console.log(storeList)
12 return res
13 }) . then (res => {
14 let { categoryList} = res
15 console.log(categoryList)
16 //渲染分类列表
17 return res
18 })
通过 all() 实现多个请求合并在一起,汇总所有请求结果,只需设置一个 loading 即可
1 function initLoad(){
2 // loading.show() //加载loading
3 Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{
4 console.log(res)
5 loading.hide() //关闭loading
6 }) catch(err=>{
7 console.log(err)
8 loading.hide() //关闭loading
9 })
10 }
通过 race 可以设置图片请求超时
1 //请求某个图片资源
2 function requestImg(){
3 var p = new Promise(function(resolve, reject){
4 var img = new Image();
5 img.onload = function(){
6 resolve(img);
7 }
8 //img.src = "https://b-gold-cdn.xitu.io/v3/static/img logo.a7995ad.svg";正确的
9 img.src = "https://b-gold-cdn.xitu.io/v3/static/img logo.a7995ad.svg";
10 });
11 return p;
12 }
13 //延时函数,用于给请求计时
14 function timeout(){
15 var p = new Promise(function(resolve, reject){
16setTimeout(function(){
17 reject('图片请求超时');
18 }, 5000);
19 });
20 return p;
21 }
22 Promise
23 .race([requestImg(), timeout()])
24 .then(function(results){
25 console.log(results);
26 })
27 .catch(function_reason){
28 console.log_reason);
29 });
- 你是怎么理解ES6中Module的?使用场景?

6.1. 介绍
模块,(Module),是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。
两个基本的特征:外部特征和内部特征
-
外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能
-
内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)
6.1.1. 为什么需要模块化
代码抽象
- 代码封装
代码复用
- 依赖管理
如果没有模块化,我们代码会怎样?
-
变量和方法不容易维护,容易污染全局作用域
-
加载资源的方式通过script标签从上到下。
-
依赖的环境主观逻辑偏重,代码较多就会比较复杂。
-
大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃因此,需要一种将JavaScript程序模块化的机制,如
CommonJs (典型代表:node.js早期)
-
AMD (典型代表:require.js)
-
CMD (典型代表: sea.js)
6.1.2. AMD
Asynchronous ModuleDefinition (AMD),异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行
代表库为 require.js
1 /**main.js入口文件/主模块**/
2 // 首先用config()指定各模块路径和引用名
3 require.config({
4 baseUrl:"js/lib",
5 paths: {
6 "jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
7 "underscore": "underscore.min",
8
9});
10 //执行基本操作
11 require([".jquery","underscore"],function(_,_){
12 // some code here
13});
6.1.3. CommonJs
CommonJS 是一套 Javascript 模块规范,用于服务端
1 // a.js
2 module.exports = { foo, bar}
3 // b.js
4 const { foo, bar } = require('/a.js')
其有如下特点:
-
所有代码都运行在模块作用域,不会污染全局作用域
-
模块是同步加载的,即只有加载完成,才能执行后面的操作
-
模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
-
require 返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值
既然存在了AMD以及CommonJs机制,ES6的Module又有什么不一样?
ES6在语言标准的层面上,实现了Module,即模块功能,完全可以取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案
CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性
1 // CommonJS模块
2 let { stat, exists, readfile } = require('fs');
3 // 等同于
4 let _fs = require('fs');
5 let stat = _fs.stat;
6 let exists = _fs EXISTS;
7 let readfile = _fs.readfile;
ES6 设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
1 // ES6模块
2 import { stat, exists, readFile } from 'fs';
上述代码,只加载3个方法,其他方法不加载,即 ES6 可以在编译时就完成模块加载
由于编译加载,使得静态分析成为可能。包括现在流行的 typeScript 也是依靠静态分析实现功能
6.2. 二、使用
ES6 模块内部自动采用了严格模式,这里就不展开严格模式的限制,毕竟这是 ES5 之前就已经规定好
模块功能主要由两个命令构成:
- export:用于规定模块的对外接口
import:用于输入其他模块提供的功能
6.2.1. export
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量
1 //profile.js
2 export var firstName = 'Michael';
3 export var lastName = 'Jackson';
4 export var year = 1958;
5或
6 // 建议使用下面写法,这样能瞬间确定输出了哪些变量
7 var firstName = 'Michael';
8 var lastName = 'Jackson';
9 var year = 1958;
10 export { firstName, lastName, year };
输出函数或类
1 export function multiply(x, y) {
2 return x * y;
3 };
通过 as 可以进行输出变量的重命名
1 function v1() { ... }
2 function v2() { ... }
3 export {
4 v1 as streamV1,
5 v2 as streamV2,
6 v2 as streamLatestVersion
7 };
6.2.2. import
使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块
1 // main.js
2 import { firstName, lastName, year} from '.profile.js';
3 function setName.elementent) {
4 element.textContent = firstName + ' ' + lastName;
5 }
同样如果想要输入变量起别名,通过 as 关键字
1 import { lastName as surname } from '.profile.js';
当加载整个模块的时候,需要用到星号 $\star$
1 // circle.js
2 export function area(radius) {
3 return Math.PI $\star$ radius $\star$ radius;
4 }
5 export function circumference(radius) {
6 return 2 $\star$ Math.PI $\star$ radius;
7 }
8 // main.js
9 import $\star$ as circle from'/circle';
10 console.log(circle) //{area:area,circumference:circumference}
输入的变量都是只读的,不允许修改,但是如果是对象,允许修改属性
1 import {a} from './xxx.js'
2 a.foo = 'hello'; // 合法操作
3 a = {}; // Syntax Error: 'a' is read-only;
不过建议即使能修改,但我们不建议。因为修改之后,我们很难差错
import 后面我们常接着 from 关键字,from 指定模块文件的位置,可以是相对路径,也可以是绝对路径
1 import { a } from './a';
如果只有一个模块名,需要有配置文件,告诉引擎模块的位置
1 import { myMethod } from 'util';
在编译阶段,import会提升到整个模块的头部,首先执行
1 foo();
2 import { foo } from 'my_module';
多次重复执行同样的导入,只会执行一次
1 import 'iodash';
2 import 'iodash';
上面的情况,大家都能看到用户在导入模块的时候,需要知道加载的变量名和函数,否则无法加载如果不需要知道变量名或函数就完成加载,就要用到export default命令,为模块指定默认输出
1 // export-default.js
2 export default function() {
3 console.log('foo');
4 }
加载该模块的时候,import命令可以为该函数指定任意名字
1 // import-default.js
2 import customName from '.export-default';
3 customName(); // 'foo'
6.2.3. 动态加载
允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势这个新功能允许您将import()作为函数调用,将其作为参数传递给模块的路径。它返回一个promise,它用一个模块对象来实现,让你可以访问该对象的导出
1 import('/modules/myModule.mjs')
2 .then((module) => {
3 // Do something with the module.
4 });
6.2.4. 复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起
1 export { foo, bar } from 'my_module';
2 // 可以简单理解为
3 import { foo, bar } from 'my_module';
4 export { foo, bar };
同理能够搭配as、 $\star$ 搭配使用
6.3. 使用场景
如今,ES6模块化已经深入我们日常项目开发中,像vue、react项目搭建项目,组件化开发处处可见,其也是依赖模块化实现
vue 组件
1 <template>
2 <div class="App">
3 组件化开发----模块化
4 </div>
5 </template>
6 <script>
7 export default {
8 name:'HelloWorld',
9 props:{
10 msg:String
11 }
12 }
13 </script>
react组件
1 function App() {
2 return (
3 <div className="App">
4 组件化开发 ---- 模块化
5 </div>
6 );
7 }
8 export default App;
包括完成一些复杂应用的时候,我们也可以拆分成各个模块
- 你是怎么理解ES6中Generator的?使用场景?

7.1. 介绍
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同
回顾下上文提到的解决异步的手段:
-
回调函数
-
promise
那么,上文我们提到 promisesie 已经是一种比较流行的解决异步方案,那么为什么还出现 Generator?甚至 async/await 呢?
该问题我们留在后面再进行分析,下面先认识下 Generator
7.1.1. Generator函数
执行Generator函数会返回一个遍历器对象,可以依次遍历Generator函数内部的每一个状态形式上,Generator函数是一个普通函数,但是有两个特征:
function关键字与函数名之间有一个星号
- 函数体内部使用 yield 表达式,定义不同的内部状态
1 function* helloWorldGenerator() {
2 yield 'hello';
3 yield 'world';
4 return 'ending';
5}
7.2. 使用
Generator 函数会返回一个遍历器对象,即具有 Symbol.iterator 属性,并且返回给自己
1 function\* gen(){
2 // some code
3 }
4 var $\mathbf{g} =$ gen();
5 g[Symbol.iteratoror]() $= = =$ g
6 // true
通过 yield 关键字可以暂停 generator 函数返回的遍历器对象的状态
1 function\* helloWorldGenerator() {
2 yield 'hello';
3 yield 'world';
4 return 'ending';
5 }
6 var hw = helloWorldGenerator();
上述存在三个状态:hello、world、return
通过 next 方法才会遍历到下一个内部状态,其运行逻辑如下:
-
遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
-
下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式
-
如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。
-
如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined
1 hw.next()
2 // { value: 'hello', done: false }
3 hw.next()
4 // { value: 'world', done: false }
5 hw.next()
6 // { value: 'ending', done: true }
7 hw.next()
8 // { value: undefined, done: true }
done 用来判断是否存在下个状态,value 对应状态值
yield 表达式本身没有返回值,或者说总是返回 undefined
通过调用 next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值
1 function\* foo(x) {
2 var y $= 2$ \* (yield $(x + 1))$ .
3 var z $=$ yield (y / 3);
4 return $(x + y + z)$
5 }
6 var a $=$ foo(5);
7 a.next() // Object{value:6, done:true}
8 a.next() // Object{value:NaN, done:true}
9 a.next() // Object{value:NaN, done:true}
10 var b $=$ foo(5);
11 b.next() // { value:6, done:true}
12 b.next(12) // { value:8, done:true}
13 b.next(13) // { value:42, done:true}
正因为Generator函数返回Iterator对象,因此我们还可以通过for...of进行遍历
1 function\* foo(){
2 yield 1;
3 yield 2;
4 yield 3;
5 yield 4;
6 yield 5;
7 return 6;
8}
9 for (let v of foo){
10 console.log(v);
11 }
12 // 1 2 3 4 5
原生对象没有遍历接口,通过Generator函数为它加上这个接口,就能使用for...of进行遍历了
1 function\* objectEntries(obj) { 2 let propKeys $=$ Reflect_ownKeys(obj);
3 for (let propKey of propKeys) {
4 yield [propKey, obj[propKey]];
5 }
6 }
7 let jane = { first: 'Jane', last: 'Doe' };
8 for (let [key, value] of objectEntries(jane)) {
9 console.log(
10 ${key}: ${value}
11 );
12 }
13 // first: Jane
14 // last: Doe
7.3. 异步解决方案
回顾之前展开异步解决的方案:
回调函数
-
Promise 对象
-
generator函数
async/await
这里通过文件读取案例,将几种解决异步的方案进行一个比较:
7.3.1. 回调函数
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,再调用这个函数
1 fs.readFile('/etc/fstab', function (err, data) {
2 if (err) throw err;
3 console.log(data);
4 fs.readFile('/etc/shells', function (err, data) {
5 if (err) throw err;
6 console.log(data);
7 });
8 });
readFile 函数的第三个参数,就是回调函数,等到操作系统返回了 /etc/passwd 这个文件以后,回调函数才会执行
7.3.2. Promise
1 const fs = require('fs');
2 const readFile = function (fileName) {
3 return new Promise(function (resolve, reject) {
4 fs.readFile(fileName, function(error, data) {
5 if (error) return reject(error);
6 resolve(data);
7 });
8 });
9 };
10 readFile('/etc/fstab').then(data => {
11 console.log(data)
12 return readFile('/etc/shells')
13 });
14 console.log(data)
15 });
这种链式操作形式,使异步任务的两段执行更清楚了,但是也存在了很明显的问题,代码变得冗杂了,语义化并不强
7.3.3. generator
yield 表达式可以暂停函数执行,next 方法用于恢复函数执行,这使得 Generator 函数非常适合将异步任务同步化
1 const gen = function\* () {
2 const f1 = yield readFile('/etc/fstab');
3 const f2 = yield readFile('/etc/shells');
4 console.log(f1.toString());
5 console.log(f2.toString());
6 };
7.3.4. async/await
将上面 Generator 函数改成 async/await 形式,更为简洁,语义化更强了
1 const asyncReadFile = async function() {
2 const f1 = await readFile('/etc/fstab');
3 const f2 = await readFile('/etc/shells');
4 console.log(f1.toString());
5 console.log(f2.toString());
6};
7.3.5.区别
通过上述代码进行分析,将 promise、Generator、async/await 进行比较:
-
promise 和 async/await 是专门用于处理异步操作的
-
Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Integrator接口...)
-
promise 编写代码相比 Generator、async 更为复杂化,且可读性也稍差
-
Generator、async 需要与 promise 对象搭配处理异步情况
async 实质是 Generator 的语法糖,相当于会自动执行 Generator 函数
- async 使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案
7.4.使用场景
Generator 是异步解决的一种方案,最大特点则是将异步操作同步化表达出来
1 function\*loadUI(){
2 showLoadingScreen();
3 yieldloadUIDataAsynchronously();
4 hideLoadingScreen();
5}
6 var loader $=$ loadUI();
7 //加载UI
8 loader.next()
9 //卸载UI
10 loader.next()
包括redis-saga中间件也充分利用了Generator特性
1 import { call, put, takeEvery, takeLatest } from 'Redux-saga/effects'
2 import Api from '..'
3 function* fetchUser(action) {
4 try {
5 const user = yield call(API fetchingUser, action.pagloadUserId);
6 yield put(type: "USER FETCH_SUCCESSED", user: user);
7 } catch (e) {
8 yield put(type: "USER Fetch_FAILED", message: e.message);
9 }
9 }
10 }
11 function $\star$ mySaga() {
12 yield takeEvery("USER FETCH_REQUESTED",fetchUser);
13 }
14 function $\star$ mySaga() {
15 yield takeLatest("USER FETCH_REQUESTED",fetchUser);
16 }
17 export default mySaga;
还能利用Generator函数,在对象上实现Iterator接口
1 function\* iterEntries(obj) {
2 let keys $=$ Object.keys(obj);
3 for (let $\mathrm{i} = 0$ .i $< _{\cdot}$ keys.length; i++) {
4 let key $=$ keys[i];
5 yield [key, obj[key]];
6 }
7 }
8 let myObj $=$ { foo:3, bar:7 };
9 for (let [key, value] of iterEntries(myObj)) {
10 console.log(key, value);
11 }
12 // foo 3
13 // bar 7
- 你是怎么理解ES6中Decorator 的?使用场景?

8.1. 介绍
Decorator,即装饰器,从名字上很容易让我们联想到装饰者模式
简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论。
ES6 中Decorator 功能亦如此,其本质也不是什么高大上的结构,就是一个普通的函数,用于扩展类属性和类方法
这里定义一个士兵,这时候他什么装备都没有
1 class soldier{ 2 }
定义一个得到AK装备的函数,即装饰器
1 function strong(target){
2 target.AK = true
3}
使用该装饰器对士兵进行增强
1 @strong
2 class soldier{
3 }
这时候士兵就有武器了
1 soldier.AK // true
上述代码虽然简单,但也能够清晰看到了使用Decorator 两大优点:
- 代码可读性变强了,装饰器命名相当于一个注释
在不改变原有代码情况下,对原来功能进行扩展
8.2. 用法
Docorator 修饰对象为下面两种:
类的装饰
类属性的装饰
8.2.1. 类的装饰
当对类本身进行装饰的时候,能够接受一个参数,即类本身
将装饰器行为进行分解,大家能够有个更深入的了解
1 @decorator
2 class A {}
3 // 等同于
4 class A {}
5 A = decorator(A) || A;
下面 @testable 就是一个装饰器,target 就是传入的类,即 MyTestableClass,实现了为类添加静态属性
1 @testable
2 class MyTestableClass {
3 // ...
4 }
5 function testable(target) {
6 target.isTestable = true;
7 }
8 MyTestableClass.isTestable // true
如果想要传递参数,可以在装饰器外层再封装一层函数
1 function testable(isTestable) {
2 return function(target) {
3 target.isTestable = isTestable;
4 }
5 }
6 @testable(true)
7 class MyTestableClass {}
8 MyTestableClass.isTestable // true
9 @testable(false)
10 class MyClass {}
11 MyClass.isTestable // false
8.2.2. 类属性的装饰
当对类属性进行装饰的时候,能够接受三个参数:
- 类的原型对象
需要装饰的属性名
- 装饰属性名的描述对象
首先定义一个 readonly 装饰器
1 function readonly(target, name, descriptor){
2 descriptor.writable = false; // 将可写属性设为false
3 return descriptor;
4 }
使用 readonly 装饰类的 name 方法
1 class Person {
2 @readonly
3 name() { return
4 ${this.first} ${this.last}
5 }
相当于以下调用
1. reckonly(Person.proto, 'name', descriptor);
如果一个方法有多个装饰器,就像洋葱一样,先从外到内进入,再由内到外执行
1 function dec(id){
2 console.log('evaluated', id);
3 return (target, property, descriptor) =>console.log('executed', id);
4 }
5 class Example {
6 @dec(1)
7 @dec(2)
8 method(){
9 }
10 // evaluated 1
11 // evaluated 2
12 // executed 2
13 // executed 1
外层装饰器 @dec(1) 先进入,但是内层装饰器 @dec(2) 先执行
8.2.3. 注意
装饰器不能用于修饰函数,因为函数存在变量声明情况
1 var counter $= 0$
2 var add $=$ function () {
3 counter++;
4 };
5 @add
6 function foo() {
7 }
编译阶段,变成下面
1 var counter;
2 var add;
3 @add
4 function foo() {
5 }
6 counter = 0;
7 add = function () {
8 counter++;
9 };
意图是执行后 counter 等于 1,但是实际上结果是 counter 等于 0
8.3. 使用场景
基于 Decorator 强大的作用,我们能够完成各种场景的需求,下面简单列举几种:
使用 react-reflux 的时候,如果写成下面这种形式,既不雅观也很麻烦
1 class MyReactComponent extends React.Component {}
2 export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
1 @connect(mapStateToProps, mapDispatchToProps)
2 export default class MyReactComponent extends React.Component {}
将 mixins,也可以写成装饰器,让使用更为简洁了
1 function mixins(...list) {
2 return function (target) {
3 Object.assign(targetprototype, ...list);
4 };
5 }
6 // 使用
7 const Foo = {
8 foo() { console.log('foo') }
9 };
10 @mixins(Foo)
11 class MyClass {}
12 let obj = new MyClass();
13 obj.foo() // "foo"
9. 你是怎么理解ES6新增Set、Map两种数据结构的?

如果要用一句来描述,我们可以说
Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构
什么是集合?什么又是字典?
- 集合
是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
字典
是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同区别?
-
共同点:集合、字典都可以存储不重复的值
-
不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储
9.1. Set
Set 是 es6 新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合
Set本身是一个构造函数,用来生成Set数据结构
1 const s = new Set();
9.1.1. 增删改查
Set 的实例关于增删改查的方法:
-
add()
-
delete()
-
has()
clear()
9.1.2. add()
添加某个值,返回 Set 结构本身
当添加实例中已经存在的元素,set 不会进行处理添加
1 s.add(1).add(2).add(2); // 2只被添加了一次
9.1.3. delete()
删除某个值,返回一个布尔值,表示删除是否成功
1 s(delete(1)
9.1.4. has()
返回一个布尔值,判断该值是否为Set的成员
1 s.has(2)
9.1.5. clear()
清除所有成员,没有返回值
1 s.clear()
9.1.6. 遍历
Set 实例遍历的方法有如下:
关于遍历的方法,有如下:
keys(): 返回键名的遍历器
-
values(): 返回键值的遍历器
-
entries(): 返回键值对的遍历器
-forEach(): 使用回调函数遍历每个成员
Set 的遍历顺序就是插入顺序
keys 方法、values 方法、entries 方法返回的都是遍历器对象
1 let set = new Set(['red', 'green', 'blue']);
2 for (let item of set.keys())
3 console.log(item);
4 }
5 // red
6 // green
7 // blue
8 for (let item of set.values())
9 console.log(item);
10 }
11 // red
12 // green
13 // blue
14 for (let item of set_entries())
15
15 console.log(item);
16}
17 //["red","red"]
18//["green","green"]
19//["blue","blue"]
forEach() 用于对每个成员执行某种操作,没有返回值,键值、键名都相等,同样的forEach 方法有第二个参数,用于绑定处理函数的 this
1 let set = new Set([1, 4, 9]);
2 set.forEach((value, key) => console.log(key + ': ' + value))
3 // 1:1
4 // 4:4
5 // 9:9
扩展运算符和 Set 结构相结合实现数组或字符串去重
1 //数组
2 let arr = [3, 5, 2, 2, 5, 5];
3 let unique = [...new Set(arr)]; // [3, 5, 2]
4 //字符串
5 let str = "352255";
6 let unique = [...new Set(str)].join(""); // "352"
实现并集、交集、和差集
1 let a = new Set([1, 2, 3]);
2 let b = new Set([4, 3, 2]);
3 // 并集
4 let union = new Set([\ldots a, ...b]);
5 // Set {1, 2, 3, 4}
6 // 交集
7 let intersect = new Set([\ldots a].filter(x => b.has(x)));
8 // set {2, 3}
9 // (a相对于b的)差集
10 let difference = new Set([\ldots a].filter(x => !b.has(x)));
11 // Set {1}
9.2. Map
Map 类型是键值对的有序列表,而键和值都可以是任意类型
Map 本身是一个构造函数,用来生成 Map 数据结构
1 const m = new Map()
9.2.1. 增删改查
Map 结构的实例针对增删改查有以下属性和操作方法:
size属性
set()
get()
has()
- delete()
clear()
9.2.2. size
size 属性返回 Map 结构的成员总数。
1 const map = new Map();
2 map.set('foo', true);
3 map.set('bar', false);
4 map.size // 2
9.2.3. set()
设置键名 key 对应的键值为 value,然后返回整个 Map 结构
如果 key 已经有值,则键值会被更新,否则就新生成该键
同时返回的是当前Map对象,可采用链式写法
1 const m = new Map();
2 m.set('edition', 6) // 键是字符串
3 m.set(262, 'standard') // 键是数值
4 m.set(undefined, 'nah') // 键是 undefined
5 m.set(1,'a').set(2,'b').set(3,'c') //链式操作
9.2.4. get()
get 方法读取 key 对应的键值,如果找不到 key,返回 undefined
1 const m = new Map();
2 const hello = function() {console.log('hello');};
3 m.set(hello, 'Hello ES6!') // 键是函数
4 m.get(hello) // Hello ES6!
9.2.5. has()
has 方法返回一个布尔值,表示某个键是否在当前 Map 对象之中
1 const m = new Map();
2 m.set('edition', 6);
3 m.set(262, 'standard');
4 m.set undefined, 'nah');
5 m.has('edition') // true
6 m.has('years') // false
7 m.has(262) // true
8 m.has undefined) // true
9.2.6. delete()
delete 方法删除某个键,返回 true。如果删除失败,返回 false
1 const m = new Map();
2 m.set(undefined, 'nah');
3 m.has(undefined) // true
4 m.delete(undefined)
5 m.has(undefined) // false
9.2.7. clear()
clear 方法清除所有成员,没有返回值
1 let map = new Map();
2 map.set('foo', true);
3 map.set('bar', false);
4 map.size // 2
5 map.clear()
6 map.size // $\theta$
9.2.8.遍历
Map结构原生提供三个遍历器生成函数和一个遍历方法:
keys(): 返回键名的遍历器
-
values(): 返回键值的遍历器
-
entries(): 返回所有成员的遍历器
-forEach():遍历Map的所有成员
遍历顺序就是插入顺序
1 const map = new Map([
2 ['F', 'no'],
3 ['T', 'yes'],
4]);
5 for (let key of map.keys()) {
6 console.log(key);
7 }
8 // "F"
9 // "T"
10 for (let value of map.values()) {
11 console.log(value);
12 }
13 // "no"
14 // "yes"
15 for (let item of map_entries()) {
16 console.log(item[0], item[1]);
17 }
18 // "F" "no"
19 // "T" "yes"
20 // 或者
21 for (let [key, value] of map_entries()) {
22 console.log(key, value);
23 }
24 // "F" "no"
25 // "T" "yes"
26 // 等同于使用map_entries()
27 for (let [key, value] of map) {
28 console.log(key, value);
29 }
30 // "F" "no"
31 // "T" "yes"
32 map.forEach(function(value, key, map) {
33 console.log("Key: %s, Value: %s", key, value);
34 });
9.3. WeakSet 和 WeakMap
9.3.1. WeakSet
创建 WeakSet 实例
1 const ws = new WeakSet();
WeakSet 可以接受一个具有 Iterator 接口的对象作为参数
1 const a = [[1, 2], [3, 4]];
2 const ws = new WeakSet(a);
3 // WeakSet {{1, 2}, [3, 4]}
在API中WeakSet与Set有两个区别:
没有遍历操作的API
- 没有 size 属性
WeakSet 只能成员只能是引用类型,而不能是其他类型的值
1 let ws=new WeakSet();
2 //成员不是引用类型
3 letweakSet=new WeakSet([2,3]);
4 console.log(weakSet) //报错
5 //成员为引用类型
6 let obj1=\{name:1\}
7 let obj2=\{name:1\}
8 let ws=new WeakSet([obj1,obj2]);
9 console.log(ws) //WeakSet{\{...}, {...}\}
WeakSet 里面的引用只要在外部消失,它在 WeakSet 里面的引用就会自动消失
9.3.2. WeakMap
WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合
在API中WeakMap与Map有两个区别:
没有遍历操作的API
- 没有 clear 清空方法
1 // WeakMap 可以使用 set 方法添加成员
2 const WM1 = new WeakMap();
3 const key = {foo: 1};
4 WM1.set(key, 2);
5 WM1.get(key) // 2
6 // WeakMap 也可以接受一个数组,
7 // 作为构造函数的参数
8 const k1 = [1, 2, 3];
9 const k2 = [4, 5, 6];
10 const WM2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
11 WM2.get(k2) // "bar"
WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
1 const map = new WeakMap();
2 map.set(1, 2)
3 // TypeError: 1 is not an object!
4 map.set(Symbol(), 2)
5 // TypeError: Invalid value used as weak map key
6 map.set(null, 2)
7 // TypeError: Invalid value used as weak map key
WeakMap 的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
举个场景例子:
在网页的DOM元素上添加数据,就可以使用WeakMap结构,当该DOM元素被清除,其所对应的WeakMap记录就会自动被移除
1 const $\mathsf{Wm} =$ new WeakMap();
2 const element $=$ document.getElementById('example');
3 WM.setelement, 'some information');
4 WM.getelement) // "some information"
注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用
1 const $\mathtt{wm} =$ new WeakMap();
2 let key $=$ {};
3 let obj $=$ {foo: 1};
4 WM.set(key, obj);
5 obj $=$ null;
6 WM.get(key)
7 // Object {foo: 1}
10. 你是怎么理解ES6中Proxy的?使用场景?

10.1. 介绍
定义:用于定义基本操作的自定义行为
本质:修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)
元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
一段代码来理解
#!/bin/bash
metaprogram
echo '#!/bin/bash' >program
for ((l=1; l<=1024; l++) do
echo "echo $l" >>program
done
chmod +x program
这段程序每执行一次能帮我们生成一个名为 program 的文件,文件内容为1024行 echo,如果我们手动来写1024行代码,效率显然低效
- 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
Proxy 亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
10.2. 用法
Proxy 为构造函数,用来生成 Proxy 实例
1 var proxy = new Proxy(target, handler)
10.2.1. 参数
target 表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler 通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为
10.2.2. handler解析
关于handler拦截属性,有如下:
-
get(target,propKey,receiver):拦截对象属性的读取
-
set(target,propKey=value,receiver):拦截对象属性的设置
-
has(target,propKey):拦截 propKey in proxy 的操作,返回一个布尔值
-
deleteProperty(target,propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值
-
ownKeys(target):拦截 Object_keysproxy)、for...in等循环,返回一个数组
-
getOwnPropertyDescriptor(target, propKey):拦截 Object.getOwnPropertyDescriptorproxy,propKey),返回属性的描述对象
defineProperty(target,_propKey,propDesc):拦截 Object_definedPropertyproxy, propKey,propDesc),返回一个布尔值
-
preventExtensions(target):拦截 Object.preventExtensionsproxy),返回一个布尔值
-
getPrototypeOf(target):拦截 Object.getPrototypeOfproxy),返回一个对象
-
isExtensible(target):拦截 Object.isExtensibleproxy),返回一个布尔值
-
setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto), 返回一个布尔值
-
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
-
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作
10.2.3. Reflect
若需要在 Proxy 内部调用对象的默认行为,建议使用 Reflect,其是 ES6 中操作对象而提供的新 API
基本特点:
-
只要 Proxy 对象具有的代理方法, Reflect 对象全部具有, 以静态方法的形式存在
-
修改某些 Object 方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回 false)
-
让 Object 操作都变成函数行为
下面我们介绍 proxy 几种用法:
10.2.4. get()
get 接受三个参数,依次为目标对象、属性名和 proxy 实例本身,最后一个参数可选
1 var person = {
2 name: "张三"
3 };
4 var proxy = new Proxy(person, {
5 get: function(target, propKey) {
6 return Reflect.get(target, propKey)
7 }
8 });
9 proxy.name // "张三"
get 能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引
1 function createArray(...elements) {
2 let handler = {
3 get(target,_propKey,receiver) {
4 let index = Number(propKey);
5 if (index < 0) {
6 propKey = String(target.length + index);
7 }
8 return Reflect.get(target,_propKey,receiver);
9 }
10 };
11 let target = {};
12 target.push(...elements);
13 return new Proxy(target,handler);
14 }
15 let arr = createArray('a', 'b', 'c');
16 arr[-1] // c
注意:如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则会报错
1 const target = Object_definedProperties({}, {
2 foo: {
3 value: 123,
4写着: false,
5 configurable: false
6 },
7 });
8 const handler = {
9 get(target, propKey) {
10 return 'abc';
11 }
12 };
13 const proxy = new Proxy(target, handler);
14 proxy.foo
15 // TypeError: Invariant check failed
10.2.5. set()
set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身
假定 Person 对象有一个 age 属性,该属性应该是一个不大于 200 的整数,那么可以使用 Proxy 保证 age 的属性值符合要求
1 let validator = {
2 set: function(obj, prop, value) {
3 if (prop == 'age') {
4 if (!Number.isInteger(value)) {
5 throw new TypeError('The age is not an integer');
6 }
7 if (value > 200) {
8 throw new ValueError('The age seems invalid');
9 }
10 }
11 // 对于满足条件的 age 属性以及其他属性,直接保存
12 obj[prop] = value;
13 }
14 };
15 let person = new Proxy{}, validator);
16 person.age = 100;
17 person.age // 100
18 person.age = 'young' // 报错
19 person.age = 300 // 报错
如果目标对象自身的某个属性,不可写且不可配置,那么 set 方法将不起作用
1 const obj = {};
2 Object_definedProperty(obj, 'foo', {3 value: 'bar', 4 writable: false, 5 }); 6 const handler = {7 set: function(obj, prop, value, receiver) {8 obj[prop] = 'baz'; 9 } 10 }; 11 const proxy = new Proxy(obj, handler); 12 proxy.foo = 'baz'; 13 proxy.foo // "bar"
注意,严格模式下,set 代理如果没有返回 true,就会报错
1 'use strict';
2 const handler = {
3 set: function(obj, prop, value, receiver) {
4 obj[prop] = receiver;
5 //无论有没有下面这一行,都会报错
6 return false;
7 };
8 };
9 const proxy = new Proxy({}, handler);
10 proxy.foo = 'bar';
11 //TypeError: 'set' on proxy: trap returned falsish for property 'foo'
10.2.6. deleteProperty()
deleteProperty 方法用于拦截 delete 操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete 命令删除
1 var handler $=$ {
2 deleteProperty(target, key){
3 invariant(key, 'delete');
4 Reflect(deleteProperty(target, key)
5 return true;
6 }
7 };
8 function invariant (key, action) {
9 if (key[0] $= = =$ _') {
10 throw new Error(
11 无法删除私有属性
12 );
13 }
14 }
15 var target $=$ { _prop: 'foo'};
16 var proxy $=$ new Proxy(target, handler);
17 delete proxy._prop
18 // Error: 无法删除私有属性
注意,目标对象自身的不可配置(configurable)的属性,不能被 deleteProperty 方法删除,否则报错
10.2.7. 取消代理
1 Proxy revocable(target, handler);
10.3. 使用场景
Proxy 其功能非常类似于设计模式中的代理模式,常用功能如下:
-
拦截和监视外部对对象的访问
-
降低函数或类的复杂度
在复杂操作前对操作进行校验或对所需资源进行管理
使用 Proxy 保障数据类型的准确性
1 letnumericDataStore = {count:0, amount:1234,total:14};
2 numericDataStore = new Proxy(numericDataStore,{
3 set(target,key, value, proxy) {
4 if (typeofvalue $! = =$ 'number') {
5 throw Error("属性只能是number类型");
6 }
7 return Reflect.set(target,key, value, proxy);
8 }
9 });
10 numericDataStore.count = "foo"
11 //Error:属性只能是number类型
12 numericDataStore.count = 333
13 //赋值成功
声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey
1 let api = {
2 _ apiKey: '123abc456def',
3 getUsers: function(){},
4 getUser: function(userId){},
5 setUser: function(userId, config){}
6 };
7 const RESTRICTED = ['_ apiKey'];
8 api = new Proxy api, {
9 get(target, key, proxy) {
10 if(RESTRICTED.indexOf(key) > -1) {
11 throw Error()
12 ${key} 不可访问.
13 });
14 } return Reflect.get(target, key, proxy);
15 },
16 set(target, key, value, proxy) {
17 if(RESTRICTED.indexOf(key) > -1) {
18 throw Error()
19 ${key} 不可修改
20);
21 } return Reflect.get(target, key, value, proxy);
22 }
23 });
24 console.log api._apiKey)
25 api._apiKey = '987654321'
26 //上述都抛出错误
还能通过使用 Proxy 实现观察者模式
观察者模式(Observermode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行 observable 函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数
1 const queuedObservers = new Set();
2 const observe = fn => queuedObservers.add(fn);
3 const observable = obj => new Proxy(obj, {set});
4 function set(target, key, value, receiver) {
5 const result = Reflect.set(target, key, value, receiver);
6 queuedObservers.forEach(observers => observer());
7 return result;
8}
观察者函数都放进 Set 集合,当修改 obj 的值,在会 set 函数中拦截,自动执行 Set 所有的观察者