ES6 核心特性
ES6 核心特性高频面试问答。
1. 说说var、let、const之间的区别
声明特性
var
在ES5中,顶层对象的属性和全局变量是等价的,用var声明的变量既是全局变量,也是顶层变量
注意:顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象
var a = 10;
console.log(window.a) // 10使用var声明的变量存在变量提升的情况
console.log(a) // undefined
var a = 20在编译阶段,编译器会将其变成以下执行
var a
console.log(a)
a = 20使用var,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明
var a = 20
var a = 30
console.log(a) // 30在函数中使用使用var声明变量时候,该变量是局部的
var a = 20
function change(){
var a = 30
}
change()
console.log(a) // 20而如果在函数内不使用var,该变量是全局的
var a = 20
function change(){
a = 30
}
change()
console.log(a) // 30let
{
let a = 20
}
console.log(a) // ReferenceError: a is not defined.不存在变量提升
console.log(a) // 报错ReferenceError
let a = 2这表示在声明它之前,变量a是不存在的,这时如果用到它,就会抛出一个错误
只要块级作用域内存在let命令,这个区域就不再受外部影响
var a = 123
if (true) {
a = 'abc' // ReferenceError
let a;
}使用let声明变量前,该变量都不可用,也就是大家常说的“暂时性死区”
最后,let不允许在相同作用域中重复声明
let a = 20
let a = 30
// Uncaught SyntaxError: Identifier 'a'has already been declared注意的是相同作用域,下面这种情况是不会报错的
let a = 20
{
let a = 304}
因此,我们不能在函数内部重新声明参数
function func(arg) {
let arg;
}func() 5 // Uncaught SyntaxError: Identifier 'arg' has already been declared
const
const声明一个只读的常量,一旦声明,常量的值就不能改变
const a = 1
a = 3
// TypeError: Assignment to constant variable.这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值
const a;
// SyntaxError: Missing initializer in const declaration如果之前用var或let声明过变量,再用 const声明同样会报错
var a = 20
let b = 20
const a = 30
const b = 30//都会报错
const实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动
对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量
对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,并不能确保改变量的结构不变
const foo = {};
//为foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
//将 foo指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only其它情况,const 与let 一致
核心差异与使用建议
var、let、const 三者的区别可以从以下方面展开:
-
变量提升
-
暂时性死区
-
块级作用域
-
重复声明
-
修改声明的变量
-
使用建议
变量提升
var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined
let和 const 不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
// var
console.log(a) // undefined
var a = 10
// let
console.log(b) // Cannot access 'b' before initialization
let b = 10
// const
console.log(c) // Cannot access 'c' before initialization
const c = 10暂时性死区
var 不存在暂时性死区
let 和 const 存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
// var
console.log(a) // undefined
var a = 10
// let
console.log(b) // Cannot access 'b' before initialization
let b = 10
// const
console.log(c) // Cannot access 'c' before initialization
const c = 10块级作用域
var 不存在块级作用域
let 和 const 存在块级作用域
// var
{
var a = 20
}
console.log(a) // 20
// let
{
let b = 20
}
console.log(b) // Uncaught ReferenceError: b is not defined
// const
{
const c = 20
}
console.log(c) // Uncaught ReferenceError: c is not defined重复声明
var 允许重复声明变量,let 和 const 在同一作用域不允许重复声明变量。
// var
var a = 10
var a = 20 // 20
// let
let b = 10
let b = 20 // Identifier 'b'has already been declared
//const
const c = 10
const c = 20 // Identifier 'c' has already been declared修改声明的变量
var 和 let 可以
const声明一个只读的常量。一旦声明,常量的值就不能改变
// var
var a = 10
a = 20
console.log(a) // 20
//let
let b = 10
b = 20
console.log(b) // 20
// const
const c = 10
C = 20
console.log(c) // Uncaught TypeError: Assignment to constant variable使用建议
能用const的情况尽量使用 const,其他情况下大多数使用 let,避免使用 var
2. ES6中数组新增了哪些扩展?
扩展运算符
ES6通过扩展元素符..,好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列
console.log(...[1, 2, 3])
//12 3
console.log(1, ...[2, 3, 4], 5)
//1 2 34
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]主要用于函数调用的时候,将一个数组变为参数序列
[...document.querySelectorAll('div')]function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42可以将某些数据结构转为数组
能够更简单实现数组复制
const a1 = [1, 2];
const [...a2] = a1;
//[1,2]数组的合并也更为简洁了
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];[...arr1, ..arr2, ...arr3] 5 'a' 'b', 'c', 'd', 'e']
注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组
下面看个例子就清楚多了
const arr1 = ['a', 'b',[1,2]];
const arr2 = ['c'];
const arr3 = [...arr1,...arr2]
arr[1][0] = 9999 // 修改arr1里面数组成员值
console.log(arr[3]) // 影响到arr3,['a','b',[9999,2],'c']扩展运算符可以与解构赋值结合起来,用于生成数组
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//[]如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
const [...butLast, last] = [1, 2, 3, 4, 5];
//报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
//报错可以将字符串转为真正的数组
1[...'hello'] 2 // [ "h", "e", "", "l", "o" ]
定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
1);
let arr = [...map.keys()]; // [1, 2, 3]如果对没有Iterator接口的对象,使用扩展运算符,将会报错
const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object构造函数新增的方法
关于构造函数,数组新增的方法有如下:
-
Array.from()
-
Array.of()
Array.from()
将两类对象转为真正的数组:类似数组的对象和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map )
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组
Array.from([1, 2, 3], (x) => x * x)
// [1,4, 9]Array.of()
用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8]没有参数的时候,返回一个空数组
当参数只有一个的时候,实际上是指定数组的长度
参数个数不少于2个时,Array()才会返回由参数组成的新数组
Array()// []
Array(3) // [,, ,]
Array(3, 11, 8) // [3, 11, 8]实例方法
关于数组实例对象新增的方法有如下:
-
copyWithin()
-
find()、findIndex()
-
fill()
-
entries(), keys(), values()
-
includes()
-
flat(), flatMap()
copyWithin()
将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
参数如下:
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为0。如果为负值,表示从末尾开始计算。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1,2,3,4,5].copyWithin(0,3) //将从 3号位直到数组结束的成员(4和5),复制
到从0号位开始的位置,结果覆盖了原来的1 和2
// [4,5,3,4,5]find()、findIndex()
find()用于找出第一个符合条件的数组成员
参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
})// 10findIndex 返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
})//2这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); //26fill()
使用给定值,填充一个数组
['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']注意,如果填充的类型为对象,则是浅拷贝
entries(), keys(), values()
keys()是对键名的遍历、values()是对键值的遍历, entries()是对键值对的遍历
or (let index of ['a', 'b'].keys()) {
console.log(index);
}
//0
//1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
/1
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
//0"a"includes()
用于判断数组是否包含给定的值
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true方法的第二个参数表示搜索的起始位置,默认为0
参数为负数则表示倒数的位置
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // trueflat(), flatMap()
将数组扁平化处理,返回一个新数组,对原数据没有影响
[1, 2, [3, 4]].flat()
//[1,2,3,4]flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1
[1, 2, [3, [4, 5]]].flat()
// [1,2,3,[4,5]]
[1, 2, [3, [4, 5]]].flat(2)
11 [1, 2, 3 4,5]flatMap()方法对原数组的每个成员执行一个函数相当于执行 Array.prototype.map(),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组
//相当于[[2,4],[3,6],[4,8]].flat()
[2,3, 4].flatMap((x) => [x, x * 2])
//[2,4,3,6,4,8]flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的 this
空位与排序稳定性
数组的空位指,数组的某一个位置没有任何值
ES6 则是明确将空位转为undefined,包括 Array.from、扩展运算符、copyWithin()、fill()、entries()、 keys() values() find()和 findIndex()
建议大家在日常书写中,避免出现空位
排序稳定性
将sort()默认设置为稳定的排序算法
const arr = [
'peach',
'straw',
'apple',
'spork'
1;
const stableSorting = (s1, s2) => {
if (s1[0] < s2[0]) return -1;
return 1;
};
arr.sort(stableSorting)
// ["apple","peach","straw","spork"]排序结果中,straw在 spork的前面,跟原始顺序一致
3. 函数新增了哪些扩展?
参数默认值与函数属性
ES6允许为函数的参数设置默认值
function log(x, y = 'World') {
console.log(x, y);
}
console.log('Hello') // Hello World
console.log('Hello', 'China') // Hello China
console.log('Hello',') // Hello函数的形参是默认声明的,不能使用let或const再次声明
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}参数默认值可以与解构赋值的默认值结合起来使用
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上面的 foo函数,当参数为对象的时候才能进行解构,如果没有提供参数的时候,变量x和y就不会生成,从而报错,这里设置默认值避免
function foo({x, y = 5} = {}) {
console.log(x, y);
foo() // undefined 5参数默认值应该是函数的尾参数,如果不是非尾部的参数设置默认值,实际上这个参数是没发省略的
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(,1) // 报错
f(undefined, 1) // [1, 1]函数的length属性
length将返回没有指定默认值的参数个数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2rest 参数也不会计入 length属性
(function(...args) {}).length // 0如果设置了默认值的参数不是尾参数,那么 length属性也不再计入后面的参数了
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1name属性
返回该函数的函数名
var f = function () {};
// ES5
f.name //""
// ES6
f.name //"f"如果将一个具名函数赋值给一个变量,则 name属性都返回这个具名函数原本的名字
const bar = function baz() {};
bar.name // "baz"Function构造函数返回的函数实例, name 属性的值为 anonymous
(new Function).name // "anonymous"bind返回的函数, name属性值会加上 bound前缀
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "参数作用域与严格模式
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的下面例子中,y=x会形成一个单独作用域,×没有被定义,所以指向全局变量x
let x = 1;
function f(y = x) {
// 等同于 let y = x
let x = 2;
console.log(y);
}
f()// 1严格模式
只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式否则会报错
//报错
function doSomething(a, b = a) {
'use strict';
// code
}
//报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
//报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
//报错
doSomething({a, b}) {
'use strict';
// code
}
};箭头函数
使用“箭头”(=>)定义函数
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" });注意点
函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
-
不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
-
不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数
4. 对象新增了哪些扩展?
对象字面量与属性定义
ES6中,当对象键名与对应值名相等的时候,可以进行简写
const baz={foo:foo}
//等同于
const baz = {foo}方法也能够进行简写
const o = {
method( ) {
return "Hello!";
}
}
//等同于
const o = {
method: function() {
return "Hello!";
}
}在函数内作为返回值,也会变得方便很多
function getPoint() {
const x = 1;
const y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}注意:简写的对象方法不能用作构造函数,否则会报错
const obj
f()
this.foo = 'bar';
}
};
new obj.f() // 报错属性名表达式
ES6允许字面量定义对象时,将表达式放在括号内
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"表达式还可以用于定义方法名
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi注意,属性名表达式与简洁表示法,不能同时使用,会报错
//报错
const foo = 'bar';
const bar ='abc';
const baz = { [foo] };
//正确
const foo = 'bar';
const baz = { [foo]: 'abc'};注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}super关键字
this 关键字总是指向函数所在的当前对象,ES6又新增了另一个类似的关键字 super,指向当前对象的原型对象
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find( ) {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto); // 为 obj 设置原型对象
obj.find()// "hello"对象展开与属性遍历
在解构赋值中,未被读取的可遍历的属性,分配到指定的对象上面
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; 2×//1
y//2z // { a: 3, b: 4 }
注意:解构赋值必须是最后一个参数,否则会报错
解构赋值是浅拷贝
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;// 修改obj里面a属性中键值
x.a.b // 2,影响到了结构出来x的值对象的扩展运算符等同于使用Object.assign()方法
属性的遍历
ES6一共有5种方法可以遍历对象的属性。
- for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol属性)
Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol 属性)的键名
-
Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含 Symbol属性,但是包括不可枚举属性)的键名
-
Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名
Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是Symbol或字符串,也不管是否可枚举
上述遍历,都遵守同样的属性遍历的次序规则:
首先遍历所有数值键,按照数值升序排列
-
其次遍历所有字符串键,按照加入时间升序排列
-
最后遍历所有Symbol键,按照加入时间升序排
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]Object 新增方法
关于对象新增的方法,分别有以下:
-
Object.is()
-
Object.assign()
-
Object.getOwnPropertyDescriptors()
-
Object.setPrototypeOf(), Object.getPrototypeOf()
-
Object.keys(), Object.values(), Object.entries()
-
Object.fromEntries()
Object.is()
严格判断两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是 +0 不等于 -0,二是NaN等于自身
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // trueObject.assign()
Object.assign()方法用于对象的合并,将源对象 source的所有可枚举属性,复制到目标对象target
bject.assign()方法的第一个参数是目标对象,后面的参数都是源对象const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}注意:Object.assign()方法是浅拷贝,遇到同名属性会进行替换
Object.getOwnPropertyDescriptors()
返回指定对象所有自身属性(非继承属性)的描述对象
const obj = {
foo: 123,
get bar() { return 'abc'}
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
/1 { value: 123,
11 writable: true,
11 enumerable: true,
11 configurable: true },
11 bar:
11 { get: [Function: get bar],
11 set: undefined,
1/ enumerable: true,
11 configurable: true }}Object.setPrototypeOf()
Object.setPrototypeOf 方法用来设置一个对象的原型对象
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]Object.setPrototypeOf(object, prototype)
//用法
const o = Object.setPrototypeOf({}, null);var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]Object.getPrototypeOf()
用于读取一个对象的原型对象
Object.getPrototypeOf(obj);Object.keys()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组
Object.values()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组
Object.entries()
返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]Object.fromEntries()
用于将一个键值对数组转为对象
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
1)
// { foo: "bar", baz: 42 }5. 你是怎么理解ES6中 Promise的?使用场景?
核心概念与状态
Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大
在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('得到最终结果:'+ finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);阅读上面代码,是不是很难受,上述形成了经典的回调地狱
现在通过Promise的改写上面的代码
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('得到最终结果:'+ finalResult);
})
.catch(failureCallback);瞬间感受到promise解决异步操作的优点:
链式操作减低了编码难度
代码可读性明显增强
下面我们正式来认识promise:
状态
promise对象仅有三种状态
pending (进行中)
fulfilled (已成功)
rejected(已失败)
特点
-
对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
-
一旦状态改变(从 pending 变为fulfilled 和从 pending变为 rejected),就不会再变,任何时候都可以得到这个结果
实例创建与链式调用
Promise 对象是一个构造函数,用来生成 Promise 实例
const promise = new Promise(function(resolve, reject) {});Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和 reject
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”
reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”
实例方法
Promise构建出来的实例存在以下方法:
-
then()
-
catch()
-
finally()
then()
then 是实例状态发生改变时的回调函数,第一个参数是 resolved 状态的回调函数,第二个参数是 rejected状态的回调函数
then方法返回的是一个新的 Promise实例,也就是promise 能链式书写的原因
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
//...
});catch
catch()方法是.then(null,rejection)或.then(undefined,rejection)的别名,用于指定发生错误时的回调函数
getJSON('/posts.json').then(function(posts) {
//..
}).catch(function(error) {
//处理getJSON和前一个回调函数运行时发生的错误
console.log('发生错误!',error);
});Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
//some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});一般来说,使用 catch方法代替 then()第二个参数
Promise对象抛出的错误不会传递到外层代码,即不会有任何反应
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};浏览器运行到这一行,会打印出错误提示ReferenceError:x is not defined,但是不会退出进程
catch()方法之中,还能再抛出错误,通过后面catch方法捕获到
finally()
finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {.··})
.finally(() => {···});静态方法
Promise构造函数存在以下方法:
-
all()
-
race()
-
allSettled()
-
resolve()
-
reject()
-
try()
all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
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方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e =>console.log(e));
// ["hello",Error: 报错了]如果p2 没有自己的 catch方法,就会调用Promise.all()的 catch 方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error:报错了race()
Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.race([p1, p2, p3]);只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变
率先改变的Promise实例的返回值则传递给p的回调函数
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);allSettled()
Promise.allSettled() 方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例只有等到所有这些参数实例都返回结果,不管是 fulfilled还是 rejected,包装实例才会结束
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
1;
await Promise.allSettled(promises);
removeLoadingIndicator();resolve()
将现有对象转为 Promise对象
Promise.resolve('foo')
//等价于
new Promise(resolve => resolve('foo'))参数可以分成四种情况,分别如下:
- 参数是一个 Promise实例,promise.resolve 将不做任何修改、原封不动地返回这个实例
参数是一个 thenable对象,promise.resolve 会将这个对象转为 Promise对象,然后就立即执行thenable对象的then()方法
- 参数不是具有 then()方法的对象,或根本就不是对象,Promise.resolve()会返回一个新的 Promise 对象,状态为 resolved
没有参数时,直接返回一个resolved状态的 Promise 对象
reject()
Promise.reject(reason)方法也会返回一个新的Promise 实例,该实例的状态为rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
//出错了Promise.reject()方法的参数,会原封不动地变成后续方法的参数
Promise.reject('出错了')
.catch(e => {
console.log(e === '出错了')
})
// true典型使用场景
将图片的加载写成一个 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;
});
};通过链式操作,将多个渲染数据分别给个then,让其各司其职。或当下个异步请求依赖上个请求结果的时候,我们也能够通过链式操作友好解决问题
//各司其职
getInfo().then(res=>{
let { bannerList } = res
//渲染轮播图
console.log(bannerList)
return res}).then(res=>{
let { storeList } = res
//渲染店铺列表
console.log(storeList)
return res
}).then(res=>{
let { categoryList } = res
console.log(categoryList)
//渲染分类列表
return res
})通过 all()实现多个请求合并在一起,汇总所有请求结果,只需设置一个 loading 即可
function initLoad(){
// loading.show() //加载loading
Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(r
es=>{
console.log(res)
loading.hide() //关闭loading
}).catch(err=>{
console.log(err)
loading.hide()//关闭loading
})
}
//数据初始化
initLoad()通过 race 可以设置图片请求超时
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
//img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995a
d.svg";正确的
img.src ="https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.s
vg1";
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});6. 你是怎么理解ES6中Module的?使用场景?
模块化背景与规范演进
模块,(Module),是能够单独命名并独立地完成一定功能的程序语句的集合(即程序代码和数据结构的集合体)。
两个基本的特征:外部特征和内部特征
- 外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能
内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码)
为什么需要模块化
-
代码抽象
-
代码封装
-
代码复用
-
依赖管理
如果没有模块化,我们代码会怎样?
变量和方法不容易维护,容易污染全局作用域
加载资源的方式通过script标签从上到下。
-
依赖的环境主观逻辑偏重,代码较多就会比较复杂。
-
大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃因此,需要一种将JavaScript程序模块化的机制,如
-
CommonJs(典型代表:node.js早期)
-
AMD(典型代表:require.js)
-
CMD(典型代表:sea.js)
AMD
Asynchronous ModuleDefinition(AMD),异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行
代表库为 require.js
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery":"jquery.min",//实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});CommonJs
CommonJS 是一套 Javascript 模块规范,用于服务端
// a.js
module.exports={ foo , bar}
//b.js
const { foo,bar } = require('./a.js')CommonJS 具有以下特点:
-
所有代码都运行在模块作用域,不会污染全局作用域
-
模块是同步加载的,即只有加载完成,才能执行后面的操作
模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值
既然存在了AMD以及CommonJs机制,ES6的Module又有什么不一样?
ES6在语言标准的层面上,实现了Module,即模块功能,完全可以取代 CommonJS和 AMD规范,成为浏览器和服务器通用的模块解决方案
CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性
// CommonJS模块
let { stat, exists, readfile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;ES6设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量
// ES6模块
import { stat, exists, readFile } from 'fs';上述代码,只加载3个方法,其他方法不加载,即ES6 可以在编译时就完成模块加载
由于编译加载,使得静态分析成为可能。包括现在流行的typeScript也是依靠静态分析实现功能
ES Module 的静态导入与导出
ES6模块内部自动采用了严格模式,这里就不展开严格模式的限制,毕竟这是ES5之前就已经规定好模块功能主要由两个命令构成
export:用于规定模块的对外接口
import:用于输入其他模块提供的功能
export
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
或
//建议使用下面写法,这样能瞬间确定输出了哪些变量
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };输出函数或类
export function multiply(x, y) {
return x * y;
};通过as 可以进行输出变量的重命名
function v1() { ... }
function v2() { ... }
export
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};import
使用export命令定义了模块的对外接口以后,其他JS 文件就可以通过import命令加载这个模块
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName +'' + lastName;
}同样如果想要输入变量起别名,通过 as 关键字
import { lastName as surname } from './profile.js';当加载整个模块的时候,需要用到星号*
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// main.js
import * as circle from './circle';
console.log(circle)// {area:area,circumference:circumference}输入的变量都是只读的,不允许修改,但是如果是对象,允许修改属性
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
a = {}; // Syntax Error : 'a' is read-only;不过建议即使能修改,但我们不建议。因为修改之后,我们很难差错
import后面我们常接着 from关键字,from指定模块文件的位置,可以是相对路径,也可以是绝对路径
// import-default.js
import customName from './export-default';
customName(); // 'fooimport { a } from './a';// export-default.js
export default function () {
console.log('foo');
}foo();
import { foo } from 'my_module';如果只有一个模块名,需要有配置文件,告诉引擎模块的位置
1 import { myMethod } from 'util';
在编译阶段,import会提升到整个模块的头部,首先执行
多次重复执行同样的导入,只会执行一次
import 'lodash';
import 'lodash';上面的情况,大家都能看到用户在导入模块的时候,需要知道加载的变量名和函数,否则无法加载
如果不需要知道变量名或函数就完成加载,就要用到 export default命令,为模块指定默认输出
加载该模块的时候,import命令可以为该函数指定任意名字
动态导入与复合导出
允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势
这个新功能允许您将 import()作为函数调用,将其作为参数传递给模块的路径。它返回一个 promise,它用一个模块对象来实现,让你可以访问该对象的导出
import('/modules/myModule.mjs')
.then((module) => {
// Do something with the module.
});复合写法
如果在一个模块之中,先输入后输出同一个模块,import 语句可以与 export语句写在一起
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };同理能够搭配as、*搭配使用
应用场景
如今,ES6 模块化已经深入我们日常项目开发中,像 vue、react项目搭建项目,组件化开发处处可见,其也是依赖模块化实现
vue 组件
<template>
<div class="App">
组件化开发 -—-- 模块化
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>react 组件
function App() {
return (
<div className="App">
组件化开发 -——— 模块化
</div>
);
}
export default App;包括完成一些复杂应用的时候,我们也可以拆分成各个模块
7. 你是怎么理解ES6中 Generator的?使用场景?
Generator 核心机制
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同回顾下上文提到的解决异步的手段
-
回调函数
-
promise
那么,上文我们提到promsie已经是一种比较流行的解决异步方案,那么为什么还出现 Generator?甚至 async/await 呢?
该问题我们留在后面再进行分析,下面先认识下Generator
Generator函数
执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态形式上,Generator函数是一个普通函数,但是有两个特征:
function关键字与函数名之间有一个星号
函数体内部使用yield 表达式,定义不同的内部状态
function\* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}迭代器与执行控制
Generator 函数会返回一个遍历器对象,即具有Symbol.iterator属性,并且返回给自己
function* gen(){
// some code
}
var g = gen( );
g[Symbol.iterator]() === g
// true通过yield 关键字可以暂停generator函数返回的遍历器对象的状态
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();上述存在三个状态:hello、world、return
通过next 方法才会遍历到下一个内部状态,其运行逻辑如下:
遇到 yield表达式,就暂停执行后面的操作,并将紧跟在 yield后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield表达式
如果没有再遇到新的 yield表达式,就一直运行到函数结束,直到 return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
如果该函数没有 return 语句,则返回的对象的 value 属性值为undefined
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }done用来判断是否存在下个状态,value对应状态值
yield表达式本身没有返回值,或者说总是返回undefined
通过调用next 方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }正因为Generator函数返回 Iterator 对象,因此我们还可以通过for...of进行遍历
function\* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
//1234 5原生对象没有遍历接口,通过Generator函数为它加上这个接口,就能使用 for...of进行遍历了
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe异步方案对比
回顾之前展开异步解决的方案:
-
回调函数
-
Promise 对象
-
generator 函数
-
async/await
这里通过文件读取案例,将几种解决异步的方案进行一个比较:
回调函数
所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,再调用这个函数
fs.readFile('/etc/fstab', function (err, data) {
if (err) throw err;
console.log(data);
fs.readFile('/etc/shells', function (err, data) {
if (err) throw err;
console.log(data);
});
});readFile函数的第三个参数,就是回调函数,等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行
Promise
Promise就是为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
readFile('/etc/fstab').then(data =>{
console.log(data)
return readFile('/etc/shells')
}).then(data => {
console.log(data)
})这种链式操作形式,使异步任务的两段执行更清楚了,但是也存在了很明显的问题,代码变得冗杂了,语义化并不强
generator
yield表达式可以暂停函数执行,next 方法用于恢复函数执行,这使得 Generator函数非常适合将异步任务同步化
const gen = function* () {
const f1= yield readFile('/etc/fstab');
const f2= yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};async/await
将上面Generator函数改成 async/await形式,更为简洁,语义化更强了
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};区别
通过上述代码进行分析,将 promise、Generator、async/await进行比较:
promise和async/await 是专门用于处理异步操作的
Generator并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署Interator接口...)
promise 编写代码相比 Generator、async 更为复杂化,且可读性也稍差
Generator、async需要与 promise对象搭配处理异步情况
async 实质是 Generator的语法糖,相当于会自动执行 Generator 函数
async使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案
典型使用场景
Generator是异步解决的一种方案,最大特点则是将异步操作同步化表达出来
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
var loader = loadUI();
// 加载UI
loader.next()
//卸载UI
loader.next()包括redux-saga 中间件也充分利用了 Generator 特性
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import Api from '...
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
function* mySaga() {
yield takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
function* mySaga() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
export default mySaga;还能利用 Generator 函数,在对象上实现 Iterator 接口
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield[key, obj[key]];
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7class soldier{2}
8. 你是怎么理解ES6中 Decorator的?使用场景?
Decorator 的概念与价值
Decorator,即装饰器,从名字上很容易让我们联想到装饰者模式
简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论。
ES6中 Decorator功能亦如此,其本质也不是什么高大上的结构,就是一个普通的函数,用于扩展类属性和类方法
这里定义一个士兵,这时候他什么装备都没有
定义一个得到AK装备的函数,即装饰器
function strong(target){
target.AK = true
}使用该装饰器对士兵进行增强
1@strong
class soldier{3}
这时候士兵就有武器了
1 soldier.AK // true
上述代码虽然简单,但也能够清晰看到了使用Decorator两大优点:
代码可读性变强了,装饰器命名相当于一个注释
- 在不改变原有代码情况下,对原来功能进行扩展
类与类成员装饰器
Docorator修饰对象为下面两种:
- 类的装饰
类属性的装饰
类的装饰
当对类本身进行装饰的时候,能够接受一个参数,即类本身
将装饰器行为进行分解,大家能够有个更深入的了解
@decorator
class A {}
//等同于
class A {}
A = decorator(A) II A;下面 @testable就是一个装饰器,target就是传入的类,即MyTestableClass,实现了为类添加静态属性
@testable
class MyTestableClass {
//..
}
function testable(target) {
target.isTestable = true;
MyTestableClass.isTestable //true如果想要传递参数,可以在装饰器外层再封装一层函数
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false类属性的装饰
当对类属性进行装饰的时候,能够接受三个参数:
- 类的原型对象
需要装饰的属性名
装饰属性名的描述对象
首先定义一个 readonly装饰器
function readonly(target, name, descriptor){
descriptor.writable = false; // 将可写属性设为false
return descriptor;
}使用 readonly 装饰类的 name 方法
class Person {
@readonly
name() { return \`\${this.first} \${this.last}}
}相当于以下调用
readonly(Person.prototype, 'name', descriptor);如果一个方法有多个装饰器,就像洋葱一样,先从外到内进入,再由内到外执行
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) =>console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method() {}
11 evaluated 1
1/ evaluated 2
// executed 2
// executed 1外层装饰器@dec(1)先进入,但是内层装饰器@dec(2)先执行
使用限制
装饰器不能用于修饰函数,因为函数存在变量声明情况
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}编译阶段,变成下面
var counter;
var add;
@add
function foo() {
}
counter = 0;
add = function ( ) {
counter++;
};意图是执行后 counter 等于1,但是实际上结果是counter等于0
典型使用场景
基于Decorator强大的作用,我们能够完成各种场景的需求,下面简单列举几种:
使用react-redux的时候,如果写成下面这种形式,既不雅观也很麻烦
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponen
t);通过装饰器就变得简洁多了
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}将mixins,也可以写成装饰器,让使用更为简洁了
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
//使用
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() //"foo"9. 你是怎么理解ES6新增Set、Map两种数据结构的?
如果要用一句来描述,我们可以说
const s = new Set();· 集合
是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
- 字典
是一些元素的集合。每个元素有一个称作key的域,不同元素的key各不相同
区别?
共同点:集合、字典都可以存储不重复的值
不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储
Set 的特性与用法
Set 是 es6 新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合
Set本身是一个构造函数,用来生成 Set 数据结构
增删改查
Set的实例关于增删改查的方法:
-
add()
-
delete()
-
has()
-
clear()
add()
添加某个值,返回 Set 结构本身
当添加实例中已经存在的元素,set不会进行处理添加
delete()
删除某个值,返回一个布尔值,表示删除是否成功
s.delete(1)has()
返回一个布尔值,判断该值是否为 Set的成员
s.has(2)clear()
清除所有成员,没有返回值
1 s.clear()
遍历
Set 实例遍历的方法有如下:
关于遍历的方法,有如下:
keys():返回键名的遍历器
- values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
Set的遍历顺序就是插入顺序
keys方法、values方法、entries方法返回的都是遍历器对象
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue","blue"]forEach()用于对每个成员执行某种操作,没有返回值,键值、键名都相等,同样的 forEach方法有第二个参数,用于绑定处理函数的 this
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key +': ' + value))
//1:1
//4: 4
11 9 9扩展运算符和 Set 结构相结合实现数组或字符串去重
Map类型是键值对的有序列表,而键和值都可以是任意类型 Map本身是一个构造函数,用来生成 Map 数据结构 1 const m = new Map()
//数组
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)]; // [3, 5, 2]
//字符串
let str = "352255";
let unique = [...new Set(str)].join(""); // "352"实现并集、交集、和差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
//并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
//交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}Map 的特性与用法
Map 的增删改查
Map 结构的实例针对增删改查有以下属性和操作方法:
-
size 属性
-
set()
-
get()
-
has()
-
delete()
-
clear()
size
size属性返回 Map 结构的成员总数。
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2set()
设置键名key对应的键值为value,然后返回整个Map结构
如果key已经有值,则键值会被更新,否则就新生成该键
同时返回的是当前Map对象,可采用链式写法
const m = new Map();
m.set('edition', 6) //键是字符串
m.set(262,'standard') //键是数值
m.set(undefined, 'nah') // 键是 undefined
m.set(1,'a').set(2,b').set(3,'c') // 链式操作get()
get 方法读取key对应的键值,如果找不到 key,返回 undefined
const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello) // Hello ES6!Map.prototype.has()
has方法返回一个布尔值,表示某个键是否在当前Map对象之中
const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // trueMap.prototype.delete()
delete方法删除某个键,返回true。如果删除失败,返回 false
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) //falseMap.prototype.clear()
clear方法清除所有成员,没有返回值
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0Map 的遍历
Map 结构原生提供三个遍历器生成函数和一个遍历方法:
keys():返回键名的遍历器
- values():返回键值的遍历器
entries():返回所有成员的遍历器
- forEach():遍历 Map 的所有成员
遍历顺序就是插入顺序
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
//"F"
// "Tⁿ
for (let value of map.values()) {
console.log(value);
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
// "F" "no"
// "T" "yes"
//或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
11 "F" "no"
11 "yes"
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});WeakSet 与 WeakMap
WeakSet
创建WeakSet 实例
const ws = new WeakSet();WeakSet可以接受一个具有Iterable接口的对象作为参数
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}在API中WeakSet与 Set 有两个区别:
没有遍历操作的 API
- 没有 size 属性
WeakSet只能成员只能是引用类型,而不能是其他类型的值
let ws=new WeakSet();
// 成员不是引用类型
let weakSet=newWeakSet([2,3]);
console.log(weakSet) // 报错
//成员为引用类型
let obj1={name:1}
let obj2={name:1}
let ws=newWeakSet([obj1,obj2]);
console.log(ws) //WeakSet {{..}, {..}}WeakSet里面的引用只要在外部消失,它在 WeakSet里面的引用就会自动消失
WeakMap
WeakMap结构与Map结构类似,也是用于生成键值对的集合
在 API中 WeakMap与 Map 有两个区别:
没有遍历操作的 API
- 没有 clear 清空方法
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2
// WeakMap 也可以接受一个数组
//作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map keyWeakMap的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
举个场景例子:
在网页的 DOM 元素上添加数据,就可以使用 WeakMap 结构,当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用
下面代码中,键值 obj会在WeakMap产生新的引用,当你修改obj不会影响到内部
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}10. 你是怎么理解ES6中Proxy的?使用场景?
Proxy 核心概念
定义:用于定义基本操作的自定义行为
本质:修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程(meta programming)
元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作
一段代码来理解
#!/bin/bash
# metaprogram
echo '#!/bin/bash'>program
for ((I=1; I<=1024; I++)) do
echo "echo $I" >>program
done
chmod +x program这段程序每执行一次能帮我们生成一个名为program的文件,文件内容为1024行echo,如果我们手动来写1024行代码,效率显然低效
- 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译
Proxy 亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
创建代理与 handler
Proxy 为 构造函数,用来生成 Proxy 实例
var proxy = new Proxy(target, handler)参数
target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理))
handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p 的行为
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.keys(proxy)、for...in等循环,返回—个数组
-
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy,propKey),返回属性的描述对象
-
defineProperty(target, propKey,propDesc):拦截 Object.defineProperty(proxy, propKey,propDesc),返回一个布尔值
-
preventExtensions(target):拦截Object.preventExtensions(proxy),返回—个布尔值
-
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回—个对象
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
-
setPrototypeOf(target,proto):拦截Object.setPrototypeOf(proxy,proto),返回—个布尔值
-
apply(target, object, args):拦截Proxy 实例作为函数调用的操作
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作
Reflect
若需要在 Proxy 内部调用对象的默认行为,建议使用 Reflect,其是 ES6 中操作对象而提供的新API
基本特点:
只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
- 修改某些Object 方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回 false)
让Object操作都变成函数行为
下面我们介绍proxy 几种用法:
常用拦截器
get()
get接受三个参数,依次为目标对象、属性名和proxy 实例本身,最后一个参数可选
var person = {
name:"张三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
return Reflect.get(target,propKey)
}
});
proxy.name // "张三"get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr =createArray('a', 'b', 'c');
arr[-1] // c注意:如果一个属性不可配置(configurable)且不可写(writable),则Proxy不能修改该属性,否则会报错
const target = Object.defineProperties({}, {
fo0: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failedset()
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和Proxy 实例本身
假定 Person对象有一个 age属性,该属性应该是一个不大于200的整数,那么可以使用 Proxy 保证 age的属性值符合要求
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable:false,
});
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = 'baz';
}
};
const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"注意,严格模式下,set代理如果没有返回true,就会报错
'use strict';
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
//无论有没有下面这一行,都会报错
return false;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'deleteProperty()
deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete 命令删除
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
Reflect.deleteProperty(target,key)
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
thrownew Error(`无法删除私有属性`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy·_prop
// Error:无法删除私有属性注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错
取消代理
Proxy.revocable(target, handler);典型使用场景
Proxy 其功能非常类似于设计模式中的代理模式,常用功能如下:
拦截和监视外部对对象的访问
降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
使用 Proxy 保障数据类型的准确性
let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
set(target, key, value, proxy) {
if (typeof value !== 'number') {
throw Error("属性只能是number类型");
}
return Reflect.set(target, key, value, proxy);
}
});
numericDataStore.count ="foo"
// Error:属性只能是number类型
numericDataStore.count =333
//赋值成功声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey
let api = {
_apiKey:'123abc456def',
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
get(target, key, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可访问.`);
} return Reflect.get(target, key, proxy);
},
set(target, key, value, proxy) {
if(RESTRICTED.indexOf(key) > -1) {
throw Error(`${key} 不可修改`);
} return Reflect.get(target, key, value, proxy);
}
});
console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误还能通过使用 Proxy 实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行
observable 函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}观察者函数都放进 Set 集合,当修改obj的值,在会 set函数中拦截,自动执行 Set所有的观察者