前端面試 手寫代碼篇

2023-02-17 10:50 更新

一、JavaScript 基礎


1. 手寫 Object.create

思路:將傳入的對象作為原型

function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

2. 手寫 instanceof 方法

instanceof 運算符用于判斷構造函數(shù)的 prototype 屬性是否出現(xiàn)在對象的原型鏈中的任何位置。

實現(xiàn)步驟:

  1. 首先獲取類型的原型
  2. 然后獲得對象的原型
  3. 然后一直循環(huán)判斷對象的原型是否等于類型的原型,直到對象原型為 null,因為原型鏈最終為 null

具體實現(xiàn):

function myInstanceof(left, right) {
  let proto = Object.getPrototypeOf(left), // 獲取對象的原型
      prototype = right.prototype; // 獲取構造函數(shù)的 prototype 對象

  // 判斷構造函數(shù)的 prototype 對象是否在對象的原型鏈上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;

    proto = Object.getPrototypeOf(proto);
  }
}

3. 手寫 new 操作符

在調(diào)用 new 的過程中會發(fā)生以上四件事情:

(1)首先創(chuàng)建了一個新的空對象

(2)設置原型,將對象的原型設置為函數(shù)的 prototype 對象。

(3)讓函數(shù)的 this 指向這個對象,執(zhí)行構造函數(shù)的代碼(為這個新對象添加屬性)

(4)判斷函數(shù)的返回值類型,如果是值類型,返回創(chuàng)建的對象。如果是引用類型,就返回這個引用類型的對象。

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判斷參數(shù)是否是一個函數(shù)
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一個空對象,對象的原型為構造函數(shù)的 prototype 對象
  newObject = Object.create(constructor.prototype);
  // 將 this 指向新建對象,并執(zhí)行函數(shù)
  result = constructor.apply(newObject, arguments);
  // 判斷返回對象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判斷返回結果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(構造函數(shù), 初始化參數(shù));

4. 手寫 Promise

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

function MyPromise(fn) {
  // 保存初始化狀態(tài)
  var self = this;

  // 初始化狀態(tài)
  this.state = PENDING;

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

  // 用于保存 resolve 的回調(diào)函數(shù)
  this.resolvedCallbacks = [];

  // 用于保存 reject 的回調(diào)函數(shù)
  this.rejectedCallbacks = [];

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

    // 保證代碼的執(zhí)行順序為本輪事件循環(huán)的末尾
    setTimeout(() => {
      // 只有狀態(tài)為 pending 時才能轉變,
      if (self.state === PENDING) {
        // 修改狀態(tài)
        self.state = RESOLVED;

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

        // 執(zhí)行回調(diào)函數(shù)
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 狀態(tài)轉變?yōu)?rejected 方法
  function reject(value) {
    // 保證代碼的執(zhí)行順序為本輪事件循環(huán)的末尾
    setTimeout(() => {
      // 只有狀態(tài)為 pending 時才能轉變
      if (self.state === PENDING) {
        // 修改狀態(tài)
        self.state = REJECTED;

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

        // 執(zhí)行回調(diào)函數(shù)
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

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

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

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };
    return new MyPromise((resolve,reject)=> {
      // 如果是等待狀態(tài),則將函數(shù)加入對應列表中
      if (this.state === PENDING) {
        this.resolvedCallbacks.push(onResolved);
        this.rejectedCallbacks.push(onRejected);
      }

      // 如果狀態(tài)已經(jīng)凝固,則直接執(zhí)行對應狀態(tài)的函數(shù)

      if (this.state === RESOLVED) {
        try{
          let res = onResolved(this.val)
          if(res instanceof MyPromsie){
            res.then(v => {
              resolve(v)
            },error => {
              reject(error)
            })
          }
        }catch(error){
          reject(error)
        }
      }

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

5. 手寫 Promise.then

then 方法返回一個新的 promise 實例,為了在 promise 狀態(tài)發(fā)生變化時(resolve / reject 被調(diào)用時)再執(zhí)行 then 里的函數(shù),我們使用一個 callbacks 數(shù)組先把傳給then的函數(shù)暫存起來,等狀態(tài)改變時再調(diào)用。

那么,怎么保證后一個 then 里的方法在前一個 then(可能是異步)結束之后再執(zhí)行呢?

我們可以將傳給 then 的函數(shù)和新 promise 的 resolve 一起 push 到前一個 promise 的 callbacks 數(shù)組中,達到承前啟后的效果:

  • 承前:當前一個 ?promise ?完成后,調(diào)用其 ?resolve ?變更狀態(tài),在這個 ?resolve ?里會依次調(diào)用 ?callbacks ?里的回調(diào),這樣就執(zhí)行了 ?then ?里的方法了
  • 啟后:上一步中,當 ?then ?里的方法執(zhí)行完成后,返回一個結果,如果這個結果是個簡單的值,就直接調(diào)用新 ?promise ?的 ?resolve?,讓其狀態(tài)變更,這又會依次調(diào)用新 ?promise ?的 ?callbacks ?數(shù)組里的方法,循環(huán)往復。如果返回的結果是個 ?promise?,則需要等它完成之后再觸發(fā)新 ?promise ?的 ?resolve?,所以可以在其結果的 ?then ?里調(diào)用新 ?promise ?的 ?resolve?
then(onFulfilled, onReject){
    // 保存前一個promise的this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封裝前一個promise成功時執(zhí)行的函數(shù)
      let fulfilled = () => {
        try{
          const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //啟后
        }catch(err){
          reject(err)
        }
      }
      // 封裝前一個promise失敗時執(zhí)行的函數(shù)
      let rejected = () => {
        try{
          const result = onReject(self.reason);
          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
        }catch(err){
          reject(err)
        }
      }
      switch(self.status){
        case PENDING: 
          self.onFulfilledCallbacks.push(fulfilled);
          self.onRejectedCallbacks.push(rejected);
          break;
        case FULFILLED:
          fulfilled();
          break;
        case REJECT:
          rejected();
          break;
      }
    })
   }

注意:

  • 連續(xù)多個 ?then ?里的回調(diào)方法是同步注冊的,但注冊到了不同的 ?callbacks ?數(shù)組中,因為每次 ?then ?都返回新的 ?promise ?實例(參考上面的例子和圖)
  • 注冊完成后開始執(zhí)行構造函數(shù)中的異步事件,異步完成之后依次調(diào)用 ?callbacks ?數(shù)組中提前注冊的回調(diào)

6. 手寫 Promise.all

1) 核心思路

  1. 接收一個 Promise 實例的數(shù)組或具有 Iterator 接口的對象作為參數(shù)
  2. 這個方法返回一個新的 promise 對象,
  3. 遍歷傳入的參數(shù),用Promise.resolve()將參數(shù)"包一層",使其變成一個promise對象
  4. 參數(shù)所有回調(diào)成功才是成功,返回值數(shù)組與參數(shù)順序一致
  5. 參數(shù)數(shù)組其中一個失敗,則觸發(fā)失敗狀態(tài),第一個觸發(fā)失敗的 Promise 錯誤信息作為 Promise.all 的錯誤信息。

2)實現(xiàn)代碼

一般來說,Promise.all 用來處理多個并發(fā)請求,也是為了頁面數(shù)據(jù)構造的方便,將一個頁面所用到的在不同接口的數(shù)據(jù)一起請求過來,不過,如果其中一個接口失敗了,多個請求也就失敗了,頁面可能啥也出不來,這就看當前頁面的耦合程度了

function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if(!Array.isArray(promises)){
        throw new TypeError(`argument must be a array`)
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedResult = [];
    for (let i = 0; i < promiseNum; i++) {
      Promise.resolve(promises[i]).then(value=>{
        resolvedCounter++;
        resolvedResult[i] = value;
        if (resolvedCounter == promiseNum) {
            return resolve(resolvedResult)
          }
      },error=>{
        return reject(error)
      })
    }
  })
}
// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})

7. 手寫 Promise.race

該方法的參數(shù)是 Promise 實例數(shù)組, 然后其 then 注冊的回調(diào)方法是數(shù)組中的某一個 Promise 的狀態(tài)變?yōu)?fulfilled 的時候就執(zhí)行. 因為 Promise 的狀態(tài)只能改變一次, 那么我們只需要把 Promise.race 中產(chǎn)生的 Promise 對象的 resolve 方法, 注入到數(shù)組中的每一個 Promise 實例中的回調(diào)函數(shù)中即可.

Promise.race = function (args) {
  return new Promise((resolve, reject)=>{
    for(let i = 0; i < args.length; i++){
      Promise.resolve(args[i]).then(resolve,reject)
    }
  })
}

8. 手寫防抖函數(shù)

函數(shù)防抖是指在事件被觸發(fā) n 秒后再執(zhí)行回調(diào),如果在這 n 秒內(nèi)事件又被觸發(fā),則重新計時。這可以使用在一些點擊請求的事件上,避免因為用戶的多次點擊向后端發(fā)送多次請求。

// 函數(shù)防抖的實現(xiàn)
function debounce(fn, wait) {
  let timer = null;

  return function() {
    let context = this,
        args = arguments;

    // 如果此時存在定時器的話,則取消之前的定時器重新記時
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    // 設置定時器,使事件間隔指定事件后執(zhí)行
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  };
}

9. 手寫節(jié)流函數(shù)

函數(shù)節(jié)流是指規(guī)定一個單位時間,在這個單位時間內(nèi),只能有一次觸發(fā)事件的回調(diào)函數(shù)執(zhí)行,如果在同一個單位時間內(nèi)某事件被觸發(fā)多次,只有一次能生效。節(jié)流可以使用在 scroll 函數(shù)的事件監(jiān)聽上,通過事件節(jié)流來降低事件調(diào)用的頻率。

// 函數(shù)節(jié)流的實現(xiàn);
function throttle(fn, delay) {
  let curTime = Date.now();

  return function() {
    let context = this,
        args = arguments,
        nowTime = Date.now();

    // 如果兩次時間間隔超過了指定時間,則執(zhí)行函數(shù)。
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

function throttle(fn,wait){
  let timer = null
  return function(){
    let context = this, args = arguments
    if(!timer){
      timer = setTimeOut(()=>{
        fn.apply(context,args)
        timer = null
      },wait)
    }
  }
}

10. 手寫類型判斷函數(shù)

function getType(value) {
  // 判斷數(shù)據(jù)是 null 的情況
  if (value === null) {
    return value + "";
  }
  // 判斷數(shù)據(jù)是引用類型的情況
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split(" ")[1].split("");
    type.pop();
    return type.join("").toLowerCase();
  } else {
    // 判斷數(shù)據(jù)是基本數(shù)據(jù)類型的情況和函數(shù)的情況
    return typeof value;
  }
}

11. 手寫 call 函數(shù)

call 函數(shù)的實現(xiàn)步驟:

  1. 判斷調(diào)用對象是否為函數(shù),即使我們是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  2. 判斷傳入上下文對象是否存在,如果不存在,則設置為 window 。
  3. 處理傳入的參數(shù),截取第一個參數(shù)后的所有參數(shù)。
  4. 將函數(shù)作為上下文對象的一個屬性。
  5. 使用上下文對象來調(diào)用這個方法,并保存返回結果。
  6. 刪除剛才新增的屬性。
  7. 返回結果。
// call函數(shù)實現(xiàn)
Function.prototype.myCall = function(context) {
  // 判斷調(diào)用對象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 獲取參數(shù)
  let args = [...arguments].slice(1),
      result = null;
  // 判斷 context 是否傳入,如果未傳入則設置為 window
  context = context || window;
  // 將調(diào)用函數(shù)設為對象的方法
  context.fn = this;
  // 調(diào)用函數(shù)
  result = context.fn(...args);
  // 將屬性刪除
  delete context.fn;
  return result;
};

12. 手寫 apply 函數(shù)

apply 函數(shù)的實現(xiàn)步驟:

  1. 判斷調(diào)用對象是否為函數(shù),即使我們是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  2. 判斷傳入上下文對象是否存在,如果不存在,則設置為 window 。
  3. 將函數(shù)作為上下文對象的一個屬性。
  4. 判斷參數(shù)值是否傳入
  5. 使用上下文對象來調(diào)用這個方法,并保存返回結果。
  6. 刪除剛才新增的屬性
  7. 返回結果
// apply 函數(shù)實現(xiàn)
Function.prototype.myApply = function(context) {
  // 判斷調(diào)用對象是否為函數(shù)
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判斷 context 是否存在,如果未傳入則為 window
  context = context || window;
  // 將函數(shù)設為對象的方法
  context.fn = this;
  // 調(diào)用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 將屬性刪除
  delete context.fn;
  return result;
};

13. 手寫 bind 函數(shù)

bind 函數(shù)的實現(xiàn)步驟:

  1. 判斷調(diào)用對象是否為函數(shù),即使我們是定義在函數(shù)的原型上的,但是可能出現(xiàn)使用 call 等方式調(diào)用的情況。
  2. 保存當前函數(shù)的引用,獲取其余傳入?yún)?shù)值。
  3. 創(chuàng)建一個函數(shù)返回
  4. 函數(shù)內(nèi)部使用 apply 來綁定函數(shù)調(diào)用,需要判斷函數(shù)作為構造函數(shù)的情況,這個時候需要傳入當前函數(shù)的 this 給 apply 調(diào)用,其余情況都傳入指定的上下文對象。
// bind 函數(shù)實現(xiàn)
Function.prototype.myBind = function(context) {
  // 判斷調(diào)用對象是否為函數(shù)
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 獲取參數(shù)
  var args = [...arguments].slice(1),
      fn = this;
  var bound = function() {
    // 根據(jù)調(diào)用方式,傳入不同綁定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
  f = Function() {}
  f.prototype = fn.prototype
  bound.prototype = new f();
  return bound;
};

14. 函數(shù)柯里化的實現(xiàn)

函數(shù)柯里化指的是一種將使用多個參數(shù)的一個函數(shù)轉換成一系列使用一個參數(shù)的函數(shù)的技術。

function curry(fn, args) {
  // 獲取函數(shù)需要的參數(shù)長度
  let length = fn.length;

  args = args || [];

  return function() {
    let subArgs = args.slice(0);

    // 拼接得到現(xiàn)有的所有參數(shù)
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }

    // 判斷參數(shù)的長度是否已經(jīng)滿足函數(shù)所需參數(shù)的長度
    if (subArgs.length >= length) {
      // 如果滿足,執(zhí)行函數(shù)
      return fn.apply(this, subArgs);
    } else {
      // 如果不滿足,遞歸返回科里化的函數(shù),等待參數(shù)的傳入
      return curry.call(this, fn, subArgs);
    }
  };
}

// es6 實現(xiàn)
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

15. 實現(xiàn)AJAX請求

AJAX是 Asynchronous JavaScript and XML 的縮寫,指的是通過 JavaScript 的 異步通信,從服務器獲取 XML 文檔從中提取數(shù)據(jù),再更新當前網(wǎng)頁的對應部分,而不用刷新整個網(wǎng)頁。

創(chuàng)建AJAX請求的步驟:

  • 創(chuàng)建一個 XMLHttpRequest 對象
  • 在這個對象上使用 open 方法創(chuàng)建一個 HTTP 請求,open 方法所需要的參數(shù)是請求的方法、請求的地址、是否異步和用戶的認證信息。
  • 在發(fā)起請求前,可以為這個對象添加一些信息和監(jiān)聽函數(shù)。比如說可以通過 setRequestHeader 方法來為請求添加頭信息。還可以為這個對象添加一個狀態(tài)監(jiān)聽函數(shù)。一個 XMLHttpRequest 對象一共有 5 個狀態(tài),當它的狀態(tài)變化時會觸發(fā)onreadystatechange 事件,可以通過設置監(jiān)聽函數(shù),來處理請求成功后的結果。當對象的 readyState 變?yōu)?4 的時候,代表服務器返回的數(shù)據(jù)接收完成,這個時候可以通過判斷請求的狀態(tài),如果狀態(tài)是 2xx 或者 304 的話則代表返回正常。這個時候就可以通過 response 中的數(shù)據(jù)來對頁面進行更新了。
  • 當對象的屬性和監(jiān)聽函數(shù)設置完成后,最后調(diào)用 sent 方法來向服務器發(fā)起請求,可以傳入?yún)?shù)作為發(fā)送的數(shù)據(jù)體。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 創(chuàng)建 Http 請求
xhr.open("GET", SERVER_URL, true);
// 設置狀態(tài)監(jiān)聽函數(shù)
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 當請求成功時
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 設置請求失敗時的監(jiān)聽函數(shù)
xhr.onerror = function() {
  console.error(this.statusText);
};
// 設置請求頭信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 發(fā)送 Http 請求
xhr.send(null);

16. 使用Promise封裝AJAX請求

// promise 封裝實現(xiàn):
function getJSON(url) {
  // 創(chuàng)建一個 promise 對象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一個 http 請求
    xhr.open("GET", url, true);
    // 設置狀態(tài)的監(jiān)聽函數(shù)
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 當請求成功或失敗時,改變 promise 的狀態(tài)
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 設置錯誤監(jiān)聽函數(shù)
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 設置響應的數(shù)據(jù)類型
    xhr.responseType = "json";
    // 設置請求頭信息
    xhr.setRequestHeader("Accept", "application/json");
    // 發(fā)送 http 請求
    xhr.send(null);
  });
  return promise;
}

17. 實現(xiàn)淺拷貝

淺拷貝是指,一個新的對象對原始對象的屬性值進行精確地拷貝,如果拷貝的是基本數(shù)據(jù)類型,拷貝的就是基本數(shù)據(jù)類型的值,如果是引用數(shù)據(jù)類型,拷貝的就是內(nèi)存地址。如果其中一個對象的引用內(nèi)存地址發(fā)生改變,另一個對象也會發(fā)生變化。

(1)Object.assign()

Object.assign()是ES6中對象的拷貝方法,接受的第一個參數(shù)是目標對象,其余參數(shù)是源對象,用法:Object.assign(target, source_1, ···),該方法可以實現(xiàn)淺拷貝,也可以實現(xiàn)一維對象的深拷貝。

注意:

  • 如果目標對象和源對象有同名屬性,或者多個源對象有同名屬性,則后面的屬性會覆蓋前面的屬性。
  • 如果該函數(shù)只有一個參數(shù),當參數(shù)為對象時,直接返回該對象;當參數(shù)不是對象時,會先將參數(shù)轉為對象然后返回。
  • 因為 ?null ?和 ?undefined? 不能轉化為對象,所以第一個參數(shù)不能為 ?null?或 ?undefined?,會報錯。
let target = {a: 1};
let object2 = {b: 2};
let object3 = {c: 3};
Object.assign(target,object2,object3);  
console.log(target);  // {a: 1, b: 2, c: 3}

(2)擴展運算符

使用擴展運算符可以在構造字面量對象的時候,進行屬性的拷貝。語法:let cloneObj = { ...obj };

let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}

(3)數(shù)組方法實現(xiàn)數(shù)組淺拷貝

1)Array.prototype.slice

  • ?slice()?方法是JavaScript數(shù)組的一個方法,這個方法可以從已有數(shù)組中返回選定的元素:用法:?array.slice(start, end)?,該方法不會改變原始數(shù)組。
  • 該方法有兩個參數(shù),兩個參數(shù)都可選,如果兩個參數(shù)都不寫,就可以實現(xiàn)一個數(shù)組的淺拷貝。
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false

2)Array.prototype.concat

  • ?concat()? 方法用于合并兩個或多個數(shù)組。此方法不會更改現(xiàn)有數(shù)組,而是返回一個新數(shù)組。
  • 該方法有兩個參數(shù),兩個參數(shù)都可選,如果兩個參數(shù)都不寫,就可以實現(xiàn)一個數(shù)組的淺拷貝。
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false

(4)手寫實現(xiàn)淺拷貝

// 淺拷貝的實現(xiàn);

function shallowCopy(object) {
  // 只拷貝對象
  if (!object || typeof object !== "object") return;

  // 根據(jù) object 的類型判斷是新建一個數(shù)組還是對象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍歷 object,并且判斷是 object 的屬性才拷貝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  Object.keys(object).forEach(key => {
    if(object.hasOwnProperty(key)){
      newObject[key] = object[key]
    }
  })

  return newObject;
}// 淺拷貝的實現(xiàn);

function shallowCopy(object) {
  // 只拷貝對象
  if (!object || typeof object !== "object") return;

  // 根據(jù) object 的類型判斷是新建一個數(shù)組還是對象
  let newObject = Array.isArray(object) ? [] : {};

  // 遍歷 object,并且判斷是 object 的屬性才拷貝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
      //newObject[key] = typeof object[key] === 'object' ? deepCopy(object[key]) : object[key]
    }
  }

  return newObject;
}// 淺拷貝的實現(xiàn);
function shallowCopy(object) {
  // 只拷貝對象
  if (!object || typeof object !== "object") return;
  // 根據(jù) object 的類型判斷是新建一個數(shù)組還是對象
  let newObject = Array.isArray(object) ? [] : {};
  // 遍歷 object,并且判斷是 object 的屬性才拷貝
  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] = object[key];
    }
  }
  return newObject;
}

18. 實現(xiàn)深拷貝

  • 淺拷貝:淺拷貝指的是將一個對象的屬性值復制到另一個對象,如果有的屬性的值為引用類型的話,那么會將這個引用的地址復制給對象,因此兩個對象會有同一個引用類型的引用。淺拷貝可以使用 Object.assign 和展開運算符來實現(xiàn)。
  • 深拷貝:深拷貝相對淺拷貝而言,如果遇到屬性值為引用類型的時候,它新建一個引用類型并將對應的值復制給它,因此對象獲得的一個新的引用類型而不是一個原有類型的引用。深拷貝對于一些對象可以使用 JSON 的兩個函數(shù)來實現(xiàn),但是由于 JSON 的對象格式比 js 的對象格式更加嚴格,所以如果屬性值里邊出現(xiàn)函數(shù)或者 Symbol 類型的值時,會轉換失敗

(1)JSON.stringify()

  • ?JSON.parse(JSON.stringify(obj))?是目前比較常用的深拷貝方法之一,它的原理就是利用 ?JSON.stringify? 將 ?js?對象序列化(JSON字符串),再使用 ?JSON.parse?來反序列化(還原)js對象。
  • 這個方法可以簡單粗暴的實現(xiàn)深拷貝,但是還存在問題,拷貝的對象中如果有函數(shù),undefined,symbol,當使用過 ?JSON.stringify()?進行處理之后,都會消失。
let obj1 = {  a: 0,
              b: {
                 c: 0
                 }
            };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a = 1;
obj1.b.c = 1;
console.log(obj1); // {a: 1, b: {c: 1}}
console.log(obj2); // {a: 0, b: {c: 0}}

(2)函數(shù)庫lodash的_.cloneDeep方法

該函數(shù)庫也有提供_.cloneDeep用來做 Deep Copy

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

(3)手寫實現(xiàn)深拷貝函數(shù)

// 深拷貝的實現(xiàn)
function deepCopy(object) {
  if (!object || typeof object !== "object") return;

  let newObject = Array.isArray(object) ? [] : {};

  for (let key in object) {
    if (object.hasOwnProperty(key)) {
      newObject[key] =
        typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
    }
  }
  
  Object.keys(object).forEach(key => {
    if(object.hasOwnProperty(key)){
      newObject[key] = typeof object[key] === 'object' ? deepCopy(object[key]) : object[key]
    }
  })

  return newObject;
}

二、數(shù)據(jù)處理


1. 實現(xiàn)日期格式化函數(shù)

輸入:

dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
const dateFormat = (dateInput, format)=>{
    var day = dateInput.getDate() 
    var month = dateInput.getMonth() + 1  
    var year = dateInput.getFullYear()   
    format = format.replace(/yyyy/, year)
    format = format.replace(/MM/,month)
    format = format.replace(/dd/,day)
    return format
}

2. 交換a,b的值,不能用臨時變量

巧妙的利用兩個數(shù)的和、差:

a = a + b
b = a - b
a = a - b

3. 實現(xiàn)數(shù)組的亂序輸出

主要的實現(xiàn)思路就是:

  • 取出數(shù)組的第一個元素,隨機產(chǎn)生一個索引值,將該第一個元素和這個索引對應的元素進行交換。
  • 第二次取出數(shù)據(jù)數(shù)組第二個元素,隨機產(chǎn)生一個除了索引為1的之外的索引值,并將第二個元素與該索引值對應的元素進行交換
  • 按照上面的規(guī)律執(zhí)行,直到遍歷完成
var arr = [1,2,3,4,5,6,7,8,9,10];
for (var i = 0; i < arr.length; i++) {
  const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
  [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)

還有一方法就是倒序遍歷:

var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length,
    randomIndex,
    temp;
  while (length) {
    randomIndex = Math.floor(Math.random() * length--);
    temp = arr[length];
    arr[length] = arr[randomIndex];
    arr[randomIndex] = temp;
  }
console.log(arr)

4. 實現(xiàn)數(shù)組元素求和

  • arr=[1,2,3,4,5,6,7,8,9,10],求和
let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);
  • arr=[1,2,3,[[4,5],6],7,8,9],求和
var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);
console.log(arr);

遞歸實現(xiàn):

let arr = [1, 2, 3, 4, 5, 6] 

function add(arr) {
    if (arr.length == 1) return arr[0] 
    return arr[0] + add(arr.slice(1)) 
}
console.log(add(arr)) // 21

5. 實現(xiàn)數(shù)組的扁平化

(1)遞歸實現(xiàn)

普通的遞歸思路很容易理解,就是通過循環(huán)遞歸的方式,一項一項地去遍歷,如果每一項還是一個數(shù)組,那么就繼續(xù)往下遍歷,利用遞歸程序的方法,來實現(xiàn)數(shù)組的每一項的連接:

let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
  let result = [];

  for(let i = 0; i < arr.length; i++) {
    if(Array.isArray(arr[i])) {
      result = result.concat(flatten(arr[i]));
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}
flatten(arr);  //  [1, 2, 3, 4,5]

(2)reduce 函數(shù)迭代

從上面普通的遞歸函數(shù)中可以看出,其實就是對數(shù)組的每一項進行處理,那么其實也可以用reduce 來實現(xiàn)數(shù)組的拼接,從而簡化第一種方法的代碼,改造后的代碼如下所示:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.reduce(function(prev, next){
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}
console.log(flatten(arr));//  [1, 2, 3, 4,5]

(3)擴展運算符實現(xiàn)

這個方法的實現(xiàn),采用了擴展運算符和 some 的方法,兩者共同使用,達到數(shù)組扁平化的目的:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

(4)split 和 toString

可以通過 split 和 toString 兩個方法來共同實現(xiàn)數(shù)組扁平化,由于數(shù)組會默認帶一個 toString 的方法,所以可以把數(shù)組直接轉換成逗號分隔的字符串,然后再用 split 方法把字符串重新轉換為數(shù)組,如下面的代碼所示:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
    return arr.toString().split(',');
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

通過這兩個方法可以將多維數(shù)組直接轉換成逗號連接的字符串,然后再重新分隔成數(shù)組。

(5)ES6 中的 flat

我們還可以直接調(diào)用 ES6 中的 flat 方法來實現(xiàn)數(shù)組扁平化。flat 方法的語法:arr.flat([depth])

其中 depth 是 flat 的參數(shù),depth 是可以傳遞數(shù)組的展開深度(默認不填、數(shù)值是 1),即展開一層數(shù)組。如果層數(shù)不確定,參數(shù)可以傳進 Infinity,代表不論多少層都要展開:

let arr = [1, [2, [3, 4]]];
function flatten(arr) {
  return arr.flat(Infinity);
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

可以看出,一個嵌套了兩層的數(shù)組,通過將 flat 方法的參數(shù)設置為 Infinity,達到了我們預期的效果。其實同樣也可以設置成 2,也能實現(xiàn)這樣的效果。在編程過程中,如果數(shù)組的嵌套層數(shù)不確定,最好直接使用 Infinity,可以達到扁平化。

(6)正則和 JSON 方法

在第4種方法中已經(jīng)使用 toString 方法,其中仍然采用了將 JSON.stringify 的方法先轉換為字符串,然后通過正則表達式過濾掉字符串中的數(shù)組的方括號,最后再利用 JSON.parse 把它轉換成數(shù)組:

let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
  let str = JSON.stringify(arr);
  str = str.replace(/(\[|\])/g, '');
  str = '[' + str + ']';
  return JSON.parse(str); 
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

6. 實現(xiàn)數(shù)組去重

給定某無序數(shù)組,要求去除數(shù)組中的重復數(shù)字并且返回新的無重復數(shù)組。

ES6方法(使用數(shù)據(jù)結構集合):

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存儲不重復的數(shù)字

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

7. 實現(xiàn)數(shù)組的flat方法

function _flat(arr, depth) {
  if(!Array.isArray(arr) || depth <= 0) {
    return arr;
  }
  return arr.reduce((prev, cur) => {
    if (Array.isArray(cur)) {
      return prev.concat(_flat(cur, depth - 1))
    } else {
      return prev.concat(cur);
    }
  }, []);
}

8. 實現(xiàn)數(shù)組的push方法

let arr = [];
Array.prototype.push = function() {
    for( let i = 0 ; i < arguments.length ; i++){
        this[this.length] = arguments[i] ;
    }
    return this.length;
}

9. 實現(xiàn)數(shù)組的filter方法

Array.prototype._filter = function(fn) {
    if (typeof fn !== "function") {
        throw Error('參數(shù)必須是一個函數(shù)');
    }
    const res = [];
    for (let i = 0, len = this.length; i < len; i++) {
        fn(this[i]) && res.push(this[i]);
    }
    return res;
}

10. 實現(xiàn)數(shù)組的map方法

Array.prototype._map = function(fn) {
   if (typeof fn !== "function") {
        throw Error('參數(shù)必須是一個函數(shù)');
    }
    const res = [];
    for (let i = 0, len = this.length; i < len; i++) {
        res.push(fn(this[i]));
    }
    return res;
}

11. 實現(xiàn)字符串的repeat方法

輸入字符串s,以及其重復的次數(shù),輸出重復的結果,例如輸入abc,2,輸出abcabc。

function repeat(s, n) {
    return (new Array(n + 1)).join(s);
}

遞歸:

function repeat(s, n) {
    return (n > 0) ? s.concat(repeat(s, --n)) : "";
}

12. 實現(xiàn)字符串翻轉

在字符串的原型鏈上添加一個方法,實現(xiàn)字符串翻轉:

String.prototype._reverse = function(a){
    return a.split("").reverse().join("");
}
var obj = new String();
var res = obj._reverse ('hello');
console.log(res);    // olleh

需要注意的是,必須通過實例化對象之后再去調(diào)用定義的方法,不然找不到該方法。

13. 將數(shù)字每千分位用逗號隔開

數(shù)字有小數(shù)版本:

let format = n => {
    let num = n.toString() // 轉成字符串
    let decimals = ''
        // 判斷是否有小數(shù)
    num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let temp = ''
        let remainder = len % 3
        decimals ? temp = '.' + decimals : temp
        if (remainder > 0) { // 不是3的整數(shù)倍
            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp
        } else { // 是3的整數(shù)倍
            return num.slice(0, len).match(/\d{3}/g).join(',') + temp 
        }
    }
}
format(12323.33)  // '12,323.33'

數(shù)字無小數(shù)版本:

let format = n => {
    let num = n.toString() 
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let remainder = len % 3
        if (remainder > 0) { // 不是3的整數(shù)倍
            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') 
        } else { // 是3的整數(shù)倍
            return num.slice(0, len).match(/\d{3}/g).join(',') 
        }
    }
}
format(1232323)  // '1,232,323'

14. 實現(xiàn)非負大整數(shù)相加

JavaScript對數(shù)值有范圍的限制,限制如下:

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_VALUE // 5e-324
Number.MIN_SAFE_INTEGER // -9007199254740991

如果想要對一個超大的整數(shù)(> Number.MAX_SAFE_INTEGER)進行加法運算,但是又想輸出一般形式,那么使用 + 是無法達到的,一旦數(shù)字超過 Number.MAX_SAFE_INTEGER 數(shù)字會被立即轉換為科學計數(shù)法,并且數(shù)字精度相比以前將會有誤差。

實現(xiàn)一個算法進行大數(shù)的相加:

function sumBigNumber(a, b) {
  let res = '';
  let temp = 0;
  
  a = a.split('');
  b = b.split('');
  
  while (a.length || b.length || temp) {
    temp += ~~a.pop() + ~~b.pop();
    res = (temp % 10) + res;
    temp  = temp > 9
  }
  return res.replace(/^0+/, '');
}

其主要的思路如下:

  • 首先用字符串的方式來保存大數(shù),這樣數(shù)字在數(shù)學表示上就不會發(fā)生變化
  • 初始化res,temp來保存中間的計算結果,并將兩個字符串轉化為數(shù)組,以便進行每一位的加法運算
  • 將兩個數(shù)組的對應的位進行相加,兩個數(shù)相加的結果可能大于10,所以可能要僅為,對10進行取余操作,將結果保存在當前位
  • 判斷當前位是否大于9,也就是是否會進位,若是則將temp賦值為true,因為在加法運算中,true會自動隱式轉化為1,以便于下一次相加
  • 重復上述操作,直至計算結束

13. 實現(xiàn) add(1)(2)(3)

函數(shù)柯里化概念: 柯里化(Currying)是把接受多個參數(shù)的函數(shù)轉變?yōu)榻邮芤粋€單一參數(shù)的函數(shù),并且返回接受余下的參數(shù)且返回結果的新函數(shù)的技術。

1)粗暴版

function add (a) {
return function (b) {
    return function (c) {
      return a + b + c;
    }
}
}
console.log(add(1)(2)(3)); // 6

2)柯里化解決方案

  • 參數(shù)長度固定
var add = function (m) {
  var temp = function (n) {
    return add(m + n);
  }
  temp.toString = function () {
    return m;
  }
  return temp;
};
console.log(add(3)(4)(5)); // 12
console.log(add(3)(6)(9)(25)); // 43

對于add(3)(4)(5),其執(zhí)行過程如下:

  1. 先執(zhí)行add(3),此時m=3,并且返回temp函數(shù);
  2. 執(zhí)行temp(4),這個函數(shù)內(nèi)執(zhí)行add(m+n),n是此次傳進來的數(shù)值4,m值還是上一步中的3,所以add(m+n)=add(3+4)=add(7),此時m=7,并且返回temp函數(shù)
  3. 執(zhí)行temp(5),這個函數(shù)內(nèi)執(zhí)行add(m+n),n是此次傳進來的數(shù)值5,m值還是上一步中的7,所以add(m+n)=add(7+5)=add(12),此時m=12,并且返回temp函數(shù)
  4. 由于后面沒有傳入?yún)?shù),等于返回的temp函數(shù)不被執(zhí)行而是打印,了解JS的朋友都知道對象的toString是修改對象轉換字符串的方法,因此代碼中temp函數(shù)的toString函數(shù)return m值,而m值是最后一步執(zhí)行函數(shù)時的值m=12,所以返回值是12。
  • 參數(shù)長度不固定
function add (...args) {
    //求和
    return args.reduce((a, b) => a + b)
}
function currying (fn) {
    let args = []
    return function temp (...newArgs) {
        if (newArgs.length) {
            args = [
                ...args,
                ...newArgs
            ]
            return temp
        } else {
            let val = fn.apply(this, args)
            args = [] //保證再次調(diào)用時清空
            return val
        }
    }
}
let addCurry = currying(add)
console.log(addCurry(1)(2)(3)(4, 5)())  //15
console.log(addCurry(1)(2)(3, 4, 5)())  //15
console.log(addCurry(1)(2, 3, 4, 5)())  //15

14. 實現(xiàn)類數(shù)組轉化為數(shù)組

類數(shù)組轉換為數(shù)組的方法有這樣幾種:

  • 通過 call 調(diào)用數(shù)組的 slice 方法來實現(xiàn)轉換
Array.prototype.slice.call(arrayLike);
  • 通過 call 調(diào)用數(shù)組的 splice 方法來實現(xiàn)轉換
Array.prototype.splice.call(arrayLike, 0);
  • 通過 apply 調(diào)用數(shù)組的 concat 方法來實現(xiàn)轉換
Array.prototype.concat.apply([], arrayLike);
  • 通過 Array.from 方法來實現(xiàn)轉換
Array.from(arrayLike);

15. 使用 reduce 求和

arr = [1,2,3,4,5,6,7,8,9,10],求和

let arr = [1,2,3,4,5,6,7,8,9,10]
arr.reduce((prev, cur) => { return prev + cur }, 0)

arr = [1,2,3,[[4,5],6],7,8,9],求和

let arr = [1,2,3,4,5,6,7,8,9,10]
arr.flat(Infinity).reduce((prev, cur) => { return prev + cur }, 0)

arr = [{a:1, b:3}, {a:2, b:3, c:4}, {a:3}],求和

let arr = [{a:9, b:3, c:4}, {a:1, b:3}, {a:3}] 

arr.reduce((prev, cur) => {
    return prev + cur["a"];
}, 0)

16. 將js對象轉化為樹形結構

// 轉換前:
source = [{
            id: 1,
            pid: 0,
            name: 'body'
          }, {
            id: 2,
            pid: 1,
            name: 'title'
          }, {
            id: 3,
            pid: 2,
            name: 'div'
          }]
// 轉換為: 
tree = [{
          id: 1,
          pid: 0,
          name: 'body',
          children: [{
            id: 2,
            pid: 1,
            name: 'title',
            children: [{
              id: 3,
              pid: 1,
              name: 'div'
            }]
          }
        }]

代碼實現(xiàn):

function jsonToTree(data) {
  // 初始化結果數(shù)組,并判斷輸入數(shù)據(jù)的格式
  let result = []
  if(!Array.isArray(data)) {
    return result
  }
  // 使用map,將當前對象的id與當前對象對應存儲起來
  let map = {};
  data.forEach(item => {
    map[item.id] = item;
  });
  // 
  data.forEach(item => {
    let parent = map[item.pid];
    if(parent) {
      (parent.children || (parent.children = [])).push(item);
    } else {
      result.push(item);
    }
  });
  return result;
}

17. 使用ES5和ES6求函數(shù)參數(shù)的和

ES5:

function sum() {
    let sum = 0
    Array.prototype.forEach.call(arguments, function(item) {
        sum += item * 1
    })
    return sum
}

ES6:

function sum(...nums) {
    let sum = 0
    nums.forEach(function(item) {
        sum += item * 1
    })
    return sum
}

18. 解析 URL Params 為對象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 結果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重復出現(xiàn)的 key 要組裝成數(shù)組,能被轉成數(shù)字的就轉成數(shù)字類型
  city: '北京', // 中文需解碼
  enabled: true, // 未指定值得 key 約定為 true
}
*/
function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 后面的字符串取出來
  const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割后存到數(shù)組中
  let paramsObj = {};
  // 將 params 存到對象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 處理有 value 的參數(shù)
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解碼
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉為數(shù)字
      if (paramsObj.hasOwnProperty(key)) { // 如果對象有 key,則添加一個值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 如果對象沒有這個 key,創(chuàng)建 key 并設置值
        paramsObj[key] = val;
      }
    } else { // 處理沒有 value 的參數(shù)
      paramsObj[param] = true;
    }
  })
  return paramsObj;
}

三、場景應用


1. 循環(huán)打印紅黃綠

下面來看一道比較典型的問題,通過這個問題來對比幾種異步編程方法:紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重復亮燈?

三個亮燈函數(shù):

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

這道題復雜的地方在于需要“交替重復”亮燈,而不是“亮完一次”就結束了。

(1)用 callback 實現(xiàn)

const task = (timer, light, callback) => {
    setTimeout(() => {
        if (light === 'red') {
            red()
        }
        else if (light === 'green') {
            green()
        }
        else if (light === 'yellow') {
            yellow()
        }
        callback()
    }, timer)
}
task(3000, 'red', () => {
    task(2000, 'green', () => {
        task(1000, 'yellow', Function.prototype)
    })
})

這里存在一個 bug:代碼只是完成了一次流程,執(zhí)行后紅黃綠燈分別只亮一次。該如何讓它交替重復進行呢?

上面提到過遞歸,可以遞歸亮燈的一個周期:

const step = () => {
    task(3000, 'red', () => {
        task(2000, 'green', () => {
            task(1000, 'yellow', step)
        })
    })
}
step()

注意看黃燈亮的回調(diào)里又再次調(diào)用了 step 方法 以完成循環(huán)亮燈。

(2)用 promise 實現(xiàn)

const task = (timer, light) => 
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })
const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(2100, 'yellow'))
        .then(step)
}
step()

這里將回調(diào)移除,在一次亮燈結束后,resolve 當前 promise,并依然使用遞歸進行。

(3)用 async/await 實現(xiàn)

const taskRunner =  async () => {
    await task(3000, 'red')
    await task(2000, 'green')
    await task(2100, 'yellow')
    taskRunner()
}
taskRunner()

2. 實現(xiàn)每隔一秒打印 1,2,3,4

// 使用閉包實現(xiàn)
for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}
// 使用 let 塊級作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, i * 1000);
}

3. 小孩報數(shù)問題

有30個小孩兒,編號從1-30,圍成一圈依此報數(shù),1、2、3 數(shù)到 3 的小孩兒退出這個圈, 然后下一個小孩 重新報數(shù) 1、2、3,問最后剩下的那個小孩兒的編號是多少?

function childNum(num, count){
    let allplayer = [];  
    for(let i = 0; i < num; i++){
        allplayer[i] = i + 1;
    }
  
    let exitCount = 0;    // 離開人數(shù)
    let counter = 0;      // 記錄報數(shù)
    let curIndex = 0;     // 當前下標
  
    while(exitCount < num - 1){
        if(allplayer[curIndex] !== 0) counter++;  
      
        if(counter == count){
            allplayer[curIndex] = 0;               
            counter = 0;
            exitCount++;  
        }
        curIndex++;
        if(curIndex == num){
            curIndex = 0             
        };         
    }  
    for(i = 0; i < num; i++){
        if(allplayer[i] !== 0){
            return allplayer[i]
        }    
    }
}
childNum(30, 3)

4. 用Promise實現(xiàn)圖片的異步加載

let imageAsync=(url)=>{
            return new Promise((resolve,reject)=>{
                let img = new Image();
                img.src = url;
                img.οnlοad=()=>{
                    console.log(`圖片請求成功,此處進行通用操作`);
                    resolve(image);
                }
                img.οnerrοr=(err)=>{
                    console.log(`失敗,此處進行失敗的通用操作`);
                    reject(err);
                }
            })
        }
      
imageAsync("url").then(()=>{
    console.log("加載成功");
}).catch((error)=>{
    console.log("加載失敗");
})

5. 實現(xiàn)發(fā)布-訂閱模式

class EventCenter{
  // 1. 定義事件容器,用來裝事件數(shù)組
    let handlers = {}

  // 2. 添加事件方法,參數(shù):事件名 事件方法
  addEventListener(type, handler) {
    // 創(chuàng)建新數(shù)組容器
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 觸發(fā)事件,參數(shù):事件名 事件參數(shù)
  dispatchEvent(type, params) {
    // 若沒有注冊該事件則拋出錯誤
    if (!this.handlers[type]) {
      return new Error('該事件未注冊')
    }
    // 觸發(fā)事件
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 4. 事件移除,參數(shù):事件名 要刪除事件,若無第二個參數(shù)則刪除該事件的訂閱和發(fā)布
  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]
      }
    }
  }
}

6. 查找文章中出現(xiàn)頻率最高的單詞

function findMostWord(article) {
  // 合法性判斷
  if (!article) return;
  // 參數(shù)處理
  article = article.trim().toLowerCase();
  let wordList = article.match(/[a-z]+/g),
    visited = [],
    maxNum = 0,
    maxWord = "";
  article = " " + wordList.join("  ") + " ";
  // 遍歷判斷單詞出現(xiàn)次數(shù)
  wordList.forEach(function(item) {
    if (visited.indexOf(item) < 0) {
      // 加入 visited 
      visited.push(item);
      let word = new RegExp(" " + item + " ", "g"),
        num = article.match(word).length;
      if (num > maxNum) {
        maxNum = num;
        maxWord = item;
      }
    }
  });
  return maxWord + "  " + maxNum;
}

7. 封裝異步的fetch,使用async await方式來使用

(async () => {
    class HttpRequestUtil {
        async get(url) {
            const res = await fetch(url);
            const data = await res.json();
            return data;
        }
        async post(url, data) {
            const res = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
        async put(url, data) {
            const res = await fetch(url, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
        async delete(url, data) {
            const res = await fetch(url, {
                method: 'DELETE',
                headers: {
                    'Content-Type': 'application/json'
                },
                data: JSON.stringify(data)
            });
            const result = await res.json();
            return result;
        }
    }
    const httpRequestUtil = new HttpRequestUtil();
    const res = await httpRequestUtil.get('http://golderbrother.cn/');
    console.log(res);
})();

8. 實現(xiàn)prototype繼承

所謂的原型鏈繼承就是讓新實例的原型等于父類的實例:

//父方法
function SupperFunction(flag1){
    this.flag1 = flag1;
}

//子方法
function SubFunction(flag2){
    this.flag2 = flag2;
}

//父實例
var superInstance = new SupperFunction(true);

//子繼承父
SubFunction.prototype = superInstance;

//子實例
var subInstance = new SubFunction(false);
//子調(diào)用自己和父的屬性
subInstance.flag1;   // true
subInstance.flag2;   // false

9. 實現(xiàn)雙向數(shù)據(jù)綁定

let obj = {}
let input = document.getElementById('input')
let span = document.getElementById('span')
// 數(shù)據(jù)劫持
Object.defineProperty(obj, 'text', {
  configurable: true,
  enumerable: true,
  get() {
    console.log('獲取數(shù)據(jù)了')
  },
  set(newVal) {
    console.log('數(shù)據(jù)更新了')
    input.value = newVal
    span.innerHTML = newVal
  }
})
// 輸入監(jiān)聽
input.addEventListener('keyup', function(e) {
  obj.text = e.target.value
})

10. 實現(xiàn)簡單路由

// hash路由
class Route{
  constructor(){
    // 路由存儲對象
    this.routes = {}
    // 當前hash
    this.currentHash = ''
    // 綁定this,避免監(jiān)聽時this指向改變
    this.freshRoute = this.freshRoute.bind(this)
    // 監(jiān)聽
    window.addEventListener('load', this.freshRoute, false)
    window.addEventListener('hashchange', this.freshRoute, false)
  }
  // 存儲
  storeRoute (path, cb) {
    this.routes[path] = cb || function () {}
  }
  // 更新
  freshRoute () {
    this.currentHash = location.hash.slice(1) || '/'
    this.routes[this.currentHash]()
  }
}

11. 實現(xiàn)斐波那契數(shù)列

// 遞歸
function fn (n){
    if(n==0) return 0
    if(n==1) return 1
    return fn(n-2)+fn(n-1)
}
// 優(yōu)化
function fibonacci2(n) {
    const arr = [1, 1, 2];
    const arrLen = arr.length;

    if (n <= arrLen) {
        return arr[n];
    }

    for (let i = arrLen; i < n; i++) {
        arr.push(arr[i - 1] + arr[ i - 2]);
    }

    return arr[arr.length - 1];
}
// 非遞歸
function fn(n) {
    let pre1 = 1;
    let pre2 = 1;
    let current = 2;

    if (n <= 2) {
        return current;
    }

    for (let i = 2; i < n; i++) {
        pre1 = pre2;
        pre2 = current;
        current = pre1 + pre2;
    }

    return current;
}

12. 字符串出現(xiàn)的不重復最長長度

用一個滑動窗口裝沒有重復的字符,枚舉字符記錄最大值即可。用 map 維護字符的索引,遇到相同的字符,把左邊界移動過去即可。挪動的過程中記錄最大長度:

var lengthOfLongestSubstring = function (s) {
    let map = new Map();
    let i = -1
    let res = 0
    let n = s.length
    for (let j = 0; j < n; j++) {
        if (map.has(s[j])) {
            i = Math.max(i, map.get(s[j]))
        }
        res = Math.max(res, j - i)
        map.set(s[j], j)
    }
    return res
};

13. 使用 setTimeout 實現(xiàn) setInterval

setInterval 的作用是每隔一段指定時間執(zhí)行一個函數(shù),但是這個執(zhí)行不是真的到了時間立即執(zhí)行,它真正的作用是每隔一段時間將事件加入事件隊列中去,只有當當前的執(zhí)行棧為空的時候,才能去從事件隊列中取出事件執(zhí)行。所以可能會出現(xiàn)這樣的情況,就是當前執(zhí)行棧執(zhí)行的時間很長,導致事件隊列里邊積累多個定時器加入的事件,當執(zhí)行棧結束的時候,這些事件會依次執(zhí)行,因此就不能到間隔一段時間執(zhí)行的效果。

針對 setInterval 的這個缺點,我們可以使用 setTimeout 遞歸調(diào)用來模擬 setInterval,這樣我們就確保了只有一個事件結束了,我們才會觸發(fā)下一個定時器事件,這樣解決了 setInterval 的問題。

實現(xiàn)思路是使用遞歸函數(shù),不斷地去執(zhí)行 setTimeout 從而達到 setInterval 的效果

function mySetInterval(fn, timeout) {
  // 控制器,控制定時器是否繼續(xù)執(zhí)行
  var timer = {
    flag: true
  };
  // 設置遞歸函數(shù),模擬定時器執(zhí)行。
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);
    }
  }
  // 啟動定時器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}

14. 實現(xiàn) jsonp

// 動態(tài)的加載js文件
function addScript(src) {
  const script = document.createElement('script');
  script.src = src;
  script.type = "text/javascript";
  document.body.appendChild(script);
}
addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
// 設置一個全局的callback函數(shù)來接收回調(diào)結果
function handleRes(res) {
  console.log(res);
}
// 接口返回的數(shù)據(jù)格式
handleRes({a: 1, b: 2});

15. 判斷對象是否存在循環(huán)引用

循環(huán)引用對象本來沒有什么問題,但是序列化的時候就會發(fā)生問題,比如調(diào)用 JSON.stringify()對該類對象進行序列化,就會報錯: Converting circular structure to JSON.

下面方法可以用來判斷一個對象中是否已存在循環(huán)引用:

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 = ;
const o = {d:{a:3},c}
o.c.b.aa = a;

console.log(isCycleObject(o)

查找有序二維數(shù)組的目標值:

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

二維數(shù)組斜向打?。?br>

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
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號