【3月面经】前端常考JS编程题

10,716 阅读10分钟

系列

如果你最近也在找工作or看机会,可加我进找工作群分享行情:V798595965,点击扫码加v

1、柯里化

  • 柯里化作用是拆分参数
  • 实现的核心思想是 收集参数
  • 递归中去判断当前收集的参数和函数的所有入参 是否相等,长度一致即可执行函数运算

面试题中,可能会存在很多变种,比如需要支持结果缓存, add(1)(2)执行之后再add(3)(4),需要用前一个函数的结果

const myCurrying = (fn, ...args) => {
  if (args.length >= fn.length) {
    return fn(...args);
  } else {
    return (...args2) => myCurrying(fn, ...args, ...args2);
  }
}

const add = (x, y, z) => {
  return x + y + z
}

const addCurry = myCurrying(add)
const sum1 = addCurry(1, 2, 3)
const sum2 = addCurry(1)(2)(3)

console.log(sum1, 'sum1');
console.log(sum2, 'sum2');

2、树结构转换

很常见的一道题,真的遇到很多回了,求你好好写一下

  1. 先构建map结构,以各个子项id为key
  2. 再循环目标数组,判断上面构建的map中,是否存在当前遍历的pid
  3. 存在就可以进行children的插入
  4. 不存在就是顶级节点,直接push即可
let arr = [{
  id: 1,
  pid: 0,
  name: 'body'
}, {
  id: 2,
  pid: 1,
  name: 'title'
}, {
  id: 3,
  pid: 2,
  name: 'div'
}]

function toTree (data) {
  let result = [];
  let map = {};
  // 1. 先构建map结构,以各个子项id为key
  data.forEach((item) => {
    map[item.id] = item
  })
  // 2. 再循环目标数组,判断上面构建的map中,是否存在当前遍历的pid
  data.forEach((item) => {
    let parent = map[item.pid];
    if(parent) {
    // 3. 存在就可以进行children的插入
      (parent.children || (parent.children = [])).push(item)
    } else {
    // 4. 不存在就是顶级节点,直接push即可
      result.push(item)
    }
  })
  return result;
}
console.log(toTree(arr));

递归的解法

function toTree(data) {
  const roots = [];

  for (const item of data) {
    if (item.pid === 0) {
      item.children = [];
      roots.push(item);
    } else {
      const parent = findParent(item, roots);
      if (parent) {
        item.children = [];
        parent.children.push(item);
      }
    }
  }

  return roots;
}

function findParent(item, roots) {
  for (const root of roots) {
    if (root.id === item.pid) {
      return root;
    }
    const parent = findParent(item, root.children);
    if (parent) {
      return parent;
    }
  }
}

let arr = [{
  id: 1,
  pid: 0,
  name: 'body'
}, {
  id: 2,
  pid: 1,
  name: 'title'
}, {
  id: 3,
  pid: 2,
  name: 'div'
}]

console.log(toTree(arr));

3、树路径查找

可能会由上一题进行升级到这一题

// 查询id为10的节点,输出节点路径如[1, 3, 10]
const treeData = [{
  id: 1,
  name: 'jj1',
  children: [
    { id: 2, name: 'jj2', children: [{ id: 4, name: 'jj4', }] },
    {
      id: 3,
      name: 'jj3',
      children: [
        { id: 8, name: 'jj8', children: [{ id: 5, name: 'jj5', }] },
        { id: 9, name: 'jj9', children: [] },
        { id: 10, name: 'jj10', children: [] },
      ],
    },
  ],
}];
let path = findNum(10, treeData);
console.log("path", path);

// 实现
const findNum = (target, data) => {
  let result = [];
  const DG = (path, data) => {
    if (!data.length) return
    data.forEach(item => {
      path.push(item.id)
      if (item.id === target) {
        result = JSON.parse(JSON.stringify(path));
      } else {
        const children = item.children || [];
        DG(path, children);
        path.pop();
      }
    })
  }
  DG([], data)
  return result
};

4、发布订阅模式

不光要会写,你要搞清楚怎么用,别背着写出来了,面试让你验证下,你都不知道怎么用。一眼背的,写了和没写一样

class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 订阅
  on(eventName, callback) {
    const callbacks = this.events[eventName] || [];
    callbacks.push(callback);
    this.events[eventName] = callbacks;
  }
  // 发布
  emit(eventName, ...args) {
    const callbacks = this.events[eventName] || [];
    callbacks.forEach((cb) => cb(...args));
  }
  // 取消订阅
  off(eventName, callback) {
    const index = this.events[eventName].indexOf(callback);
    if (index !== -1) {
      this.events[eventName].splice(index, 1);
    }
  }
  // 只监听一次
  once(eventName, callback) {
    const one = (...args) => {
      callback(...args);
      this.off(eventName, one);
    };
    this.on(eventName, one);
  }
}

let JJ1 = new EventEmitter()
let JJ2 = new EventEmitter()

let handleOne = (params) => {
  console.log(params,'handleOne')
}

JJ1.on('aaa', handleOne)
JJ1.emit('aaa', 'hhhh')
JJ1.off('aaa', handleOne)
// 取消订阅以后再发就没用了
JJ1.emit('aaa', 'ffff')

JJ2.once('aaa', handleOne)
JJ2.emit('aaa', 'hhhh')
// 只能发一次消息,再发就无效了
JJ2.emit('aaa', 'hhhh')

4、观察者模式

class Notifier {
  constructor() {
    this.observers = [];
  }
  
  add(observer) {
    this.observers.push(observer)
  }

  remove(observer) {
    this.observers = this.observers.filter(ober => ober !== observer)
  }

  notify() {
    this.observers.forEach((observer) => {
      observer.update()
    })
  }
}

class Observer {
  constructor (name) {
    this.name = name;
  }
   update () {
     console.log(this.name, 'name');
   }
}

const ob1 = new Observer('J1')
const ob2 = new Observer('J2')
const notifier = new Notifier()
notifier.add(ob1)
notifier.add(ob2)

notifier.notify()

5、防抖

  • 某个函数在短时间内只执行最后一次。
function debounce(fn, delay = 500) {
  let timer = null;
  return function () {
    if(timer) {
      clearTimeout(timer);
    };
    timer = setTimeout(() => {
      timer = null;
      fn.apply(this, arguments)
    }, delay)
  }
}

6、节流

  • 某个函数在指定时间段内只执行第一次,直到指定时间段结束,周而复始
// 使用定时器写法,delay毫秒后第一次执行,第二次事件停止触发后依然会再一次执行
// 可以结合时间戳 实现更准确的节流
// 写法1
function throttle(fn, delay = 500) {
  let timer = null;
  return function () {
    if(timer) return;
    timer = setTimeout(() => {
      timer = null;
      fn.apply(this, arguments)
    }, delay)
  }
}
// 写法2
function throttle(fn, delay) {
    let start = +Date.now()
    let timer = null
    return function(...args) {
        const now = +Date.now()
        if (now - start >= delay) {
            clearTimeout(timer)
            timer = null
            fn.apply(this, args)
            start = now
        } else if (!timer){
            timer = setTimeout(() => {
                fn.apply(this, args)
            }, delay)
        }
    }
}

7、手写深拷贝

  • 考虑各种数据类型,存在构造函数的可以用new Xxxx()
  • 考虑循环引用,用weakMap记录下
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
  return new Date(obj)       // 日期对象直接返回一个新的日期对象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     //正则对象直接返回一个新的正则对象

  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj)

  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //继承原型链
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}

// 下面是验证代码
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)

8、实现settimeout

  • 最简单的是直接while循环,比较消耗性能,且阻塞JS运行
  • 可以借助requestAnimationFrame + 递归来完成
  • 也可以用setInterval来实现
const mysetTimeout = (fn, delay, ...args) => {
  const start = Date.now();
  // 最简单的办法
  // while (true) {
  //   const now = Date.now();
  //   if(now - start >= delay) {
  //     fn.apply(this, args);
  //     return;
  //   }
  // }
  let timer, now;
  const loop = () => {
    timer = requestAnimationFrame(loop);
    now = Date.now();
    if(now - start >= delay) {
      fn.apply(this, args);
      cancelAnimationFrame(timer)
    }
  }
  requestAnimationFrame(loop);
}

function test() {
  console.log(1);
}

mysetTimeout(test, 1000)

9、Promise三大API

promise实现思路,上一篇已经写了:juejin.cn/post/721621…

all

// promise all
const promise1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(1);
      resolve(1);
    }, 1000);
  });
};

const promise2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(2);
      resolve(2);
    }, 2000);
  });
};
const promise3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(3);
      resolve(3);
    }, 3000);
  });
};
const promise4 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(4);
      resolve(4);
    }, 4000);
  });
};
const promiseArr = [promise1, promise2, promise3, promise4];

// Promise.all(promiseArr).then(res => {
//   console.log(res, '1111');
// })
const promiseAll = (pList) => {
  return new Promise((resolve, reject) => {
    let count = 0;
    const pLength = pList.length;
    const result = [];
    for (let i = 0; i < pLength; i++) {
      pList[i]().then(res => {
        count++
        result[i] = res
        if (pLength === count) {
          console.log(result, 'result');
          resolve(result)
        }
      })
        .catch(err => {
          reject(err)
        })
    }
  })
}
promiseAll(promiseArr)

race

Promise.race = function(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}

retry

function retry(fn, maxTries) {
  return new Promise((resolve, reject) => {
    function attempt(tryNumber) {
      console.log('重试次数',tryNumber)
      fn().then(resolve).catch(error => {
        if (tryNumber < maxTries) {
          attempt(tryNumber+1);
        } else {
          reject(error);
        }
      });
    }
    attempt(1);
  });
}


function downloadFile() {
  return new Promise((resolve, reject) => {
    // 异步下载文件
    setTimeout(() => {
      reject('test')
    }, 1000)
  });
}

retry(() => downloadFile(), 3)
  .then(data => console.log('文件下载成功'))
  .catch(err => console.error('文件下载失败:', err.message));

plimit

  • 并发控制
  • 并发的请求只能有n个,每成功一个,重新发起一个请求
  • 思路:然后在一个while循环中,判断当前正在执行的Promise个数,和并发数n对比,同时要保证Promise数组有值,条件满足就进行取到执行的操作
  • 先一次性拿出n个执行,在每个Promise的then中判断是否还有需要执行的,通过shift拿出任务执行,同时记录已经成功的Promise数量,当每个Promise的then中resolve的数量和原始Promise相等,即可进行整体结果的resolve
const promise1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(1);
      resolve(1);
    }, 1000);
  });
};

const promise2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(2);
      resolve(2);
    }, 1000);
  });
};
const promise3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(3);
      resolve(3);
    }, 1000);
  });
};
const promise4 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(4);
      resolve(4);
    }, 1000);
  });
};
const promiseArr = [promise1, promise2, promise3, promise4];

// 控制并发
const pLimit = (pList, limit) => {
  return new Promise((resolve, reject) => {
    let runCount = 0;
    let resolvedCount = 0;
    const pListLength = pList.length;
    const result = [];

    const nextP = (p, count) => {
      p().then(res => {
        result[count] = res;
        resolvedCount++
        if (pList.length) {
          const pNext = pList.shift();
          nextP(pNext, runCount)
          runCount++;
        } else if (resolvedCount === pListLength) {
          resolve(result)
        }
      })
    }

    while (runCount < limit && pList.length) {
      const p = pList.shift();
      nextP(p, runCount)
      runCount++;
    }
  })
}

pLimit(promiseArr, 3).then(res => {
  console.log(res, '1111')
})

10、实现lodash的get

  • 通过正则把[]访问形式改成.
  • 针对.进行分割,然后对每一项进行依次的遍历,找最后一级的value
function get (source, path, defaultValue = undefined) {
  // a[3].b -> a.3.b
  const paths = path.replace(/\[(\d+)\]/g,'.$1').split('.');
  let result = source;
  for(let p of paths) {
    result = result[p];
    if(result === undefined) {
      return defaultValue;
    }
  }
  return result;
}

const obj = { a:[1,2,3,{b:1}]}
const value = get(obj, 'a[3].b', 3)
console.log(value, 'value');

11、数组扁平化

  • 不用flat
  • 支持自定义层级
function flat(arr, num = 1) {
  let result = [];
  arr.forEach(item => {
    if (Array.isArray(item) && num > 0) {
      result = result.concat(flat(item, num - 1))
    } else {
      result.push(item)
    }
  })
  return result
}

12、对象数组去重

  • 场景简单,我遇到的面试官要求用reduce
  • 不用reduce的话,map配合一次循环就可以了
let arr = [{
  id: 1, name: 'JJ1',
}, {
  id: 2, name: 'JJ2',
}, {
  id: 1, name: 'JJ1',
}, {
  id: 4, name: 'JJ4',
}, {
  id: 2, name: 'JJ2',
}]

const unique = (arr) => {
  let map = new Map();
  return arr.reduce((prev, cur) => {
    // 当前map中没有,说明可以和上一个合并
    if (!map.has(cur.id)) {
      map.set(cur.id, true)
      return [...prev, cur]
    } else {
      // 已经被标记的就不用合并了
      return prev
    }
  }, [])
}

console.log(unique(arr), 'unique');

// 不使用reduce
const unique = (arr) => {
  let map = new Map();
  let result = [];
  arr.forEach(item => {
    if (!map.has(item.id)) {
      map.set(item.id, true)
      result.push(item)
    }
  })
  return result
}

13、求所有的字符串子串

// 输入:ABCD,
// 返回:ABCD,ABC,BCD,AB,BC,CD,A,B,C,D
const getAllStr = (str) => {
  let result = []
  for (let i = 0; i < str.length; i++) {
    for (let j = i + 1; j <= str.length; j++) {
      // result.push(str.slice(i, j))
      result.push(str.substring(i, j))
    }
  }
  return result;
}

console.log(getAllStr('ABCD'));

14、驼峰转换

  • 主要问题在于正则
  • 不会正则也可以字符串分割,比较麻烦
function converter(obj) {
  let newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      let newKey = key.replace(/_([a-z])/g, function (match, p1) {
        return p1.toUpperCase();
      });
      newObj[newKey] = obj[key];
    }
  }
  return newObj;
}

15、不常考,这次遇到的

比较有意思的两道题,可以自己先做试试

npm循环依赖检测

// 检测当前pkgs是否存在循环依赖
const pkgs = [
  {
    name: "a",
    dependencies: {
      b: "^1.0.0",
    },
  },
  {
    name: "b",
    dependencies: {
      c: "^1.0.0",
    },
  },
  {
    name: "c",
    dependencies: {
      a: "^1.0.0",
    },
  },
];
const checkCircularDependency = (packages) => {
  const map = {};
  const states = {}; // 存放包的状态
  // 初始化图
  packages.forEach((pkg) => {
    map[pkg.name] = pkg.dependencies || {};
    states[pkg.name] = "UNVISITED";
  })
  // 从每个包开始进行 DFS
  for (const pkgName in map) {
    if (states[pkgName] === "UNVISITED") {
      if (dfs(pkgName, map, states)) {
        return true
      }
    }
  }
  return false
};
const dfs = (pkgName, map, states) => {
  states[pkgName] = "VISITING";
  for (const dep in map[pkgName]) {
    const depState = states[dep];
    if (depState === "VISITING") {
      return true; // 存在循环依赖
    } else if (depState === "UNVISITED") {
      if (dfs(dep, map, states)) {
        return true; // 存在循环依赖
      }
    }
  }
  return false; // 不存在循环依赖
};

// 使用方法
const pkgs = [
  {
    name: "a",
    dependencies: {
      b: "^1.0.0",
    },
  },
  {
    name: "b",
    dependencies: {
      c: "^1.0.0",
    },
  },
  {
    name: "c",
    dependencies: {
      a: "^1.0.0",
    },
  },
];
console.log(checkCircularDependency(pkgs));

查找目标对象是否是源对象的子集

function checkIsChildObject(target, obj) {
  // 基于层级,把 obj 所有的属性生成 map 映射存储
  // 让target和obj有层级关系对比
  let map = new Map();
  const setMapByObj = (obj, level) => {
    for (let key in obj) {
      let current = map.get(key) || {};
      // 可能存在嵌套对象Key重复,可以合并在一个key里面
      map.set(key, {
        ...current,
        [level]: obj[key],
      });
      // 把所有对象铺开
      if (typeof obj[key] === 'object') {
        setMapByObj(obj[key], level + 1)
      }
    }
  }
  setMapByObj(obj, 0)
  // console.log(map, 'map');
  // target子对象去 map 里面寻找有没有哪一层的对象完全匹配自身,子属性只能找下一层的对象
  let level = -1;
  for (let key2 in target) {
    // 获取当前key的所有集合
    let current = map.get(key2)
    if (current !== undefined) {
      if (typeof target[key2] === 'object') {
        // 只需要再判断一级
        if (!checkIsChildObject(target[key2], current)) {
          return false
        }
      } else {
        //表示还没开始查找,需要找一下当前值在第几层
        if (level === -1) {
          for (let key3 in current) {
            if (current[key3] === target[key2]) {
              level = key3
            }
          }
        }
        // 查找到层数,目标值不相等
        if (level !== -1 && current[level] !== target[key2]) {
          return false
        }
      }
    } else {
      // Key没有直接返回false
      return false
    }
  }
  return true
}

const obj = {
  a: 0,
  c: '',
  d: true,
  e: {
    f: 1,
    e: {
      e: 0,
      f: 2,
    },
  },
};

console.log(checkIsChildObject({ a: 0 }, obj)); // true
console.log(checkIsChildObject({ e: 0 }, obj)); // true
console.log(checkIsChildObject({ a: 0, c: '' }, obj)); // true
console.log(checkIsChildObject({ a: 0, e: 0 }, obj)); // false
console.log(checkIsChildObject({ e: { f: 1 } }, obj)); // true
console.log(checkIsChildObject({ e: { f: 2, e: 0 } }, obj)); // true
console.log(checkIsChildObject({ e: { e: 0, f: 2 } }, obj)); // true
console.log(checkIsChildObject({ e: { f: 2 } }, obj)); // true

往期

image.png

本文正在参加「金石计划」