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

前言

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

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

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

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

腦圖👇web

30道JS高頻手撕題

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 === nullreturn 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 = [
    [122],
    [3455],
    [6789, [1112, [1213, [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 = [122312152523162516];

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 = [122312152523162516];

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+1this);
      ++i;
    } else {
      prev = fn(prev, this[i], i, this);
    }
  }
  return prev
}

測試用例

let sum = [123].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, 12// 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. 歡迎加我微信「 Augenstern9728」一塊兒交流學習...
  3. 關注公衆號「 前端時光屋」,持續爲你推送精選好文。


本文分享自微信公衆號 - 前端時光屋(javascriptlab)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索