说说前端经常考的手写题

2022-11-04 09:23:28 浏览数 (1)

实现数组扁平化flat方法

题目描述: 实现一个方法使多维数组变成一维数组

代码语言:javascript复制
let ary = [1, [2, [3, [4, 5]]], 6];
let str = JSON.stringify(ary);

第0种处理:直接的调用

代码语言:javascript复制
arr_flat = arr.flat(Infinity);

第一种处理

代码语言:javascript复制
ary = str.replace(/([|])/g, '').split(',');

第二种处理

代码语言:javascript复制
str = str.replace(/([]))/g, '');
str = '['   str   ']';
ary = JSON.parse(str);

第三种处理:递归处理

代码语言:javascript复制
let result = [];
let fn = function(ary) {
  for(let i = 0; i < ary.length; i  ) }{
    let item = ary[i];
    if (Array.isArray(ary[i])){
      fn(item);
    } else {
      result.push(item);
    }
  }
}

第四种处理:用 reduce 实现数组的 flat 方法

代码语言:javascript复制
function flatten(ary) {
    return ary.reduce((pre, cur) => {
        return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
    }, []);
}
let ary = [1, 2, [3, 4], [5, [6, 7]]]
console.log(flatten(ary))

第五种处理:能用迭代的思路去实现

代码语言:javascript复制
function flatten(arr) {
  if (!arr.length) return;
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}
// console.log(flatten([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

第六种处理:扩展运算符

代码语言:javascript复制
while (ary.some(Array.isArray)) {
  ary = [].concat(...ary);
}

类数组转化为数组

类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。

方法一:Array.from
代码语言:javascript复制
Array.from(document.querySelectorAll('div'))
方法二:Array.prototype.slice.call()
代码语言:javascript复制
Array.prototype.slice.call(document.querySelectorAll('div'))
方法三:扩展运算符
代码语言:javascript复制
[...document.querySelectorAll('div')]
方法四:利用concat
代码语言:javascript复制
Array.prototype.concat.apply([], document.querySelectorAll('div'));

手写 Promise

代码语言:javascript复制
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
  // 保存初始化状态
  var self = this;

  // 初始化状态
  this.state = PENDING;

  // 用于保存 resolve 或者 rejected 传入的值
  this.value = null;

  // 用于保存 resolve 的回调函数
  this.resolvedCallbacks = [];

  // 用于保存 reject 的回调函数
  this.rejectedCallbacks = [];

  // 状态转变为 resolved 方法
  function resolve(value) {
    // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变,
      if (self.state === PENDING) {
        // 修改状态
        self.state = RESOLVED;

        // 设置传入的值
        self.value = value;

        // 执行回调函数
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 状态转变为 rejected 方法
  function reject(value) {
    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变
      if (self.state === PENDING) {
        // 修改状态
        self.state = REJECTED;

        // 设置传入的值
        self.value = value;

        // 执行回调函数
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 将两个方法传入函数执行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到错误时,捕获错误,执行 reject 函数
    reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };

  // 如果是等待状态,则将函数加入对应列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }

  // 如果状态已经凝固,则直接执行对应状态的函数

  if (this.state === RESOLVED) {
    onResolved(this.value);
  }

  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

实现防抖函数(debounce)

防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

那么与节流函数的区别直接看这个动画实现即可。

手写简化版:

代码语言:javascript复制
// 防抖函数
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

生存环境请用lodash.debounce

参考:前端手写面试题详细解答

数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组

代码语言:JavaScript复制
const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]
方法一:使用flat()
代码语言:javascript复制
const res1 = arr.flat(Infinity);
方法二:利用正则
代码语言:javascript复制
const res2 = JSON.stringify(arr).replace(/[|]/g, '').split(',');

但数据类型都会变为字符串

方法三:正则改良版本
代码语言:javascript复制
const res3 = JSON.parse('['   JSON.stringify(arr).replace(/[|]/g, '')   ']');
方法四:使用reduce
代码语言:javascript复制
const flatten = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
  }, [])
}
const res4 = flatten(arr);
方法五:函数递归
代码语言:javascript复制
const res5 = [];
const fn = arr => {
  for (let i = 0; i < arr.length; i  ) {
    if (Array.isArray(arr[i])) {
      fn(arr[i]);
    } else {
      res5.push(arr[i]);
    }
  }
}
fn(arr);

字符串解析问题

代码语言:javascript复制
var a = {
    b: 123,
    c: '456',
    e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'

实现函数使得将str字符串中的{}内的变量替换,如果属性不存在保持原样(比如{a.d}

类似于模版字符串,但有一点出入,实际上原理大差不差

代码语言:javascript复制
const fn1 = (str, obj) => {
    let res = '';
    // 标志位,标志前面是否有{
    let flag = false;
    let start;
    for (let i = 0; i < str.length; i  ) {
        if (str[i] === '{') {
            flag = true;
            start = i   1;
            continue;
        }
        if (!flag) res  = str[i];
        else {
            if (str[i] === '}') {
                flag = false;
                res  = match(str.slice(start, i), obj);
            }
        }
    }
    return res;
}
// 对象匹配操作
const match = (str, obj) => {
    const keys = str.split('.').slice(1);
    let index = 0;
    let o = obj;
    while (index < keys.length) {
        const key = keys[index];
        if (!o[key]) {
            return `{${str}}`;
        } else {
            o = o[key];
        }
        index  ;
    }
    return o;
}

实现日期格式化函数

输入:

代码语言:javascript复制
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日
代码语言:javascript复制
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
}

实现字符串的repeat方法

输入字符串s,以及其重复的次数,输出重复的结果,例如输入abc,2,输出abcabc。

代码语言:javascript复制
function repeat(s, n) {
    return (new Array(n   1)).join(s);
}

递归:

代码语言:javascript复制
function repeat(s, n) {
    return (n > 0) ? s.concat(repeat(s, --n)) : "";
}

字符串查找

请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。

代码语言:javascript复制
a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
代码语言:css复制
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;
}

实现发布-订阅模式

代码语言:javascript复制
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]
      }
    }
  }
}

Promise.race

代码语言:javascript复制
Promise.race = function(promiseArr) {
  return new Promise((resolve, reject) => {
    promiseArr.forEach(p => {
      // 如果不是Promise实例需要转化为Promise实例
      Promise.resolve(p).then(
        val => resolve(val),
        err => reject(err),
      )
    })
  })
}

实现一个队列

基于链表结构实现队列

代码语言:javascript复制
const LinkedList = require('./实现一个链表结构')

// 用链表默认使用数组来模拟队列,性能更佳
class Queue {
  constructor() {
    this.ll = new LinkedList()
  }
  // 向队列中添加
  offer(elem) {
    this.ll.add(elem)
  }
  // 查看第一个
  peek() {
    return this.ll.get(0)
  }
  // 队列只能从头部删除
  remove() {
    return this.ll.remove(0)
  }
}

var queue = new Queue()

queue.offer(1)
queue.offer(2)
queue.offer(3)
var removeVal = queue.remove(3)

console.log(queue.ll,'queue.ll')
console.log(removeVal,'queue.remove')
console.log(queue.peek(),'queue.peek')

Object.is

Object.is解决的主要是这两个问题:

代码语言:javascript复制
 0 === -0  // true
NaN === NaN // false
代码语言:javascript复制
const is= (x, y) => {
  if (x === y) {
    //  0和-0应该不相等
    return x !== 0 || y !== 0 || 1/x === 1/y;
  } else {
    return x !== x && y !== y;
  }
}

实现一个迭代器生成函数

ES6对迭代器的实现

JS原生的集合类型数据结构,只有Array(数组)和Object(对象);而ES6中,又新增了MapSet。四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套 统一的接口机制 ——迭代器(Iterator)。

ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for...of...循环和迭代器的next方法遍历。 事实上,for...of...的背后正是对next方法的反复调用。

在ES6中,针对ArrayMapSetStringTypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for...of...进行遍历。原理都是一样的,此处我们拿最简单的数组进行举例,当我们用for...of...遍历数组时:

代码语言:javascript复制
const arr = [1, 2, 3]
const len = arr.length
for(item of arr) {
   console.log(`当前元素是${item}`)
}

之所以能够按顺序一次一次地拿到数组里的每一个成员,是因为我们借助数组的Symbol.iterator生成了它对应的迭代器对象,通过反复调用迭代器对象的next方法访问了数组成员,像这样:

代码语言:javascript复制
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()

丢进控制台,我们可以看到next每次会按顺序帮我们访问一个集合成员:

for...of...做的事情,基本等价于下面这通操作:

代码语言:javascript复制
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()

// 初始化一个迭代结果
let now = { done: false }

// 循环往外迭代成员
while(!now.done) {
    now = iterator.next()
    if(!now.done) {
        console.log(`现在遍历到了${now.value}`)
    }
}

可以看出,for...of...其实就是iterator循环调用换了种写法。在ES6中我们之所以能够开心地用for...of...遍历各种各种的集合,全靠迭代器模式在背后给力。

ps:此处推荐阅读迭代协议 (opens new window),相信大家读过后会对迭代器在ES6中的实现有更深的理解。

实现Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

代码语言:javascript复制
// 模拟 Object.create

function create(proto) {
  function F() {}
  F.prototype = proto;

  return new F();
}

验证是否是身份证

代码语言:javascript复制
function isCardNo(number) {
    var regx = /(^d{15}$)|(^d{18}$)|(^d{17}(d|X|x)$)/;
    return regx.test(number);
}

实现一个compose函数

组合多个函数,从右到左,比如:compose(f, g, h) 最终得到这个结果 (...args) => f(g(h(...args))).

题目描述:实现一个 compose 函数

代码语言:javascript复制
// 用法如下:
function fn1(x) {
  return x   1;
}
function fn2(x) {
  return x   2;
}
function fn3(x) {
  return x   3;
}
function fn4(x) {
  return x   4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1 4 3 2 1=11

实现代码如下

代码语言:javascript复制
function compose(...funcs) {
  if (!funcs.length) return (v) => v;

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => {
    return (...args) => a(b(...args)))
  }
}

compose创建了一个从右向左执行的数据流。如果要实现从左到右的数据流,可以直接更改compose的部分代码即可实现

  • 更换Api接口:把reduce改为reduceRight
  • 交互包裹位置:把a(b(...args))改为b(a(...args))

event模块

实现node中回调函数的机制,node中回调函数其实是内部使用了观察者模式

观察者模式:定义了对象间一种一对多的依赖关系,当目标对象Subject发生改变时,所有依赖它的对象Observer都会得到通知。

代码语言:javascript复制
function EventEmitter() {
  this.events = new Map();
}

// 需要实现的一些方法:
// addListener、removeListener、once、removeAllListeners、emit

// 模拟实现addlistener方法
const wrapCallback = (fn, once = false) => ({ callback: fn, once });
EventEmitter.prototype.addListener = function(type, fn, once = false) {
  const hanlder = this.events.get(type);
  if (!hanlder) {
    // 没有type绑定事件
    this.events.set(type, wrapCallback(fn, once));
  } else if (hanlder && typeof hanlder.callback === 'function') {
    // 目前type事件只有一个回调
    this.events.set(type, [hanlder, wrapCallback(fn, once)]);
  } else {
    // 目前type事件数>=2
    hanlder.push(wrapCallback(fn, once));
  }
}
// 模拟实现removeListener
EventEmitter.prototype.removeListener = function(type, listener) {
  const hanlder = this.events.get(type);
  if (!hanlder) return;
  if (!Array.isArray(this.events)) {
    if (hanlder.callback === listener.callback) this.events.delete(type);
    else return;
  }
  for (let i = 0; i < hanlder.length; i  ) {
    const item = hanlder[i];
    if (item.callback === listener.callback) {
      hanlder.splice(i, 1);
      i--;
      if (hanlder.length === 1) {
        this.events.set(type, hanlder[0]);
      }
    }
  }
}
// 模拟实现once方法
EventEmitter.prototype.once = function(type, listener) {
  this.addListener(type, listener, true);
}
// 模拟实现emit方法
EventEmitter.prototype.emit = function(type, ...args) {
  const hanlder = this.events.get(type);
  if (!hanlder) return;
  if (Array.isArray(hanlder)) {
    hanlder.forEach(item => {
      item.callback.apply(this, args);
      if (item.once) {
        this.removeListener(type, item);
      }
    })
  } else {
    hanlder.callback.apply(this, args);
    if (hanlder.once) {
      this.events.delete(type);
    }
  }
  return true;
}
EventEmitter.prototype.removeAllListeners = function(type) {
  const hanlder = this.events.get(type);
  if (!hanlder) return;
  this.events.delete(type);
}

分片思想解决大数据量渲染问题

题目描述: 渲染百万条结构简单的大数据时 怎么使用分片思想优化渲染

代码语言:javascript复制
let ul = document.getElementById("container");
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total / once;
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal, curIndex) {
  if (curTotal <= 0) {
    return false;
  }
  //每页多少条
  let pageCount = Math.min(curTotal, once);
  window.requestAnimationFrame(function () {
    for (let i = 0; i < pageCount; i  ) {
      let li = document.createElement("li");
      li.innerText = curIndex   i   " : "   ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex   pageCount);
  });
}
loop(total, index);

扩展思考 :对于大数据量的简单 dom 结构渲染可以用分片思想解决 如果是复杂的 dom 结构渲染如何处理?

这时候就需要使用虚拟列表了,虚拟列表和虚拟表格在日常项目使用还是很多的

请实现 DOM2JSON 一个函数,可以把一个 DOM 节点输出 JSON 的格式

代码语言:javascript复制
<div>
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

把上面dom结构转成下面的JSON格式

{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

实现代码如下:

代码语言:javascript复制
function dom2Json(domtree) {
  let obj = {};
  obj.name = domtree.tagName;
  obj.children = [];
  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
  return obj;
}

0 人点赞