「靈魂之做」2020斬獲30道高頻JS手撕面試題

前言

最近在準備面試,恰好利用幾天的時間系統的整理了下JS常常考的手撕題類型。html

在這裏,以腦圖的形式帶你們手撕這篇文章裏的全部題(帶註釋)。前端

想領取腦圖的關注前端時光屋公衆號,回覆「JS手撕」,便可領取❤️面試

腦圖裏的全部題型便是本文中的30道常考高頻題ajax

腦圖👇 跨域

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.手動實現一個深克隆(簡易版)

深克隆:層層拷貝對象或數組的每一層內容promise

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:一直循環(執行原型鏈的查找機制)markdown

  • 取得當前實例對象原型鏈的原型鏈(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.readyState == 4(表示服務器響應完成,能夠獲取使用服務器的響應了)

    • xhr.status == 200,返回resolve狀態
    • xhr.status == 404,返回reject狀態
  • 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封裝異步任務

/** * * @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("aaa");
}

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都已經fulfilledrejected後的promise,並帶有一個對象數組,每一個對象表示對應的promise結果。

當您有多個彼此不依賴的異步任務成功完成時,或者您老是想知道每一個promise的結果時,一般使用它。

【譯】Promise.allSettledPromise.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最大的做用

  1. finally裏的函數,不管如何都會執行,並會把前面的值原封不動傳遞給下一個then方法中

    (至關於起了一箇中間過渡的做用)——對應狀況1,2,3

  2. 若是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,拋出去
    });
  })
}
複製代碼

本文總結

詳見腦圖哦~

看完三件事❤️

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。

  2. 關注公衆號 『前端時光屋』,不按期分享原創知識。

  3. 同時能夠期待後續文章ing🚀

相關文章
相關標籤/搜索