Promise对象

在使用 JavaScript 时,有时会遇到“一件事做完之后,再做另一件事”的处理要求。之前我们都是通过回调函数来完成,在 ES6 中我们新增了 Promise 对象来处理这些情况。

目前,Chrome 32以上、Edge、Firefox 29以上、Opera 19以上以及Safari 8以上版本的浏览器支持 Promise 对象。

基本概念

Promise 是一种抽象异步处理对象,其核心概念为“确保在一件事做完之后,再做另一件事”。

创建Promise对象

Promise 类的构造函数中使用一个参数,参数值为一个回调函数。该回调函数又接收两个函数作为参数,当执行成功时则调用 resolve 函数,当执行失败时则调用 reject 函数。

  1. var p = new Promise((resolve, reject)=> {
  2. // 执行一些异步操作
  3. if(/*执行正常*/) {
  4. resolve(/*正常的结果*/)
  5. } else {
  6. reject(Error('处理失败'));
  7. }
  8. });

在调用 reject 时使用 Error 对象只是一个惯例,并不是必须。使用 Error 对象的好处在于它可以捕捉到一个错误堆栈,从而使调试工具变得更加有用。

then处理成功返回

promise 对象有一个 then 方法,接收两个回调函数作为参数,第一个回调函数用于对 Promise 函数中执行成功调用后(即resolve调用)的结果进行处理,第二个回调函数执行错误(即reject调用)的结果进行处理。

  1. // 这两个函数都是可选的
  2. p.then((result) => {
  3. /*执行成功的处理*/
  4. }, (err) => {
  5. /*执行失败的处理*/
  6. });

同时,Promise 对象还可以执行 then 函数的链式调用。如果第一个 then 方法返回的是一个值,下一个 then 方法将被立即调用,并使用该返回值;如果第一个 then 方法返回的是一个 Promise 对象,下一个 then 将进入等待,直到该 Promise 对象的回调函数中返回一个结果以后才被调用。

  1. p.then((result) => {
  2. }, (err) => {
  3. /*执行失败的处理*/
  4. });
  5. p.then((result) => {
  6. }).catch((err) => {
  7. /*执行失败的处理*/
  8. });

catch处理错误返回

在 then 中我们说过,可以通过 then 的第二个参数来处理执行失败时调用。同样,也可以使用 catch 机制来捕捉 Promise 构造函数参数值回调函数中抛出的异常。

  1. p.then((result) => {
  2. return new Promise((resolve,reject)=> {
  3. resolve(/*正常的结果*/)
  4. });
  5. }).then((result2)=> { // 等待Promise处理后的结果,再调用then函数
  6. console.log(result2);
  7. });

finally处理返回

自 ECMAScript 2017 开始,可以使用 finally 方法指定无论 Promise 对象返回肯定结果或否定结果都需要执行的处理。

  1. p.then((result) => {
  2. console.log(result);
  3. }).catch((err)=> {
  4. console.log(err);
  5. }).finally(()=>{
  6. /*不管执行成功还是失败,finally都会被调用*/
  7. });

并行处理结果

浏览器很擅长同时执行多个异步处理,如果我们一个一个地执行这些异步函数则会丧失性能。Promise 类为我们提供了 all 方法来并行执行多个异步处理。

Promise.all 方法以一个 Promise 对象数组作为参数并创建一个当所有执行结果都已成功时返回肯定结果的Promise 对象。

  1. var getData = function (file) {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. resolve(file);
  5. }, 1000);
  6. });
  7. };
  8. Promise.all([getData("file1.txt"), getData("file2.txt"), getData("file3.txt")]).then(
  9. responses => {
  10. responses.forEach(res => { // responses依次存放着三个getData的返回的肯定结果
  11. console.log(res);
  12. });
  13. },
  14. () => {
  15. console.log("执行读取失败");
  16. }
  17. );

ECMAScript 2015 中还提供一个 Promise.race 方法,当数组中任何元素返回肯定结果时 Promise.race 方法立即返回肯定结果,或者当任何元素返回否定结果时立即返回否定结果。

async和await函数

在 ECMAScript 2016 中,新增了 async 关键字与 await 关键字,这两个关键字均用来实现异步处理,如果使用这两个关键字,可以书写比 Promise 更为简洁的异步处理代码。通常将这两个方法配合一起使用。await 关键字用于在 async 函数内部强制等待 Promise 返回结果(暂停其他处理)

目前,Chrome 55以上、Edge、Firefox 52以上、Opera 42以上以及Safari 10以上版本的浏览器支持 async 和 await 函数。

  1. var getData = function (file) {
  2. return new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. resolve(file);
  5. }, 1000);
  6. });
  7. };
  8. async function read() {
  9. var result = await getData("file1.txt");
  10. console.log(result);
  11. }
  12. read();

全局唯一标识符:symbol

symbol的概念

在 ECMAScript 2015 中,新增一种用于表示唯一值的简单原始数据类型:symbol

在对象中使用 symbol 作为属性名并保存其属性值后,在使用 for…in 循环遍历时不能列举出这些属性值。另外 symbol 只在其被创建时的作用域中有效。

创建及使用symbol

在 ECMAScript 2015 中,可以通过调用 Symbol() 方法得到一个 symbol,即一个不等于其他任何值的值,同时可以在 Symbol("描述符") 来添加描述符。

  1. let person = { firstName: "tony", lastName: "swift" };
  2. let age = Symbol("年龄");
  3. person[age] = 30;
  4. let area = Symbol("地区");
  5. person[area] = "江苏省";
  6. console.log(person);
  7. console.log(person[age]);
  8. console.log(person[area]);

在这个示例中,“年龄”、“地区”字符串被称为描述符,age 和 area 被称作一个以 Symbol 为属性名的属性。
像一个数组元素一样,不能使用“.”符号来访问以 symbol 为属性名的属性,必须使用方括号来进行访问。

通过 Symbol 创建的属性名是无法通过 Object.keys(obj)Object.getOwnPropertyNames(obj) 来遍历获取的。但可以通过 ECMAScript 2015 中新增的 Object.getOwnPropertySymbols(obj) 方法来列举一个对象中的所有 symbol 属性名。

注意事项

  • symbol 与其他简单类型不一样。一经创建,它是不可被修改的。
  • symbol 不能被自动转换为字符串。将 symbol 当作字符串使用将会引发类型错误。你可以显式地使用 String(sym) 或 sym.toString() 将 symbol 转换为字符串以避免类型错误。

获取symbol的三种方法

可以通过以下三种方法取得一个 symbol:

  • 调用 Symbol 方法:每次调用 Symbol 方法将会返回一个唯一的 symbol;
  • 调用 Symbol.for(string) 方法。调用 Symbol.for 方法会先在 symbol 注册表中查找是否有以该参数作为名称的 symbol,如果有直接返回,否则就创建并返回一个以该参数作为名称的symbol。Symbol.keyFor 方法返回一个已登记的 symbol 的名称,用于检测该 symbol 是否已被创建。
  • 使用标准中定义的 Symbol.iterator 属性。
    1. let key1 = Symbol.for("名称");
    2. let name = Symbol.keyFor(key1);
    3. console.log(key1); // Symbol对象
    4. console.log(name); // 名称
    image.png

Symbol.iterator属性

该属性为对象定义一个用于创建对象迭代器的方法,可以使用 for…of 循环遍历这个迭代器。

  1. var string = new String("这是个苹果");
  2. string[Symbol.iterator] = function () {
  3. let s = this;
  4. let length = s.length;
  5. return {
  6. next: function () {
  7. if (length > 0) {
  8. length--;
  9. return { done: false, value: s[length] };
  10. } else {
  11. return { done: true };
  12. }
  13. },
  14. };
  15. };
  16. var result = "";
  17. for (const item of string) {
  18. result += item;
  19. }
  20. console.log(result);

在该示例中,首先创建一个 String 对象,然后设定其 Symbol.iterator 属性值。为了可以使它能用 for…of 循环来进行遍历,这里我们返回一个带有 next 方法的对象,在 for…of 中每次遍历返回一个值,最终得出一个翻转的结果。

代理与反射

在 ECMAScript 2015 标准中,为对象定义了14个内置方法,如下表所示(双括号[[]]标识方法为内置方法,你不能在 JavaScript 代码中调用、删除或重载对象的这些方法)。
新增对象及数据类型 - 图2

代理

在 ECMAScript 2015 中定义了一种新的全局构造器:代理(Proxy)。它使用两个参数,一个目标对象、一个处理器对象。

var target={}, handler = {};
var proxy = new Proxy(target, handler);

在 ECMAScript 2015 中,所有代理的内部方法都是被提交给目标的。也就是说,如果调用 proxy.[Enumerate] 方法,将返回 target.[Enumerate] 方法。例如,下述代码将调用 proxy.[Set]

proxy.color = pink;

proxy.[Set] 方法将内部调用 target.[Set] 方法。

代理处理器

处理器对象的方法可以重载一个代理的内部方法。
例如,如果你想拦截对于一个对象属性值的设置,可以通过定义处理器的 set 方法来达到这个目的,代码如下所示:

var target = {};
var handler = {
    set: function (target, key, value, receiver) {
        throw new Error("对象的属性值不能被设置");
    },
};
var proxy = new Proxy(target, handler);
proxy.name = "张三";

处理器的所有方法都与对象的内置方法一一对应,前述内置方法的对应处理器方法如表所示。
image.png

代理使用示例

下面我们编写一个示例,创建一个只读对象。

function NOPE() {
    throw new Error("不能修改只读对象");
}
var handler = {
    // 重载所有用于修改对象的方法
    set: NOPE,
    defineProperty: NOPE,
    deleteProperty: NOPE,
    preventExtensions: NOPE,
    setPrototypeOf: NOPE,
};

function readOnlyObj(target) {
    return new Proxy(target, handler);
}

var newMath = readOnlyObj(Math);
console.log(newMath.min(54, 40)); // 40
newMath.max = newMath.min; // 不能修改只读对象
delete newMath.max; //不能修改只读对象

反射

ECMAScript 2015 中新增了反射对象,该反射对象中内置了所有与处理器对象方法一一对应的方法,如下表所示。
image.png

处理器对象的方法可以重载一个代理的内部方法,在重载完毕之后,需要使用反射对象来获取重载之后的方法,代码如下所示。

var target = {};
var handler = {
    set: function (target, key, value, receiver) {
        console.log(`set ${key}`);
        return Reflect.set(target, key, value, receiver);
    },
};
var proxy = new Proxy(target, handler);
proxy.name = "test";

目前,Chrome 49以上、Edge、Firefox 18以上、Opera 36以上以及Safari 10 以上版本的浏览器支持代理及反射。

新增的集合对象

ECMAScript 2015 中的新增集合:Set、Map、WeakSet与WeakMap。

集合的基本概念

JavaScript 本身就有一个类似于哈希表的东西:对象。一个对象,就是一个由键名/键值对所组成的集合。对象中存在一些缺陷:

  • 开发者们必须小心避免脚本程序将对象的方法解析为对象的数据;
  • 属性的键名必须为字符串或ECMAScript 2015中新增的 symbol,不能为对象;
  • 不能直接得知一个对象具有多少属性;

因为 ECMAScript 2015 是被设计用来避免对象的方法与数据之间的冲突的,所以 ECMAScript 2015 中新增的集合不将它们携带的数据保存为属性,这意味着不能使用 obj.key 或 obj[key] 的形式来访问集合中的数据,必须以其他形式(例如使用Map对象的get方法)来访问。

目前,Chrome 38以上、IE 11、Edge、Firefox 13以上、Opera25以上以及Safari 7.1以上版本的浏览器支持Set对象及Map对象。

set对象

Set 对象是一些元素的集合。set 对象类似于数组,但与数组不同,一个 Set 中不会保存重复的元素。
Set 对象具有一个 size 属性,属性值为Set对象中保存的元素的数量。

Set 对象有如下方法。

  • add(value):Set 对象尾部添加一个元素,返回 Set 对象;
  • clear():移除 Set 对象内所有元素;
  • delete(value):移除 Set 对象中的指定元素。移除成功返回true,失败返回false;
  • entries():返回一个新的迭代器对象,包含 Set 对象中每个元素的键名/键值对;
  • keys():返回一个新的迭代器对象,包含 Set 对象中每个元素的键名;
  • values():返回一个新的迭代器对象,包含 Set 对象中每个元素的键值;
  • forEach(callbackFn[, thisArg]):按照插入顺序,为每一个 Set 元素调用 callbackFn 函数。thisArg 参数代表调用 callbackFn 回调中使用的 this 对象。
  • has(value):返回指定值是否存在于 Set 对象中;

下面是一个示例。

let set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set.size);
set.delete(3);
console.log(set.size);

数组与 Set 之间的转换。

// Set对象转数组对象
let set = new Set();
set.add(1);
set.add(2);
let myArr = Array.from(mySet);

// 数组对象转Set对象
let mySet = new Set([1,2,3]);

Map对象

Map 对象是由一些键名/键值对组成的集合。Map 对象具有一个 size 属性,属性值为 Map 对象中保存的键名/键值对的数量。

Map 对象具有如下的方法:

  • get(key):返回给定键名的键值,该键名不存在于 map 对象中时返回 undefined;
  • set(key,value):在 Map 对象中添加一个键名 key 并设置键值为 value,如果该键名已存在 Map 对象中则修改其键值为 value;
  • clear():移除 Map 对象中保存的所有键名/键值对;
  • has(key):返回指定值是否存在于 Map 对象中;
  • delete(key):从 Map 对象中删除指定的键名/键值对;
  • entries():返回一个新的迭代器对象,迭代器对象中包含 Map 对象中保存的所有键名/键值对;
  • keys():返回一个新的迭代器对象,迭代器对象中包含 Map 对象中保存的所有键名;
  • values():返回一个新的迭代器对象,迭代器对象中包含 Map 对象中保存的所有键值;
  • forEach(callbackFn[,thisArg]):按照插入顺序,为 Map 对象中的每一个键名/键值对调用一次callBackFn 回调函数。如果指定了 thisArg 参数,thisArg 参数代表 callBackFn 回调中使用的 this 对象。

Map 对象的示例如下。

let map = new Map();
let key = "字符串键名";
let object = {};
map.set(key, "字符串键值");
map.set(object, 100);
console.log(map.get(key));
console.log(map.get(object));

WeakSet对象与WeakMap对象

Set 集合与 Map 集合有一个缺点,它们对其中的数据维持着强引用。例如当一个 DOM 元素已被从页面中移除并被浏览器废弃时,内存回收机制不会自动回收 DOM 元素所占内存,除非它被从 Set 对象与 Map 对象中移除,这意味着脚本代码将有可能引起内存泄露。

为此,ECMAScript 2015 提供 WeakSet 集合与 WeakMap 集合以避免引起内存泄露。 WeakSet、WeakMap 与 Set、Map 类似,但有一些限制:

  • 只能对 WeakSet 集合使用 new、has、add 以及 delete 方法;
  • 只能对 WeakMap 集合使用 new、has、get、set 以及 delete 方法;
  • 存储在 WeakSet 集合中的数据必须为对象,存储在 WeakMap 集合中的键名必须为对象;
  • 请注意不能通过遍历的方法从 WeakSet 集合与 WeakMap 集合中取出所有对象,只能通过使用 WeakSet 集合的 has 方法判断一个对象是否存在于集合中,只能通过使用 WeakMap 集合的 get 方法根据给定键名获取键值。

下面是一个 WeakSet 的例子。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div{
      width:200px;
      height: 200px;
      float: left;
    }
    #divA{
      background-color: red;
    }
    #divB{
      background-color: pink;
    }
    #divC{
      background-color: yellow;
    }
  </style>
</head>
<body>
  <div id="divA">第一个元素</div>
  <div id="divB">第二个元素</div>
  <div id="divC">第三个元素</div>
  <input type="button" value="移除元素" onclick="removeElem()">
</body>
</html>
<script>
  var set = new WeakSet();
  document.querySelectorAll('div').forEach(Elem=> {
    set.add(Elem);
  });
  function removeElem() {
    console.log(set); // WeakSet集合中存在divA元素
    console.log(set.has(document.getElementById('divA')));

    // 移除元素A
    document.body.removeChild(document.getElementById('divA'))

    setTimeout(function() {
      console.log(set);
      console.log(set.has(document.getElementById('divA')));
    }, 10*1000);
  }
</script>

效果如下图所示。这个例子很多的说明了 当DOM元素不存在时,将在浏览器执行垃圾回收后从 WeakMap 集合中自动被删除。
image.png
image.png

to be continue…