前言
最近在準備面試,恰好利用幾天的時間系統的整理了下JS常常考的手撕題類型。javascript
在這裏,以腦圖的形式帶你們手撕這篇文章裏的全部題(帶註釋)。html
想領取腦圖的關注前端時光屋公衆號,回覆「JS手撕」,便可領取❤️前端
腦圖裏的全部題型便是本文中的30道
常考高頻題java
腦圖👇web
1.手動實現一個淺克隆
淺克隆: 只拷貝對象或數組的第一層內容面試
const shallClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) { // 遍歷對象自身可枚舉屬性(不考慮繼承屬性和原型對象)
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
2.手動實現一個深克隆(簡易版)
深克隆: 層層拷貝對象或數組的每一層內容ajax
function deepClone(target) {
if (target === null) return null;
if (typeof target !== 'object') return target;
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop]);
}
}
return cloneTarget;
}
3.手動實現一個深克隆(考慮日期/正則等特殊對象 和 解決循環引用狀況)
const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
function deepClone (target, map = new Map()) {
// 先判斷該引用類型是否被 拷貝過
if (map.get(target)) {
return target;
}
// 獲取當前值的構造函數:獲取它的類型
let constructor = target.constructor;
// 檢測當前對象target是否與 正則、日期格式對象匹配
if (/^(RegExp|Date)$/i.test(constructor.name)){
return new constructor(target); // 建立一個新的特殊對象(正則類/日期類)的實例
}
if (isObject(target)) {
map.set(target, true); // 爲循環引用的對象作標記
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = deepClone(target[prop], map);
}
}
return cloneTarget;
} else {
return target;
}
}
4.手動實現instanceOf的機制
思路:跨域
步驟1: 先取得當前類的原型,當前實例對象的原型鏈數組
步驟2: 一直循環(執行原型鏈的查找機制)promise
-
取得當前實例對象原型鏈的原型鏈(
proto = proto.__proto__
,沿着原型鏈一直向上查找) -
若是 當前實例的原型鏈
__proto__
上找到了當前類的原型prototype
,則返回true
-
若是 一直找到
Object.prototype.__proto__ == null
,Object的基類(null)上面都沒找到,則返回 false
function _instanceof (instanceObject, classFunc) {
let classFunc = classFunc.prototype; // 取得當前類的原型
let proto = instanceObject.__proto__; // 取得當前實例對象的原型鏈
while (true) {
if (proto === null) { // 找到了 Object的基類 Object.prototype.__proto__
return false;
};
if (proto === classFunc) { // 在當前實例對象的原型鏈上,找到了當前類
return true;
}
proto = proto.__proto__; // 沿着原型鏈__ptoto__一層一層向上查找
}
}
優化版 (處理兼容問題)
Object.getPrototypeOf:用來獲取某個實例對象的原型(內部[[prototype]]屬性的值,包含proto屬性)
function _instanceof (instanceObject, classFunc) {
let classFunc = classFunc.prototype; // 取得當前類的原型
let proto = Object.getPrototypeOf(instanceObject); // 取得當前實例對象的原型鏈上的屬性
while (true) {
if (proto === null) { // 找到了 Object的基類 Object.prototype.__proto__
return false;
};
if (proto === classFunc) { // 在當前實例對象的原型鏈上,找到了當前類
return true;
}
proto = Object.getPrototypeOf(proto); // 沿着原型鏈__ptoto__一層一層向上查找
}
}
5. 手動實現防抖函數
實現函數的防抖(目的是頻繁觸發中只執行一次)
以最後一次觸發爲標準
/**
* 實現函數的防抖(目的是頻繁觸發中只執行一次)
* @param {*} func 須要執行的函數
* @param {*} wait 檢測防抖的間隔頻率
* @param {*} immediate 是不是當即執行 True:第一次,默認False:最後一次
* @return {可被調用執行的函數}
*/
function debounce(func, wati = 500, immediate = false) {
let timer = null
return function anonymous(... params) {
clearTimeout(timer)
timer = setTimeout(_ => {
// 在下一個500ms 執行func以前,將timer = null
//(由於clearInterval只能清除定時器,但timer還有值)
// 爲了確保後續每一次執行都和最初結果同樣,賦值爲null
// 也能夠經過 timer 是否 爲 null 是否有定時器
timer = null
func.call(this, ...params)
}, wait)
}
}
以第一次觸發爲標準
/**
* 實現函數的防抖(目的是頻繁觸發中只執行一次)
* @param {*} func 須要執行的函數
* @param {*} wait 檢測防抖的間隔頻率
* @param {*} immediate 是不是當即執行 True:第一次,默認False:最後一次
* @return {可被調用執行的函數}
*/
function debounce(func, wait = 500, immediate = true) {
let timer = null
return function anonymous(... params) {
// 第一點擊 沒有設置過任何定時器 timer就要爲 null
let now = immediate && !timer
clearTimeout(timer)
timer = setTimeout(_ => {
// 在下一個500ms 執行func以前,將timer = null
//(由於clearInterval只能在系統內清除定時器,但timer還有值)
// 爲了確保後續每一次執行都和最初結果同樣,賦值爲null
// 也能夠經過 timer 是否 爲 null 是否有定時器
timer = null!immediate ? func.call(this, ...params) : null
}, wait)
now ? func.call(this, ...params) : null
}
}
function func() {
console. log('ok')
}
btn. onclick = debounce(func, 500)
6.手動實現節流函數
實現函數的節流 (目的是頻繁觸發中縮減頻率)
帶註釋說明版
【第一次觸發:reamining是負數,previous被賦值爲當前時間】
【第二次觸發:假設時間間隔是500ms,第一次執行完以後,
20ms
以後,當即觸發第二次,則remaining = 500 - ( 新的當前時間 - 上一次觸發時間 ) = 500 - 20 = 480 】
/**
* 實現函數的節流 (目的是頻繁觸發中縮減頻率)
* @param {*} func 須要執行的函數
* @param {*} wait 檢測節流的間隔頻率
* @param {*} immediate 是不是當即執行 True:第一次,默認False:最後一次
* @return {可被調用執行的函數}
*/
function throttle(func, wait) {
let timer = null
let previous = 0 // 記錄上一次操做的時間點
return function anonymous(... params) {
let now = new Date() // 當前操做的時間點
remaining = wait - (now - previous) // 剩下的時間
if (remaining <= 0) {
// 兩次間隔時間超過頻率,把方法執行
clearTimeout(timer); // clearTimeout是從系統中清除定時器,但timer值不會變爲null
timer = null; // 後續能夠經過判斷 timer是否爲null,而判斷是否有 定時器
// 此時已經執行func 函數,應該將上次觸發函數的時間點 = 如今觸發的時間點 new Date()
previous = new Date(); // 把上一次操做時間修改成當前時間
func.call(this, ...params);
} else if(!timer){
// 兩次間隔的事件沒有超過頻率,說明尚未達到觸發標準,設置定時器等待便可(還差多久等多久)
// 假設事件間隔爲500ms,第一次執行完以後,20ms後再次點擊執行,則剩餘 480ms,就能等待480ms
timer = setTimeout( _ => {
clearTimeout(timer)
timer = null // 確保每次執行完的時候,timer 都清 0,回到初始狀態
//過了remaining時間後,纔去執行func,因此previous不能等於初始時的 now
previous = new Date(); // 把上一次操做時間修改成當前時間
func.call(this, ...params);
}, remaining)
}
}
}
function func() {
console. log('ok')
}
btn. onclick = throttle(func, 500)
不帶註釋版
/**
* 實現函數的節流 (目的是頻繁觸發中縮減頻率)
* @param {*} func 須要執行的函數
* @param {*} wait 檢測節流的間隔頻率
* @param {*} immediate 是不是當即執行 True:第一次,默認False:最後一次
* @return {可被調用執行的函數}
*/
function throttle(func, wait) {
let timer = null;
let previous = 0;
return function anonymous(... params) {
let now = new Date();
remaining = wait - (now - previous);
if (remaining <= 0) {
clearTimeout(timer);
timer = null;
previous = new Date();
func.call(this, ...params);
} else if(!timer){
timer = setTimeout( _ => {
clearTimeout(timer);
timer = null;
previous = new Date();
func.call(this, ...params);
}, remaining)
}
}
}
function func() {
console. log('ok')
}
btn. onclick = throttle(func, 500);
7.手動實現Object.create
Object.create() = function create(prototype) {
// 排除傳入的對象是 null 和 非object的狀況
if (prototype === null || typeof prototype !== 'object') {
throw new TypeError(`Object prototype may only be an Object: ${prototype}`);
}
// 讓空對象的 __proto__指向 傳進來的 對象(prototype)
// 目標 {}.__proto__ = prototype
function Temp() {};
Temp.prototype = prototype;
return new Temp;
}
8.手動實現內置new的原理
簡化版
-
步驟1: 建立一個Func的實例對象(實例.proto = 類.prototype)
-
步驟2: 把
Func
當作普通函數執行,並改變this
指向 -
步驟3: 分析函數的返回值
/**
* Func: 要操做的類(最後要建立這個類的實例)
* args:存儲將來傳遞給Func類的實參
*/
function _new(Func, ...args) {
// 建立一個Func的實例對象(實例.____proto____ = 類.prototype)
let obj = {};
obj.__proto__ = Func.prototype;
// 把Func當作普通函數執行,並改變this指向
let result = Func.call(obj, ...args);
// 分析函數的返回值
if (result !== null && /^(object|function)$/.test(typeof result)) {
return result;
}
return obj;
}
優化版
__proto__
在IE瀏覽器中不支持
let x = { name: "lsh" };
Object.create(x);
{}.__proto__ = x;
function _new(Func, ...args) {
// let obj = {};
// obj.__proto__ = Func.prototype;
// 建立一個Func的實例對象(實例.____proto____ = 類.prototype)
let obj = Object.create(Func.prototype);
// 把Func當作普通函數執行,並改變this指向
let result = Func.call(obj, ...args);
// 分析函數的返回值
if (result !== null && /^(object|function)$/.test(typeof result)) {
return result;
}
return obj;
}
9.手動實現call方法
簡易版(不考慮context非對象狀況,不考慮Symbol\BigInt 不能 new.constructor( context )狀況)
/**
* context: 要改變的函數中的this指向,寫誰就是誰
* args:傳遞給函數的實參信息
* this:要處理的函數 fn
*/
Function.prototype.call = function(context, ...args) {
// null,undefined,和不傳時,context爲 window
context = context == null ? window : context;
let result;
context['fn'] = this; // 把函數做爲對象的某個成員值
result = context['fn'](...args); // 把函數執行,此時函數中的this就是
delete context['fn']; // 設置完成員屬性後,刪除
return result;
}
完善版(context必須對象類型,兼容Symbol等狀況)
/**
* context: 要改變的函數中的this指向,寫誰就是誰
* args:傳遞給函數的實參信息
* this:要處理的函數 fn
*/
Function.prototype.call = function(context, ...args) {
// null,undefined,和不傳時,context爲 window
context = context == null ? window : context;
// 必須保證 context 是一個對象類型
let contextType = typeof context;
if (!/^(object|function)$/i.test(contextType)) {
// context = new context.constructor(context); // 不適用於 Symbol/BigInt
context = Object(context);
}
let result;
context['fn'] = this; // 把函數做爲對象的某個成員值
result = context['fn'](...args); // 把函數執行,此時函數中的this就是
delete context['fn']; // 設置完成員屬性後,刪除
return result;
}
10.手動實現apply方法
/**
* context: 要改變的函數中的this指向,寫誰就是誰
* args:傳遞給函數的實參信息
* this:要處理的函數 fn
*/
Function.prototype.apply = function(context, args) {
context = context == null ? window : context;
let contextType = typeof context;
if (!/^(object|function)$/i.test(contextType)) {
context = Object(context);
}
let result;
context['fn'] = this;
result = context['fn'](...args);
delete context['fn'];
return result;
}
11.手動實現bind方法
/**
* this: 要處理的函數 func
* context: 要改變的函數中的this指向 obj
* params:要處理的函數傳遞的實參 [10, 20]
*/
Function.prototype._bind = function(context, ...params) {
let _this = this; // this: 要處理的函數
return function anonymous (...args) {
// args: 可能傳遞的事件對象等信息 [MouseEvent]
// this:匿名函數中的this是由當初綁定的位置 觸發決定的 (總之不是要處理的函數func)
// 因此須要_bind函數 剛進來時,保存要處理的函數 _this = this
_this.call(context, ...params.concat(args));
}
}
12.ES5實現數組扁平化flat方法
思路:
-
循環數組裏的每個元素 -
判斷該元素是否爲數組 -
是數組的話,繼續循環遍歷這個元素——數組 -
不是數組的話,把元素添加到新的數組中
let arr = [
[1, 2, 2],
[3, 4, 5, 5],
[6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10
];
function myFlat() {
_this = this; // 保存 this:arr
let newArr = [];
// 循環arr中的每一項,把不是數組的元素存儲到 newArr中
let cycleArray = (arr) => {
for (let i=0; i< arr.length; i++) {
let item = arr[i];
if (Array.isArray(item)) { // 元素是數組的話,繼續循環遍歷該數組
cycleArray(item);
continue;
} else{
newArr.push(item); // 不是數組的話,直接添加到新數組中
}
}
}
cycleArray(_this); // 循環數組裏的每一個元素
return newArr; // 返回新的數組對象
}
Array.prototype.myFlat = myFlat;
arr = arr.myFlat(); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
13.ES6實現數組扁平化flat方法
const myFlat = (arr) => {
let newArr = [];
let cycleArray = (arr) => {
for(let i = 0; i < arr.length; i++) {
let item = arr[i];
if (Array.isArray(item)) {
cycleArray(item);
continue;
} else {
newArr.push(item);
}
}
}
cycleArray(arr);
return newArr;
}
myFlat(arr); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10]
14.使用reduce手動實現數組扁平化flat方法
根據Array.isArray
逐個判斷數組裏的每一項是否爲數組元素
const myFlat = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? myFlat(cur) : cur);
}, []);
};
console.log(myFlat(arr));
// [12, 23, 34, 56, 78, 90, 100, 110, 120, 130, 140]
15.用不一樣的三種思想實現數組去重?
思想一:數組最後一項元素替換掉當前項元素,並刪除最後一項元素
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
for(let i = 0; i < arr.length - 1; i++) {
let item = arr[i]; // 取得當前數組中的每一項
let remainArgs = arr.slice(i+1); // 從 i+1項開始截取數組中剩餘元素,包括i+1位置的元素
if (remainArgs.indexOf(item) > -1) { // 數組的後面元素 包含當前項
arr[i] = arr[arr.length - 1]; // 用數組最後一項替換當前項
arr.length--; // 刪除數組最後一項
i--; // 仍從當前項開始比較
}
}
console.log(arr); // [ 16, 23, 12, 15, 25 ]
思想二:新容器存儲思想——對象鍵值對
思想:
把數組元素做爲對象屬性
,經過遍歷數組,判斷數組元素是否已是對象的屬性
,若是對象屬性定義過,則證實是重複元素,進而刪除重複元素
let obj = {};
for (let i=0; i < arr.length; i++) {
let item = arr[i]; // 取得當前項
if (typeof obj[item] !== 'undefined') {
// obj 中存在當前屬性,則證實當前項 以前已是 obj屬性了
// 刪除當前項
arr[i] = arr[arr.length-1];
arr.length--;
i--;
}
obj[item] = item; // obj {10: 10, 16: 16, 25: 25 ...}
}
obj = null; // 垃圾回收
console.log(arr); // [ 16, 23, 12, 15, 25 ]
思想三:相鄰項的處理方案思想——基於正則
let arr = [12, 23, 12, 15, 25, 23, 16, 25, 16];
arr.sort((a,b) => a-b);
arrStr = arr.join('@') + '@';
let reg = /(\d+@)\1*/g,
newArr = [];
arrStr.replace(reg, (val, group1) => {
// newArr.push(Number(group1.slice(0, group1.length-1)));
newArr.push(parseFloat(group1));
})
console.log(newArr); // [ 12, 15, 16, 23, 25 ]
16.基於Generator函數實現async/await原理
核心: 傳遞給我一個
Generator
函數,把函數中的內容基於Iterator
迭代器的特色一步步的執行
function readFile(file) {
return new Promise(resolve => {
setTimeout(() => {
resolve(file);
}, 1000);
})
};
function asyncFunc(generator) {
const iterator = generator(); // 接下來要執行next
// data爲第一次執行以後的返回結果,用於傳給第二次執行
const next = (data) => {
let { value, done } = iterator.next(data); // 第二次執行,並接收第一次的請求結果 data
if (done) return; // 執行完畢(到第三次)直接返回
// 第一次執行next時,yield返回的 promise實例 賦值給了 value
value.then(data => {
next(data); // 當第一次value 執行完畢且成功時,執行下一步(並把第一次的結果傳遞下一步)
});
}
next();
};
asyncFunc(function* () {
// 生成器函數:控制代碼一步步執行
let data = yield readFile('a.js'); // 等這一步驟執行執行成功以後,再往下走,沒執行完的時候,直接返回
data = yield readFile(data + 'b.js');
return data;
})
17.基於Promise封裝Ajax
思路:
-
返回一個新的 Promise
實例 -
建立 HMLHttpRequest
異步對象 -
調用 open
方法,打開url
,與服務器創建連接(發送前的一些處理) -
監聽 Ajax
狀態信息 -
xhr.status == 200
,返回resolve
狀態 -
xhr.status == 404
,返回reject
狀態 -
若是 xhr.readyState == 4
(表示服務器響應完成,能夠獲取使用服務器的響應了) -
xhr.readyState !== 4
,把請求主體的信息基於send
發送給服務器
function ajax(url, method) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(url, method, true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(xhr.responseText)
} else if (xhr.status === 404) {
reject(new Error('404'))
}
} else {
reject('請求數據失敗')
}
}
xhr.send(null)
})
}
18.手動實現JSONP跨域
思路:
-
建立 script
標籤 -
設置 script
標籤的src
屬性,以問號傳遞參數
,設置好回調函數callback
名稱 -
插入到 html
文本中 -
調用回調函數, res
參數就是獲取的數據
let script = document.createElement('script');
script.src = 'http://www.baidu.cn/login?username=JasonShu&callback=callback';
document.body.appendChild(script);
function callback (res) {
console.log(res);
}
19.手動實現sleep
某個時間事後,就去執行某個函數,基於Promise
封裝異步任務。
await
後面的代碼都會放到微任務隊列中去異步執行。
/**
*
* @param {*} fn 要執行的函數
* @param {*} wait 等待的時間
*/
function sleep(wait) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, wait)
})
}
let sayHello = (name) => console.log(`hello ${name}`);
async function autoRun() {
await sleep(3000);
let demo1 = sayHello('時光屋小豪');
let demo2 = sayHello('掘友們');
let demo3 = sayHello('公衆號的朋友們');
};
autoRun();
20.ES5手動實現數組reduce
特色:
-
初始值不傳時的特殊處理:會默認使用數組中的第一個元素 -
函數的返回結果會做爲下一次循環的 prev
-
回調函數一共接受四個參數
(arr.reduce(prev, next, currentIndex, array))
) -
prev
:上一次調用回調時返回的值 -
正在處理的元素 -
正在處理的元素的索引 -
正在遍歷的集合對象
Array.prototype.myReduce = function(fn, prev) {
for (let i = 0; i < this.length; i++) {
if (typeof prev === 'undefined') {
prev = fn(this[i], this[i+1], i+1, this);
++i;
} else {
prev = fn(prev, this[i], i, this);
}
}
return prev
}
測試用例
let sum = [1, 2, 3].myReduce((prev, next) => {
return prev + next
});
console.log(sum); // 6
21.手動實現通用柯理化函數
柯理化函數含義: 是給函數分步傳遞參數,每次傳遞部分參數,並返回一個更具體的函數接收剩下的參數,這中間可嵌套多層這樣的接收部分參數的函數,直至返回最後結果。
// add的參數不固定,看有幾個數字累計相加
function add (a,b,c,d) {
return a+b+c+d
}
function currying (fn, ...args) {
// fn.length 回調函數的參數的總和
// args.length currying函數 後面的參數總和
// 如:add (a,b,c,d) currying(add,1,2,3,4)
if (fn.length === args.length) {
return fn(...args)
} else {
// 繼續分步傳遞參數 newArgs 新一次傳遞的參數
return function anonymous(...newArgs) {
// 將先傳遞的參數和後傳遞的參數 結合在一塊兒
let allArgs = [...args, ...newArgs]
return currying(fn, ...allArgs)
}
}
}
let fn1 = currying(add, 1, 2) // 3
let fn2 = fn1(3) // 6
let fn3 = fn2(4) // 10
23.ES5實現一個繼承
寄生組合繼承(ES5繼承的最佳方式)
所謂寄生組合式繼承,即經過借用構造函數來繼承屬性
,經過原型鏈的形式來繼承方法
。
只調用了一次父類
構造函數,效率更高。避免在子類.prototype
上面建立沒必要要的、多餘的屬性,與其同時,原型鏈還能保持不變。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child(name, age) {
Parent.call(this, name); // 調用父類的構造函數,將父類構造函數內的this指向子類的實例
this.age = age;
}
//寄生組合式繼承
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.getAge = function () {
return this.age;
}
let girl = new Child('Lisa', 18);
girl.getName();
24.手動實現發佈訂閱
發佈訂閱的核心:: 每次event. emit(發佈),就會觸發一次event. on(註冊)
class EventEmitter {
constructor() {
// 事件對象,存放訂閱的名字和事件
this.events = {};
}
// 訂閱事件的方法
on(eventName,callback) {
if (!this.events[eventName]) {
// 注意數據,一個名字能夠訂閱多個事件函數
this.events[eventName] = [callback];
} else {
// 存在則push到指定數組的尾部保存
this.events[eventName].push(callback)
}
}
// 觸發事件的方法
emit(eventName) {
// 遍歷執行全部訂閱的事件
this.events[eventName] && this.events[eventName].forEach(cb => cb());
}
}
測試用例
let em = new EventEmitter();
function workDay() {
console.log("天天工做");
}
function makeMoney() {
console.log("賺100萬");
}
function sayLove() {
console.log("向喜歡的人示愛");
}
em.on("money",makeMoney);
em.on("love",sayLove);
em.on("work", workDay);
em.emit("money");
em.emit("love");
em.emit("work");
26.手動實現觀察者模式
觀察者模式(基於發佈訂閱模式) 有觀察者,也有被觀察者
觀察者須要放到被觀察者中
,被觀察者的狀態變化須要通知觀察者
我變化了 內部也是基於發佈訂閱模式,收集觀察者,狀態變化後要主動通知觀察者
class Subject { // 被觀察者 學生
constructor(name) {
this.state = '開心的'
this.observers = []; // 存儲全部的觀察者
}
// 收集全部的觀察者
attach(o){ // Subject. prototype. attch
this.observers.push(o)
}
// 更新被觀察者 狀態的方法
setState(newState) {
this.state = newState; // 更新狀態
// this 指被觀察者 學生
this.observers.forEach(o => o.update(this)) // 通知觀察者 更新它們的狀態
}
}
class Observer{ // 觀察者 父母和老師
constructor(name) {
this.name = name
}
update(student) {
console.log('當前' + this.name + '被通知了', '當前學生的狀態是' + student.state)
}
}
let student = new Subject('學生');
let parent = new Observer('父母');
let teacher = new Observer('老師');
// 被觀察者存儲觀察者的前提,須要先接納觀察者
student. attach(parent);
student. attach(teacher);
student. setState('被欺負了');
27.手動實現Object.freeze
Object.freeze
凍結一個對象,讓其不能再添加/刪除屬性,也不能修改該對象已有屬性的可枚舉性、可配置可寫性
,也不能修改已有屬性的值和它的原型屬性,最後返回一個和傳入參數相同的對象。
function myFreeze(obj){
// 判斷參數是否爲Object類型,若是是就封閉對象,循環遍歷對象。去掉原型屬性,將其writable特性設置爲false
if(obj instanceof Object){
Object.seal(obj); // 封閉對象
for(let key in obj){
if(obj.hasOwnProperty(key)){
Object.defineProperty(obj,key,{
writable:false // 設置只讀
})
// 若是屬性值依然爲對象,要經過遞歸來進行進一步的凍結
myFreeze(obj[key]);
}
}
}
}
28.手動實現Promise.all
Promise.all
:有一個promise任務失敗就所有失敗
Promise.all
方法返回的是一個promise
function isPromise (val) {
return typeof val.then === 'function'; // (123).then => undefined
}
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let arr = []; // 存放 promise執行後的結果
let index = 0; // 計數器,用來累計promise的已執行次數
const processData = (key, data) => {
arr[key] = data; // 不能使用數組的長度來計算
/*
if (arr.length == promises.length) {
resolve(arr); // [null, null , 1, 2] 因爲Promise異步比較慢,因此還未返回
}
*/
if (++index === promises.length) {
// 必須保證數組裏的每個
resolve(arr);
}
}
// 遍歷數組依次拿到執行結果
for (let i = 0; i < promises.length; i++) {
let result = promises[i];
if(isPromise(result)) {
// 讓裏面的promise執行,取得成功後的結果
// data promise執行後的返回結果
result.then((data) => {
// 處理數據,按照原數組的順序依次輸出
processData(i ,data)
}, reject) // reject本事就是個函數 因此簡寫了
} else {
// 1 , 2
processData(i ,result)
}
}
})
}
測試用例
let fs = require('fs').promises;
let getName = fs.readFile('./name.txt', 'utf8');
let getAge = fs.readFile('./age.txt', 'utf8');
Promise.all([1, getName, getAge, 2]).then(data => {
console.log(data); // [ 1, 'name', '11', 2 ]
})
29.手動實現Promise.allSettled
MDN:
Promise.allSettled()
方法返回一個在全部給定的promise都已經fulfilled
或rejected
後的promise,並帶有一個對象數組,每一個對象表示對應的promise結果。
當您有多個彼此不依賴的異步任務成功完成時,或者您老是想知道每一個
promise
的結果時,一般使用它。
【譯】Promise.allSettled
跟Promise.all
相似, 其參數接受一個Promise
的數組, 返回一個新的Promise
, 惟一的不一樣在於, 其不會進行短路, 也就是說當Promise
所有處理完成後咱們能夠拿到每一個Promise
的狀態, 而無論其是否處理成功。
用法 | 測試用例
let fs = require('fs').promises;
let getName = fs.readFile('./name.txt', 'utf8'); // 讀取文件成功
let getAge = fs.readFile('./age.txt', 'utf8');
Promise.allSettled([1, getName, getAge, 2]).then(data => {
console.log(data);
});
// 輸出結果
/*
[
{ status: 'fulfilled', value: 1 },
{ status: 'fulfilled', value: 'zf' },
{ status: 'fulfilled', value: '11' },
{ status: 'fulfilled', value: 2 }
]
*/
let getName = fs.readFile('./name123.txt', 'utf8'); // 讀取文件失敗
let getAge = fs.readFile('./age.txt', 'utf8');
// 輸出結果
/*
[
{ status: 'fulfilled', value: 1 },
{
status: 'rejected',
value: [Error: ENOENT: no such file or directory, open './name123.txt'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: './name123.txt'
}
},
{ status: 'fulfilled', value: '11' },
{ status: 'fulfilled', value: 2 }
]
*/
實現
function isPromise (val) {
return typeof val.then === 'function'; // (123).then => undefined
}
Promise.allSettled = function(promises) {
return new Promise((resolve, reject) => {
let arr = [];
let times = 0;
const setData = (index, data) => {
arr[index] = data;
if (++times === promises.length) {
resolve(arr);
}
console.log('times', times)
}
for (let i = 0; i < promises.length; i++) {
let current = promises[i];
if (isPromise(current)) {
current.then((data) => {
setData(i, { status: 'fulfilled', value: data });
}, err => {
setData(i, { status: 'rejected', value: err })
})
} else {
setData(i, { status: 'fulfilled', value: current })
}
}
})
}
30.手動實現Promise.prototype.finally
前面的promise
無論成功仍是失敗,都會走到finally
中,而且finally
以後,還能夠繼續then
(說明它仍是一個then
方法是關鍵
),而且會將初始的promise
值原封不動的傳遞給後面的then.
Promise.prototype.finally最大的做用
-
finally
裏的函數,不管如何都會執行,並會把前面的值原封不動傳遞給下一個then
方法中(至關於起了一箇中間過渡的做用)——對應狀況1,2,3
-
若是
finally
函數中有promise
等異步任務,會等它們所有執行完畢,再結合以前的成功與否狀態,返回值
Promise.prototype.finally六大狀況用法
// 狀況1
Promise.resolve(123).finally((data) => { // 這裏傳入的函數,不管如何都會執行
console.log(data); // undefined
})
// 狀況2 (這裏,finally方法至關於作了中間處理,起一個過渡的做用)
Promise.resolve(123).finally((data) => {
console.log(data); // undefined
}).then(data => {
console.log(data); // 123
})
// 狀況3 (這裏只要reject,都會走到下一個then的err中)
Promise.reject(123).finally((data) => {
console.log(data); // undefined
}).then(data => {
console.log(data);
}, err => {
console.log(err, 'err'); // 123 err
})
// 狀況4 (一開始就成功以後,會等待finally裏的promise執行完畢後,再把前面的data傳遞到下一個then中)
Promise.resolve(123).finally((data) => {
console.log(data); // undefined
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok');
}, 3000)
})
}).then(data => {
console.log(data, 'success'); // 123 success
}, err => {
console.log(err, 'err');
})
// 狀況5 (雖然一開始成功,可是隻要finally函數中的promise失敗了,就會把其失敗的值傳遞到下一個then的err中)
Promise.resolve(123).finally((data) => {
console.log(data); // undefined
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('rejected');
}, 3000)
})
}).then(data => {
console.log(data, 'success');
}, err => {
console.log(err, 'err'); // rejected err
})
// 狀況6 (雖然一開始失敗,可是也要等finally中的promise執行完,才能把一開始的err傳遞到err的回調中)
Promise.reject(123).finally((data) => {
console.log(data); // undefined
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('resolve');
}, 3000)
})
}).then(data => {
console.log(data, 'success');
}, err => {
console.log(err, 'err'); // 123 err
})
源碼實現
Promise.prototype.finally = function (callback) {
return this.then((data) => {
// 讓函數執行 內部會調用方法,若是方法是promise,須要等待它完成
// 若是當前promise執行時失敗了,會把err傳遞到,err的回調函數中
return Promise.resolve(callback()).then(() => data); // data 上一個promise的成功態
}, err => {
return Promise.resolve(callback()).then(() => {
throw err; // 把以前的失敗的err,拋出去
});
})
}
本文總結
詳見腦圖哦~
感謝你們
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
-
點個「 在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-) -
歡迎加我微信「 Augenstern9728」一塊兒交流學習... -
關注公衆號「 前端時光屋」,持續爲你推送精選好文。
本文分享自微信公衆號 - 前端時光屋(javascriptlab)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。