一、Symbol
- 为啥需要Symbol?
ES5里面对象的属性名都是字符串,如果你需要使用一个别人提供的对象,你对这个对象有哪些属性也不是很清楚,但又想为这个对象新增一些属性,那么你新增的属性名就很可能和原来的属性名发送冲突,显然我们是不希望这种情况发生的。所以,我们需要确保每个属性名都是独一无二的,这样就可以防止属性名的冲突了。因此,ES6里就引入了Symbol,用它来产生一个独一无二的值。 - Symbol是什么?
Symbol实际上是ES6引入的一种原始数据类型,除了Symbol,JavaScript还有其他6种数据类型,分别是Undefined、Null、Boolean、String、Number、对象,这6种数据类型都是ES5中就有的。 - 怎么生成一个Symbol类型的值?
Symbol值是通过Symbol函数生成的。
let s = Symbol();
console.log(s); // Symbol()
typeof s; // "symbol"
- Symbol函数前不能用new。
Symbol函数不是一个构造函数,前面不能用new操作符。所以Symbol类型的值也不是一个对象,不能添加任何属性,它只是一个类似于字符型的数据类型。如果强行在Symbol函数前加上new操作符,会报错,如下:
let s = new Symbol();
// Uncaught TypeError: Symbol is not a constructor(…)
- Symbol函数的参数。
- 字符串作为参数。
用上面的方法生成的Symbol值不好进行区分,Symbol函数还可以接受一个字符串参数,来对产生的Symbol值进行描述,方便我们区分不同的Symbol值。
let s1 = Symbol('s1');
let s2 = Symbol('s2');
console.log(s1); // Symbol(s1)
console.log(s2); // Symbol(s2)
s1 === s2; // false
let s3 = Symbol('s2');
s2 === s3; // false
给Symbol函数加了参数之后,控制台输出的时候可以区分到底是哪一个值。Symbol函数的参数只是对当前Symbol值的描述,因此相同参数的Symbol函数返回值是不相等的。
- 对象作为参数。
如果Symbol函数的参数是一个对象,就会调用该对象的toString方法,将其转化为一个字符串,然后才生成一个Symbol值。所以,说到底,Symbol函数的参数只能是字符串。 - Symbol值不可以进行运算。
既然Symbol是一种数据类型,那我们一定想知道Symbol值是否能进行运算。告诉你,Symbol值是不能进行运算的,不仅不能和Symbol值进行运算,也不能和其他类型的值进行运算,否则会报错。
Symbol值可以显式转化为字符串和布尔值,但是不能转为数值。
var mysym1 = Symbol('my symbol');
mysym1.toString() // 'Symbol('my symbol')'
String(mysym1) // 'Symbol('my symbol')'
var mysym2 = Symbol();
Boolean(mysym2); // true
Number(mysym2) // TypeError: Cannot convert a Symbol value to a number(…)
- Symbol作属性名。
Symbol就是为对象的属性名而生,那么Symbol值怎么作为对象的属性名呢?有下面几种写法:
let a = {};
let s4 = Symbol();
// 第一种写法
a[s4] = 'mySymbol';
// 第二种写法
a = {
[s4]: 'mySymbol'
}
// 第三种写法
Object.defineProperty(a, s4, {value: 'mySymbol1'});
a.s4; // undefined
a.s4 = 'mySymbol2';
a[s4] // mySymbol1
a['s4'] // 'mySymbol2'
使用对象的Symbol值作为属性名时,获取相应的属性值不能用点运算符;
如果用点运算符来给对象的属性赋Symbol类型的值,实际上属性名会变成一个字符串,而不是一个Symbol值;
在对象内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中,否则只是一个字符串。
- Symbol值作为属性名的遍历。
使用for...in和for...of都无法遍历到Symbol值的属性,Symbol值作为对象的属性名,也无法通过Object.keys()、Object.getOwnPropertyNames()来获取了。但是,不同担心,这种平常的需求肯定是会有解决办法的。我们可以使用Object.getOwnPropertySymbols()方法获取一个对象上的Symbol属性名。也可以使用Reflect.ownKeys()返回所有类型的属性名,包括常规属性名和 Symbol属性名。
let s5 = Symbol('s5');
let s6 = Symbol('s6');
let a = {
[s5]: 's5',
[s6]: 's6'
}
Object.getOwnPropertySymbols(a); // [Symbol(s5), Symbol(s6)]
a.hello = 'hello';
Reflect.ownKeys(a); // ["hello", Symbol(s5), Symbol(s6)]
利用Symbol值作为对象属性的名称时,不会被常规方法遍历到这一特性,可以为对象定义一些非私有的但是又希望只有内部可用的方法。
- Symbol.for()和Symbol.keyFor()。
Symbol.for()函数也可以用来生成Symbol值,但该函数有一个特殊的用处,就是可以重复使用一个Symbol值。
let s1 = Symbol.for("s11");
let s2 = Symbol.for("s22");
console.log(s1===s2)//false
let s3 = Symbol("s33");
let s4 = Symbol("s33");
console.log(s3===s4)//false
console.log(Symbol.keyFor(s3))//undefined
console.log(Symbol.keyFor(s2))//"s22"
console.log(Symbol.keyFor(s1))//"s11"
Symbol.for()
函数要接受一个字符串作为参数,先搜索有没有以该参数作为名称的Symbol值,如果有,就直接返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
Symbol.keyFor()
函数是用来查找一个Symbol值的登记信息的,Symbol()写法没有登记机制,所以返回undefined;而Symbol.for()函数会将生成的Symbol值登记在全局环境中,所以Symbol.keyFor()函数可以查找到用Symbol.for()函数生成的Symbol值。
- 内置Symbol值。
ES6提供了11个内置的Symbol值,分别是Symbol.hasInstance 、Symbol.isConcatSpreadable 、Symbol.species 、Symbol.match 、Symbol.replace 、Symbol.search 、Symbol.split 、Symbol.iterator 、Symbol.toPrimitive 、Symbol.toStringTag 、Symbol.unscopables 等。
二、Set和Map数据结构
- Set
es6提供了新的数据结构Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成Set数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
//通过add方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
Set 函数可以接受一个数组作为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
function divs () {
return [...document.querySelectorAll('div')];
}
const set = new Set(divs());
set.size // 56
// 类似于
divs().forEach(div => set.add(div));
set.size // 56
利用Set数据结构的成员都是唯一的这个特性,可以轻松对数组去重。
// 去除数组的重复成员
[...new Set(array)]
向 Set 加入值的时候,不会发生类型转换,所以5
和"5"
是两个不同的值。
Set 内部判断两个值是否不同,使用的算法叫做“Same-value equality”,它类似于精确相等运算符(===
),主要的区别是NaN
等于自身,而精确相等运算符认为NaN
不等于自身。
两个对象总是不相等的。
Set 结构的实例有以下属性:
-
Set.prototype.constructor
:构造函数,默认就是Set
函数。 -
Set.prototype.size
:返回Set
实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
-
add(value)
:添加某个值,返回 Set 结构本身。 -
delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。 -
has(value)
:返回一个布尔值,表示该值是否为Set
的成员。 -
clear()
:清除所有成员,没有返回值。
Array.from()可以将 Set 结构转为数组。
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
Set 结构的实例有四个遍历方法,可以用于遍历成员。
-
keys()
:返回键名的遍历器 -
values()
:返回键值的遍历器 -
entries()
:返回键值对的遍历器 -
forEach()
:使用回调函数遍历每个成员
keys
方法、values
方法、entries
方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致。
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"]
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。
Set 结构的实例与数组一样,也拥有forEach
方法,用于对每个成员执行某种操作,没有返回值。
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
forEach
方法还可以有第二个参数,表示绑定处理函数内部的this
对象。
- WeakSet
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:
- WeakSet 的成员只能是对象,而不能是其他类型的值。
- WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 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(…)
WeakSet 结构有以下三个方法。
-
WeakSet.prototype.add(value)
:向 WeakSet 实例添加一个新成员。 -
WeakSet.prototype.delete(value)
:清除 WeakSet 实例的指定成员。 -
WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
WeakSet 没有size
属性,没有办法遍历它的成员。
- Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
只有对同一个对象的引用,Map 结构才将其视为同一个键。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然NaN
不严格相等于自身,但 Map 将其视为同一个键。
实例的属性和操作方法
- size属性 返回成员总数
- set(key,value) 设置键值对,返回Map结构
- get(key) 读取key对应的值,找不到就是undefined
- has(key) 返回布尔值,表示key是否在Map中
- delete(key) 删除某个键,返回true,失败返回false
- clear() 清空所有成员,没有返回值
Map 结构原生提供三个遍历器生成函数和一个遍历方法。 -
keys()
:返回键名的遍历器。 -
values()
:返回键值的遍历器。 -
entries()
:返回所有成员的遍历器。 -
forEach()
:遍历 Map 的所有成员。
Map 的遍历顺序就是插入顺序。遍历行为基本与set的一致。
Map可以转为数组。
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap] //[[true, 7], [{foo: 3},["abc"]]]
数组也可以转为Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
如果所有 Map 的键都是字符串,它可以转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
对象也可以转为Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
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}'
另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
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。这往往是数组转为 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
结构与Map
结构类似,也是用于生成键值对的集合。
WeakMap
与Map
的区别有两点:
-
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。 -
WeakMap
的键名所指向的对象,不计入垃圾回收机制。
WeakMap
的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
//e1和e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arr对e1和e2的引用。
//一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2占用的内存。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
无法被遍历,因为没有size。无法被清空,因为没有clear(),跟WeakSet相似。
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
上面代码中,myElement
是一个 DOM 节点,每当发生click
事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement
。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
三、Proxy
- Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
//上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
let proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
如果handler
没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
上面代码中,handler
是一个空对象,没有任何拦截效果,访问proxy
就等同于访问target
。
同一个拦截器函数,可以设置拦截多个操作。
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
下面是 Proxy 支持的拦截操作一览,一共 13 种:
- get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 - set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 - has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。 - deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 - defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - 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 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
apply
方法拦截函数的调用、call
和apply
操作。
get
方法用于拦截某个属性的读取操作。
let obj2 = new Proxy(obj,{
get(target,property,a){
//return 35;
/*console.log(target)
console.log(property)*/
let Num = ++wkMap.get(obj).getPropertyNum;
console.log(`当前访问对象属性次数为:${Num}`)
return target[property]
},
deleteProperty(target,property){
return false;
},
apply(target,ctx,args){
return Reflect.apply(...[target,[],args]);;
}
})
-
Proxy.revocable
方法返回一个可取消的 Proxy 实例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable
方法返回一个对象,该对象的proxy
属性是Proxy
实例,revoke
属性是一个函数,可以取消Proxy
实例。上面代码中,当执行revoke
函数之后,再访问Proxy
实例,就会抛出一个错误。
Proxy.revocable
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
- this问题。
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this
关键字会指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
//一旦proxy代理target.m,后者内部的this就是指向proxy,而不是target。
四、Reflect
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。
设计目的:
- 将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。 - 修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。 - 让
Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。 -
Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
五、Promise
- 概念。
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。 - 特点。
- 对象的状态不受外界影响。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
- 状态。
Promise
对象代表一个异步操作,有三种状态:
pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。 - 缺点。
- 无法取消
Promise
,一旦新建它就会立即执行,无法中途取消。 - 如果不设置回调函数,
Promise
内部抛出的错误,不会反应到外部。 - 当处于
pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
- 用法。
let p = new Promise((resolve,reject)=>{
//一些异步操作
setTimeout(()=>{
console.log("123")
resolve("abc");
},0)
})
.then(function(data){
//resolve状态
console.log(data)
},function(err){
//reject状态
})
//'123'
//'abc'
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
也就是说,状态由实例化时的参数(函数)执行来决定的,根据不同的状态,看看需要走then的第一个参数还是第二个。
resolve()和reject()的参数会传递到对应的回调函数的data或err。
- 链式操作的用法。
从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
//异步任务1执行完成
//随便什么数据1
//异步任务2执行完成
//随便什么数据2
//异步任务3执行完成
//随便什么数据3
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步任务1执行完成');
resolve('随便什么数据1');
}, 1000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步任务2执行完成');
resolve('随便什么数据2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('异步任务3执行完成');
resolve('随便什么数据3');
}, 2000);
});
return p;
}
在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中也可以接收到数据:
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return '直接返回数据'; //这里直接返回数据
})
.then(function(data){
console.log(data);
});
//异步任务1执行完成
//随便什么数据1
//异步任务2执行完成
//随便什么数据2
//直接返回数据
- reject的用法。
前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。
let num = 10;
let p1 = function() {
return new Promise((resolve,reject)=>{
if (num <= 5) {
resolve("<=5,走resolce")
console.log('resolce不能结束Promise')
}else{
reject(">5,走reject")
console.log('reject不能结束Promise')
}
})
}
p1()
.then(function(data){
console.log(data)
},function(err){
console.log(err)
})
//reject不能结束Promise
//>5,走reject
resolve和reject永远会在当前环境的最后执行,所以后面的同步代码会先执行。
如果resolve和reject之后还有代码需要执行,最好放在then里。
然后在resolve和reject前面写上return。
-
Promise.prototype.catch
。
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
p1()
.then(function(data){
console.log(data)
})
.catch(function(err){
console.log(err)
})
//reject不能结束Promise
//>5,走reject
-
Promise.all()
。
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
p
的状态由p1
、p2
、p3
决定,分成两种情况。
- 只有
p1
、p2
、p3
的状态都变成resolve
,p
的状态才会变成resolve
。 此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。 - 只要
p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
promises
是包含 3 个 Promise 实例的数组,只有这 3 个实例的状态都变成resolve
,或者其中有一个变为rejected
,才会调用Promise.all
方法后面的回调函数。
如果作为参数的 Promise 实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法,如果没有参数没有定义自己的catch,就会调用Promise.all()
的catch
方法。
-
Promise.race
。
Promise.race
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
-
Promise.resolve()
。
有时需要将现有对象转为 Promise 对象,Promise.resolve
方法就起到这个作用。下面代码将123转为一个 Promise 对象。
const jsPromise = Promise.resolve('123');
Promise.resolve
等价于下面的写法。
Promise.resolve('123')
// 等价于
new Promise(resolve => resolve('123'))
Promise.resolve
方法的参数分成四种情况。
- 参数是一个 Promise 实例。
如果参数是 Promise 实例,那么Promise.resolve
将不做任何修改、原封不动地返回这个实例。 - 参数是一个thenable对象。
thenable
对象指的是具有then
方法的对象,比如下面这个对象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then
方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
上面代码中,thenable
对象的then
方法执行后,对象p1
的状态就变为resolved
,从而立即执行最后那个then
方法指定的回调函数,输出 42。
- 参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then
方法的对象,则Promise.resolve
方法返回一个新的 Promise 对象,状态为resolved
。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
上面代码生成一个新的 Promise 对象的实例p
。由于字符串Hello
不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved
,所以回调函数会立即执行。Promise.resolve
方法的参数,会同时传给回调函数。
- 不带有任何参数
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的 Promise 对象。
所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve
方法。
const p = Promise.resolve();
p.then(function () {
// ...
});
上面代码的变量p
就是一个 Promise 对象。
需要注意的是,立即resolve
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代码中,setTimeout(fn, 0)
在下一轮“事件循环”开始时执行,Promise.resolve()
在本轮“事件循环”结束时执行,console.log('one')
则是立即执行,因此最先输出。
-
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 对象的实例p
,状态为rejected
,回调函数会立即执行。
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代码中,Promise.reject
方法的参数是一个thenable
对象,执行以后,后面catch
方法的参数不是reject
抛出的“出错了”这个字符串,而是thenable
对象。
六、Fetch
- 传统Ajax指的是XMLHttpRequest(XHR),现在和将来会被Fetch取代。
XMLHttpRequest是一个设计粗糙的API,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的Promise,generator/yield,async/await友好。
Fetch的出现就是为了解决XHR的问题。但是Fetch API是基于Promise设计,旧浏览器不支持Promise,需要使用polyfill es6-promise。
使用XHR发送一个json请求一般是这样:
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function(){
console.log(xhr.response);
};
xhr.onerror = function(){
console.log("Oops, error");
};
xhr.send();
使用Fetch后:
fetch(url)
.then(function(response){
return response.json();
}).then(function(data){
console.log(data);
}).catch(function(e){
console.log("Oops, error");
});
使用ES6的箭头函数后:
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
.catch(e => console.log("Oops, error", e))
使用async/await来做最终优化:
try{
let response = await fetch(url);
let data = await response.json();
console.log(data);
}catch(e){
console.log("Oops, error", e);
}
//注:这段代码如果想运行,外面需要包一个async function
- 用法
fetch(url, options).then(function(response){
//handle HTTP response
}, function(error){
//handle network error
})
url:定义要获取的资源。这可能是:
- 一个
USVString
字符串,包含要获取资源的URL。 - 一个
Request
对象。
options(可选)
一个配置项对象,包括所有对请求的设置,可选的参数有: -
method
:请求使用的方法,如GET、POST。 -
headers
:请求的头信息,形式为Headers对象或ByteString。 -
body
:请求的body信息,可能是一个Blob、BufferSource、FormData、URLSearchParams或者USVString对象。注意GET或HEAD方法的请求不能包含body信息。 -
mode
:请求的模式,如cors、no-cors或者same-origin。 -
credentials
:请求的credentials,如omit、same-origin或者include。 -
cache
:请求的cache模式:default,no-store,reload,no-cache,force-cache,或者only-if-cached。
response:
一个Promise,resolve时回传Response
对象:
属性:
- status(number)-HTTP请求结果参数,在100~599范围。
- statusText(String)-服务器返回的状态报告。
- ok(boolean)-如果返回200表示请求成功则为true。
- headers(Headers)-返回头部信息,下面详细介绍。
- url(String)-请求的地址。
方法: - text() -以string的形式生成请求text。
- json() -生成JSON.parse(responseText)的结果。
- blob() -生成一个Blob。
- arrayBuffer() -生成一个ArrayBuffer。
- formData() -生成格式化的数据,可用于其他的请求。
其他方法: - clone() -创建一个Response对象的克隆。
- Response.error() -返回一个绑定了网络错误的新的Response对象。
- Response.redirect() -用另一个url创建一个新的response。
response.headers: - has(name)(boolean) -判断是否存在该信息头。
- get(name)(String) -获取信息头的数据。
- getAll(name)(Array) -获取所有头部数据。
- set(name, value) -设置信息头的参数。
append(name, value) -添加header的内容。
delete(name) -删除header的信息。
forEach(function(value, name){...},[thisContext]) -循环读取header的信息。
get
fetch('/users.html')
.then(function(response){
return response.text();
}).then(function(body){
document.body.innerHTML = body
})
post
fetch('/users', {
method:'POST',
headers:{
'Accept':'application/json',
'Content-Type':'application/json'
},
body:JSON.stringify({
name:'Hubot',
login:'hubot',
})
})
- Fetch优点主要有:
- 语法简洁,更加语义化。
- 基于标准Promise实现,支持async/await
- Fetch的原生支持率不高,需要引入下面的polyfill后才完美支持IE8+:
- 由于IE8是ES3,需要引入ES5的polyfill:es5-shim,es5-sham
- 引入Promise的polyfill:es6-promise
- 引入fetch探测库:fetch-detector
- 引入fetch的polyfill:fetch-ie8
- 可选:如果还使用了jsonp,引入fetch-jsonp
- 可选:开启Babel的runtime模式,现在就使用async/await