在面試中,常常會遇到一些手寫XXX之類的面試題,因此好好總結一下,對於鞏固我們的原生js的基礎是非常必要的。
儘管在網上已經有了非常多的總結文章,但在我看來有一個普遍的問題,那就是把原理性的東西過於複雜化了。如果站在面試官的角度,他的目的是在最短的時間內考察出面試者對於JS語言的理解程度,但是在看了網站的諸多總結文章後我發現其中的代碼有很大一部分是做意義不大的操作,比如實現一個簡單的防抖,只要是核心的流程展示即可,至於其他的一些等模式則沒有必要再去深挖,一大堆的if-else讓人看上去也眼花繚亂,甚至誤導別人直接去背代碼,另外,核心的邏輯都能展示出來,再去橫向的實現其他的類似情況恐怕也不是什麼問題了。
在以下的整理中,建議大家先照的核心要點自己寫一遍,然後對照下面的代碼,複習效果更好。本文的目的就在於以最簡潔的代碼幫你從第一性原理的角度理解api的內部運作流程,凡是對於我們理解api沒有幫助的的邊界情況都不做處理。
一、用ES5實現數組的map方法
核心要點:
1.回調函數的參數有哪些,返回值如何處理。
2.不修改原來的數組。
Array.prototype.MyMap = function(fn, context){
var arr = Array.prototype.slice.call(this);//由於是ES5所以就不用...展開符了
var mappedArr = [];
for (var i = 0; i < arr.length; i++ ){
if(!arr.hasOwnProperty(i))continue;
mappedArr.push(fn.call(context, arr[i], i, this));
}
return mappedArr;
}
二、用ES5實現數組的reduce方法
核心要點:
1、初始值不傳怎麼處理
2、回調函數的參數有哪些,返回值如何處理。
Array.prototype.myReduce = function(fn, initialValue) {
var arr = Array.prototype.slice.call(this);
var res, startIndex;
res = initialValue ? initialValue : arr[0];
startIndex = initialValue ? 0 : 1;
for(var i = startIndex; i < arr.length; i++) {
res = fn.call(null, res, arr[i], i, this);
}
return res;
}
三、實現call/apply
思路: 利用this的上下文特性。
//實現apply只要把下一行中的…args換成args即可
Function.prototype.myCall = function(context = window, ...args) {
let func = this;
let fn = Symbol("fn");
context[fn] = func;
let res = context[fn](...args);//重點代碼,利用this指向,相當於context.caller(...args)
delete context[fn];
return res;
}
四、實現Object.create方法(常用)
function create(proto) {
function F() {};
F.prototype = proto;
return new F();
}
五、實現bind方法
核心要點:
1.對於普通函數,綁定this指向
2.對於構造函數,要保證原函數的原型對象上的屬性不能丟失
Function.prototype.bind = function(context, ...args) {
let self = this;//謹記this表示調用bind的函數
let fBound = function() {
//this instanceof fBound爲true表示構造函數的情況。如new func.bind(obj)
return self.apply(this instanceof fBound ? this : context || window, args);
}
fBound.prototype = Object.create(this.prototype);//保證原函數的原型對象上的屬性不丟失
return fBound;
}
大家平時說的手寫bind,其實就這麼簡單:)
六、實現new關鍵字
核心要點:
創建一個全新的對象,這個對象的__proto__要指向構造函數的原型對象
執行構造函數
返回值爲object類型則作爲new方法的返回值返回,否則返回上述全新對象
function myNew(fn, ...args) {
let instance = Object.create(fn.prototype);
let res = fn.apply(instance, args);
return typeof res === 'object' ? res: instance;
}
七、實現instanceof的作用
核心要點:原型鏈的向上查找。
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left);
while(true) {
if(proto == null) return false;
if(proto == right.prototype) return true;
proto = Object.getPrototypeof(proto);
}
}
八、實現單例模式
核心要點: 用閉包和Proxy屬性攔截
function proxy(func) {
let instance;
let handler = {
constructor(target, args) {
if(!instance) {
instance = Reflect.constructor(fun, args);
}
return instance;
}
}
return new Proxy(func, handler);
}
九、實現數組的flat
方式其實很多,之前我做過系統整理,有六種方法,請參考:
JS數組扁平化(flat)方法總結
十、實現防抖功能
核心要點:
如果在定時器的時間範圍內再次觸發,則重新計時。
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
十一、實現節流功能
核心要點:
如果在定時器的時間範圍內再次觸發,則不予理睬,等當前定時器完成,才能啓動下一個定時器。
const throttle = (fn, delay = 500) => {
let flag = true;
return (...args) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, delay);
};
};
十二、用發佈訂閱模式實現EventEmit
參考我的另一篇文章:
基於"發佈-訂閱"的原生JS插件封裝中的手寫發佈訂閱部分。
十三、實現深拷貝
以下爲簡易版深拷貝,沒有考慮循環引用的情況和Buffer、Promise、Set、Map的處理,如果一一實現,過於複雜,面試短時間寫出來不太現實,如果有興趣可以去這裏深入實現:
深拷貝終極探索。
const clone = parent => {
// 判斷類型
const isType = (target, type) => `[object ${type}]` === Object.prototype.toString.call(target)
// 處理正則
const getRegExp = re => {
let flags = "";
if (re.global) flags += "g";
if (re.ignoreCase) flags += "i";
if (re.multiline) flags += "m";
return flags;
};
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);
}
for (let i in parent) {
// 遞歸
child[i] = _clone(parent[i]);
}
return child;
};
return _clone(parent);
};
十四、實現Promise
重點難點,比較複雜,請參考我的另一篇步步拆解文章:
我如何實現Promise
十五、使用ES5實現類的繼承效果
也是重點知識,我之前做過詳細拆解,有五個版本,如果每一版本都能說清楚,能夠很好的體現自己對於原型鏈的理解,文章地址:
ES5實現繼承的那些事