點擊關注本公衆號獲取文檔最新更新,並能夠領取配套於本指南的 《前端面試手冊》 以及最標準的簡歷模板.javascript
防抖函數原理:在事件被觸發n秒後再執行回調,若是在這n秒內又被觸發,則從新計時。前端
那麼與節流函數的區別直接看這個動畫實現便可。java
手寫簡化版:node
// 防抖函數
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
複製代碼
適用場景:git
生存環境請用lodash.debounce程序員
防抖函數原理:規定在一個單位時間內,只能觸發一次函數。若是這個單位時間內觸發屢次函數,只有一次生效。es6
// 手寫簡化版github
// 節流函數
const throttle = (fn, delay = 500) => {
let flag = true;
return (...args) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, delay);
};
};
複製代碼
適用場景:面試
簡單版:正則表達式
const newObj = JSON.parse(JSON.stringify(oldObj));
複製代碼
侷限性:
他沒法實現對函數 、RegExp等特殊對象的克隆
會拋棄對象的constructor,全部的構造函數會指向Object
對象有循環引用,會報錯
面試版:
/** * 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);
};
複製代碼
侷限性:
原理詳解實現深克隆
event bus既是node中各個模塊的基石,又是前端組件通訊的依賴手段之一,同時涉及了訂閱-發佈設計模式,是很是重要的基礎。
簡單版:
class EventEmeitter {
constructor() {
this._events = this._events || new Map(); // 儲存事件/回調鍵值對
this._maxListeners = this._maxListeners || 10; // 設立監聽上限
}
}
// 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 從儲存事件鍵值對的this._events中獲取對應事件回調函數
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
return true;
};
// 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 將type事件以及對應的fn函數放入this._events中儲存
if (!this._events.get(type)) {
this._events.set(type, fn);
}
};
複製代碼
面試版:
class EventEmeitter {
constructor() {
this._events = this._events || new Map(); // 儲存事件/回調鍵值對
this._maxListeners = this._maxListeners || 10; // 設立監聽上限
}
}
// 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
// 從儲存事件鍵值對的this._events中獲取對應事件回調函數
handler = this._events.get(type);
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
return true;
};
// 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
// 將type事件以及對應的fn函數放入this._events中儲存
if (!this._events.get(type)) {
this._events.set(type, fn);
}
};
// 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
let handler;
handler = this._events.get(type);
if (Array.isArray(handler)) {
// 若是是一個數組說明有多個監聽者,須要依次此觸發裏面的函數
for (let i = 0; i < handler.length; i++) {
if (args.length > 0) {
handler[i].apply(this, args);
} else {
handler[i].call(this);
}
}
} else {
// 單個函數的狀況咱們直接觸發便可
if (args.length > 0) {
handler.apply(this, args);
} else {
handler.call(this);
}
}
return true;
};
// 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
const handler = this._events.get(type); // 獲取對應事件名稱的函數清單
if (!handler) {
this._events.set(type, fn);
} else if (handler && typeof handler === "function") {
// 若是handler是函數說明只有一個監聽者
this._events.set(type, [handler, fn]); // 多個監聽者咱們須要用數組儲存
} else {
handler.push(fn); // 已經有多個監聽者,那麼直接往數組裏push函數便可
}
};
EventEmeitter.prototype.removeListener = function(type, fn) {
const handler = this._events.get(type); // 獲取對應事件名稱的函數清單
// 若是是函數,說明只被監聽了一次
if (handler && typeof handler === "function") {
this._events.delete(type, fn);
} else {
let postion;
// 若是handler是數組,說明被監聽屢次要找到對應的函數
for (let i = 0; i < handler.length; i++) {
if (handler[i] === fn) {
postion = i;
} else {
postion = -1;
}
}
// 若是找到匹配的函數,從數組中清除
if (postion !== -1) {
// 找到數組對應的位置,直接清除此回調
handler.splice(postion, 1);
// 若是清除後只有一個函數,那麼取消數組,以函數形式保存
if (handler.length === 1) {
this._events.set(type, handler[0]);
}
} else {
return this;
}
}
};
複製代碼
實現具體過程和思路見實現event
// 模擬 instanceof
function instance_of(L, R) {
//L 表示左表達式,R 表示右表達式
var O = R.prototype; // 取 R 的顯示原型
L = L.__proto__; // 取 L 的隱式原型
while (true) {
if (L === null) return false;
if (O === L)
// 這裏重點:當 O 嚴格等於 L 時,返回 true
return true;
L = L.__proto__;
}
}
複製代碼
new操做符作了這些事:
// objectFactory(name, 'cxk', '18')
function objectFactory() {
const obj = new Object();
const Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
const ret = Constructor.apply(obj, arguments);
return typeof ret === "object" ? ret : obj;
}
複製代碼
call作了什麼:
// 模擬 call bar.mycall(null);
//實現一個call方法:
Function.prototype.myCall = function(context) {
//此處沒有考慮context非object狀況
context.fn = this;
let args = [];
for (let i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
}
context.fn(...args);
let result = context.fn(...args);
delete context.fn;
return result;
};
複製代碼
apply原理與call很類似,很少贅述
// 模擬 apply
Function.prototype.myapply = function(context, arr) {
var context = Object(context) || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push("arr[" + i + "]");
}
result = eval("context.fn(" + args + ")");
}
delete context.fn;
return result;
};
複製代碼
實現bind要作什麼
// mdn的實現
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// this instanceof fBound === true時,說明返回的fBound被當作new的構造函數調用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 獲取調用時(fBound)的傳參.bind 返回的函數入參每每是這麼傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 維護原型關係
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
// 下行的代碼使fBound.prototype是fNOP的實例,所以
// 返回的fBound若做爲new的構造函數,new生成的新對象做爲this傳入fBound,新對象的__proto__就是fNOP的實例
fBound.prototype = new fNOP();
return fBound;
};
}
複製代碼
Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。
// 模擬 Object.create
function create(proto) {
function F() {}
F.prototype = proto;
return new F();
}
複製代碼
類的繼承在幾年前是重點內容,有n種繼承方式各有優劣,es6普及後愈來愈不重要,那麼多種寫法有點『回字有四樣寫法』的意思,若是還想深刻理解的去看紅寶書便可,咱們目前只實現一種最理想的繼承方式。
function Parent(name) {
this.parent = name
}
Parent.prototype.say = function() {
console.log(`${this.parent}: 你打籃球的樣子像kunkun`)
}
function Child(name, parent) {
// 將父類的構造函數綁定在子類上
Parent.call(this, parent)
this.child = name
}
/** 1. 這一步不用Child.prototype =Parent.prototype的緣由是怕共享內存,修改父類原型對象就會影響子類 2. 不用Child.prototype = new Parent()的緣由是會調用2次父類的構造方法(另外一次是call),會存在一份多餘的父類實例屬性 3. Object.create是建立了父類原型的副本,與父類原型徹底隔離 */
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {
console.log(`${this.parent}好,我是練習時長兩年半的${this.child}`);
}
// 注意記得把子類的構造指向子類自己
Child.prototype.constructor = Child;
var parent = new Parent('father');
parent.say() // father: 你打籃球的樣子像kunkun
var child = new Child('cxk', 'father');
child.say() // father好,我是練習時長兩年半的cxk
複製代碼
var json = '{"name":"cxk", "age":25}';
var obj = eval("(" + json + ")");
複製代碼
此方法屬於黑魔法,極易容易被xss攻擊,還有一種new Function
大同小異。
簡單的教程看這個半小時實現一個 JSON 解析器
我很早以前實現過一版,並且註釋不少,可是竟然找不到了,這是在網絡上找了一版帶註釋的,目測沒有大問題,具體過程能夠看這篇史上最易讀懂的 Promise/A+ 徹底實現
var PromisePolyfill = (function () {
// 和reject不一樣的是resolve須要嘗試展開thenable對象
function tryToResolve (value) {
if (this === value) {
// 主要是防止下面這種狀況
// let y = new Promise(res => setTimeout(res(y)))
throw TypeError('Chaining cycle detected for promise!')
}
// 根據規範2.32以及2.33 對對象或者函數嘗試展開
// 保證S6以前的 polyfill 也能和ES6的原生promise混用
if (value !== null &&
(typeof value === 'object' || typeof value === 'function')) {
try {
// 這裏記錄此次then的值同時要被try包裹
// 主要緣由是 then 多是一個getter, 也也就是說
// 1. value.then可能報錯
// 2. value.then可能產生反作用(例如屢次執行可能結果不一樣)
var then = value.then
// 另外一方面, 因爲沒法保證 then 確實會像預期的那樣只調用一個onFullfilled / onRejected
// 因此增長了一個flag來防止resolveOrReject被屢次調用
var thenAlreadyCalledOrThrow = false
if (typeof then === 'function') {
// 是thenable 那麼嘗試展開
// 而且在該thenable狀態改變以前this對象的狀態不變
then.bind(value)(
// onFullfilled
function (value2) {
if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
tryToResolve.bind(this, value2)()
}.bind(this),
// onRejected
function (reason2) {
if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
resolveOrReject.bind(this, 'rejected', reason2)()
}.bind(this)
)
} else {
// 擁有then 可是then不是一個函數 因此也不是thenable
resolveOrReject.bind(this, 'resolved', value)()
}
} catch (e) {
if (thenAlreadyCalledOrThrow) return
thenAlreadyCalledOrThrow = true
resolveOrReject.bind(this, 'rejected', e)()
}
} else {
// 基本類型 直接返回
resolveOrReject.bind(this, 'resolved', value)()
}
}
function resolveOrReject (status, data) {
if (this.status !== 'pending') return
this.status = status
this.data = data
if (status === 'resolved') {
for (var i = 0; i < this.resolveList.length; ++i) {
this.resolveList[i]()
}
} else {
for (i = 0; i < this.rejectList.length; ++i) {
this.rejectList[i]()
}
}
}
function Promise (executor) {
if (!(this instanceof Promise)) {
throw Error('Promise can not be called without new !')
}
if (typeof executor !== 'function') {
// 非標準 但與Chrome谷歌保持一致
throw TypeError('Promise resolver ' + executor + ' is not a function')
}
this.status = 'pending'
this.resolveList = []
this.rejectList = []
try {
executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected'))
} catch (e) {
resolveOrReject.bind(this, 'rejected', e)()
}
}
Promise.prototype.then = function (onFullfilled, onRejected) {
// 返回值穿透以及錯誤穿透, 注意錯誤穿透用的是throw而不是return,不然的話
// 這個then返回的promise狀態將變成resolved即接下來的then中的onFullfilled
// 會被調用, 然而咱們想要調用的是onRejected
if (typeof onFullfilled !== 'function') {
onFullfilled = function (data) {
return data
}
}
if (typeof onRejected !== 'function') {
onRejected = function (reason) {
throw reason
}
}
var executor = function (resolve, reject) {
setTimeout(function () {
try {
// 拿到對應的handle函數處理this.data
// 並以此爲依據解析這個新的Promise
var value = this.status === 'resolved'
? onFullfilled(this.data)
: onRejected(this.data)
resolve(value)
} catch (e) {
reject(e)
}
}.bind(this))
}
// then 接受兩個函數返回一個新的Promise
// then 自身的執行永遠異步與onFullfilled/onRejected的執行
if (this.status !== 'pending') {
return new Promise(executor.bind(this))
} else {
// pending
return new Promise(function (resolve, reject) {
this.resolveList.push(executor.bind(this, resolve, reject))
this.rejectList.push(executor.bind(this, resolve, reject))
}.bind(this))
}
}
// for prmise A+ test
Promise.deferred = Promise.defer = function () {
var dfd = {}
dfd.promise = new Promise(function (resolve, reject) {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
// for prmise A+ test
if (typeof module !== 'undefined') {
module.exports = Promise
}
return Promise
})()
PromisePolyfill.all = function (promises) {
return new Promise((resolve, reject) => {
const result = []
let cnt = 0
for (let i = 0; i < promises.length; ++i) {
promises[i].then(value => {
cnt++
result[i] = value
if (cnt === promises.length) resolve(result)
}, reject)
}
})
}
PromisePolyfill.race = function (promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; ++i) {
promises[i].then(resolve, reject)
}
})
}
複製代碼
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 ], // 重複出現的 key 要組裝成數組,能被轉成數字的就轉成數字類型 city: '北京', // 中文需解碼 enabled: true, // 未指定值得 key 約定爲 true } */
複製代碼
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
let paramsObj = {};
// 將 params 存到對象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 處理有 value 的參數
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解碼
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字
if (paramsObj.hasOwnProperty(key)) { // 若是對象有 key,則添加一個值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 若是對象沒有這個 key,建立 key 並設置值
paramsObj[key] = val;
}
} else { // 處理沒有 value 的參數
paramsObj[param] = true;
}
})
return paramsObj;
}
複製代碼
let template = '我是{{name}},年齡{{age}},性別{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年齡18,性別undefined
複製代碼
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
if (reg.test(template)) { // 判斷模板裏是否有模板字符串
const name = reg.exec(template)[1]; // 查找當前模板裏第一個模板字符串的字段
template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
return render(template, data); // 遞歸的渲染並返回渲染後的結構
}
return template; // 若是模板沒有模板字符串直接返回
}
複製代碼
var s1 = "get-element-by-id"
// 轉化爲 getElementById
複製代碼
var f = function(s) {
return s.replace(/-\w/g, function(x) {
return x.slice(1).toUpperCase();
})
}
複製代碼
例: abbcccddddd -> 字符最多的是d,出現了5次
let str = "abcabcabcbbccccc";
let num = 0;
let char = '';
// 使其按照必定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"
// 定義正則表達式
let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {
if(num < $0.length){
num = $0.length;
char = $1;
}
});
console.log(`字符最多的是${char},出現了${num}次`);
複製代碼
請使用最基本的遍從來實現判斷字符串 a 是否被包含在字符串 b 中,並返回第一次出現的位置(找不到返回 -1)。
a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
複製代碼
function isContain(a, b) {
for (let i in b) {
if (a[0] === b[i]) {
let tmp = true;
for (let j in a) {
if (a[j] !== b[~~i + ~~j]) {
tmp = false;
}
}
if (tmp) {
return i;
}
}
}
return -1;
}
複製代碼
// 保留三位小數
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
複製代碼
function parseToMoney(num) {
num = parseFloat(num.toFixed(3));
let [integer, decimal] = String.prototype.split.call(num, '.');
integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
return integer + '.' + (decimal ? decimal : '');
}
複製代碼
正則表達式(運用了正則的前向聲明和反前向聲明):
function parseToMoney(str){
// 僅僅對位置進行匹配
let re = /(?=(?!\b)(\d{3})+$)/g;
return str.replace(re,',');
}
複製代碼
function isPhone(tel) {
var regx = /^1[34578]\d{9}$/;
return regx.test(tel);
}
複製代碼
function isEmail(email) {
var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
return regx.test(email);
}
複製代碼
function isCardNo(number) {
var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return regx.test(number);
}
複製代碼
想要實時關注筆者最新的文章和最新的文檔更新請關注公衆號程序員面試官,後續的文章會優先在公衆號更新.
簡歷模板: 關注公衆號回覆「模板」獲取
《前端面試手冊》: 配套於本指南的突擊手冊,關注公衆號回覆「fed」獲取