写点什么

京东前端二面常考手写面试题(必备)

  • 2023-03-01
    浙江
  • 本文字数:8064 字

    阅读完需:约 26 分钟

实现发布-订阅模式

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);  };};
复制代码


适用场景:


  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动

  • 缩放场景:监控浏览器 resize

  • 动画场景:避免短时间内多次触发动画引起性能问题


参考 前端进阶面试题详细解答

实现日期格式化函数

输入:


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) 核心思路


  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数

  2. 这个方法返回一个新的 promise 对象,

  3. 遍历传入的参数,用 Promise.resolve()将参数"包一层",使其变成一个 promise 对象

  4. 参数所有回调成功才是成功,返回值数组与参数顺序一致

  5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 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)) : "";}
复制代码

实现发布订阅模式

简介:


发布订阅者模式,一种对象间一对多的依赖关系,但一个对象的状态发生改变时,所依赖它的对象都将得到状态改变的通知。


主要的作用(优点):


  1. 广泛应用于异步编程中(替代了传递回调函数)

  2. 对象之间松散耦合的编写代码


缺点:


  • 创建订阅者本身要消耗一定的时间和内存

  • 多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护


实现的思路:


  • 创建一个对象(缓存列表)

  • 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;}
复制代码


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
京东前端二面常考手写面试题(必备)_JavaScript_helloworld1024fd_InfoQ写作社区