JavaScript中各種原始碼實現(前端面試筆試必備)

雲魚Cloudy發表於2020-03-14

前言

能夠手撕各種JavaScript原生函式,可以說是進大廠必備!同時對JavaScript原始碼的學習和實現也能幫助我們快速紮實地提升自己的前端程式設計能力。

最近很多人和我一樣在積極地準備前端面試筆試,所以就整理了一些前端面試筆試中非常容易被問到的原生函式實現和各種前端原理實現,其中部分原始碼戳這裡

實現一個new操作符

我們首先知道new做了什麼:

  1. 建立一個空的簡單JavaScript物件(即{});
  2. 連結該物件(即設定該物件的建構函式)到另一個物件 ;
  3. 將步驟(1)新建立的物件作為this的上下文 ;
  4. 如果該函式沒有返回物件,則返回this。

知道new做了什麼,接下來我們就來實現它

function create(Con, ...args){
  // 建立一個空的物件
  this.obj = {};
  // 將空物件指向建構函式的原型鏈
  Object.setPrototypeOf(this.obj, Con.prototype);
  // obj繫結到建構函式上,便可以訪問建構函式中的屬性,即this.obj.Con(args)
  let result = Con.apply(this.obj, args);
  // 如果返回的result是一個物件則返回
  // new方法失效,否則返回obj
  return result instanceof Object ? result : this.obj;
}
複製程式碼

實現一個Array.isArray

思路很簡單,就是利用Object.prototype.toString

Array.myIsArray = function(o) { 
  return Object.prototype.toString.call(Object(o)) === '[object Array]'; 
}; 
複製程式碼

實現一個Object.create()方法

function create =  function (o) {
    var F = function () {};
    F.prototype = o;
    return new F();
};
複製程式碼

實現一個EventEmitter

真實經歷,最近在位元組跳動的面試中就被面試官問到了,要求手寫實現一個簡單的Event類。

class Event {
  constructor () {
    // 儲存事件的資料結構
    // 為查詢迅速, 使用物件(字典)
    this._cache = {}
  }

  // 繫結
  on(type, callback) {
    // 為了按類查詢方便和節省空間
    // 將同一型別事件放到一個陣列中
    // 這裡的陣列是佇列, 遵循先進先出
    // 即新繫結的事件先觸發
    let fns = (this._cache[type] = this._cache[type] || [])
    if(fns.indexOf(callback) === -1) {
      fns.push(callback)
    }
    return this
	}

  // 解綁
  off (type, callback) {
    let fns = this._cache[type]
    if(Array.isArray(fns)) {
      if(callback) {
        let index = fns.indexOf(callback)
        if(index !== -1) {
          fns.splice(index, 1)
        }
      } else {
        // 全部清空
        fns.length = 0
      }
    }
    return this
  }
  // 觸發emit
  trigger(type, data) {
    let fns = this._cache[type]
    if(Array.isArray(fns)) {
      fns.forEach((fn) => {
        fn(data)
      })
    }
    return this
  }

  // 一次性繫結
  once(type, callback) {
    let wrapFun = () => {
      callback.call(this);
      this.off(type, wrapFun);
    };
    this.on(type, wrapFun);
    return this;
  }
}

let e = new Event()

e.on('click',function(){
  console.log('on')
})
// e.trigger('click', '666')
console.log(e)

複製程式碼

實現一個Array.prototype.reduce

首先觀察一下Array.prototype.reduce語法

Array.prototype.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
複製程式碼

然後就可以動手實現了:

Array.prototype.myReduce = function(callback, initialValue) {
  let accumulator = initialValue ? initialValue : this[0];
  for (let i = initialValue ? 0 : 1; i < this.length; i++) {
    let _this = this;
    accumulator = callback(accumulator, this[i], i, _this);
  }
  return accumulator;
};

// 使用
let arr = [1, 2, 3, 4];
let sum = arr.myReduce((acc, val) => {
  acc += val;
  return acc;
}, 5);

console.log(sum); // 15
複製程式碼

實現一個call或apply

先來看一個call例項,看看call到底做了什麼:

let foo = {
  value: 1
};
function bar() {
  console.log(this.value);
}
bar.call(foo); // 1
複製程式碼

從程式碼的執行結果,我們可以看到,call首先改變了this的指向,使函式的this指向了foo,然後使bar函式執行了。 總結一下:

  1. call改變函式this指向
  2. 呼叫函式

思考一下:我們如何實現上面的效果呢?程式碼改造如下:

Function.prototype.myCall = function(context) {
  context = context || window;
  //將函式掛載到物件的fn屬性上
  context.fn = this;
  //處理傳入的引數
  const args = [...arguments].slice(1);
  //通過物件的屬性呼叫該方法
  const result = context.fn(...args);
  //刪除該屬性
  delete context.fn;
  return result
};
複製程式碼

我們看一下上面的程式碼:

  1. 首先我們對引數context做了相容處理,不傳值,context預設值為window;
  2. 然後我們將函式掛載到context上面,context.fn = this;
  3. 處理引數,將傳入myCall的引數擷取,去除第一位,然後轉為陣列;
  4. 呼叫context.fn,此時fn的this指向context;
  5. 刪除物件上的屬性 delete context.fn;
  6. 將結果返回。

以此類推,我們順便實現一下apply,唯一不同的是引數的處理,程式碼如下:

Function.prototype.myApply = function(context) {
  context = context || window
  context.fn = this
  let result
  // myApply的引數形式為(obj,[arg1,arg2,arg3]);
  // 所以myApply的第二個引數為[arg1,arg2,arg3]
  // 這裡我們用擴充套件運算子來處理一下引數的傳入方式
  if (arguments[1]) {
    result = context.fn(…arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn;
  return result
};
複製程式碼

以上便是call和apply的模擬實現,唯一不同的是對引數的處理方式。

實現一個Function.prototype.bind

function Person(){
  this.name="zs";
  this.age=18;
  this.gender="男"
}
let obj={
  hobby:"看書"
}
//  將建構函式的this繫結為obj
let changePerson = Person.bind(obj);
//  直接呼叫建構函式,函式會操作obj物件,給其新增三個屬性;
changePerson();
//  1、輸出obj
console.log(obj);
//  用改變了this指向的建構函式,new一個例項出來
let p = new changePerson();
// 2、輸出obj
console.log(p);
複製程式碼

仔細觀察上面的程式碼,再看輸出結果。

我們對Person類使用了bind將其this指向obj,得到了changeperson函式,此處如果我們直接呼叫changeperson會改變obj,若用new呼叫changeperson會得到例項 p,並且其__proto__指向Person,我們發現bind失效了。

我們得到結論:用bind改變了this指向的函式,如果用new操作符來呼叫,bind將會失效。

這個物件就是這個建構函式的例項,那麼只要在函式內部執行 this instanceof 建構函式 來判斷其結果是否為true,就能判斷函式是否是通過new操作符來呼叫了,若結果為true則是用new操作符呼叫的,程式碼修正如下:

// bind實現
Function.prototype.mybind = function(){
  // 1、儲存函式
  let _this = this;
  // 2、儲存目標物件
  let context = arguments[0]||window;
  // 3、儲存目標物件之外的引數,將其轉化為陣列;
  let rest = Array.prototype.slice.call(arguments,1);
  // 4、返回一個待執行的函式
  return function F(){
    // 5、將二次傳遞的引數轉化為陣列;
    let rest2 = Array.prototype.slice.call(arguments)
    if(this instanceof F){
      // 6、若是用new操作符呼叫,則直接用new 呼叫原函式,並用擴充套件運算子傳遞引數
      return new _this(...rest2)
    }else{
      //7、用apply呼叫第一步儲存的函式,並繫結this,傳遞合併的引數陣列,即context._this(rest.concat(rest2))
      _this.apply(context,rest.concat(rest2));
    }
  }
};
複製程式碼

實現一個JS函式柯里化

Currying的概念其實並不複雜,用通俗易懂的話說:只傳遞給函式一部分引數來呼叫它,讓它返回一個函式去處理剩下的引數。

function progressCurrying(fn, args) {

    let _this = this
    let len = fn.length;
    let args = args || [];

    return function() {
        let _args = Array.prototype.slice.call(arguments);
        Array.prototype.push.apply(args, _args);

        // 如果引數個數小於最初的fn.length,則遞迴呼叫,繼續收集引數
        if (_args.length < len) {
            return progressCurrying.call(_this, fn, _args);
        }

        // 引數收集完畢,則執行fn
        return fn.apply(this, _args);
    }
}

複製程式碼

手寫防抖(Debouncing)和節流(Throttling)

節流

防抖函式 onscroll 結束時觸發一次,延遲執行

function debounce(func, wait) {
  let timeout;
  return function() {
    let context = this; // 指向全域性
    let args = arguments;
    if (timeout) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func.apply(context, args); // context.func(args)
    }, wait);
  };
}
// 使用
window.onscroll = debounce(function() {
  console.log('debounce');
}, 1000);

複製程式碼
節流

節流函式 onscroll 時,每隔一段時間觸發一次,像水滴一樣

function throttle(fn, delay) {
  let prevTime = Date.now();
  return function() {
    let curTime = Date.now();
    if (curTime - prevTime > delay) {
      fn.apply(this, arguments);
      prevTime = curTime;
    }
  };
}
// 使用
var throtteScroll = throttle(function() {
  console.log('throtte');
}, 1000);
window.onscroll = throtteScroll;
複製程式碼

手寫一個JS深拷貝

乞丐版

JSON.parse(JSON.stringfy));
複製程式碼

非常簡單,但缺陷也很明顯,比如拷貝其他引用型別、拷貝函式、迴圈引用等情況。

基礎版

function clone(target){
  if(typeof target === 'object'){
    let cloneTarget = {};
    for(const key in target){
      cloneTarget[key] = clone(target[key])
    }
    return cloneTarget;
  } else {
    return target
  }
}
複製程式碼

寫到這裡已經可以幫助你應付一些面試官考察你的遞迴解決問題的能力。但是顯然,這個深拷貝函式還是有一些問題。

一個比較完整的深拷貝函式,需要同時考慮物件和陣列,考慮迴圈引用:

function clone(target, map = new WeakMap()) {
  if(typeof target === 'object'){
    let cloneTarget = Array.isArray(target) ? [] : {};
    if(map.get(target)) {
      return target;
    }
    map.set(target, cloneTarget);
    for(const key in target) {
      cloneTarget[key] = clone(target[key], map)
    }
    return cloneTarget;
  } else {
    return target;
  }
}
複製程式碼

實現一個instanceOf

原理: L 的 proto 是不是等於 R.prototype,不等於再找 L.proto.proto 直到 proto 為 null

// L 表示左表示式,R 表示右表示式
function instance_of(L, R) {
	var O = R.prototype;
  L = L.__proto__;
  while (true) {
		if (L === null){
			return false;
		}
    	// 這裡重點:當 O 嚴格等於 L 時,返回 true
    	if (O === L) {
			return true;
		}
    	L = L.__proto__;
  }
}
複製程式碼

實現一個原型鏈繼承

function myExtend(C, P) {
    var F = function(){};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.prototype.constructor = C;
    C.super = P.prototype;
}
複製程式碼

實現一個async/await

原理

就是利用 generator(生成器)分割程式碼片段。然後我們使用一個函式讓其自迭代,每一個yield 用 promise 包裹起來。執行下一步的時機由 promise 來控制

實現
function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments;
    // 將返回值promise化
    return new Promise(function(resolve, reject) {
      // 獲取迭代器例項
      var gen = fn.apply(self, args);
      // 執行下一步
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      // 丟擲異常
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      // 第一次觸發
      _next(undefined);
    });
  };
}
複製程式碼

實現一個Array.prototype.flat()函式

最近位元組跳動的前端面試中也被面試官問到,要求手寫實現。

Array.prototype.myFlat = function(num = 1) {
  if (Array.isArray(this)) {
    let arr = [];
    if (!Number(num) || Number(num) < 0) {
      return this;
    }
    this.forEach(item => {
      if(Array.isArray(item)){
        let count = num
        arr = arr.concat(item.myFlat(--count))
      } else {
        arr.push(item)
      }  
    });
    return arr;
  } else {
    throw tihs + ".flat is not a function";
  }
};
複製程式碼

實現一個事件代理

這個問題一般還會讓你講一講事件冒泡和事件捕獲機制

<ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
  </ul>
  <script>
    (function () {
      var color_list = document.getElementById('color-list');
      color_list.addEventListener('click', showColor, true);
      function showColor(e) {
        var x = e.target;
        if (x.nodeName.toLowerCase() === 'li') {
          alert(x.innerHTML);
        }
      }
    })();
  </script>
複製程式碼

實現一個Vue雙向繫結

Vue 2.x的Object.defineProperty版本

// 資料
const data = {
  text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 資料劫持
Object.defineProperty(data, 'text', {
  // 資料變化 —> 修改檢視
  set(newVal) {
    input.value = newVal;
    span.innerHTML = newVal;
  }
});
// 檢視更改 --> 資料變化
input.addEventListener('keyup', function(e) {
  data.text = e.target.value;
});
複製程式碼

Vue 3.x的proxy 版本

// 資料
const data = {
  text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 資料劫持
const handler = {
  set(target, key, value) {
    target[key] = value;
    // 資料變化 —> 修改檢視
    input.value = value;
    span.innerHTML = value;
    return value;
  }
};
const proxy = new Proxy(data, handler);

// 檢視更改 --> 資料變化
input.addEventListener('keyup', function(e) {
  proxy.text = e.target.value;
});

複製程式碼

思考:Vue雙向繫結的實現,使用 ES6 的 Proxy 相比 Object.defineProperty 有什麼優勢?

實現一個Array.prototype.map()

先看看reduce和map的使用方法

let new_array = arr.map(function callback(currentValue[, index[,array) {}[, thisArg])

let result = arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
複製程式碼

先用for迴圈實現:

Array.prototype.myMap = function(callback, thisArg) {
  let arr = [];
  for (let i = 0; i < this.length; i++) {
    arr.push(callback.call(thisArg, this[i], i, this));
  }
  return arr;
};
複製程式碼

再用reduce實現

Array.prototype.myMap2 = function(callback, thisArg) {
  let result = this.reduce((accumulator, currentValue, index, array) => {
    accumulator.push(callback.call(thisArg, currentValue, index, array));
    return accumulator;
  }, []);
  return result;
};
複製程式碼

結語

看完覺得對你有幫助勞煩點個贊鼓勵鼓勵,學習使我快樂!

參考:

重寫手動實現bind函式 - 雲+社群 - 騰訊雲

各種原始碼實現,你想要的這裡都有


我是Cloudy,現居上海,年輕的前端攻城獅一枚,愛專研,愛技術,愛分享。 個人筆記,整理不易,感謝關注閱讀點贊收藏 文章有任何問題歡迎大家指出,也歡迎大家一起交流各種前端問題!

相關文章