JavaScript筆試部分
點擊關注本公眾號獲取文檔最新更新,并可以領取配套于本指南的 《前端面試手冊》 以及最標準的簡歷模板.
實現防抖函數(debounce)防抖函數原理:在事件被觸發n秒后再執行回調,如果在這n秒內又被觸發,則重新計時。
那么與節流函數的區別直接看這個動畫實現即可。
手寫簡化版:
// 防抖函數 const debounce = (fn, delay) => { let timer = null; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; };
適用場景:
按鈕提交場景:防止多次提交按鈕,只執行最后提交的一次
服務端驗證場景:表單驗證需要服務端配合,只執行一段連續的輸入事件的最后一次,還有搜索聯想詞功能類似
生存環境請用lodash.debounce
實現節流函數(throttle)防抖函數原理:規定在一個單位時間內,只能觸發一次函數。如果這個單位時間內觸發多次函數,只有一次生效。
// 手寫簡化版
// 節流函數 const throttle = (fn, delay = 500) => { let flag = true; return (...args) => { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, delay); }; };
適用場景:
拖拽場景:固定時間內只執行一次,防止超高頻次觸發位置變動
縮放場景:監控瀏覽器resize
動畫場景:避免短時間內多次觸發動畫引起性能問題
深克隆(deepclone)簡單版:
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); };
局限性:
一些特殊情況沒有處理: 例如Buffer對象、Promise、Set、Map
另外對于確保沒有循環引用的對象,我們可以省去對循環引用的特殊處理,因為這很消耗時間
原理詳解實現深克隆實現Event(event bus)
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
// 模擬 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
new操作符做了這些事:
它創建了一個全新的對象
它會被執行[[Prototype]](也就是__proto__)鏈接
它使this指向新創建的對象
通過new創建的每個對象將最終被[[Prototype]]鏈接到這個函數的prototype對象上
如果函數沒有返回對象類型Object(包含Functoin, Array, Date, RegExg, Error),那么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做了什么:
將函數設為對象的屬性
執行&刪除這個函數
指定this到函數并傳入給定參數執行函數
如果不傳入參數,默認指向為 window
// 模擬 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; };
具體實現參考JavaScript深入之call和apply的模擬實現實現apply方法
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
實現bind要做什么
返回一個函數,綁定this,傳遞預置參數
bind返回的函數可以作為構造函數使用。故作為構造函數時應使得this失效,但是傳入的參數依然有效
// 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; }; }
詳解請移步JavaScript深入之bind的模擬實現 #12模擬Object.create
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實現JSON.parse
var json = "{"name":"cxk", "age":25}"; var obj = eval("(" + json + ")");
此方法屬于黑魔法,極易容易被xss攻擊,還有一種new Function大同小異。
簡單的教程看這個半小時實現一個 JSON 解析器
實現Promise我很早之前實現過一版,而且注釋很多,但是居然找不到了,這是在網絡上找了一版帶注釋的,目測沒有大問題,具體過程可以看這篇史上最易讀懂的 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) } }) }解析 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 ], // 重復出現的 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(); }) }查找字符串中出現最多的字符和個數
例: abbcccffffddd -> 字符最多的是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 = /(?=(?!)(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」獲取
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/106700.html
摘要:解析第題第題為什么的和的中不能做異步操作解析第題第題京東下面代碼中在什么情況下會打印解析第題第題介紹下及其應用。盡量減少操作次數。解析第題第題京東快手周一算法題之兩數之和給定一個整數數組和一個目標值,找出數組中和為目標值的兩個數。 引言 半年時間,幾千人參與,精選大廠前端面試高頻 100 題,這就是「壹題」。 在 2019 年 1 月 21 日這天,「壹題」項目正式開始,在這之后每個工...
摘要:正如我標題所說,簡歷被拒。看了我簡歷之后說頭條競爭激烈,我背景不夠,點到為止。。三準備面試其實從三月份投遞簡歷開始準備面試到四月份收,也不過個月的時間,但這都是建立在我過去一年的積累啊。 本文是 無精瘋 同學投稿的面試經歷 關注微信公眾號:進擊的java程序員K,即可獲取最新BAT面試資料一份 在此感謝 無精瘋 同學的分享 目錄: 印象中的頭條 面試背景 準備面試 ...
摘要:正如我標題所說,簡歷被拒。看了我簡歷之后說頭條競爭激烈,我背景不夠,點到為止。。三準備面試其實從三月份投遞簡歷開始準備面試到四月份收,也不過個月的時間,但這都是建立在我過去一年的積累啊。 本文是 無精瘋 同學投稿的面試經歷 關注微信公眾號:進擊的java程序員K,即可獲取最新BAT面試資料一份 在此感謝 無精瘋 同學的分享目錄:印象中的頭條面試背景準備面試頭條一面(Java+項目)頭條...
摘要:引言半月刊第四期來啦,這段時間新增了道高頻面試題,今天就把最近半月匯總的面試題和部分答案發給大家,幫助大家查漏補缺,歡迎加群互相學習。更多更全的面試題和答案匯總在下面的項目中,點擊查看。引言 半月刊第四期來啦,這段時間 Daily-Interview-Question 新增了 14 道高頻面試題,今天就把最近半月匯總的面試題和部分答案發給大家,幫助大家查漏補缺,歡迎 加群 互相學習。 更多更...
摘要:前言在阿里和騰訊工作了年,當了年的前端面試官,把期間我和我的同事常問的面試題和答案匯總在我的中。項目地址是我是小蝌蚪,騰訊高級前端工程師,跟著我一起每周攻克幾個前端技術難點。 前言 在阿里和騰訊工作了6年,當了3年的前端面試官,把期間我和我的同事常問的面試題和答案匯總在我 Github 的 Weekly-FE-Interview 中。希望對大家有所幫助。 如果你在bat面試的時候遇到了...
閱讀 3066·2021-11-23 09:51
閱讀 1046·2021-09-02 15:21
閱讀 3012·2019-08-30 13:56
閱讀 1835·2019-08-29 14:12
閱讀 715·2019-08-29 13:53
閱讀 1671·2019-08-29 11:32
閱讀 1334·2019-08-29 11:25
閱讀 1497·2019-08-28 17:51