目录
40.字符串查找
> 请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。
暴力解
思路及算法
我们可以让字符串 needle 与字符串 haystack 的所有长度为 m 的子串均匹配一次。
为了减少不必要的匹配,我们每次匹配失败即立刻停止当前子串的匹配,对下一个子串继续匹配。如果当前子串匹配成功,我们返回当前子串的开始位置即可。如果所有子串都匹配失败,则返回 -1。
时间复杂度:O(n×m),其中 nn 是字符串 haystack 的长度,m 是字符串 needle 的长度。最坏情况下我们需要将字符串 needle 与字符串haystack 的所有长度为 m 的子串均匹配一次。
空间复杂度:O(1)。我们只需要常数的空间保存若干变量。
var strStr = function(haystack, needle) { let m = haystack.length; let n = needle.length; for (let i = 0; i <= m - n; i++) { let j; for (j = 0; j < n; j++) { if (needle[j] !== haystack[i + j]) break; } // needle子串全都匹配了 if (j === n) return i; } // haystack中不存在needle return -1; };
KMP算法
KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。
上述的朴素解法,不考虑剪枝的话复杂度是 O(m∗n) 的,而 KMP 算法的复杂度为 O(m+n)。
KMP 之所以能够在O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。
- 匹配过程
在模拟 KMP 匹配过程之前,我们先建立两个概念:
前缀:对于字符串 abcxxxxefg,我们称 abc 属于 abcxxxxefg 的某个前缀。
后缀:对于字符串 abcxxxxefg,我们称 efg 属于 abcxxxxefg 的某个后缀。
然后我们假设原串为 abeababeabf,匹配串为 abeabf:
我们可以先看看如果不使用 KMP,会如何进行匹配(不使用 substring 函数的情况下)。
首先在「原串」和「匹配串」分别各自有一个指针指向当前匹配的位置。
首次匹配的「发起点」是第一个字符 a。显然,后面的 abeab 都是匹配的,两个指针会同时往右移动(黑标)。
在都能匹配上 abeab 的部分,「朴素匹配」和「KMP」并无不同。
直到出现第一个不同的位置(红标):
接下来,正是「朴素匹配」和「KMP」出现不同的地方:
先看下「朴素匹配」逻辑:
将原串的指针移动至本次「发起点」的下一个位置(b 字符处);匹配串的指针移动至起始位置。
尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。
如图:
也就是说,对于「朴素匹配」而言,一旦匹配失败,将会将原串指针调整至下一个「发起点」,匹配串的指针调整至起始位置,然后重新尝试匹配。
这也就不难理解为什么「朴素匹配」的复杂度是O(m∗n) 了。
然后我们再看看「KMP 匹配」过程:
首先匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:
跳转到下一匹配位置后,尝试匹配,发现两个指针的字符对不上,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始:
到这里,你应该清楚 KMP 为什么相比于朴素解法更快:
因为 KMP 利用已匹配部分中相同的「前缀」和「后缀」来加速下一次的匹配。
因为 KMP 的原串指针不会进行回溯(没有朴素匹配中回到下一个「发起点」的过程)。
第一点很直观,也很好理解。
我们可以把重点放在第二点上,原串不回溯至「发起点」意味着什么?
其实是意味着:随着匹配过程的进行,原串指针的不断右移,我们本质上是在不断地在否决一些「不可能」的方案。
当我们的原串指针从 i 位置后移到 j 位置,不仅仅代表着「原串」下标范围为 [i,j)[i,j) 的字符与「匹配串」匹配或者不匹配,更是在否决那些以「原串」下标范围为 [i,j)[i,j) 为「匹配发起点」的子集。
- 分析实现
到这里,就结束了吗?要开始动手实现上述匹配过程了吗?
我们可以先分析一下复杂度。如果严格按照上述解法的话,最坏情况下我们需要扫描整个原串,复杂度为 O(n)O(n)。同时在每一次匹配失败时,去检查已匹配部分的相同「前缀」和「后缀」,跳转到相应的位置,如果不匹配则再检查前面部分是否有相同「前缀」和「后缀」,再跳转到相应的位置 ... 这部分的复杂度是 O(m^2)O(m
2
) ,因此整体的复杂度是 O(n * m^2)O(n∗m
2
),而我们的朴素解法是 O(m * n)O(m∗n) 的。
说明还有一些性质我们没有利用到。
显然,扫描完整原串操作这一操作是不可避免的,我们可以优化的只能是「检查已匹配部分的相同前缀和后缀」这一过程。
再进一步,我们检查「前缀」和「后缀」的目的其实是「为了确定匹配串中的下一段开始匹配的位置」。
同时我们发现,对于匹配串的任意一个位置而言,由该位置发起的下一个匹配点位置其实与原串无关。
举个 🌰,对于匹配串 abcabd 的字符 d 而言,由它发起的下一个匹配点跳转必然是字符 c 的位置。因为字符 d 位置的相同「前缀」和「后缀」字符 ab 的下一位置就是字符 c。
可见从匹配串某个位置跳转下一个匹配位置这一过程是与原串无关的,我们将这一过程称为找 next 点。
显然我们可以预处理出 next 数组,数组中每个位置的值就是该下标应该跳转的目标位置( next 点)。
当我们进行了这一步优化之后,复杂度是多少呢?
预处理 next 数组的复杂度未知,匹配过程最多扫描完整个原串,复杂度为 O(n)。
因此如果我们希望整个 KMP 过程是 O(m+n) 的话,那么我们需要在 O(m) 的复杂度内预处理出 next数组。
所以我们的重点在于如何在 O(m) 复杂度内处理处 next 数组。
- next 数组的构建
接下来,我们看看 next 数组是如何在 O(m)O(m) 的复杂度内被预处理出来的。
假设有匹配串 aaabbab,我们来看看对应的 next 是如何被构建出来的。
这就是整个 next
数组的构建过程,时空复杂度均为 O(m)。
至此整个 KMP 匹配过程复杂度是 O(m+n) 的。
var strStr = function(haystack, needle) { // next数组当前位指针,原串和匹配串的长度 let k = -1, n = haystack.length, p = needle.length; if (p == 0) return 0; // -1表示不存在相同的最大前缀和后缀 let next = Array(p).fill(-1); // 计算next数组 calNext(needle, next); for (let i = 0; i < n; i++) { while (k > -1 && needle[k + 1] != haystack[i]) { // 有部分匹配,往前回溯 k = next[k]; } if (needle[k + 1] == haystack[i]) { k++; } if (k == p - 1) { // 说明k移动到needle的最末端,返回相应的位置 return i - p + 1; } } return -1; }; // 辅函数- 计算next数组 function calNext(needle, next) { // 构造过程 j = 1,p = -1 开始 for (let j = 1, p = -1; j < needle.length; j++) { while (p > -1 && needle[p + 1] != needle[j]) { // 如果下一位不同,往前回溯 p = next[p]; } if (needle[p + 1] == needle[j]) { // 如果下一位相同,更新相同的最大前缀和最大后缀长 p++; } // 位置j处更新最长前缀 next[j] = p; } }
马拉车水平不够,看都没看懂,就不写了。
41.实现千位分隔符
反转整数部分
实现思路是将数字转换为字符数组,再循环整个数组, 每三位添加一个分隔逗号,最后再合并成字符串。因为分隔符在顺序上是从后往前添加的:比如 1234567添加后是1,234,567 而不是 123,456,7 ,所以方便起见可以先把数组倒序,添加完之后再倒序回来,就是正常的顺序了。要注意的是如果数字带小数的话,要把小数部分分开处理。
function numFormat(num) { // 按小数点分割 num = Number(num).toString().split('.'); // 将整数部分转换成字符数组并且倒序排列 let arr = num[0].split('').reverse(); // 存放添加','的整数 let res = [arr[0]]; for (let i = 1; i < arr.length; i++) { // 添加分隔符 if (i % 3 === 0) res.push(','); res.push(arr[i]); } // 再次将整数部分倒序成为正确的顺序,并拼接成字符串 res = res.reverse().join(''); // 如果有小数的话添加小数部分 if (num[1]) { res += '.' + num[1]; } return res; } let a = 1234567894532; let b = 673439.4542; console.log(numFormat(a)); // "1,234,567,894,532" console.log(numFormat(b)); // "673,439.4542"
自带函数toLocaleSting
使用JS自带的函数 toLocaleString
> 语法: numObj.toLocaleString([locales [, options]])
toLocaleString()
方法返回这个数字在特定语言环境下的表示字符串。
let a = 1234567894532; let b = 673439.4542; console.log(a.toLocaleString()); // "1,234,567,894,532" console.log(b.toLocaleString()); // "673,439.454" (小数部分四舍五入了)
要注意的是这个函数在没有指定区域的基本使用时,返回使用默认的语言环境和默认选项格式化的字符串,所以不同地区数字格式可能会有一定的差异。最好确保使用 locales 参数指定了使用的语言。
注:我测试的环境下小数部分会根据四舍五入只留下三位。
正则表达式
使用正则表达式和replace函数,相对前两种我更喜欢这种方法,虽然正则有点难以理解。
> replace 语法:str.replace(regexp|substr, newSubStr|function)
其中第一个 RegExp 对象或者其字面量所匹配的内容会被第二个参数的返回值替换。
function numFormat(num) { let res = num.toString().replace(/\d+/, function(n) { // 先提取整数部分 // console.log(n); return n.replace(/(\d)(?=(\d{3})+$)/g, function($1) { // 正向搜索后面有3个倍数的数字 // console.log($1); return $1 + ","; }) }) return res; } let a = 1234567894532; let b = 673439.4542; console.log(numFormat(a)); // "1,234,567,894,532" console.log(numFormat(b)); // "673,439.4542"
42.正则表达式的基本运用
判断是否是电话号码
function isPhone(tel) { let regx = /^1[345789]\d{9}$/; return regx.test(tel); }
验证是否是邮箱
function isEmail(email) { let regx = /^([a-zA-Z0-9_\-]+@([a-zA-Z0-9_\-]+\.)+([a-zA-Z]+)$/; return regx.test(email); }
验证是否是身份证
function isCardNo(number) { // 15位身份证,18位身份, let regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/; return regx.test(number); }
43.手写trim
记住空格的转义符是\s
字符串拆分数组
String.prototype.myTrim = function() { let arr = this.split(''); let i = 0; while (arr[i] === ' ') { arr.shift(); } i = arr.length - 1; while (arr[i] === ' ') { arr.pop(); i--; } return arr.join(''); } console.log(' ab cdd '.myTrim());
正则表达式
String.prototype.myTrim = function() { return this.replace(/^\s+/, '').replace(/\s+$/, ''); } console.log(' ab cdd '.myTrim());
可以利用g后缀合并
String.prototype.myTrim = function() { return this.replace(/^\s+|\s+$/g, ''); } console.log(' ab cdd '.myTrim());
上述方法假设至少存在一个空白符,因此效率较低,效率较高的写法如下
String.prototype.myTrim = function() { return this.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); } console.log(' ab cdd '.myTrim());
字符串截取
普通的原生字符串截取方法是远胜于正则替换,虽然是复杂一点。但只要正则不过于复杂,我们就可以利用浏览器对正则的优化,改善程序执行效率。
String.prototype.myTrim = function() { // 优化正则替换前面的空格 let str = this.replace(/^\s\s*/, ''); let ws = /\s/; // 从后向前查找末尾空格 let i = str.length; // while循环字符串charAt查找效率比较高 while (ws.test(str.charAt(--i))); return str.slice(0, i + 1); } console.log(' ab cdd '.myTrim());
更多方法及效率分析请参考JavaScript trim函数大赏
44.版本号比较
将输入字符串数组,按照版本号排序,
例如:
输入:var versions=['1.45.0','1.5','6','3.3.3.3.3.3.3']
输出:var sorted=['1.5','1.45.0','3.3.3.3.3.3','6']
// 比较两个版本的大小 function compareVersion(version1, version2) { // 先判断2个版本号是否是字符串 if (!version1 || !version2 || Object.prototype.toString.call(version1) !== '[object String]'|| Object.prototype.toString.call(version2) !== '[object String]') throw new Error("Version is null!"); // 按.将2个version进行分割 let arr1 = version1.trim().split('.'); let arr2 = version2.trim().split('.'); // 长度 const len = Math.min(arr1.length, arr2.length); for(let i = 0; i < len; i++) { if (Number(arr1[i]) < Number(arr2[i])) return - 1; else if (Number(arr1[i]) > Number(arr2[i])) return 1; } return 0; } let versions = ['1.45.0', '1.5', '6', '3.3.3.3.3.3.3']; console.log(versions.sort((a, b) => compareVersion(a, b)));
45.手写Object.freeze
Object.freeze()功能介绍
> Object.freeze
冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象
需要用到Object.seal()
,该方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。
function freeze(obj){ // 判断参数是否为Object类型 if (obj instanceof Object) { // 封闭对象 Object.seal(obj); } for (let key in obj) { if (obj.hasOwnProperty(key)) { // 设置只读 Object.defineProperty(obj, key, { writable: false }); // 如果属性值依然为对象,要通过递归来进行进一步的冻结 if (isObject(obj[key])) freeze(obj[key]); } } }
46.实现ES6的extends
Object.setPrototypeOf():
该方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null
。
语法
Object.setPrototypeOf(obj, proto);
参数
obj:要设置原型对象的对象。
proto:该对象的新原型对象或null,否则抛出TypeError异常。
返回值
设置了新的原型对象的对象。
Object.getPrototypeOf():
该方法用于获取指定对象的原型对象。
语法
Object.getPrototypeOf(obj);
参数
obj:要获取原型对象的对象。
返回值
返回指定对象的原型对象或null。
function B(name) { this.name = name; } function A(name, age) { // 1.将A的原型指向B Object.setPrototypeOf(A, B); // 2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用super Object.getPrototypeOf(A).call(this, name); // 3.将A原有的属性添加到新实例上 this.age = age; // 4.返回新实例对象 return this; } let a = new A('poetry',22); console.log(a); /* age: 22 name: "poetry" */
47.手写实现Set
Set是ES6提供给我们的构造函数,能够造出一种新的存储数据的结构,只有属性值,成员值唯一(不重复)。手写全部方法有点难,只有部分常用的add、has、delete一定要写出来,引用类型测试错误,估计也不会挖那么深
class MySet{ constructor(iterator = []) { // 判断构造的初始数据是否是可迭代对象 if (typeof iterator[Symbol.iterator] !== 'function') { throw new Error(`你提供的${iterator}不是一个可迭代的对象`); } // 存储数据 this.items = {}; // 长度; this.size = 0; // 循环可迭代对象,将结果加入到MySet中 for (const item of iterator) { this.add(item); } } // 在MySet对象尾部添加一个元素。返回该MySet对象。 add(data) { if (!this.has(data)) { this.items[data] = data; this.size++; } return this; } // 返回一个布尔值,表示该值在MySet中存在与否 has(data) { return this.items.hasOwnProperty(data); } // 移除MySet中与这个值相等的元素 delete(data) { if (this.has(data)) { delete this.items[data]; this.size--; return true; } else { return false; } } // 移除MySet对象内的所有元素。 clear() { this.items = {}; this.size = 0; } // 返回一个新的迭代器对象,该对象包含MySet对象中的按插入顺序排列的所有元素的值。 keys() { let keys = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { keys.push(key); } } return keys; } // 返回一个新的迭代器对象,该对象包含MySet对象中的按插入顺序排列的所有元素的值。 values() { let values = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { values.push(this.items[key]); } } return values; } // 返回一个新的迭代器对象,该对象包含MySet对象中的按插入顺序排列的所有元素的值的[value, value]数组。为了使这个方法和Map对象保持相似, 每个值的键和值相等。 entries() { let entries = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { entries.push([key, this.items[key]]); } } return entries; } // 遍历,返回一个新的迭代器对象,该对象包含MySet对象中的按插入顺序排列的所有元素的值。 *[Symbol.iterator](){ for (const item of this.items) { yield item; } } // 按照插入顺序,为MySet对象中的每一个值调用一次callBackFn。如果提供了thisArg参数,回调中的this会是这个参数。 forEach(callBackFn, thisArgs = this) { for (const item of this.items) { callBackFn.call(thisArgs, item, item, this.items); } } } let mySet = new MySet(); mySet.add(1); // Set [ 1 ] mySet.add(5); // Set [ 1, 5 ] mySet.add(5); // Set [ 1, 5 ] mySet.add("some text"); // Set [ 1, 5, "some text" ] console.log(mySet); let o = {a: 1, b: 2}; mySet.add(o); console.log(mySet); mySet.add({a: 1, b: 2}); // o 指向的是不同的对象,所以没问题 console.log(mySet); console.log(mySet.has(1)); // true console.log(mySet.has(3)); // false console.log(mySet.has(5)); // true console.log(mySet.has(Math.sqrt(25))); // true console.log(mySet.has("Some Text".toLowerCase())); // true console.log(mySet.has(o)); // true console.log(mySet.size); // 5 console.log(mySet.delete(5)); // true, 从set中移除5 console.log(mySet.has(5)); // false, 5已经被移除 console.log(mySet.size); // 4, 刚刚移除一个值 console.log(mySet); // logs Set(4) { 1, "some text", {…}, {…} }
还可以尝试着实现基本集合操作或者js模拟实现一个Set集合,实现两个集合的并集、交集、差集和子集。
48.手写实现Map
map也是ES6提供给我们的构造函数,能够造出一种新的存储数据的结构。本质上是键值对的集合。key对应value,key和value唯一,任何值都可以当属性。自己的手写版问题和Set类似。
class MyMap { constructor(iterator = []) { // 判断构造的初始数据是否是可迭代对象 if (typeof iterator[Symbol.iterator] !== 'function') { throw new Error(`你提供的${iterator}不是一个可迭代的对象`); } // 存储数据 this.items = {}; // 长度; this.size = 0; // 循环可迭代对象,将结果加入到MySet中 for (const item of iterator) { // item也是一个可迭代的对象 if (typeof item[Symbol.iterator] !== "function") { throw new Error(`你提供的${item}不是一个可迭代的对象`) } const iterator = item[Symbol.iterator](); const key = iterator.next().value; const value = iterator.next().value; this.set(key, value); } } // 设置MyMap对象中键的值。返回该MyMap对象。 set(key, value) { if (!this.items.hasOwnProperty(key)) { this.size++; } this.items[key] = value; return this; } // 返回一个布尔值,表示MyMap实例是否包含键对应的值 has(key) { return this.items.hasOwnProperty(key); } // 返回键对应的值,如果不存在,则返回undefined。 get(key) { if (this.items.hasOwnProperty(key)) { return this.items[key]; } else { return undefined; } } // 如果MyMap对象中存在该元素,则移除它并返回 true;否则如果该元素不存在则返回 false delete(key) { if (this.items.hasOwnProperty(key)) { delete this.items[key]; this.size--; return true; } else { return false; } } // 移除MyMap对象内的所有元素。 clear() { this.items = {}; this.size = 0; } // 返回一个新的迭代器对象,该对象包含MyMap对象中的按插入顺序排列的所有元素的值。 keys() { let keys = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { keys.push(key); } } return keys; } // 返回一个新的迭代器对象,该对象包含MyMap对象中的按插入顺序排列的所有元素的值。 values() { let values = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { values.push(this.items[key]); } } return values; } // 返回一个新的迭代器对象,该对象包含Set对象中的按插入顺序排列的所有元素的值的[value, value]数组。为了使这个方法和Map对象保持相似, 每个值的键和值相等。 entries() { let entries = []; for (let key in this.items) { if (this.items.hasOwnProperty(key)) { entries.push([key, this.items[key]]); } } return entries; } // 遍历,返回一个新的迭代器对象,该对象包含MyMap对象中的按插入顺序排列的所有元素的值。 *[Symbol.iterator]() { for (const key in this.items) { yield [this.items[key], key]; } } // 按照插入顺序,为MyMap对象中的每一个值调用一次callBackFn。如果提供了thisArg参数,回调中的this会是这个参数。 forEach(callBackFn, thisArgs = this) { for (const key in this.items) { callBackFn.call(thisArgs, this.items[key], key, this.items); } } } let myMap = new MyMap(); let keyObj = {}; let keyFunc = function() {}; let keyString = 'a string'; // 添加键 myMap.set(keyString, "和键'a string'关联的值"); myMap.set(keyObj, "和键keyObj关联的值"); myMap.set(keyFunc, "和键keyFunc关联的值"); console.log(myMap); console.log(myMap.size); // 3 // 读取值 console.log(myMap.get(keyString)); // "和键'a string'关联的值" console.log(myMap.get(keyObj)); // "和键keyObj关联的值" console.log(myMap.get(keyFunc)); // "和键keyFunc关联的值" console.log(myMap.get('a string')); // "和键'a string'关联的值" // 因为keyString === 'a string' console.log(myMap.get({})); // undefined, 因为keyObj !== {} console.log(myMap.get(function() {})); // undefined, 因为keyFunc !== function () {}
49.检测对象循环引用
检测对象自身是否循环引用,其实改进后的深拷贝已经囊括了这一检查方案
为此,WeakSet非常适合处理这种情况使用WeakSet简化,注意需要在第一次运行时创建WeakSet
,并将其与每个后续函数调用一起传递(使用内部参数_refs)。 WeakSet只能存放对象,且对象的数量或它们的遍历顺序无关紧要,因此,WeakSet比Set
更适合(和执行)跟踪对象引用,尤其是在涉及大量对象时。
// 对传入的obj对象 检查有无循环引用情况 function execRecursively(obj) { // 存储前层级对象 let ws = new WeakSet(); // 标志位 let flag = false; function dp(obj) { // 保证当前的元素是对象,若已经存在循环,也直接返回 if (typeof obj !== "object" || flag) return; // 存储当前层对象 let cws = new WeakSet(); if (!ws.has(obj)) ws.add(obj); // 一次遍历检查当前层是否有相同元素 for (let key in obj) { if (typeof obj[key] === "object") { // 如果同层级的引用相同,把它删除 if (cws.has(obj[key])) { // 找到循环引用 delete obj[key]; } else { cws.add(obj[key]); } } } // 二次遍历检查当前层是否存在循环引用 for (let key in obj) { if (typeof obj[key] === "object") { if (ws.has(obj[key])) { // 找到循环引用 flag = true; break; } else { ws.add(obj[key]); } // 递归检查有无循环使用 dp(obj[key]); } } } dp(obj); return flag; } let obj1 = { a: "1" }; obj1.b = {}; obj1.b.a = obj1.b; obj1.b.b = obj1.b; let obj2 = { a: { c: "1" } }; obj2.a.b = obj2; let obj3 = { a: 1, b: 2, c: { d: 4 }, d: {}, e: {} } let obj4 = { a: "1" }; obj4.b = { c: 1 }; obj4.aa = obj4.b; obj4.bb = obj4.b; let obj5 = { a: "1" }; obj5.b = {}; obj5.b.a = obj5.b; obj5.b.b = obj5.b; let obj6 = { a: {c: "1"} }; obj6.b = {}; obj6.b.d = obj6.a; console.log(execRecursively(obj1)); // true console.log(execRecursively(obj2)); // true console.log(execRecursively(obj3)); // false console.log(execRecursively(obj4)); // false console.log(execRecursively(obj5)); // true console.log(execRecursively(obj6)); // false
50.单例模式
在合适的时候才创建对象,并且只创建唯一的一个。在单例模式下创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
使用闭包实现单例模式,我写的这个又被称为懒汉式单例模式,没有一开始就对这个类进行实例化:
function Singleton (name) { this.name = name; }; Singleton.getInstance = (function(name) { // 实例对象 let instance; return function(name) { if (!instance) { instance = new Singleton (name); } return instance; } })(); // 测试 var a = Singleton.getInstance('ConardLi'); var b = Singleton.getInstance('ConardLi2'); console.log(a === b); //true
51.观察者模式
首先想分析一下观察者模式和发布/订阅模式的异同
观察者模式与发布/订阅模式区别
在翻阅资料的时候,有人把观察者(Observer)模式等同于发布(Publish)/订阅(Subscribe)模式,也有人认为这两种模式还是存在差异,而我认为确实是存在差异的,本质上的区别是调度的地方不同。
观察者模式
比较概念的解释是,目标和观察者是基类,目标提供维护观察者的一系列方法,观察者提供更新接口。具体观察者和具体目标继承各自的基类,然后具体观察者把自己注册到具体目标里,在具体目标发生变化时候,调度观察者的更新方法。
比如有个“天气中心”的具体目标A,专门监听天气变化,而有个显示天气的界面的观察者B,B就把自己注册到A里,当A触发天气变化,就调度B的更新方法,并带上自己的上下文。
发布/订阅模式
比较概念的解释是,订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。
比如有个界面是实时显示天气,它就订阅天气事件(注册到调度中心,包括处理程序),当天气变化时(定时获取数据),就作为发布者发布天气信息到调度中心,调度中心就调度订阅者的天气处理程序。
总结
1.从两张图片可以看到,最大的区别是调度的地方。
虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。
2.两种模式都可以用于松散耦合,改进代码管理和潜在的复用。
观察者模式的实现
观察者模式的优点
- 可以广泛应用于异步编程,它可以代替我们传统的回调函数
- 我们不需要关注对象在异步执行阶段的内部状态,我们只关心事件完成的时间点
- 角色很明确,没有事件调度中心作为中间者一个对象不必显式调用另一个对象的接口,而是松耦合的联系在一起 。目标对象
Subject
和观察者Observer
都要实现约定的成员方法。 - 双方联系紧密,目标对象的主动性很强,自己收集和维护观察者,并在状态变化时主动通知观察者更新。虽然不知道彼此的细节,但不影响相互通信。更重要的是,其中一个对象改变不会影响另一个对象。
订阅者的能力非常简单,作为被动的一方,它的行为只有两个——被通知、去执行(本质上是接受发布者的调用,这步我们在发布者中已经做掉了)。
发布者的基本操作首先是增加订阅者,然后是通知订阅者,最后是移除订阅者。
// 观察者 class Observer { /** * 构造器 * @param {Function} cb 回调函数,收到目标对象通知时执行 */ constructor(cb) { if (typeof cb === 'function') { this.cb = cb; } else { throw new Error('Observer构造器必须传入函数类型!'); } } /** * 被目标对象通知时执行回调函数 */ update() { this.cb(); } } // 目标对象,发布者类 class Subject { constructor() { // 维护观察者列表 this.observers = []; } /** * 添加一个观察者 * @param {Observer} observer Observer实例 */ add(observer) { this.observers.push(observer); } /** * 移除一个观察者 * @param {Observer} observer Observer实例 */ remove(observer) { this.observers.forEach((item, i) => { if (item === observer) { this.observers.splice(i, 1); } }); } /** * 通知所有的观察者 */ notify() { this.observers.forEach(observer => { observer.update(); }); } } const observerCallback = function() { console.log('我被通知了'); } const observer = new Observer(observerCallback); const subject = new Subject(); subject.add(observer); subject.notify(); // 我被通知了
52.发布/订阅模式 (EventBus/EventEmitter)
EventEmitter
是一个典型的发布/订阅模式,实现了事件调度中心。发布订阅模式中,包含发布者,事件调度中心,订阅者三个角色。发布者和订阅者是松散耦合的,互不关心对方是否存在,他们关注的是事件本身。发布者借用事件调度中心提供的emit
方法发布事件,而订阅者则通过on
进行订阅。
优点:
- 发布订阅模式中,对于发布者
Publisher
和订阅者Subscriber
没有特殊的约束,他们好似是匿名活动,借助事件调度中心提供的接口发布和订阅事件,互不了解对方是谁。 - 松散耦合,灵活度高,常用作事件总线
- 易理解,可类比于
DOM
事件中的dispatchEvent
和addEventListener
。
缺点:
- 当事件类型越来越多时,难以维护,需要考虑事件命名的规范,也要防范数据流混乱。
Event Bus
(Vue、Flutter 等前端框架中有出镜)和 Event Emitter
(Node中有出镜)出场的“剧组”不同,但是它们都对应一个共同的角色——全局事件总线。
在Vue中使用Event Bus来实现组件间的通讯
Event Bus/Event Emitter
作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。
在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex
之外,我们还可以通过 Event Bus
来实现我们的需求。整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisher
和DeveloperObserver
),全程只有bus
这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!
class EventEmitter { constructor() { // 存储事件监听器及回调函数 this.listeners = {}; } /** * 注册事件监听者 on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数 * @param {String} eventName 事件类型 * @param {Function} cb 回调函数 */ on(eventName, cb) { // 先检查一下目标事件名有没有对应的监听函数队列 if (!this.listeners[eventName]) { // 如果没有,那么首先初始化一个监听函数队列 this.listeners[eventName] = []; } // 把回调函数推入目标事件的监听函数队列里去 this.listeners[eventName].push(cb); } /** * 发布事件 emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数 * @param {String} eventName 事件类型 * @param {...any} args 参数列表,把emit传递的参数赋给回调函数 */ emit(eventName, ...args) { // 检查目标事件是否有监听函数队列 if (this.listeners[eventName]) { // 如果有,则逐个调用队列里的回调函数 this.listeners[eventName].forEach((cb) => { cb(...args); }) } } /** * off方法移除某个事件的一个监听者,移除某个事件回调队列里的指定回调函数 * @param {String} eventName 事件类型 * @param {Function} cb 回调函数 */ off(eventName, cb) { if (this.listeners[eventName]) { const callbacks = this.listeners[eventName]; const index = callbacks.indexOf(cb); if(index !== -1) callbacks.splice(index, 1); if (this.listeners[eventName].length === 0) delete this.listeners[eventName]; } } /** * offAll移除某个事件的所有监听者 * @param {String} eventName事件类型 */ offAll(eventName) { if (this.listeners[eventName]) { delete this.listeners[eventName]; } } /** * once方法为事件注册单次监听器 * @param {String} eventName 事件类型 * @param {Function} cb 回调函数 */ once(eventName, cb) { // 对回调函数进行包装,使其执行完毕自动被移除 const wrapper = (...args) => { // 使用箭头函数使this指向EventEmitter实例 cb.apply(this, args); // 移除该回调函数 this.off(eventName, cb); } this.on(eventName, wrapper); } } // 测试 // 创建事件管理器实例 const ee = new EventEmitter(); // 注册一个chifan事件监听者 ee.on('chifan', function() { console.log('吃饭了,我们走!') }); // 发布事件chifan ee.emit('chifan'); // 也可以emit传递参数 ee.on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) }); ee.emit('chifan', '三食堂', '铁板饭'); // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者 // 测试移除事件监听 const toBeRemovedListener = function() { console.log('我是一个可以被移除的监听者') }; ee.on('testoff', toBeRemovedListener); ee.emit('testoff'); ee.off('testoff', toBeRemovedListener); ee.emit('testoff'); // 此时事件监听已经被移除,不会再有console.log打印出来了 // 测试移除chifan的所有事件监听 ee.offAll('chifan'); console.log(ee); // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了
53.手写事件代理
事件代理,可能是代理模式最常见的一种应用方式,也是一道实打实的高频面试题。它的场景是一个父元素下有多个子元素,像这样:
事件代理,可能是代理模式最常见的一种应用方式,也是一道实打实的高频面试题。它的场景是一个父元素下有多个子元素,像这样:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>事件代理</title> </head> <body> <div id="father"> <a href="#">链接1号</a> <a href="#">链接2号</a> <a href="#">链接3号</a> <a href="#">链接4号</a> <a href="#">链接5号</a> <a href="#">链接6号</a> </div> </body> </html>
我们现在的需求是,希望鼠标点击每个 a 标签,都可以弹出“我是xxx”这样的提示。比如点击第一个 a 标签,弹出“我是链接1号”这样的提示。这意味着我们至少要安装 6
个监听函数给 6
个不同的的元素(一般我们会用循环,代码如下所示),如果我们的 a
标签进一步增多,那么性能的开销会更大。
// 假如不用代理模式,我们将循环安装监听函数 // 所有a标签节点 const aNodes = document.getElementById('father').getElementsByTagName('a'); // 循环安装监听函数 for (let i = 0; i < aNodes.length; i++) { aNodes[i].addEventListener('click', funtion(e) { // 阻止事件默认行为 e.preventDefault(); alert(`我是${aNodes[i].innerText}`); }) }
考虑到事件本身具有“冒泡”的特性,当我们点击 a 元素时,点击事件会“冒泡”到父元素 div 上,从而被监听到。如此一来,点击事件的监听函数只需要在 div 元素上被绑定一次即可,而不需要在子元素上被绑定 N 次——这种做法就是事件代理,它可以很大程度上提高我们代码的性能。
事件代理的实现
用代理模式实现多个子元素的事件监听,代码会简单很多:
// 获取父元素 const father = document.getElementId('father'); // 给父元素安装一次监听函数 father.addEventListener('click', function(e) { // 识别是否是目标子元素 if (e.target.tagName === 'A') { // 以下是监听函数的函数体 // 阻止事件默认行为 e.preventDefault(); alert(`我是${e.target.innerText}`); } })
在这种做法下,我们的点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。
55.手写Promise
为什么用Promise?在传统的异步编程中,如果异步之间存在依赖关系,我们就需要通过层层嵌套回调来满足这种依赖,如果嵌套层数过多,可读性和可维护性都变得很差,产生所谓“回调地狱”,而Promise将回调嵌套改为链式调用,增加可读性和可维护性。
romise本质是一个状态机,且状态只能为以下三种:Pending(等待态)
、Fulfilled(执行态)
、Rejected(拒绝态)
,状态的变更是单向的,只能从Pending -> Fulfilled 或 Pending -> Rejected,状态变更不可逆P,Promise一旦新建就立刻执行, 此时的状态是Pending(进行中)。
then方法
接收两个可选参数,分别对应状态改变时触发的回调,resolve和reject。它们是两个函数.
resolve函数的作用是将Promise对象的状态从'未完成'变为'成功'(由Pending变为Resolved), 在异步操作成功时,将操作结果作为参数传递出去;
reject函数的作用是将Promise对象的状态从'未完成'变为失败(由Pending变为Rejected),在异步操作失败时调用,并将异步操作的错误作为参数传递出去。
基础版本
class MyPromise { // 构造方法接收一个回调 constuctor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } }
then方法
在基础版本上增加then方法
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } } let promise1 = new MyPromise((resolve, reject) => { resolve('Success!'); }); promise1.then((value) => { console.log(value); // expected output: "Success!" });
catch方法
catch方法本质上就是第一个参数为空函数的then方法
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } } let p1 = new MyPromise(function(resolve, reject) { resolve('Success'); }); p1.then(function(value) { console.log(value); // "Success!" throw 'oh, no!'; }).catch(function(e) { console.log(e); // "oh, no!" }).then(function(){ console.log('after a catch the chain is restored'); }, function () { console.log('Not fired due to the catch'); });
finally方法
无论当前 Promise 是成功还是失败,调用finally之后都会执行 finally 中传入的函数,并且将值原封不动的往下传。
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } // finally方法 finally(cb) { return this.then( // 执行回调,并return value传递给后面的then value => MyPromise.resolve(cb()).then(() => value), // reject同理 reason => MyPromise.resolve(cb()).then(() => {throw reason}) ) } } let promise1 = new MyPromise((resolve, reject) => { resolve('Success!'); }); promise1.then((value) => { console.log(value); // expected output: "Success!" }).finally(() => console.log('Finally!')); let p1 = new MyPromise(function(resolve, reject) { resolve('Success'); }); p1.then(function(value) { console.log(value); // "Success!" throw 'oh, no!'; }).finally(function(){ console.log('Finally!'); });
Promise.resolve和Promise.reject
Promise.resolve(value)
方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有"then" 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
而Promise.reject()
方法返回一个带有拒绝原因的Promise对象。
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } // finally方法 finally(cb) { return this.then( // 执行回调,并return value传递给后面的then value => MyPromise.resolve(cb()).then(() => value), // reject同理 reason => MyPromise.resolve(cb()).then(() => {throw reason}) ) } // 静态的resolve方法 static resolve(value) { // 根据规范, 如果参数是Promise实例, 直接return这个实例 if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); } // 静态的reject方法 static reject(reason) { return new MyPromise((resolve, reject) => reject(reason)); } } let promise1 = MyPromise.resolve(123); promise1.then((value) => { console.log(value); // expected output: 123 }); function resolved(result) { console.log('Resolved'); } function rejected(result) { console.error(result); } MyPromise.reject(new Error('fail')).then(resolved, rejected); // expected output: Error: fail
Promise.all
Promise.all() 它接收一个promise对象组成的数组作为参数,并返回一个新的promise对象。
当数组中所有的对象都resolve时,新对象状态变为fulfilled,所有对象的resolve的value依次添加组成一个新的数组,并以新的数组作为新对象resolve的value。
当数组中有一个对象reject时,新对象状态变为rejected,并以当前对象reject的reason作为新对象reject的reason。
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } // finally方法 finally(cb) { return this.then( // 执行回调,并return value传递给后面的then value => MyPromise.resolve(cb()).then(() => value), // reject同理 reason => MyPromise.resolve(cb()).then(() => {throw reason}) ) } // 静态的resolve方法 static resolve(value) { // 根据规范, 如果参数是Promise实例, 直接return这个实例 if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); } // 静态的reject方法 static reject(reason) { return new MyPromise((resolve, reject) => reject(reason)); } // 静态的all方法 static all(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let res = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { index++; // 结果数组按照原数组的顺序依次输出 res[i] = value; // 所有MyPromise都resolve if (index === promises.length) resolve(res); },function(reason) { // 第一个reject的MyPromise reject(reason); } ) } }) } } let promise1 = MyPromise.resolve(3); let promise2 = 42; let promise3 = new MyPromise((resolve, reject) => { setTimeout(resolve, 100, 'foo'); }); MyPromise.all([promise1, promise2, promise3]).then((values) => { console.log(values); }); // expected output: Array [3, 42, "foo"]
Promise.race
Promise.race() 它同样接收一个promise对象组成的数组作为参数,并返回一个新的promise对象。
与Promise.all()不同,它是在数组中有一个对象(最早改变状态)resolve或reject时,就改变自身的状态,并执行响应的回调。
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } // finally方法 finally(cb) { return this.then( // 执行回调,并return value传递给后面的then value => MyPromise.resolve(cb()).then(() => value), // reject同理 reason => MyPromise.resolve(cb()).then(() => {throw reason}) ) } // 静态的resolve方法 static resolve(value) { // 根据规范, 如果参数是Promise实例, 直接return这个实例 if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); } // 静态的reject方法 static reject(reason) { return new MyPromise((resolve, reject) => reject(reason)); } // 静态的all方法 static all(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let res = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { index++; // 结果数组按照原数组的顺序依次输出 res[i] = value; // 所有MyPromise都resolve if (index === promises.length) resolve(res); },function(reason) { // 第一个reject的MyPromise reject(reason); } ) } }) } // 静态的race方法,只要有一个promise成功了 就算成功。如果第一个失败了就失败了 static race(promises) { // 返回一个MyPromise对象 return new MyPromise(function(resolve, reject) { // 同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态 for (let promise of promises) { // Promise.resolve(promise)用于处理传入值不为Promise的情况 MyPromise.resolve(promise).then(function(value) { // 注意这个resolve是上边new MyPromise的 resolve(value); }, function(error) { reject(error); }) } }) } } let promise1 = new MyPromise((resolve, reject) => { setTimeout(resolve, 500, 'one'); }); let promise2 = new MyPromise((resolve, reject) => { setTimeout(resolve, 100, 'two'); }); MyPromise.race([promise1, promise2]).then((value) => { console.log(value); // Both resolve, but promise2 is faster }); // expected output: "two"
Promise.allSettled
接受的结果与入参时的promise实例一一对应,且结果的每一项都是一个对象,告诉你结果和值,对象内都有一个属性叫“status”,用来明确知道对应的这个promise实例的状态(fulfilled或rejected),fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。
重要的一点是,他不论接受入参的promise本身的状态,会返回所有promise的结果,但这一点Promise.all
做不到,如果你需要知道所有入参的异步操作的所有结果,或者需要知道这些异步操作是否全部结束,应该使用promise.allSettled()
。
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } // finally方法 finally(cb) { return this.then( // 执行回调,并return value传递给后面的then value => MyPromise.resolve(cb()).then(() => value), // reject同理 reason => MyPromise.resolve(cb()).then(() => {throw reason}) ) } // 静态的resolve方法 static resolve(value) { // 根据规范, 如果参数是Promise实例, 直接return这个实例 if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); } // 静态的reject方法 static reject(reason) { return new MyPromise((resolve, reject) => reject(reason)); } // 静态的all方法 static all(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let res = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { index++; // 结果数组按照原数组的顺序依次输出 res[i] = value; // 所有MyPromise都resolve if (index === promises.length) resolve(res); },function(reason) { // 第一个reject的MyPromise reject(reason); } ) } }) } // 静态的race方法,只要有一个promise成功了 就算成功。如果第一个失败了就失败了 static race(promises) { // 返回一个MyPromise对象 return MyPromise(function(resolve, reject) { // 同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态 for (let promise of promises) { // Promise.resolve(promise)用于处理传入值不为Promise的情况 MyPromise.resolve(promise).then(function(value) { // 注意这个resolve是上边new MyPromise的 resolve(value); }, function(error) { reject(error); }) } }) } // 静态的allSettled方法 static allSettled(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let res = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { index++; // 结果数组按照原数组的顺序依次输出 res[i] = { status: 'fulfilled', value: value }; // 所有MyPromise都resolve if (index === promises.length) resolve(res); },function(reason) { // 和第一个类似,但要注意状态 index++; // 结果数组按照原数组的顺序依次输出 res[i] = { status: 'rejected', reason: reason }; // 所有MyPromise都resolve if (index === promises.length) resolve(res); } ) } }) } } let resolved = MyPromise.resolve(42); let rejected = MyPromise.reject(-1); Promise.allSettled([resolved, rejected]) .then(function (results) { console.log(results); }); // [ // { status: 'fulfilled', value: 42 }, // { status: 'rejected', reason: -1 } // ]
Promise.any
Promise.any() 是 ES2021 新增的特性,它接收一个 Promise 可迭代对象(例如数组),
只要其中的一个 promise 成功,就返回那个已经成功的 promise
如果可迭代对象中没有一个 promise 成功(即所有的 promises 都失败/拒绝),就返回一个失败的 promise 和 AggregateError 类型的实例,它是 Error 的一个子类,用于把单一的错误集合在一起
class MyPromise { // 构造方法接收一个回调 constructor(fn) { // Promise三种状态 this.status = 'pending'; // 定义状态为resolved(fulfilled)的时候的状态 this.value = undefined; // 定义状态为rejected的时候的状态 this.reason = undefined; // 成功队列, 存放成功的回调,resolve时触发 this.onResolvedCallbacks = []; // 失败队列, 存放失败的回调,reject时触发 this.onRejectedCallbacks = []; // 由于resolve/reject是在fn内部被调用, 因此需要使用箭头函数固定this指向,指向Promise实例 let resolve = (value) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.value = value; // 变更状态 this.status = 'fulfilled'; this.onResolvedCallbacks.forEach(callback => callback(value)); } } // 实现同resolve let reject = (reason) => { // 对应规范中的"状态只能由pending到fulfilled或rejected" if (this.status === 'pending') { this.reason = reason; // 变更状态 this.status = 'rejected'; this.onRejectedCallbacks.forEach(callback => callback(reason)); } } // 执行时可能会发生异常 try { // new Promise()时立即执行fn,并传入resolve和reject fn(resolve, reject); } catch(e) { reject(e); } } // then方法,接收一个成功的回调和一个失败的回调 then(onFullfilled, onRejected) { // 防止值的穿透 if (typeof onFullfilled !== 'function') onFullfilled = v => v; if (typeof onRejected !== 'function') onRejected = v => v; // return一个新的promise return new MyPromise((resolve, reject) => { // 把onFullfilled重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论,使用箭头函数,使this指向实例 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = onFullfilled(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 把onRejected重新包装一下,使用箭头函数,使this指向实例 const rejectedFn = value => { try { // 执行第一个(当前的)Promise的失败回调,并获取返回值 let x = onRejected(value); // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceof MyPromise ? x.then(resolve, reject) : resolve(x); } catch (error) { reject(error); } } // 根据状态变换 switch(this.status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case 'pending': this.onResolvedCallbacks.push(fulfilledFn); this.onRejectedCallbacks.push(rejectedFn); break; // 当状态已经变为resolve/reject时,直接执行then回调 case 'fulfilled': fulfilledFn(this.value); break; case 'rejected': rejectedFn(this.reason); break; } }) } // catch方法,接收一个失败的回调 catch(onRejected) { return this.then(undefined, onRejected); } // finally方法 finally(cb) { return this.then( // 执行回调,并return value传递给后面的then value => MyPromise.resolve(cb()).then(() => value), // reject同理 reason => MyPromise.resolve(cb()).then(() => {throw reason}) ) } // 静态的resolve方法 static resolve(value) { // 根据规范, 如果参数是Promise实例, 直接return这个实例 if (value instanceof MyPromise) return value; return new MyPromise(resolve => resolve(value)); } // 静态的reject方法 static reject(reason) { return new MyPromise((resolve, reject) => reject(reason)); } // 静态的all方法 static all(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let res = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { index++; // 结果数组按照原数组的顺序依次输出 res[i] = value; // 所有MyPromise都resolve if (index === promises.length) resolve(res); },function(reason) { // 第一个reject的MyPromise reject(reason); } ) } }) } // 静态的race方法,只要有一个promise成功了 就算成功。如果第一个失败了就失败了 static race(promises) { // 返回一个MyPromise对象 return MyPromise(function(resolve, reject) { // 同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态 for (let promise of promises) { // Promise.resolve(promise)用于处理传入值不为Promise的情况 MyPromise.resolve(promise).then(function(value) { // 注意这个resolve是上边new MyPromise的 resolve(value); }, function(error) { reject(error); }) } }) } // 静态的allSettled方法 static allSettled(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let res = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { index++; // 结果数组按照原数组的顺序依次输出 res[i] = { status: 'fulfilled', value: value }; // 所有MyPromise都resolve if (index === promises.length) resolve(res); },function(reason) { // 和第一个类似,但要注意状态 index++; // 结果数组按照原数组的顺序依次输出 res[i] = { status: 'rejected', reason: reason }; // 所有MyPromise都resolve if (index === promises.length) resolve(res); } ) } }) } // 静态的any方法 static any(promises) { // 计数器,用来累计promise的已执行次数 let index = 0; // 存放 promise执行后的结果 let reasons = []; // 返回一个MyPromise对象 return new MyPromise(function (resolve, reject) { for (let i = 0; i < promises.length; i++) { MyPromise.resolve(promises[i]).then( function(value) { // 第一个resolve的MyPromise resolve(value); }, function(reason) { // 结果数组按照原数组的顺序依次输出 index++; reasons.push(reason); // 所有MyPromise都resolve if (index === promises.length) reject(new AggregateError('All promises were rejected', reasons)); } ) } }) } } let promises1 = [ MyPromise.reject('ERROR A'), MyPromise.reject('ERROR B'), MyPromise.resolve('result'), ] MyPromise.any(promises1).then((value) => { console.log('value: ', value); }).catch((err) => { console.log('err: ', err); }) // value: result // 如果所有传入的 promises 都失败: let promises2 = [ MyPromise.reject('ERROR A'), MyPromise.reject('ERROR B'), MyPromise.reject('ERROR C'), ] MyPromise.any(promises2).then((value) => { console.log('value:', value); }).catch((err) => { console.log('err:', err); console.log(err.message); console.log(err.name); console.log(err.errors); }) // err:AggregateError: All promises were rejected // All promises were rejected // AggregateError // ["ERROR A", "ERROR B", "ERROR C"]
最后的全部reject的失败了。
56.手写ajax封装
原生ajax封装
步骤
- 创建
XMLHttpRequest
实例 - 发出 HTTP 请求
- 服务器返回 XML 格式的字符串
- JS 解析 XML,并更新局部页面
- 不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。
了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。
function ajax(url, method = 'get', param = {}) { // 创建 XMLHttpRequest 对象 let xhr = new XMLHttpRequest(); // 三个参数,规定请求的类型、URL 以及是否异步处理请求。 xhr.open(method, url, true); // 设置请求头,发送信息至服务器时内容编码类型,可以不写 xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); // 每当 readyState 属性改变时,就会调用该函数。 xhr.onreadystatechange = function() { // XMLHttpRequest 代理当前所处状态。 if (xhr.readyState === 4) { // 200-300请求成功 if ((xhr.status >= 200 && xhr.status < 300) || xhr === 304) { // JSON.parse() 方法用来解析JSON字符串,构造由字符串描述的JavaScript值或对象 success(JSON.parse(xhr.responseText)); } else { fail && fail(); } } } // 发送请求,用于实际发出 HTTP 请求。不带参数为GET请求 xhr.send(null); }
Promise封装ajax
- 返回一个新的Promise实例
- 创建HMLHttpRequest异步对象
- 调用open方法,打开url,与服务器建立链接(发送前的一些处理)
- 监听Ajax状态信息
- 如果
xhr.readyState == 4
(表示服务器响应完成,可以获取使用服务器的响应了)xhr.status == 200
,返回resolve状态xhr.status == 404
,返回reject状态
xhr.readyState !== 4
,把请求主体的信息基于send发送给服务器
function ajax(url, method = 'get', param = {}) { return new Promise((resolve, reject) => { // 创建 XMLHttpRequest 对象 let xhr = new XHLHttpRequest(); // 三个参数,规定请求的类型、URL 以及是否异步处理请求。 xhr.open(method, url, true); // 设置请求头,发送信息至服务器时内容编码类型,可以不写 xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); // 每当 readyState 属性改变时,就会调用该函数。 xhr.onreadystatechange() = function() { // XMLHttpRequest 代理当前所处状态。 if (xhr.readyState === 4) { // 200-300请求成功 if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { resolve(JSON.parse(xhr.responseText)); } else { reject('请求出错'); } } } }) }
57.手写实现sleep
使用Promise
function sleep(time) { return new Promise(function(resolve) { setTimeout(resolve, time); }) } // 使用 sleep(1000).then(() => { console.log(1); })
使用生成器Generator
function* sleepGenerator(time){ yield new Promise(function(resolve, reject) { setTimeout(resolve, time); }) } // 使用 sleepGenerator(1000).next().value.then(() => { console.log(1); })
使用async/await
function sleep(time) { return new Promise(function(resolve) { setTimeout(resolve, time); }) } async function output(time) { let out = await sleep(time); console.log(1); return out; } output(1000);
ES5
function sleep(callback, time) { if (typeof(callback) === 'function') { setTimeout(callback, time); } } function output() { console.log(1); } sleep(output, 1000);
变种题 将setTimeout包装成sleep的函数
手写f函数
setTimeout(() => console.log('hi'), 500); const sleep = f(setTimeout); sleep(500).then(() => console.log('hi'));
即实现高阶函数
function f(fn) { return function(...args) { return new Promise(function(resolve) { args.unshift(resolve); fn(...args); }) } } const sleep = f(setTimeout); sleep(500).then(() => console.log('hi'));
全部评论
(0) 回帖