京東前端二面高頻手寫面試題(持續更新中)

hello_world_1024發表於2023-03-06

實現釋出-訂閱模式

class EventCenter{
  // 1. 定義事件容器,用來裝事件陣列
    let handlers = {}

  // 2. 新增事件方法,引數:事件名 事件方法
  addEventListener(type, handler) {
    // 建立新陣列容器
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 觸發事件,引數:事件名 事件引數
  dispatchEvent(type, params) {
    // 若沒有註冊該事件則丟擲錯誤
    if (!this.handlers[type]) {
      return new Error('該事件未註冊')
    }
    // 觸發事件
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 4. 事件移除,引數:事件名 要刪除事件,若無第二個引數則刪除該事件的訂閱和釋出
  removeEventListener(type, handler) {
    if (!this.handlers[type]) {
      return new Error('事件無效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {
      const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {
        return new Error('無該繫結事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {
        delete this.handlers[type]
      }
    }
  }
}

判斷物件是否存在迴圈引用

迴圈引用物件本來沒有什麼問題,但是序列化的時候就會發生問題,比如呼叫JSON.stringify()對該類物件進行序列化,就會報錯: Converting circular structure to JSON.

下面方法可以用來判斷一個物件中是否已存在迴圈引用:

const isCycleObject = (obj,parent) => {
    const parentArr = parent || [obj];
    for(let i in obj) {
        if(typeof obj[i] === 'object') {
            let flag = false;
            parentArr.forEach((pObj) => {
                if(pObj === obj[i]){
                    flag = true;
                }
            })
            if(flag) return true;
            flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
            if(flag) return true;
        }
    }
    return false;
}


const a = 1;
const b = {a};
const c = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;

console.log(isCycleObject(o)

查詢有序二維陣列的目標值:

var findNumberIn2DArray = function(matrix, target) {
    if (matrix == null || matrix.length == 0) {
        return false;
    }
    let row = 0;
    let column = matrix[0].length - 1;
    while (row < matrix.length && column >= 0) {
        if (matrix[row][column] == target) {
            return true;
        } else if (matrix[row][column] > target) {
            column--;
        } else {
            row++;
        }
    }
    return false;
};

二維陣列斜向列印:

function printMatrix(arr){
  let m = arr.length, n = arr[0].length
    let res = []

  // 左上角,從0 到 n - 1 列進行列印
  for (let k = 0; k < n; k++) {
    for (let i = 0, j = k; i < m && j >= 0; i++, j--) {
      res.push(arr[i][j]);
    }
  }

  // 右下角,從1 到 n - 1 行進行列印
  for (let k = 1; k < m; k++) {
    for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) {
      res.push(arr[i][j]);
    }
  }
  return res
}

手寫 Promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
  // 儲存初始化狀態
  var self = this;

  // 初始化狀態
  this.state = PENDING;

  // 用於儲存 resolve 或者 rejected 傳入的值
  this.value = null;

  // 用於儲存 resolve 的回撥函式
  this.resolvedCallbacks = [];

  // 用於儲存 reject 的回撥函式
  this.rejectedCallbacks = [];

  // 狀態轉變為 resolved 方法
  function resolve(value) {
    // 判斷傳入元素是否為 Promise 值,如果是,則狀態改變必須等待前一個狀態改變後再進行改變
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保證程式碼的執行順序為本輪事件迴圈的末尾
    setTimeout(() => {
      // 只有狀態為 pending 時才能轉變,
      if (self.state === PENDING) {
        // 修改狀態
        self.state = RESOLVED;

        // 設定傳入的值
        self.value = value;

        // 執行回撥函式
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 狀態轉變為 rejected 方法
  function reject(value) {
    // 保證程式碼的執行順序為本輪事件迴圈的末尾
    setTimeout(() => {
      // 只有狀態為 pending 時才能轉變
      if (self.state === PENDING) {
        // 修改狀態
        self.state = REJECTED;

        // 設定傳入的值
        self.value = value;

        // 執行回撥函式
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 將兩個方法傳入函式執行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到錯誤時,捕獲錯誤,執行 reject 函式
    reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判斷兩個引數是否為函式型別,因為這兩個引數是可選引數
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };

  // 如果是等待狀態,則將函式加入對應列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }

  // 如果狀態已經凝固,則直接執行對應狀態的函式

  if (this.state === RESOLVED) {
    onResolved(this.value);
  }

  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

實現Promise相關方法

實現Promise的resolve

實現 resolve 靜態方法有三個要點:
  • 傳參為一個 Promise, 則直接返回它。
  • 傳參為一個 thenable 物件,返回的 Promise 會跟隨這個物件,採用它的最終狀態作為自己的狀態。
  • 其他情況,直接返回以該值為成功狀態的promise物件。
Promise.resolve = (param) => {
  if(param instanceof Promise) return param;
  return new Promise((resolve, reject) => {
    if(param && param.then && typeof param.then === 'function') {
      // param 狀態變為成功會呼叫resolve,將新 Promise 的狀態變為成功,反之亦然
      param.then(resolve, reject);
    }else {
      resolve(param);
    }
  })
}

實現 Promise.reject

Promise.reject 中傳入的引數會作為一個 reason 原封不動地往下傳, 實現如下:
Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

實現 Promise.prototype.finally

前面的promise不管成功還是失敗,都會走到finally中,並且finally之後,還可以繼續then(說明它還是一個then方法是關鍵),並且會將初始的promise值原封不動的傳遞給後面的then.

Promise.prototype.finally最大的作用

  • finally裡的函式,無論如何都會執行,並會把前面的值原封不動傳遞給下一個then方法中
  • 如果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,丟擲去
    });
  })
}

實現 Promise.all

對於 all 方法而言,需要完成下面的核心功能:
  • 傳入引數為一個空的可迭代物件,則直接進行resolve
  • 如果引數中有一個promise失敗,那麼Promise.all返回的promise物件失敗。
  • 在任何情況下,Promise.all 返回的 promise 的完成狀態的結果都是一個陣列
Promise.all = function(promises) {
  return new Promise((resolve, reject) => {
    let result = [];
    let index = 0;
    let len = promises.length;
    if(len === 0) {
      resolve(result);
      return;
    }

    for(let i = 0; i < len; i++) {
      // 為什麼不直接 promise[i].then, 因為promise[i]可能不是一個promise
      Promise.resolve(promise[i]).then(data => {
        result[i] = data;
        index++;
        if(index === len) resolve(result);
      }).catch(err => {
        reject(err);
      })
    }
  })
}

實現promise.allsettle

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 })
      }
    }
  })
}

實現 Promise.race

race 的實現相比之下就簡單一些,只要有一個 promise 執行完,直接 resolve 並停止執行
Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    let len = promises.length;
    if(len === 0) return;
    for(let i = 0; i < len; i++) {
      Promise.resolve(promise[i]).then(data => {
        resolve(data);
        return;
      }).catch(err => {
        reject(err);
        return;
      })
    }
  })
}

實現一個簡版Promise

// 使用
var promise = new Promise((resolve,reject) => {
    if (操作成功) {
        resolve(value)
    } else {
        reject(error)
    }
})
promise.then(function (value) {
    // success
},function (value) {
    // failure
})
function myPromise(constructor) {
    let self = this;
    self.status = "pending"   // 定義狀態改變前的初始狀態
    self.value = undefined;   // 定義狀態為resolved的時候的狀態
    self.reason = undefined;  // 定義狀態為rejected的時候的狀態
    function resolve(value) {
       if(self.status === "pending") {
          self.value = value;
          self.status = "resolved";
       }
    }
    function reject(reason) {
       if(self.status === "pending") {
          self.reason = reason;
          self.status = "rejected";
       }
    }
    // 捕獲構造異常
    try {
       constructor(resolve,reject);
    } catch(e) {
       reject(e);
    }
}
// 新增 then 方法
myPromise.prototype.then = function(onFullfilled,onRejected) {
   let self = this;
   switch(self.status) {
      case "resolved":
        onFullfilled(self.value);
        break;
      case "rejected":
        onRejected(self.reason);
        break;
      default:       
   }
}

var p = new myPromise(function(resolve,reject) {
    resolve(1)
});
p.then(function(x) {
    console.log(x) // 1
})

使用class實現

class MyPromise {
  constructor(fn) {
    this.resolvedCallbacks = [];
    this.rejectedCallbacks = [];

    this.state = 'PENDING';
    this.value = '';

    fn(this.resolve.bind(this), this.reject.bind(this));

  }

  resolve(value) {
    if (this.state === 'PENDING') {
      this.state = 'RESOLVED';
      this.value = value;

      this.resolvedCallbacks.map(cb => cb(value));   
    }
  }

  reject(value) {
    if (this.state === 'PENDING') {
      this.state = 'REJECTED';
      this.value = value;

      this.rejectedCallbacks.map(cb => cb(value));
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === 'PENDING') {
      this.resolvedCallbacks.push(onFulfilled);
      this.rejectedCallbacks.push(onRejected);

    }

    if (this.state === 'RESOLVED') {
      onFulfilled(this.value);
    }

    if (this.state === 'REJECTED') {
      onRejected(this.value);
    }
  }
}

Promise 實現-詳細

  • 可以把 Promise 看成一個狀態機。初始是 pending 狀態,可以透過函式 resolvereject ,將狀態轉變為 resolved或者 rejected 狀態,狀態一旦改變就不能再次變化。
  • then 函式會返回一個 Promise 例項,並且該返回值是一個新的例項而不是之前的例項。因為 Promise 規範規定除了 pending 狀態,其他狀態是不可以改變的,如果返回的是一個相同例項的話,多個 then 呼叫就失去意義了。
  • 對於 then來說,本質上可以把它看成是 flatMap
// 三種狀態
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一個函式引數,該函式會立即執行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用於儲存 then 中的回撥,只有當 promise
  // 狀態為 pending 時才會快取,並且每個例項至多快取一個
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是個 Promise,遞迴執行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 非同步執行,保證執行順序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 非同步執行,保證執行順序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用於解決以下問題
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 規範 2.2.7,then 必須返回一個新的 promise
  var promise2;
  // 規範 2.2.onResolved 和 onRejected 都為可選引數
  // 如果型別不是函式需要忽略,同時也實現了透傳
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 規範 2.2.4,保證 onFulfilled,onRjected 非同步執行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 非同步執行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考慮到可能會有報錯,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 規範 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 規範 2.3.1,x 不能和 promise2 相同,避免迴圈引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 規範 2.3.2
  // 如果 x 為 Promise,狀態為 pending 需要繼續等待否則執行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次呼叫該函式是為了確認 x resolve 的
        // 引數是什麼型別,如果是基本型別就再次 resolve
        // 把值傳給下個 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 規範 2.3.3.3.3
  // reject 或者 resolve 其中一個執行過得話,忽略其他的
  let called = false;
  // 規範 2.3.3,判斷 x 是否為物件或者函式
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 規範 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 規範 2.3.3.1
      let then = x.then;
      // 如果 then 是函式,呼叫 x.then
      if (typeof then === "function") {
        // 規範 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 規範 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 規範 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 規範 2.3.4,x 為基本型別
    resolve(x);
  }
}

實現Promisify

const fs = require('fs')
const path = require('path')

// node中使用
// const fs = require('fs').promises 12.18版
// const promisify = require('util').promisify

// 包裝node api promise化 典型的高階函式
const promisify = fn=>{
  return (...args)=>{
    return new Promise((resolve,reject)=>{
      fn(...args, (err,data)=>{
        if(err) {
          reject(err)
        } 
        resolve(data)
      })
    })
  }
}

// const read = promisify(fs.readFile)

// read(path.join(__dirname, './promise.js'), 'utf8').then(d=>{
//   console.log(d)
// })

// promise化node所有api
const promisifyAll = target=>{
  Reflect.ownKeys(target).forEach(key=>{
    if(typeof target[key] === 'function') {
      target[key+'Async'] = promisify(target[key])
    }
  })
  return target
}

// promise化fs下的函式
const promisifyNew = promisifyAll(fs)

promisifyNew.readFileAsync(path.join(__dirname, './promise.js'), 'utf8').then(d=>{
  console.log(d)
})

module.exports = {
  promisify,
  promisifyAll
}

完整實現Promises/A+規範

/**
 * Promises/A+規範 實現一個promise
 * https://promisesaplus.com/
*/

const EMUM = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
}

// x 返回值
// promise2 then的時候new的promise
// promise2的resolve, reject
const resolvePromise = (x, promise2, resolve, reject)=>{
  // 解析promise的值解析promise2是成功還是失敗 傳遞到下層then
  if(x === promise2) {
    reject(new TypeError('型別錯誤'))
  }
  // 這裡的x如果是一個promise的話 可能是其他的promise,可能呼叫了成功 又呼叫了失敗
  // 防止resolve的時候 又throw err丟擲異常到reject了
  let called
  // 如果x是promise 那麼就採用他的狀態
  // 有then方法是promise
  if(typeof x === 'object' && typeof x!== null || typeof x === 'function') {
    // x是物件或函式
    try {
      let then = x.then // 快取,不用多次取值
      if(typeof then === 'function') {
        // 是promise,呼叫then方法裡面有this,需要傳入this為x才能取到then方法裡面的值this.value
        then.call(x, y=>{// 成功
          // y值可能也是一個promise 如resolve(new Promise()) 此時的y==new Promise()
          // 遞迴解析y,直到拿到普通的值resolve(x出去)
          if(called) return;
          called = true;

          resolvePromise(y, promise2, resolve, reject)
        },r=>{// 一旦失敗直接失敗
          if(called) return;
          called = true;
          reject(r)
        })
      } else {
        // 普通物件不是promise
        resolve(x)
      }
    } catch (e) {
      // 物件取值可能報錯,用defineProperty定義get 丟擲異常
      if(called) return;
      called = true;
      reject(e)
    }
  } else {
    // x是普通值
    resolve(x) // 直接成功
  }

}
class myPromise {
  constructor(executor) {
    this.status = EMUM.PENDING // 當前狀態
    this.value = undefined // resolve接收值
    this.reason = undefined // reject失敗返回值

    /**
     * 同一個promise可以then多次(釋出訂閱模式)
     * 呼叫then時 當前狀態是等待態,需要將當前成功或失敗的回撥存放起來(訂閱)
     * 呼叫resolve時 將訂閱函式進行執行(釋出)
    */
    // 成功佇列
    this.onResolvedCallbacks = []
    // 失敗佇列
    this.onRejectedCallbacks = []
    const resolve = value =>{
      // 如果value是一個promise,需要遞迴解析
      // 如 myPromise.resolve(new myPromise()) 需要解析value
      if(value instanceof myPromise) {
        // 不停的解析 直到值不是promise
        return value.then(resolve,reject)
      }

      if(this.status === EMUM.PENDING) {
        this.status = EMUM.FULFILLED
        this.value = value

        this.onResolvedCallbacks.forEach(fn=>fn())
      }
    }
    const reject = reason =>{
      if(this.status === EMUM.PENDING) {
        this.status = EMUM.REJECTED
        this.reason = reason

        this.onRejectedCallbacks.forEach(fn=>fn())
      }
    }
    try {
      executor(resolve,reject)
    } catch(e) {
      reject(e)
    }
  }
  then(onFulFilled, onRejected) {
    // 透傳 處理預設不傳的情況
    // new Promise((resolve,reject)=>{
    //   resolve(1)
    // }).then().then().then(d=>{})
    // new Promise((resolve,reject)=>{
    //   resolve(1)
    // }).then(v=>v).then(v=>v).then(d=>{})
    // new Promise((resolve,reject)=>{
    //   reject(1)
    // }).then().then().then(null, e=>{console.log(e)})
    // new Promise((resolve,reject)=>{
    //   reject(1)
    // }).then(null,e=>{throw e}).then(null,e=>{throw e}).then(null,e=>{console.log(e)})
    onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : v => v
    onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}

    // 呼叫then 建立一個新的promise
    let promise2 = new myPromise((resolve,reject)=>{
      // 根據value判斷是resolve 還是reject value也可能是promise
      if(this.status === EMUM.FULFILLED) {
        setTimeout(() => {
          try {
            // 成功回撥結果
            let x = onFulFilled(this.value)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        }, 0);
      }
      if(this.status === EMUM.REJECTED) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        }, 0);
      }
      // 使用者還未呼叫resolve或reject方法
      if(this.status === EMUM.PENDING) {
        this.onResolvedCallbacks.push(()=>{
          try {
            let x = onFulFilled(this.value)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        })
        this.onRejectedCallbacks.push(()=>{
          try {
            let x = onRejected(this.reason)
            // 解析promise
            resolvePromise(x, promise2,resolve,reject)
          } catch (error) {
            reject(error)
          }
        })
      }
    })

    return promise2
  }
  catch(errCallback) {
    // 等同於沒有成功,把失敗放進去而已
    return this.then(null, errCallback)
  }
  // myPromise.resolve 具備等待功能的 如果引數的promise會等待promise解析完畢在向下執行
  static resolve(val) {
    return new myPromise((resolve,reject)=>{
      resolve(val)
    })
  }
  // myPromise.reject 直接將值返回
  static reject(reason) {
    return new myPromise((resolve,reject)=>{
      reject(reason)
    })
  }
  // finally傳入的函式 無論成功或失敗都執行
  // Promise.reject(100).finally(()=>{console.log(1)}).then(d=>console.log('success',d)).catch(er=>console.log('faild',er))
  // Promise.reject(100).finally(()=>new Promise()).then(d=>console.log(d)).catch(er=>)
  finally(callback) {
    return this.then((val)=>{
      return myPromise.resolve(callback()).then(()=>val)
    },(err)=>{
      return myPromise.resolve(callback()).then(()=>{throw err})
    })
  }
  // Promise.all
  static all(values) {
    return new myPromise((resolve,reject)=>{
      let resultArr = []
      let orderIndex = 0
      const processResultByKey = (value,index)=>{
        resultArr[index] = value 
        // 處理完全部
        if(++orderIndex === values.length) {
          resolve(resultArr) // 處理完成的結果返回去
        }
      }
      for (let i = 0; i < values.length; i++) {
        const value = values[i];
        // 是promise
        if(value && typeof value.then === 'function') {
          value.then((val)=>{
            processResultByKey(val,i)
          },reject)
        } else {
          // 不是promise情況
          processResultByKey(value,i)
        }
      }
    })
  }
  static race(promises) {
    // 採用最新成功或失敗的作為結果
    return new myPromise((resolve,reject)=>{
      for (let i = 0; i < promises.length; i++) {
        let val = promises[i]
        if(val && typeof val.then === 'function') {
          // 任何一個promise先呼叫resolve或reject就返回結果了 也就是返回執行最快的那個promise的結果
          val.then(resolve,reject)
        }else{
          // 普通值
          resolve(val)
        }
      }
    })
  }
}


/**
 * =====測試用例-====
 */
// let promise1 = new myPromise((resolve,reject)=>{
//   setTimeout(() => {
//     resolve('成功')
//   }, 900);
// })

// promise1.then(val=>{
//   console.log('success', val)
// },reason=>{
//   console.log('fail', reason)
// })

/**
 * then的使用方式 普通值意味不是promise
 * 
 * 1、then中的回撥有兩個方法 成功或失敗 他們的結果返回(普通值)會傳遞給外層的下一個then中
 * 2、可以在成功或失敗中丟擲異常,走到下一次then的失敗中
 * 3、返回的是一個promsie,那麼會用這個promise的狀態作為結果,會用promise的結果向下傳遞
 * 4、錯誤處理,會預設先找離自己最新的錯誤處理,找不到就向下查詢,找打了就執行
 */

// read('./name.txt').then(data=>{
//   return '123'
// }).then(data=>{

// }).then(null,err=>{

// })
// // .catch(err=>{ // catch就是沒有成功的promise

// // })

/**
 * promise.then實現原理:透過每次返回一個新的promise來實現(promise一旦成功就不能失敗,失敗就不能成功)
 * 
 */

// function read(data) {
//   return new myPromise((resolve,reject)=>{
//     setTimeout(() => {
//       resolve(new myPromise((resolve,reject)=>resolve(data)))
//     }, 1000);
//   })
// }

// let promise2 = read({name: 'poetry'}).then(data=>{
//   return data
// }).then().then().then(data=>{
//   console.log(data,'-data-')
// },(err)=>{
//   console.log(err,'-err-')
// })

// finally測試
// myPromise
//   .resolve(100)
//   .finally(()=>{
//     return new myPromise((resolve,reject)=>setTimeout(() => {
//       resolve(100)
//     }, 100))
//   })
//   .then(d=>console.log('finally success',d))
//   .catch(er=>console.log(er, 'finally err'))


/**
 * promise.all 測試
 * 
 * myPromise.all 解決併發問題 多個非同步併發獲取最終的結果
*/

// myPromise.all([1,2,3,4,new myPromise((resolve,reject)=>{
//   setTimeout(() => {
//     resolve('ok1')
//   }, 1000);
// }),new myPromise((resolve,reject)=>{
//   setTimeout(() => {
//     resolve('ok2')
//   }, 1000);
// })]).then(d=>{
//   console.log(d,'myPromise.all.resolve')
// }).catch(err=>{
//   console.log(err,'myPromise.all.reject')
// })


// 實現promise中斷請求
let promise = new Promise((resolve,reject)=>{
  setTimeout(() => {
    // 模擬介面呼叫 ajax呼叫超時
    resolve('成功') 
  }, 10000);
})

function promiseWrap(promise) {
  // 包裝一個promise 可以控制原來的promise是成功 還是失敗
  let abort
  let newPromsie = new myPromise((resolve,reject)=>{
    abort = reject
  })
  // 只要控制newPromsie失敗,就可以控制被包裝的promise走向失敗
  // Promise.race 任何一個先成功或者失敗 就可以獲得結果
  let p = myPromise.race([promise, newPromsie])
  p.abort = abort

  return p
}

let newPromise = promiseWrap(promise)

setTimeout(() => {
  // 超過3秒超時
  newPromise.abort('請求超時')
}, 3000);

newPromise.then(d=>{
  console.log('d',d)
}).catch(err=>{
  console.log('err',err)
})


// 使用promises-aplus-tests 測試寫的promise是否規範
// 全域性安裝 cnpm i -g promises-aplus-tests
// 命令列執行 promises-aplus-tests promise.js
// 測試入口 產生延遲物件
myPromise.defer = myPromise.deferred = function () {
  let dfd = {}
  dfd.promise = new myPromise((resolve,reject)=>{
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

// 延遲物件使用者
// ![](http://img-repo.poetries.top/images/20210509172817.png)
// promise解決巢狀問題
// function readData(url) {
//   let dfd = myPromise.defer()
//   fs.readFile(url, 'utf8', function (err,data) {
//     if(err) {
//       dfd.reject()
//     }
//     dfd.resolve(data)
//   })
//   return dfd.promise
// }
// readData().then(d=>{
//   return d
// })

module.exports = myPromise

實現觀察者模式

觀察者模式(基於釋出訂閱模式) 有觀察者,也有被觀察者

觀察者需要放到被觀察者中,被觀察者的狀態變化需要通知觀察者 我變化了 內部也是基於釋出訂閱模式,收集觀察者,狀態變化後要主動通知觀察者

class Subject { // 被觀察者 學生
  constructor(name) {
    this.state = 'happy'
    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('被欺負了');

圖片懶載入

// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {
  const position = el.getBoundingClientRect()
  const windowHeight = document.documentElement.clientHeight
  // 頂部邊緣可見
  const topVisible = position.top > 0 && position.top < windowHeight;
  // 底部邊緣可見
  const bottomVisible = position.bottom < windowHeight && position.bottom > 0;
  return topVisible || bottomVisible;
}

function imageLazyLoad() {
  const images = document.querySelectorAll('img')
  for (let img of images) {
    const realSrc = img.dataset.src
    if (!realSrc) continue
    if (isVisible(img)) {
      img.src = realSrc
      img.dataset.src = ''
    }
  }
}

// 測試
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))

參考 前端進階面試題詳細解答

實現一個JSON.parse

JSON.parse(text[, reviver])
用來解析JSON字串,構造由字串描述的JavaScript值或物件。提供可選的reviver函式用以在返回之前對所得到的物件執行變換(操作)

第一種:直接呼叫 eval

function jsonParse(opt) {
    return eval('(' + opt + ')');
}
jsonParse(jsonStringify({x : 5}))
// Object { x: 5}
jsonParse(jsonStringify([1, "false", false]))
// [1, "false", falsr]
jsonParse(jsonStringify({b: undefined}))
// Object { b: "undefined"}
避免在不必要的情況下使用 evaleval() 是一個危險的函式,他執行的程式碼擁有著執行者的權利。如果你用eval()執行的字串程式碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴充套件程式的許可權下,在使用者計算機上執行惡意程式碼。它會執行JS程式碼,有XSS漏洞。

如果你只想記這個方法,就得對引數json做校驗。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
    rx_one.test(
        json
            .replace(rx_two, "@")
            .replace(rx_three, "]")
            .replace(rx_four, "")
    )
) {
    var obj = eval("(" +json + ")");
}

第二種:Function

核心:Function與eval有相同的字串引數特性
var func = new Function(arg1, arg2, ..., functionBody);

在轉換JSON的實際應用中,只需要這麼做

var jsonStr = '{ "age": 20, "name": "jack" }'
var json = (new Function('return ' + jsonStr))();
evalFunction都有著動態編譯js程式碼的作用,但是在實際的程式設計中並不推薦使用

請實現一個 add 函式,滿足以下功能

add(1);             // 1
add(1)(2);      // 3
add(1)(2)(3);// 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
function add(...args) {
  // 在內部宣告一個函式,利用閉包的特性儲存並收集所有的引數值
  let fn = function(...newArgs) {
   return add.apply(null, args.concat(newArgs))
  }

  // 利用toString隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回
  fn.toString = function() {
    return args.reduce((total,curr)=> total + curr)
  }

  return fn
}

考點:

  • 使用閉包, 同時要對JavaScript 的作用域鏈(原型鏈)有深入的理解
  • 重寫函式的 toSting()方法
// 測試,呼叫toString方法觸發求值

add(1).toString();             // 1
add(1)(2).toString();      // 3
add(1)(2)(3).toString();// 6
add(1)(2, 3).toString(); // 6
add(1, 2)(3).toString(); // 6
add(1, 2, 3).toString(); // 6

實現一個連結串列結構

連結串列結構

看圖理解next層級

// 連結串列 從頭尾刪除、增加 效能比較好
// 分為很多類 常用單向連結串列、雙向連結串列

// js模擬連結串列結構:增刪改查

// node節點
class Node {
  constructor(element,next) {
    this.element = element
    this.next = next
  } 
}

class LinkedList {
 constructor() {
   this.head = null // 預設應該指向第一個節點
   this.size = 0 // 透過這個長度可以遍歷這個連結串列
 }
 // 增加O(n)
 add(index,element) {
   if(arguments.length === 1) {
     // 向末尾新增
     element = index // 當前元素等於傳遞的第一項
     index = this.size // 索引指向最後一個元素
   }
  if(index < 0 || index > this.size) {
    throw new Error('新增的索引不正常')
  }
  if(index === 0) {
    // 直接找到頭部 把頭部改掉 效能更好
    let head = this.head
    this.head = new Node(element,head)
  } else {
    // 獲取當前頭指標
    let current = this.head
    // 不停遍歷 直到找到最後一項 新增的索引是1就找到第0個的next賦值
    for (let i = 0; i < index-1; i++) { // 找到它的前一個
      current = current.next
    }
    // 讓建立的元素指向上一個元素的下一個
    // 看圖理解next層級
    current.next = new Node(element,current.next) // 讓當前元素指向下一個元素的next
  }

  this.size++;
 }
 // 刪除O(n)
 remove(index) {
  if(index < 0 || index >= this.size) {
    throw new Error('刪除的索引不正常')
  }
  this.size--
  if(index === 0) {
    let head = this.head
    this.head = this.head.next // 移動指標位置

    return head // 返回刪除的元素
  }else {
    let current = this.head
    for (let i = 0; i < index-1; i++) { // index-1找到它的前一個
      current = current.next
    }
    let returnVal = current.next // 返回刪除的元素
    // 找到待刪除的指標的上一個 current.next.next 
    // 如刪除200, 100=>200=>300 找到200的上一個100的next的next為300,把300賦值給100的next即可
    current.next = current.next.next 

    return returnVal
  }
 }
 // 查詢O(n)
 get(index) {
  if(index < 0 || index >= this.size) {
    throw new Error('查詢的索引不正常')
  }
  let current = this.head
  for (let i = 0; i < index; i++) {
    current = current.next
  }
  return current
 }
}


var ll = new LinkedList()

ll.add(0,100) // Node { ellement: 100, next: null }
ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }
ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }
ll.add(300)
ll.remove(0)

console.log(ll.get(2),'get')
console.log(ll.head)

module.exports = LinkedList

實現Object.is

Object.is不會轉換被比較的兩個值的型別,這點和===更為相似,他們之間也存在一些區別

  • NaN===中是不相等的,而在Object.is中是相等的
  • +0-0在===中是相等的,而在Object.is中是不相等的
Object.is = function (x, y) {
  if (x === y) {
    // 當前情況下,只有一種情況是特殊的,即 +0 -0
    // 如果 x !== 0,則返回true
    // 如果 x === 0,則需要判斷+0和-0,則可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity來進行判斷
    return x !== 0 || 1 / x === 1 / y;
  }

  // x !== y 的情況下,只需要判斷是否為NaN,如果x!==x,則說明x是NaN,同理y也一樣
  // x和y同時為NaN時,返回true
  return x !== x && y !== y;
};

實現ES6的const

由於ES5環境沒有block的概念,所以是無法百分百實現const,只能是掛載到某個物件下,要麼是全域性的window,要麼就是自定義一個object來當容器
var __const = function __const (data, value) {
    window.data = value // 把要定義的data掛載到window下,並賦值value
    Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持當前物件,並修改其屬性描述符
      enumerable: false,
      configurable: false,
      get: function () {
        return value
      },
      set: function (data) {
        if (data !== value) { // 當要對當前屬性進行賦值時,則丟擲錯誤!
          throw new TypeError('Assignment to constant variable.')
        } else {
          return value
        }
      }
    })
  }
  __const('a', 10)
  console.log(a)
  delete a
  console.log(a)
  for (let item in window) { // 因為const定義的屬性在global下也是不存在的,所以用到了enumerable: false來模擬這一功能
    if (item === 'a') { // 因為不可列舉,所以不執行
      console.log(window[item])
    }
  }
  a = 20 // 報錯
Vue目前雙向繫結的核心實現思路就是利用Object.definePropertygetset進行劫持,監聽使用者對屬性進行呼叫以及賦值時的具體情況,從而實現的雙向繫結

實現apply方法

思路: 利用this的上下文特性。apply其實就是改一下引數的問題
Function.prototype.myApply = function(context = window, args) {
  // this-->func  context--> obj  args--> 傳遞過來的引數

  // 在context上加一個唯一值不影響context上的屬性
  let key = Symbol('key')
  context[key] = this; // context為呼叫的上下文,this此處為函式,將這個函式作為context的方法
  // let args = [...arguments].slice(1)   //第一個引數為obj所以刪除,偽陣列轉為陣列

  let result = context[key](...args); // 這裡和call傳參不一樣

  // 清除定義的this 不刪除會導致context屬性越來越多
  delete context[key]; 

  // 返回結果
  return result;
}
// 使用
function f(a,b){
 console.log(a,b)
 console.log(this.name)
}
let obj={
 name:'張三'
}
f.myApply(obj,[1,2])  //arguments[1]

實現async/await

分析

// generator生成器  生成迭代器iterator

// 預設這樣寫的類陣列是不能被迭代的,缺少迭代方法
let likeArray = {'0': 1, '1': 2, '2': 3, '3': 4, length: 4}

// // 使用迭代器使得可以展開陣列
// // Symbol有很多超程式設計方法,可以改js本身功能
// likeArray[Symbol.iterator] = function () {
//   // 迭代器是一個物件 物件中有next方法 每次呼叫next 都需要返回一個物件 {value,done}
//   let index = 0
//   return {
//     next: ()=>{
//       // 會自動呼叫這個方法
//       console.log('index',index)
//       return {
//         // this 指向likeArray
//         value: this[index],
//         done: index++ === this.length
//       }
//     }
//   }
// }
// let arr = [...likeArray]

// console.log('arr', arr)

// 使用生成器返回迭代器
// likeArray[Symbol.iterator] = function *() {
//   let index = 0
//   while (index != this.length) {
//     yield this[index++]
//   }
// }
// let arr = [...likeArray]

// console.log('arr', arr)


// 生成器 碰到yield就會暫停
// function *read(params) {
//   yield 1;
//   yield 2;
// }
// 生成器返回的是迭代器
// let it = read()
// console.log(it.next())
// console.log(it.next())
// console.log(it.next())

// 透過generator來最佳化promise(promise的缺點是不停的鏈式呼叫)
const fs = require('fs')
const path = require('path')
// const co = require('co') // 幫我們執行generator

const promisify = fn=>{
  return (...args)=>{
    return new Promise((resolve,reject)=>{
      fn(...args, (err,data)=>{
        if(err) {
          reject(err)
        } 
        resolve(data)
      })
    })
  }
}

// promise化
let asyncReadFile = promisify(fs.readFile)

function * read() {
  let content1 = yield asyncReadFile(path.join(__dirname,'./data/name.txt'),'utf8')
  let content2 = yield asyncReadFile(path.join(__dirname,'./data/' + content1),'utf8')
  return content2
}

// 這樣寫太繁瑣 需要藉助co來實現
// let re = read()
// let {value,done} = re.next()
// value.then(data=>{
//   // 除了第一次傳參沒有意義外 剩下的傳參都賦予了上一次的返回值 
//   let {value,done} = re.next(data) 
//   value.then(d=>{
//     let {value,done} = re.next(d)
//     console.log(value,done)
//   })
// }).catch(err=>{
//   re.throw(err) // 手動丟擲錯誤 可以被try catch捕獲
// })



// 實現co原理
function co(it) {// it 迭代器
  return new Promise((resolve,reject)=>{
    // 非同步迭代 需要根據函式來實現
    function next(data) {
      // 遞迴得有中止條件
      let {value,done} = it.next(data)
      if(done) {
        resolve(value) // 直接讓promise變成成功 用當前返回的結果
      } else {
        // Promise.resolve(value).then(data=>{
        //   next(data)
        // }).catch(err=>{
        //   reject(err)
        // })
        // 簡寫
        Promise.resolve(value).then(next,reject)
      }
    }
    // 首次呼叫
    next()
  })
}

co(read()).then(d=>{
  console.log(d)
}).catch(err=>{
  console.log(err,'--')
})

整體看一下結構

function asyncToGenerator(generatorFunc) {
    return function() {
      const gen = generatorFunc.apply(this, arguments)
      return new Promise((resolve, reject) => {
        function step(key, arg) {
          let generatorResult
          try {
            generatorResult = gen[key](arg)
          } catch (error) {
            return reject(error)
          }
          const { value, done } = generatorResult
          if (done) {
            return resolve(value)
          } else {
            return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
          }
        }
        step("next")
      })
    }
}

分析

function asyncToGenerator(generatorFunc) {
  // 返回的是一個新的函式
  return function() {

    // 先呼叫generator函式 生成迭代器
    // 對應 var gen = testG()
    const gen = generatorFunc.apply(this, arguments)

    // 返回一個promise 因為外部是用.then的方式 或者await的方式去使用這個函式的返回值的
    // var test = asyncToGenerator(testG)
    // test().then(res => console.log(res))
    return new Promise((resolve, reject) => {

      // 內部定義一個step函式 用來一步一步的跨過yield的阻礙
      // key有next和throw兩種取值,分別對應了gen的next和throw方法
      // arg引數則是用來把promise resolve出來的值交給下一個yield
      function step(key, arg) {
        let generatorResult

        // 這個方法需要包裹在try catch中
        // 如果報錯了 就把promise給reject掉 外部透過.catch可以獲取到錯誤
        try {
          generatorResult = gen[key](arg)
        } catch (error) {
          return reject(error)
        }

        // gen.next() 得到的結果是一個 { value, done } 的結構
        const { value, done } = generatorResult

        if (done) {
          // 如果已經完成了 就直接resolve這個promise
          // 這個done是在最後一次呼叫next後才會為true
          // 以本文的例子來說 此時的結果是 { done: true, value: 'success' }
          // 這個value也就是generator函式最後的返回值
          return resolve(value)
        } else {
          // 除了最後結束的時候外,每次呼叫gen.next()
          // 其實是返回 { value: Promise, done: false } 的結構,
          // 這裡要注意的是Promise.resolve可以接受一個promise為引數
          // 並且這個promise引數被resolve的時候,這個then才會被呼叫
          return Promise.resolve(
            // 這個value對應的是yield後面的promise
            value
          ).then(
            // value這個promise被resove的時候,就會執行next
            // 並且只要done不是true的時候 就會遞迴的往下解開promise
            // 對應gen.next().value.then(value => {
            //    gen.next(value).value.then(value2 => {
            //       gen.next() 
            //
            //      // 此時done為true了 整個promise被resolve了 
            //      // 最外部的test().then(res => console.log(res))的then就開始執行了
            //    })
            // })
            function onResolve(val) {
              step("next", val)
            },
            // 如果promise被reject了 就再次進入step函式
            // 不同的是,這次的try catch中呼叫的是gen.throw(err)
            // 那麼自然就被catch到 然後把promise給reject掉啦
            function onReject(err) {
              step("throw", err)
            },
          )
        }
      }
      step("next")
    })
  }
}

陣列去重方法彙總

首先:我知道多少種去重方式

1. 雙層 for 迴圈

function distinct(arr) {
    for (let i=0, len=arr.length; i<len; i++) {
        for (let j=i+1; j<len; j++) {
            if (arr[i] == arr[j]) {
                arr.splice(j, 1);
                // splice 會改變陣列長度,所以要將陣列長度 len 和下標 j 減一
                len--;
                j--;
            }
        }
    }
    return arr;
}
思想: 雙重 for 迴圈是比較笨拙的方法,它實現的原理很簡單:先定義一個包含原始陣列第一個元素的陣列,然後遍歷原始陣列,將原始陣列中的每個元素與新陣列中的每個元素進行比對,如果不重複則新增到新陣列中,最後返回新陣列;因為它的時間複雜度是O(n^2),如果陣列長度很大,效率會很低

2. Array.filter() 加 indexOf/includes

function distinct(a, b) {
    let arr = a.concat(b);
    return arr.filter((item, index)=> {
        //return arr.indexOf(item) === index
        return arr.includes(item)
    })
}
思想: 利用indexOf檢測元素在陣列中第一次出現的位置是否和元素現在的位置相等,如果不等則說明該元素是重複元素

3. ES6 中的 Set 去重

function distinct(array) {
   return Array.from(new Set(array));
}
思想: ES6 提供了新的資料結構 Set,Set 結構的一個特性就是成員值都是唯一的,沒有重複的值。

4. reduce 實現物件陣列去重複

var resources = [
    { name: "張三", age: "18" },
    { name: "張三", age: "19" },
    { name: "張三", age: "20" },
    { name: "李四", age: "19" },
    { name: "王五", age: "20" },
    { name: "趙六", age: "21" }
]
var temp = {};
resources = resources.reduce((prev, curv) => {
 // 如果臨時物件中有這個名字,什麼都不做
 if (temp[curv.name]) {

 }else {
    // 如果臨時物件沒有就把這個名字加進去,同時把當前的這個物件加入到prev中
    temp[curv.name] = true;
    prev.push(curv);
 }
 return prev
}, []);
console.log("結果", resources);
這種方法是利用高階函式 reduce 進行去重, 這裡只需要注意initialValue得放一個空陣列[],不然沒法push

深克隆(deepclone)

簡單版:

const newObj = JSON.parse(JSON.stringify(oldObj));

侷限性:

  1. 他無法實現對函式 、RegExp等特殊物件的克隆
  2. 會拋棄物件的constructor,所有的建構函式會指向Object
  3. 物件有迴圈引用,會報錯

面試版:

/**
 * deep clone
 * @param  {[type]} parent object 需要進行克隆的物件
 * @return {[type]}        深克隆後的物件
 */
const clone = parent => {
  // 判斷型別
  const isType = (obj, type) => {
    if (typeof obj !== "object") return false;
    const typeString = Object.prototype.toString.call(obj);
    let flag;
    switch (type) {
      case "Array":
        flag = typeString === "[object Array]";
        break;
      case "Date":
        flag = typeString === "[object Date]";
        break;
      case "RegExp":
        flag = typeString === "[object RegExp]";
        break;
      default:
        flag = false;
    }
    return flag;
  };

  // 處理正則
  const getRegExp = re => {
    var flags = "";
    if (re.global) flags += "g";
    if (re.ignoreCase) flags += "i";
    if (re.multiline) flags += "m";
    return flags;
  };
  // 維護兩個儲存迴圈引用的陣列
  const parents = [];
  const children = [];

  const _clone = parent => {
    if (parent === null) return null;
    if (typeof parent !== "object") return parent;

    let child, proto;

    if (isType(parent, "Array")) {
      // 對陣列做特殊處理
      child = [];
    } else if (isType(parent, "RegExp")) {
      // 對正則物件做特殊處理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, "Date")) {
      // 對Date物件做特殊處理
      child = new Date(parent.getTime());
    } else {
      // 處理物件原型
      proto = Object.getPrototypeOf(parent);
      // 利用Object.create切斷原型鏈
      child = Object.create(proto);
    }

    // 處理迴圈引用
    const index = parents.indexOf(parent);

    if (index != -1) {
      // 如果父陣列存在本物件,說明之前已經被引用過,直接返回此物件
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      // 遞迴
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};

侷限性:

  1. 一些特殊情況沒有處理: 例如Buffer物件、Promise、Set、Map
  2. 另外對於確保沒有迴圈引用的物件,我們可以省去對迴圈引用的特殊處理,因為這很消耗時間
原理詳解實現深克隆

實現陣列去重

給定某無序陣列,要求去除陣列中的重複數字並且返回新的無重複陣列。

ES6方法(使用資料結構集合):

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]

ES5方法:使用map儲存不重複的數字

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

uniqueArray(array); // [1, 2, 3, 5, 9, 8]

function uniqueArray(array) {
  let map = {};
  let res = [];
  for(var i = 0; i < array.length; i++) {
    if(!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1;
      res.push(array[i]);
    }
  }
  return res;
}

實現一下hash路由

基礎的html程式碼:

<html>
  <style>
    html, body {
      margin: 0;
      height: 100%;
    }
    ul {
      list-style: none;
      margin: 0;
      padding: 0;
      display: flex;
      justify-content: center;
    }
    .box {
      width: 100%;
      height: 100%;
      background-color: red;
    }
  </style>
  <body>
  <ul>
    <li>
      <a href="#red">紅色</a>
    </li>
    <li>
      <a href="#green">綠色</a>
    </li>
    <li>
      <a href="#purple">紫色</a>
    </li>
  </ul>
  </body>
</html>

簡單實現:

<script>
  const box = document.getElementsByClassName('box')[0];
  const hash = location.hash
  window.onhashchange = function (e) {
    const color = hash.slice(1)
    box.style.background = color
  }
</script>

封裝成一個class:

<script>
  const box = document.getElementsByClassName('box')[0];
  const hash = location.hash
  class HashRouter {
    constructor (hashStr, cb) {
      this.hashStr = hashStr
      this.cb = cb
      this.watchHash()
      this.watch = this.watchHash.bind(this)
      window.addEventListener('hashchange', this.watch)
    }
    watchHash () {
      let hash = window.location.hash.slice(1)
      this.hashStr = hash
      this.cb(hash)
    }
  }
  new HashRouter('red', (color) => {
    box.style.background = color
  })
</script>

實現Node的require方法

require 基本原理

require 查詢路徑

requiremodule.exports 乾的事情並不複雜,我們先假設有一個全域性物件{},初始情況下是空的,當你 require 某個檔案時,就將這個檔案拿出來執行,如果這個檔案裡面存在module.exports,當執行到這行程式碼時將 module.exports 的值加入這個物件,鍵為對應的檔名,最終這個物件就長這樣:
{
  "a.js": "hello world",
  "b.js": function add(){},
  "c.js": 2,
  "d.js": { num: 2 }
}
當你再次 require 某個檔案時,如果這個物件裡面有對應的值,就直接返回給你,如果沒有就重複前面的步驟,執行目標檔案,然後將它的 module.exports 加入這個全域性物件,並返回給呼叫者。這個全域性物件其實就是我們經常聽說的快取。所以 requiremodule.exports 並沒有什麼黑魔法,就只是執行並獲取目標檔案的值,然後加入快取,用的時候拿出來用就行

手寫實現一個require

const path = require('path'); // 路徑操作
const fs = require('fs'); // 檔案讀取
const vm = require('vm'); // 檔案執行

// node模組化的實現
// node中是自帶模組化機制的,每個檔案就是一個單獨的模組,並且它遵循的是CommonJS規範,也就是使用require的方式匯入模組,透過module.export的方式匯出模組。
// node模組的執行機制也很簡單,其實就是在每一個模組外層包裹了一層函式,有了函式的包裹就可以實現程式碼間的作用域隔離

// require載入模組
// require依賴node中的fs模組來載入模組檔案,fs.readFile讀取到的是一個字串。
// 在javascrpt中我們可以透過eval或者new Function的方式來將一個字串轉換成js程式碼來執行。

// eval
// const name = 'poetry';
// const str = 'const a = 123; console.log(name)';
// eval(str); // poetry;

// new Function
// new Function接收的是一個要執行的字串,返回的是一個新的函式,呼叫這個新的函式字串就會執行了。如果這個函式需要傳遞引數,可以在new Function的時候依次傳入引數,最後傳入的是要執行的字串。比如這裡傳入引數b,要執行的字串str
// const b = 3;
// const str = 'let a = 1; return a + b';
// const fun = new Function('b', str);
// console.log(fun(b, str)); // 4
// 可以看到eval和Function例項化都可以用來執行javascript字串,似乎他們都可以來實現require模組載入。不過在node中並沒有選用他們來實現模組化,原因也很簡單因為他們都有一個致命的問題,就是都容易被不屬於他們的變數所影響。
// 如下str字串中並沒有定義a,但是確可以使用上面定義的a變數,這顯然是不對的,在模組化機制中,str字串應該具有自身獨立的執行空間,自身不存在的變數是不可以直接使用的
// const a = 1;
// const str = 'console.log(a)';
// eval(str);
// const func = new Function(str);
// func();

// node存在一個vm虛擬環境的概念,用來執行額外的js檔案,他可以保證javascript執行的獨立性,不會被外部所影響
// vm 內建模組
// 雖然我們在外部定義了hello,但是str是一個獨立的模組,並不在村hello變數,所以會直接報錯。
// 引入vm模組, 不需要安裝,node 自建模組
// const vm = require('vm');
// const hello = 'poetry';
// const str = 'console.log(hello)';
// wm.runInThisContext(str); // 報錯
// 所以node執行javascript模組時可以採用vm來實現。就可以保證模組的獨立性了

// 分析實現步驟
// 1.匯入相關模組,建立一個Require方法。
// 2.抽離透過Module._load方法,用於載入模組。
// 3.Module.resolveFilename 根據相對路徑,轉換成絕對路徑。
// 4.快取模組 Module._cache,同一個模組不要重複載入,提升效能。
// 5.建立模組 id: 儲存的內容是 exports = {}相當於this。
// 6.利用tryModuleLoad(module, filename) 嘗試載入模組。
// 7.Module._extensions使用讀取檔案。
// 8.Module.wrap: 把讀取到的js包裹一個函式。
// 9.將拿到的字串使用runInThisContext執行字串。
// 10.讓字串執行並將this改編成exports

// 定義匯入類,引數為模組路徑
function Require(modulePath) {
    // 獲取當前要載入的絕對路徑
    let absPathname = path.resolve(__dirname, modulePath);

    // 自動給模組新增字尾名,實現省略字尾名載入模組,其實也就是如果檔案沒有字尾名的時候遍歷一下所有的字尾名看一下檔案是否存在
    // 獲取所有字尾名
    const extNames = Object.keys(Module._extensions);
    let index = 0;
    // 儲存原始檔案路徑
    const oldPath = absPathname;
    function findExt(absPathname) {
        if (index === extNames.length) {
            throw new Error('檔案不存在');
        }
        try {
            fs.accessSync(absPathname);
            return absPathname;
        } catch(e) {
            const ext = extNames[index++];
            findExt(oldPath + ext);
        }
    }
    // 遞迴追加字尾名,判斷檔案是否存在
    absPathname = findExt(absPathname);

    // 從快取中讀取,如果存在,直接返回結果
    if (Module._cache[absPathname]) {
        return Module._cache[absPathname].exports;
    }

    // 建立模組,新建Module例項
    const module = new Module(absPathname);

    // 新增快取
    Module._cache[absPathname] = module;

    // 載入當前模組
    tryModuleLoad(module);

    // 返回exports物件
    return module.exports;
}

// Module的實現很簡單,就是給模組建立一個exports物件,tryModuleLoad執行的時候將內容加入到exports中,id就是模組的絕對路徑
// 定義模組, 新增檔案id標識和exports屬性
function Module(id) {
    this.id = id;
    // 讀取到的檔案內容會放在exports中
    this.exports = {};
}

Module._cache = {};

// 我們給Module掛載靜態屬性wrapper,裡面定義一下這個函式的字串,wrapper是一個陣列,陣列的第一個元素就是函式的引數部分,其中有exports,module. Require,__dirname, __filename, 都是我們模組中常用的全域性變數。注意這裡傳入的Require引數是我們自己定義的Require
// 第二個引數就是函式的結束部分。兩部分都是字串,使用的時候我們將他們包裹在模組的字串外部就可以了
Module.wrapper = [
    "(function(exports, module, Require, __dirname, __filename) {",
    "})"
]

// _extensions用於針對不同的模組副檔名使用不同的載入方式,比如JSON和javascript載入方式肯定是不同的。JSON使用JSON.parse來執行。
// javascript使用vm.runInThisContext來執行,可以看到fs.readFileSync傳入的是module.id也就是我們Module定義時候id儲存的是模組的絕對路徑,讀取到的content是一個字串,我們使用Module.wrapper來包裹一下就相當於在這個模組外部又包裹了一個函式,也就實現了私有作用域。
// 使用call來執行fn函式,第一個引數改變執行的this我們傳入module.exports,後面的引數就是函式外面包裹引數exports, module, Require, __dirname, __filename
Module._extensions = {
    '.js'(module) {
        const content = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
        const fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
    },
    '.json'(module) {
        const json = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(json); // 把檔案的結果放在exports屬性上
    }
}

// tryModuleLoad函式接收的是模組物件,透過path.extname來獲取模組的字尾名,然後使用Module._extensions來載入模組
// 定義模組載入方法
function tryModuleLoad(module) {
    // 獲取副檔名
    const extension = path.extname(module.id);
    // 透過字尾載入當前模組
    Module._extensions[extension](module);
}

// 至此Require載入機制我們基本就寫完了,我們來重新看一下。Require載入模組的時候傳入模組名稱,在Require方法中使用path.resolve(__dirname, modulePath)獲取到檔案的絕對路徑。然後透過new Module例項化的方式建立module物件,將模組的絕對路徑儲存在module的id屬性中,在module中建立exports屬性為一個json物件
// 使用tryModuleLoad方法去載入模組,tryModuleLoad中使用path.extname獲取到檔案的副檔名,然後根據副檔名來執行對應的模組載入機制
// 最終將載入到的模組掛載module.exports中。tryModuleLoad執行完畢之後module.exports已經存在了,直接返回就可以了


// 給模組新增快取
// 新增快取也比較簡單,就是檔案載入的時候將檔案放入快取中,再去載入模組時先看快取中是否存在,如果存在直接使用,如果不存在再去重新,載入之後再放入快取

// 測試
let json = Require('./test.json');
let test2 = Require('./test2.js');
console.log(json);
console.log(test2);

實現JSON.parse

var json = '{"name":"cxk", "age":25}';
var obj = eval("(" + json + ")");

此方法屬於黑魔法,極易容易被xss攻擊,還有一種new Function大同小異。

手寫 instanceof 方法

instanceof 運算子用於判斷建構函式的 prototype 屬性是否出現在物件的原型鏈中的任何位置。

實現步驟:

  1. 首先獲取型別的原型
  2. 然後獲得物件的原型
  3. 然後一直迴圈判斷物件的原型是否等於型別的原型,直到物件原型為 null,因為原型鏈最終為 null

具體實現:

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left), // 獲取物件的原型
      prototype = right.prototype; // 獲取建構函式的 prototype 物件

  // 判斷建構函式的 prototype 物件是否在物件的原型鏈上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;

    proto = Object.getPrototypeOf(proto);
  }
}

相關文章