实现发布-订阅模式
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 很相似,不多赘述
// 模拟 applyFunction.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); };};
复制代码
适用场景:
参考 前端进阶面试题详细解答
实现日期格式化函数
输入:
dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01dateFormat(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) }) } })}// testlet 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
思路:深度比较两个对象,就是要深度比较对象的每一个元素。=> 递归
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;}
复制代码
评论