高频 JavaScript 手写面试题及答案

242 阅读2分钟

内容参考自juejin.cn/post/684490…

防抖函数(debounce)

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

// 非立即执行版
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 如果已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 立即执行版function debounce(func,wait) {
  let timeout;
  return function () {
      const context = this;
      const args = [...arguments];
      if (timeout) clearTimeout(timeout);
      const callNow = !timeout;
      timeout = setTimeout(() => {
          timeout = null;
      }, wait)
      if (callNow) func.apply(context, args)
  }
}
function useDebounce(fn, delay, dep = []) {
  const { current } = useRef({ fn, timer: null });
  useEffect(function () {
    current.fn = fn;
  }, [fn]);

  return useCallback(function f(...args) {
    if (current.timer) {
      clearTimeout(current.timer);
    }
    current.timer = setTimeout(() => {
      current.fn.call(this, ...args);
    }, delay);
  }, dep)
}

适用场景:

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

生存环境请用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);
  };
};
// 立即执行版
function throttle(func, wait) {
    var previous = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 动画场景:避免短时间内多次触发动画引起性能问题

实现Event(event bus)

event bus既是node中各个模块的基石,又是前端组件通信的依赖手段之一,同时涉及了订阅-发布设计模式,是非常重要的基础。

function EventBus() {}

EventBus.prototype.on = function (name, callback) {
    //如果没有事件对象,新增一个
    if (!this._events) {
        //创建一个干净的没有原型链的对象
        this._events = Object.create(null);
    }
    //如果没有这个事件的订阅,新增一个,如果有,push进去
    if (!this._events[name]) {
        this._events[name] = [callback];
    } else {
        this._events[name].push(callback);
    }
}

EventBus.prototype.emit = function (name, ...args) {
    //发布的时候,如果有这个事件,循环执行所有这个订阅的方法
    if (this._events[name]) {
        this._events[name].forEach(callback => {
            callback(...args);
        })
    }
}

EventBus.prototype.off = function (name) {
    //如果有这个事件的订阅,清除所有订阅
    if (this._events[name]) {
        delete this._events[name];
    }
}

EventBus.prototype.once = function (name, callback) {
    let once = (...args) => {
        callback(...args);
        this.off(name);
    };
    this.on(name, once);
}
const eventBus = new EventBus();
eventBus.on('fn1', function (msg) {
    console.log(`收到了信息:${msg}`)
})

实现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操作符做了这些事:

用new Object() 的方式新建了一个对象 obj

取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数

将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性

使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性

如果函数没有返回对象类型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;
}

柯里化curry

原理:比较多次接受的参数总数与函数定义时的入参数量,当接受参数的数量大于或等于被 Currying函数的传入参数数量时,就返回计算结果,否则返回一个继续接受参数的函数。 

// 关键步骤:使用一个变量(args)来记录传入的所有参数;递归调用函数
function curry(fn, ...args) {
    return (...arr) => {
        let result = [...args, ...arr]
        if (result.length === fn.length) {
            return fn(...result)
        } else {
            return curry(fn, ...result)
        }
    }
}
// es6 实现
function curry(fn, ...args) {
  return args.length >= fn.length ? fn(...args) : curry.bind(null, fn, ...args);
}

实现flatten(数组扁平化)

//迭代实现
function flatten(arr) {
    let arrs = [...arr]
    let newArr = [];
    while (arrs.length) {
        let item = arrs.shift()
        if (Array.isArray(item)) {
            arrs.unshift(...item)
        } else {
            newArr.push(item)
        }
    }
    return newArr
}
//递归实现
function flatten(arr) {
    let arrs = [];
    arr.map(item => {
        if (Array.isArray(item)) {
            arrs.push(...flatten(item))
        } else {
            arrs.push(item)
        }
    })
    return arrs
}
//字符串转换
arr.join(',').split(',').map(item => Number(item))
// 用 reduce 展开一层 + 递归
const flat = arr => {
  return arr.reduce((pre, cur) => {
    return pre.concat(Array.isArray(cur) ? flat(cur) : cur);
  }, []);
};
const flat = (arr, num = 1) => {
    return num > 0 ?
        arr.reduce((pre, cur) => {
            return pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur);
        }, []) :
        arr.slice()
};

实现数组转树

let list =[
    {id:1,name:'部门A',parentId:0},
    {id:2,name:'部门B',parentId:0},
    {id:3,name:'部门C',parentId:1},
    {id:4,name:'部门D',parentId:1},
    {id:5,name:'部门E',parentId:2},
    {id:6,name:'部门F',parentId:3},
    {id:7,name:'部门G',parentId:2},
    {id:8,name:'部门H',parentId:4}
];
function arrayToTree(input, parentId) {
    const array = [];
    const buildTree = (arr, parentId, childrenArray) => {
        arr.forEach((item) => {
            if (item.parentId === parentId) {
                item.children = [];
                buildTree(arr, item.id, item.children);
                childrenArray.push(item);
            }
        });
    }
    buildTree(input, parentId, array);
    return array;
}
arrayToTree(list, 0);
function convert(list) {
    const res = []
    const map = list.reduce((res, v) => (res[v.id] = v, res), {})
    for (const item of list) {
        if (item.parentId === null) {
            res.push(item)
            continue
        }
        if (item.parentId in map) {
            const parent = map[item.parentId]
            parent.children = parent.children || []
            parent.children.push(item)
        }
    }
    return res
}        
convert(list)

实现树转数组(扁平化)

function treeToArray(obj, res = []) { // 默认初始结果数组为[]
    res.push(obj); // 当前元素入栈
    // 若元素包含children,则遍历children并递归调用使每一个子元素入栈
    if (obj.children && obj.children.length) {
        for (const item of obj.children) {
            treeToArray(item, res);
        }
    }
    return res;
}

function treeToArray(obj) {
    const stack = []; // 声明栈,用来存储待处理元素
    const res = []; // 接收结果
    stack.push(obj); // 将初始元素压入栈
    while (stack.length) { // 栈不为空则循环执行
        const item = stack[0]; // 取出栈顶元素
        res.push(item); // 元素本身压入结果数组
        stack.shift(); // 将当前元素弹出栈
        // 逻辑处理,如果当前元素包含子元素,则将子元素压入栈
        if (item.children && item.children.length) {
            stack.push(...item.children);
        }
    }
    return res;
}

给一个 id 找出链条中其对应的所有的父级 id

function fn(data, value) {
    let res = []
    const dfs = (arr, temp = []) => {
        for (const node of arr) {
            if (node.id === value) {
                res = temp
                return
            } else {
                node.children && dfs(node.children, temp.concat(node.id))
            }
        }
    }
    dfs(data)
    return res
}
function search(object, value) {
    for (var key in object) {
        if (object[key] == value) return [key];
        if (typeof (object[key]) == "object") {
            var temp = search(object[key], value);
            if (temp) return [key, temp].flat();
        }
    }
}

实现call

call做了什么:

将函数设为对象的属性

执行&删除这个函数

指定this到函数并传入给定参数执行函数

如果不传入参数,默认指向为 window 

Function.prototype.myCall = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  const args = [...arguments].slice(1)
  const result = context.fn(...args)
  delete context.fn
  return result
}

实现apply

第二项为数组

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let result
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }
  delete context.fn
  return result
}

实现bind

返回一个函数,绑定this,传递预置参数

bind返回的函数可以作为构造函数使用。故作为构造函数时应使得this失效,但是传入的参数依然有效 

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  const args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

实现map

Array.prototype.copyMap = function (fn, toThis) {
    let arr = this;
    const result = [];
    const redirectThis = toThis || Object.create(null);
    for (let i = 0; i < arr.length; i++) {
        const item = fn.call(redirectThis, arr[i], i, arr);
        result.push(item);
    }
    return result;
};

实现reduce

Array.prototype.reduce = function(fn, init) {
    var arr = this   // this就是调用reduce方法的数组
    var total =  init || arr[0]   // 有初始值使用初始值
    // 有初始值的话从0遍历, 否则从1遍历
    for (var i = init ? 0 : 1; i < arr.length; i++) {
        // (四个参数,前两个是重点,1:当前累加总和,2:当前要累加的值,3:当前要累加值的索引,4:当前的数组)
        total = fn(total, arr[i], i , arr)
    } 
    return total
}
var arr = [1,2,3]
console.log(arr.reduce((prev, item) => prev + item, 10)) 

模拟Object.create

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

// 模拟 Object.create

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

  return new F();
}

实现Promise.all Promise.race Promise.retry

Promise.prototype.promiseAll = (promises) => {
    if (!Array.isArray(promises)) {
        throw new Error("promises must be an array")
    }
    return new Promise((resolve, reject) => {
        // 用来存储每个promise的返回值
        let values = new Array(promises.length);
        // 当前已经完成了几个promise
        let finishCount = 0;
        for (let i = 0; i < promises.length; ++i) {
            Promise.resolve(promises[i]).then(val => {
                values[i] = val;
                ++finishCount;
                if (finishCount === promises.length) {
                    resolve(values);
                }
            }).catch(err => {
                reject(err)
            })
        }
    });
};
Promise.prototype.promiseRace = (promises) => {
    if (!Array.isArray(promises)) {
        throw new Error("promises must be an array")
    }
    return new Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; ++i) {
            Promise.resolve(promises[i]).then(val => {
                resolve(val);
            }).catch(err => {
                reject(err)
            })
        }
    });
};
Promise.prototype.retry = (fn,times,delay) => {
    let time = 0;
    return new Promise((resolve,reject)=>{
        const attemp = ()=>{
            Promise.resolve(fn)
            .then(resolve)
            .catch(err=>{
                time++;
                console.log("尝试失败");
                if(time==times){
                    reject(err);
                }else{
                    setTimeOut(()=>{
                        attemp();
                    },delay)
                }
            })
        }
        attemp();
    })
}

实现Promise

const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function MyPromise(fn) {
    const that = this
    that.state = PENDING
    that.value = null
    that.resolvedCallbacks = []
    that.rejectedCallbacks = []
    // 待完善 resolve 和 reject 函数
    // 待完善执行 fn 函数
    function resolve(value) {
        if (that.state === PENDING) {
            that.state = RESOLVED
            that.value = value
            that.resolvedCallbacks.map(cb => cb(that.value))
        }
    }

    function reject(value) {
        if (that.state === PENDING) {
            that.state = REJECTED
            that.value = value
            that.rejectedCallbacks.map(cb => cb(that.value))
        }
    }
}
MyPromise.prototype.then = function (onFulfilled, onRejected) {
    const that = this
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
    onRejected =
        typeof onRejected === 'function' ?
        onRejected :
        r => {
            throw r
        }
    if (that.state === PENDING) {
        that.resolvedCallbacks.push(onFulfilled)
        that.rejectedCallbacks.push(onRejected)
    }
    if (that.state === RESOLVED) {
        onFulfilled(that.value)
    }
    if (that.state === REJECTED) {
        onRejected(that.value)
    }
}
MyPromise.prototype.then = function(onFulfilled, onRejected) {    let promise2 = new Promise((resolve, reject) => {
      if (this.status === RESOLVED) {
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }

      if (this.status === REJECTED) {
        let x = onRejected(this.reason);
        resolvePromise(promise2, x, resolve, reject);
      }

      if (this.status === PENDING) {
        this.resolveCbs.push(() => {
          let x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        });

        this.rejectCbs.push(() => {
          let x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        });
      }
    });

    return promise2;
  function resolvePromise (promise2, x, resolve, reject) {
    if (promise2 === x) {
      // 不允许 promise2 === x; 避免自己等待自己
      return reject(new TypeError('Chaining cycle detected for promise'));
    }

    // 防止重复调用
    let called = false;

    try {
      if (x instanceof Promise) {
        let then = x.then;
        // 第一个参数指定调用对象
        // 第二个参数为成功的回调,将结果作为 resolvePromise 的参数进行递归
        // 第三个参数为失败的回调
        then.call(x, y => {
          if (called) return;
          called = true;
          // resolve 的结果依旧是 Promise 那就继续解析
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if (called) return;
          called = true;
          reject(err);
        });
      } else {
        resolve(x);
      }
    } catch (e) {
      reject(e);
    }
  }  }

解析 URL Params 为对象

const getUrlParams = (url) => {
  const arrSearch = url.split('?').pop().split('#').shift().split('&');
  let obj = {};
  arrSearch.forEach((item) => {
    const [k, v] = item.split('=');
    obj[k] = v;
    return obj;
  });
  return obj;
};const getUrlParams2 = (url) => {
  const u = new URL(url);
  const s = new URLSearchParams(u.search);
  const obj = {};
  s.forEach((v, k) => (obj[k] = v));
  return obj;
};

模板引擎实现

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();
    })
}

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是d,出现了5次

var str="sssfgtdfssddfsssfssss";
function max(){
    var json={};
    var num=0;
    var value=null;
    for(var i=0;i<str.length;i++){
        var k=str[i];
        if(!json[k]){
            json[k]=[];
        }
        json[k].push(k);     //这里不需要else,否则只有存在这个字符时才添加。次数会少一次
    }
    for(var attr in json){
        if(num<json[attr].length){
            num=json[attr].length;
            value=json[attr][0];
        }
    }
    alert("出现最多的字符是:"+value+',出现次数是:'+num);
};
max(str);

字符串查找

请使用最基本的遍历来实现判断字符串 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[Number(i) + Number(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 = /(?=(?!\b)(\d{3})+$)/g; 
   return str.replace(re,','); 
}