實現釋出-訂閱模式
class EventCenter{
// 1. 定義事件容器,用來裝事件陣列
let handlers = {}
// 2. 新增事件方法,引數:事件名 事件方法
addEventListener(type, handler) {
// 建立新陣列容器
if (!this.handlers[type]) {
this.handlers[type] = []
}
// 存入事件
this.handlers[type].push(handler)
}
// 3. 觸發事件,引數:事件名 事件引數
dispatchEvent(type, params) {
// 若沒有註冊該事件則丟擲錯誤
if (!this.handlers[type]) {
return new Error('該事件未註冊')
}
// 觸發事件
this.handlers[type].forEach(handler => {
handler(...params)
})
}
// 4. 事件移除,引數:事件名 要刪除事件,若無第二個引數則刪除該事件的訂閱和釋出
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]
}
}
}
}
手寫 new 運算子
在呼叫 new
的過程中會發生以上四件事情:
(1)首先建立了一個新的空物件
(2)設定原型,將物件的原型設定為函式的 prototype 物件。
(3)讓函式的 this 指向這個物件,執行建構函式的程式碼(為這個新物件新增屬性)
(4)判斷函式的返回值型別,如果是值型別,返回建立的物件。如果是引用型別,就返回這個引用型別的物件。
function objectFactory() {
let newObject = null;
let constructor = Array.prototype.shift.call(arguments);
let result = null;
// 判斷引數是否是一個函式
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一個空物件,物件的原型為建構函式的 prototype 物件
newObject = Object.create(constructor.prototype);
// 將 this 指向新建物件,並執行函式
result = constructor.apply(newObject, arguments);
// 判斷返回物件
let flag = result && (typeof result === "object" || typeof result === "function");
// 判斷返回結果
return flag ? result : newObject;
}
// 使用方法
objectFactory(建構函式, 初始化引數);
實現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;
};
debounce(防抖)
觸發高頻時間後n秒內函式只會執行一次,如果n秒內高頻時間再次觸發,則重新計算時間。
const debounce = (fn, time) => {
let timeout = null;
return function() {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, time);
}
};
防抖常應用於使用者進行搜尋輸入節約請求資源,window
觸發resize
事件時進行防抖只觸發一次。
判斷物件是否存在迴圈引用
迴圈引用物件本來沒有什麼問題,但是序列化的時候就會發生問題,比如呼叫JSON.stringify()
對該類物件進行序列化,就會報錯: Converting circular structure to JSON.
下面方法可以用來判斷一個物件中是否已存在迴圈引用:
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 = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;
console.log(isCycleObject(o)
查詢有序二維陣列的目標值:
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;
};
二維陣列斜向列印:
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
}
實現節流函式(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
- 動畫場景:避免短時間內多次觸發動畫引起效能問題
參考 前端進階面試題詳細解答
實現日期格式化函式
輸入:
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
}
實現AJAX請求
AJAX是 Asynchronous JavaScript and XML 的縮寫,指的是透過 JavaScript 的 非同步通訊,從伺服器獲取 XML 文件從中提取資料,再更新當前網頁的對應部分,而不用重新整理整個網頁。
建立AJAX請求的步驟:
- 建立一個 XMLHttpRequest 物件。
- 在這個物件上使用 open 方法建立一個 HTTP 請求,open 方法所需要的引數是請求的方法、請求的地址、是否非同步和使用者的認證資訊。
- 在發起請求前,可以為這個物件新增一些資訊和監聽函式。比如說可以透過 setRequestHeader 方法來為請求新增頭資訊。還可以為這個物件新增一個狀態監聽函式。一個 XMLHttpRequest 物件一共有 5 個狀態,當它的狀態變化時會觸發onreadystatechange 事件,可以透過設定監聽函式,來處理請求成功後的結果。當物件的 readyState 變為 4 的時候,代表伺服器返回的資料接收完成,這個時候可以透過判斷請求的狀態,如果狀態是 2xx 或者 304 的話則代表返回正常。這個時候就可以透過 response 中的資料來對頁面進行更新了。
- 當物件的屬性和監聽函式設定完成後,最後調用 sent 方法來向伺服器發起請求,可以傳入引數作為傳送的資料體。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 建立 Http 請求
xhr.open("GET", SERVER_URL, true);
// 設定狀態監聽函式
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 當請求成功時
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 設定請求失敗時的監聽函式
xhr.onerror = function() {
console.error(this.statusText);
};
// 設定請求頭資訊
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 傳送 Http 請求
xhr.send(null);
驗證是否是身份證
function isCardNo(number) {
var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return regx.test(number);
}
陣列去重
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
方法一:利用Set
const res1 = Array.from(new Set(arr));
方法二:兩層for迴圈+splice
const unique1 = arr => {
let len = arr.length;
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
// 每刪除一個樹,j--保證j的值經過自加後不變。同時,len--,減少迴圈次數提升效能
len--;
j--;
}
}
}
return arr;
}
方法三:利用indexOf
const unique2 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
}
當然也可以用include、filter,思路大同小異。
方法四:利用include
const unique3 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
方法五:利用filter
const unique4 = arr => {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
方法六:利用Map
const unique5 = arr => {
const map = new Map();
const res = [];
for (let i = 0; i < arr.length; i++) {
if (!map.has(arr[i])) {
map.set(arr[i], true)
res.push(arr[i]);
}
}
return res;
}
驗證是否是郵箱
function isEmail(email) {
var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
return regx.test(email);
}
手寫 Promise.all
1) 核心思路
- 接收一個 Promise 例項的陣列或具有 Iterator 介面的物件作為引數
- 這個方法返回一個新的 promise 物件,
- 遍歷傳入的引數,用Promise.resolve()將引數"包一層",使其變成一個promise物件
- 引數所有回撥成功才是成功,返回值陣列與引數順序一致
- 引數陣列其中一個失敗,則觸發失敗狀態,第一個觸發失敗的 Promise 錯誤資訊作為 Promise.all 的錯誤資訊。
2)實現程式碼
一般來說,Promise.all 用來處理多個併發請求,也是為了頁面資料構造的方便,將一個頁面所用到的在不同介面的資料一起請求過來,不過,如果其中一個介面失敗了,多個請求也就失敗了,頁面可能啥也出不來,這就看當前頁面的耦合程度了
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]
})
實現陣列的push方法
let arr = [];
Array.prototype.push = function() {
for( let i = 0 ; i < arguments.length ; i++){
this[this.length] = arguments[i] ;
}
return this.length;
}
模板引擎實現
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; // 如果模板沒有模板字串直接返回
}
實現字串的repeat方法
輸入字串s,以及其重複的次數,輸出重複的結果,例如輸入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)) : "";
}
實現釋出訂閱模式
簡介:
釋出訂閱者模式,一種物件間一對多的依賴關係,但一個物件的狀態發生改變時,所依賴它的物件都將得到狀態改變的通知。
主要的作用(優點):
- 廣泛應用於非同步程式設計中(替代了傳遞迴調函式)
- 物件之間鬆散耦合的編寫程式碼
缺點:
- 建立訂閱者本身要消耗一定的時間和記憶體
- 多個釋出者和訂閱者巢狀一起的時候,程式難以跟蹤維護
實現的思路:
- 建立一個物件(快取列表)
on
方法用來把回撥函式fn
都加到快取列表中emit
根據key
值去執行對應快取列表中的函式off
方法可以根據key
值取消訂閱
class EventEmiter {
constructor() {
// 事件物件,存放訂閱的名字和事件
this._events = {}
}
// 訂閱事件的方法
on(eventName,callback) {
if(!this._events) {
this._events = {}
}
// 合併之前訂閱的cb
this._events[eventName] = [...(this._events[eventName] || []),callback]
}
// 觸發事件的方法
emit(eventName, ...args) {
if(!this._events[eventName]) {
return
}
// 遍歷執行所有訂閱的事件
this._events[eventName].forEach(fn=>fn(...args))
}
off(eventName,cb) {
if(!this._events[eventName]) {
return
}
// 刪除訂閱的事件
this._events[eventName] = this._events[eventName].filter(fn=>fn != cb && fn.l != cb)
}
// 繫結一次 觸發後將繫結的移除掉 再次觸發掉
once(eventName,callback) {
const one = (...args)=>{
// 等callback執行完畢在刪除
callback(args)
this.off(eventName,one)
}
one.l = callback // 自定義屬性
this.on(eventName,one)
}
}
測試用例
let event = new EventEmiter()
let login1 = function(...args) {
console.log('login success1', args)
}
let login2 = function(...args) {
console.log('login success2', args)
}
// event.on('login',login1)
event.once('login',login2)
event.off('login',login1) // 解除訂閱
event.emit('login', 1,2,3,4,5)
event.emit('login', 6,7,8,9)
event.emit('login', 10,11,12)
釋出訂閱者模式和觀察者模式的區別?
- 釋出/訂閱模式是觀察者模式的一種變形,兩者區別在於,釋出/訂閱模式在觀察者模式的基礎上,在目標和觀察者之間增加一個排程中心。
- 觀察者模式是由具體目標排程,比如當事件觸發,
Subject
就會去呼叫觀察者的方法,所以觀察者模式的訂閱者與釋出者之間是存在依賴的。 - 釋出/訂閱模式由統一排程中心呼叫,因此釋出者和訂閱者不需要知道對方的存在。
使用ES5和ES6求函式引數的和
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
}
實現斐波那契數列
// 遞迴
function fn (n){
if(n==0) return 0
if(n==1) return 1
return fn(n-2)+fn(n-1)
}
// 最佳化
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;
}
手寫深度比較isEqual
思路:深度比較兩個物件,就是要深度比較物件的每一個元素。=> 遞迴
遞迴退出條件:
- 被比較的是兩個值型別變數,直接用“===”判斷
- 被比較的兩個變數之一為
null
,直接判斷另一個元素是否也為null
提前結束遞推:
- 兩個變數
keys
數量不同 - 傳入的兩個引數是同一個變數
- 兩個變數
- 遞推工作:深度比較每一個
key
function isEqual(obj1, obj2){
//其中一個為值型別或null
if(!isObject(obj1) || !isObject(obj2)){
return obj1 === obj2;
}
//判斷是否兩個引數是同一個變數
if(obj1 === obj2){
return true;
}
//判斷keys數是否相等
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj2);
if(obj1Keys.length !== obj2Keys.length){
return false;
}
//深度比較每一個key
for(let key in obj1){
if(!isEqual(obj1[key], obj2[key])){
return false;
}
}
return true;
}
樹形結構轉成列表(處理選單)
[
{
id: 1,
text: '節點1',
parentId: 0,
children: [
{
id:2,
text: '節點1_1',
parentId:1
}
]
}
]
轉成
[
{
id: 1,
text: '節點1',
parentId: 0 //這裡用0表示為頂級節點
},
{
id: 2,
text: '節點1_1',
parentId: 1 //透過這個欄位來確定子父級
}
...
]
實現程式碼如下:
function treeToList(data) {
let res = [];
const dfs = (tree) => {
tree.forEach((item) => {
if (item.children) {
dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}