作为前端开发,JS是重中之重,最近完结了面试的高峰期,基本上offer也定下来了就等开奖,趁着这个工夫总结下32个手写JS问题,这些都是高频面试题,心愿对你能有所帮忙。

对于源码都紧遵标准,都可跑通MDN示例,其余的大多会波及一些对于JS的应用题和自己面试过程

01.数组扁平化

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

const arr = [1, [2, [3, [4, 5]]], 6];// => [1, 2, 3, 4, 5, 6]

办法一:应用flat()

const res1 = arr.flat(Infinity);

办法二:利用正则

const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');

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

办法三:正则改进版本

const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');

办法四:应用reduce

const flatten = arr => {  return arr.reduce((pre, cur) => {    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);  }, [])}const res4 = flatten(arr);

办法五:函数递归

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

02.数组去重

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

03.类数组转化为数组

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

办法一:Array.from

Array.from(document.querySelectorAll('div'))

办法二:Array.prototype.slice.call()

Array.prototype.slice.call(document.querySelectorAll('div'))

办法三:扩大运算符

[...document.querySelectorAll('div')]

办法四:利用concat

Array.prototype.concat.apply([], document.querySelectorAll('div'));

04.Array.prototype.filter()

Array.prototype.filter = function(callback, thisArg) {  if (this == undefined) {    throw new TypeError('this is null or not undefined');  }  if (typeof callback !== 'function') {    throw new TypeError(callback + 'is not a function');  }  const res = [];  // 让O成为回调函数的对象传递(强制转换对象)  const O = Object(this);  // >>>0 保障len为number,且为正整数  const len = O.length >>> 0;  for (let i = 0; i < len; i++) {    // 查看i是否在O的属性(会查看原型链)    if (i in O) {      // 回调函数调用传参      if (callback.call(thisArg, O[i], i, O)) {        res.push(O[i]);      }    }  }  return res;}

对于>>>0有疑难的:解释>>>0的作用

05.Array.prototype.map()

Array.prototype.map = function(callback, thisArg) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callback + ' is not a function');  }  const res = [];  // 同理  const O = Object(this);  const len = O.length >>> 0;  for (let i = 0; i < len; i++) {    if (i in O) {      // 调用回调函数并传入新数组      res[i] = callback.call(thisArg, O[i], i, this);    }  }  return res;}

06.Array.prototype.forEach()


forEach跟map相似,惟一不同的是forEach是没有返回值的。

Array.prototype.forEach = function(callback, thisArg) {  if (this == null) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== "function") {    throw new TypeError(callback + ' is not a function');  }  const O = Object(this);  const len = O.length >>> 0;  let k = 0;  while (k < len) {    if (k in O) {      callback.call(thisArg, O[k], k, O);    }    k++;  }}

07.Array.prototype.reduce()

Array.prototype.reduce = function(callback, initialValue) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callbackfn + ' is not a function');  }  const O = Object(this);  const len = this.length >>> 0;  let accumulator = initialValue;  let k = 0;  // 如果第二个参数为undefined的状况下  // 则数组的第一个有效值作为累加器的初始值  if (accumulator === undefined) {    while (k < len && !(k in O)) {      k++;    }    // 如果超出数组界线还没有找到累加器的初始值,则TypeError    if (k >= len) {      throw new TypeError('Reduce of empty array with no initial value');    }    accumulator = O[k++];  }  while (k < len) {    if (k in O) {      accumulator = callback.call(undefined, accumulator, O[k], k, O);    }    k++;  }  return accumulator;}

08.Function.prototype.apply()

第一个参数是绑定的this,默认为window,第二个参数是数组或类数组

Function.prototype.apply = function(context = window, args) {  if (typeof this !== 'function') {    throw new TypeError('Type Error');  }  const fn = Symbol('fn');  context[fn] = this;  const res = context[fn](...args);  delete context[fn];  return res;}

09.Function.prototype.call

call惟一不同的是,call()办法承受的是一个参数列表

Function.prototype.call = function(context = window, ...args) {  if (typeof this !== 'function') {    throw new TypeError('Type Error');  }  const fn = Symbol('fn');  context[fn] = this;  const res = context[fn](...args);  delete context[fn];  return res;}

10.Function.prototype.bind

Function.prototype.bind = function(context, ...args) {  if (typeof this !== 'function') {    throw new Error("Type Error");  }  // 保留this的值  var self = this;  return function F() {    // 思考new的状况    if(this instanceof F) {      return new self(...args, ...arguments)    }    return self.apply(context, [...args, ...arguments])  }}

11.debounce(防抖)

触发高频工夫后n秒内函数只会执行一次,如果n秒内高频工夫再次触发,则从新计算工夫。

const debounce = (fn, time) => {  let timeout = null;  return function() {    clearTimeout(timeout)    timeout = setTimeout(() => {      fn.apply(this, arguments);    }, time);  }};

防抖常利用于用户进行搜寻输出节约申请资源,window触发resize事件时进行防抖只触发一次。

12.throttle(节流)

高频工夫触发,但n秒内只会执行一次,所以节流会浓缩函数的执行频率。

const throttle = (fn, time) => {  let flag = true;  return function() {    if (!flag) return;    flag = false;    setTimeout(() => {      fn.apply(this, arguments);      flag = true;    }, time);  }}

节流常利用于鼠标一直点击触发、监听滚动事件。

13.函数珂里化

指的是将一个承受多个参数的函数 变为 承受一个参数返回一个函数的固定模式,这样便于再次调用,例如f(1)(2)

经典面试题:实现add(1)(2)(3)(4)=10;add(1)(1,2,3)(2)=9;

function add() {  const _args = [...arguments];  function fn() {    _args.push(...arguments);    return fn;  }  fn.toString = function() {    return _args.reduce((sum, cur) => sum + cur);  }  return fn;}

14.模仿new操作

3个步骤:

  1. ctor.prototype为原型创立一个对象。
  2. 执行构造函数并将this绑定到新创建的对象上。
  3. 判断构造函数执行返回的后果是否是援用数据类型,若是则返回构造函数执行的后果,否则返回创立的对象。

    function newOperator(ctor, ...args) {  if (typeof ctor !== 'function') { throw new TypeError('Type Error');  }  const obj = Object.create(ctor.prototype);  const res = ctor.apply(obj, args);  const isObject = typeof res === 'object' && res !== null;  const isFunction = typeof res === 'function';  return isObject || isFunction ? res : obj;}

15.instanceof

instanceof运算符用于检测构造函数的prototype属性是否呈现在某个实例对象的原型链上。

const myInstanceof = (left, right) => {  // 根本数据类型都返回false  if (typeof left !== 'object' || left === null) return false;  let proto = Object.getPrototypeOf(left);  while (true) {    if (proto === null) return false;    if (proto === right.prototype) return true;    proto = Object.getPrototypeOf(proto);  }}

16.原型继承

这里只写寄生组合继承了,两头还有几个演变过去的继承但都有一些缺点

function Parent() {  this.name = 'parent';}function Child() {  Parent.call(this);  this.type = 'children';}Child.prototype = Object.create(Parent.prototype);Child.prototype.constructor = Child;

17.Object.is

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

+0 === -0  // trueNaN === NaN // false
const is= (x, y) => {  if (x === y) {    // +0和-0应该不相等    return x !== 0 || y !== 0 || 1/x === 1/y;  } else {    return x !== x && y !== y;  }}

18.Object.assign

Object.assign()办法用于将所有可枚举属性的值从一个或多个源对象复制到指标对象。它将返回指标对象(请留神这个操作是浅拷贝)

Object.defineProperty(Object, 'assign', {  value: function(target, ...args) {    if (target == null) {      return new TypeError('Cannot convert undefined or null to object');    }        // 指标对象须要对立是援用数据类型,若不是会主动转换    const to = Object(target);    for (let i = 0; i < args.length; i++) {      // 每一个源对象      const nextSource = args[i];      if (nextSource !== null) {        // 应用for...in和hasOwnProperty双重判断,确保只拿到自身的属性、办法(不蕴含继承的)        for (const nextKey in nextSource) {          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {            to[nextKey] = nextSource[nextKey];          }        }      }    }    return to;  },  // 不可枚举  enumerable: false,  writable: true,  configurable: true,})

19.深拷贝

递归的残缺版本(思考到了Symbol属性):

const cloneDeep1 = (target, hash = new WeakMap()) => {  // 对于传入参数解决  if (typeof target !== 'object' || target === null) {    return target;  }  // 哈希表中存在间接返回  if (hash.has(target)) return hash.get(target);  const cloneTarget = Array.isArray(target) ? [] : {};  hash.set(target, cloneTarget);  // 针对Symbol属性  const symKeys = Object.getOwnPropertySymbols(target);  if (symKeys.length) {    symKeys.forEach(symKey => {      if (typeof target[symKey] === 'object' && target[symKey] !== null) {        cloneTarget[symKey] = cloneDeep1(target[symKey]);      } else {        cloneTarget[symKey] = target[symKey];      }    })  }  for (const i in target) {    if (Object.prototype.hasOwnProperty.call(target, i)) {      cloneTarget[i] =        typeof target[i] === 'object' && target[i] !== null        ? cloneDeep1(target[i], hash)        : target[i];    }  }  return cloneTarget;}

20.Promise

实现思路:Promise源码实现

// 模仿实现Promise// Promise利用三大伎俩解决回调天堂:// 1. 回调函数提早绑定// 2. 返回值穿透// 3. 谬误冒泡// 定义三种状态const PENDING = 'PENDING';      // 进行中const FULFILLED = 'FULFILLED';  // 已胜利const REJECTED = 'REJECTED';    // 已失败class Promise {  constructor(exector) {    // 初始化状态    this.status = PENDING;    // 将胜利、失败后果放在this上,便于then、catch拜访    this.value = undefined;    this.reason = undefined;    // 胜利态回调函数队列    this.onFulfilledCallbacks = [];    // 失败态回调函数队列    this.onRejectedCallbacks = [];    const resolve = value => {      // 只有进行中状态能力更改状态      if (this.status === PENDING) {        this.status = FULFILLED;        this.value = value;        // 胜利态函数顺次执行        this.onFulfilledCallbacks.forEach(fn => fn(this.value));      }    }    const reject = reason => {      // 只有进行中状态能力更改状态      if (this.status === PENDING) {        this.status = REJECTED;        this.reason = reason;        // 失败态函数顺次执行        this.onRejectedCallbacks.forEach(fn => fn(this.reason))      }    }    try {      // 立刻执行executor      // 把外部的resolve和reject传入executor,用户可调用resolve和reject      exector(resolve, reject);    } catch(e) {      // executor执行出错,将谬误内容reject抛出去      reject(e);    }  }  then(onFulfilled, onRejected) {    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;    onRejected = typeof onRejected === 'function'? onRejected :      reason => { throw new Error(reason instanceof Error ? reason.message : reason) }    // 保留this    const self = this;    return new Promise((resolve, reject) => {      if (self.status === PENDING) {        self.onFulfilledCallbacks.push(() => {          // try捕捉谬误          try {            // 模仿微工作            setTimeout(() => {              const result = onFulfilled(self.value);              // 分两种状况:              // 1. 回调函数返回值是Promise,执行then操作              // 2. 如果不是Promise,调用新Promise的resolve函数              result instanceof Promise ? result.then(resolve, reject) : resolve(result);            })          } catch(e) {            reject(e);          }        });        self.onRejectedCallbacks.push(() => {          // 以下同理          try {            setTimeout(() => {              const result = onRejected(self.reason);              // 不同点:此时是reject              result instanceof Promise ? result.then(resolve, reject) : resolve(result);            })          } catch(e) {            reject(e);          }        })      } else if (self.status === FULFILLED) {        try {          setTimeout(() => {            const result = onFulfilled(self.value);            result instanceof Promise ? result.then(resolve, reject) : resolve(result);          });        } catch(e) {          reject(e);        }      } else if (self.status === REJECTED) {        try {          setTimeout(() => {            const result = onRejected(self.reason);            result instanceof Promise ? result.then(resolve, reject) : resolve(result);          })        } catch(e) {          reject(e);        }      }    });  }  catch(onRejected) {    return this.then(null, onRejected);  }  static resolve(value) {    if (value instanceof Promise) {      // 如果是Promise实例,间接返回      return value;    } else {      // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED      return new Promise((resolve, reject) => resolve(value));    }  }  static reject(reason) {    return new Promise((resolve, reject) => {      reject(reason);    })  }  static all(promiseArr) {    const len = promiseArr.length;    const values = new Array(len);    // 记录曾经胜利执行的promise个数    let count = 0;    return new Promise((resolve, reject) => {      for (let i = 0; i < len; i++) {        // Promise.resolve()解决,确保每一个都是promise实例        Promise.resolve(promiseArr[i]).then(          val => {            values[i] = val;            count++;            // 如果全副执行完,返回promise的状态就能够扭转了            if (count === len) resolve(values);          },          err => reject(err),        );      }    })  }  static race(promiseArr) {    return new Promise((resolve, reject) => {      promiseArr.forEach(p => {        Promise.resolve(p).then(          val => resolve(val),          err => reject(err),        )      })    })  }}

21.Promise.all

Promise.all是反对链式调用的,实质上就是返回了一个Promise实例,通过resolvereject来扭转实例状态。

Promise.myAll = function(promiseArr) {  return new Promise((resolve, reject) => {    const ans = [];    let index = 0;    for (let i = 0; i < promiseArr.length; i++) {      promiseArr[i]      .then(res => {        ans[i] = res;        index++;        if (index === promiseArr.length) {          resolve(ans);        }      })      .catch(err => reject(err));    }  })}

22.Promise.race

Promise.race = function(promiseArr) {  return new Promise((resolve, reject) => {    promiseArr.forEach(p => {      // 如果不是Promise实例须要转化为Promise实例      Promise.resolve(p).then(        val => resolve(val),        err => reject(err),      )    })  })}

23.Promise并行限度

就是实现有并行限度的Promise调度器问题。

具体实现思路:某条高频面试原题:实现有并行限度的Promise调度器

class Scheduler {  constructor() {    this.queue = [];    this.maxCount = 2;    this.runCounts = 0;  }  add(promiseCreator) {    this.queue.push(promiseCreator);  }  taskStart() {    for (let i = 0; i < this.maxCount; i++) {      this.request();    }  }  request() {    if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {      return;    }    this.runCounts++;    this.queue.shift()().then(() => {      this.runCounts--;      this.request();    });  }}   const timeout = time => new Promise(resolve => {  setTimeout(resolve, time);})  const scheduler = new Scheduler();  const addTask = (time,order) => {  scheduler.add(() => timeout(time).then(()=>console.log(order)))}    addTask(1000, '1');addTask(500, '2');addTask(300, '3');addTask(400, '4');scheduler.taskStart()// 2// 3// 1// 4

24.JSONP

script标签不遵循同源协定,能够用来进行跨域申请,长处就是兼容性好但仅限于GET申请

const jsonp = ({ url, params, callbackName }) => {  const generateUrl = () => {    let dataSrc = '';    for (let key in params) {      if (Object.prototype.hasOwnProperty.call(params, key)) {        dataSrc += `${key}=${params[key]}&`;      }    }    dataSrc += `callback=${callbackName}`;    return `${url}?${dataSrc}`;  }  return new Promise((resolve, reject) => {    const scriptEle = document.createElement('script');    scriptEle.src = generateUrl();    document.body.appendChild(scriptEle);    window[callbackName] = data => {      resolve(data);      document.removeChild(scriptEle);    }  })}

25.AJAX

const getJSON = function(url) {  return new Promise((resolve, reject) => {    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');    xhr.open('GET', url, false);    xhr.setRequestHeader('Accept', 'application/json');    xhr.onreadystatechange = function() {      if (xhr.readyState !== 4) return;      if (xhr.status === 200 || xhr.status === 304) {        resolve(xhr.responseText);      } else {        reject(new Error(xhr.responseText));      }    }    xhr.send();  })}

26.event模块

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

观察者模式:定义了对象间一种一对多的依赖关系,当指标对象Subject产生扭转时,所有依赖它的对象Observer都会失去告诉。
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));  }}// 模仿实现removeListenerEventEmitter.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);}

27.图片懒加载

能够给img标签对立自定义属性data-src='default.png',当检测到图片呈现在窗口之后再补充src属性,此时才会进行图片资源加载。

function lazyload() {  const imgs = document.getElementsByTagName('img');  const len = imgs.length;  // 视口的高度  const viewHeight = document.documentElement.clientHeight;  // 滚动条高度  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;  for (let i = 0; i < len; i++) {    const offsetHeight = imgs[i].offsetTop;    if (offsetHeight < viewHeight + scrollHeight) {      const src = imgs[i].dataset.src;      imgs[i].src = src;    }  }}// 能够应用节流优化一下window.addEventListener('scroll', lazyload);

28.滚动加载

原理就是监听页面滚动事件,剖析clientHeightscrollTopscrollHeight三者的属性关系。

window.addEventListener('scroll', function() {  const clientHeight = document.documentElement.clientHeight;  const scrollTop = document.documentElement.scrollTop;  const scrollHeight = document.documentElement.scrollHeight;  if (clientHeight + scrollTop >= scrollHeight) {    // 检测到滚动至页面底部,进行后续操作    // ...  }}, false);

一个Demo:页面滚动加载的Demo

29.渲染几万条数据不卡住页面

渲染大数据时,正当应用createDocumentFragmentrequestAnimationFrame,将操作切分为一小段一小段执行。

setTimeout(() => {  // 插入十万条数据  const total = 100000;  // 一次插入的数据  const once = 20;  // 插入数据须要的次数  const loopCount = Math.ceil(total / once);  let countOfRender = 0;  const ul = document.querySelector('ul');  // 增加数据的办法  function add() {    const fragment = document.createDocumentFragment();    for(let i = 0; i < once; i++) {      const li = document.createElement('li');      li.innerText = Math.floor(Math.random() * total);      fragment.appendChild(li);    }    ul.appendChild(fragment);    countOfRender += 1;    loop();  }  function loop() {    if(countOfRender < loopCount) {      window.requestAnimationFrame(add);    }  }  loop();}, 0)

30.打印出以后网页应用了多少种HTML元素

一行代码能够解决:

const fn = () => {  return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;}

值得注意的是:DOM操作返回的是类数组,须要转换为数组之后才能够调用数组的办法。

31.将VirtualDom转化为实在DOM构造

这是以后SPA利用的外围概念之一

// vnode构造:// {//   tag,//   attrs,//   children,// }//Virtual DOM => DOMfunction render(vnode, container) {  container.appendChild(_render(vnode));}function _render(vnode) {  // 如果是数字类型转化为字符串  if (typeof vnode === 'number') {    vnode = String(vnode);  }  // 字符串类型间接就是文本节点  if (typeof vnode === 'string') {    return document.createTextNode(vnode);  }  // 一般DOM  const dom = document.createElement(vnode.tag);  if (vnode.attrs) {    // 遍历属性    Object.keys(vnode.attrs).forEach(key => {      const value = vnode.attrs[key];      dom.setAttribute(key, value);    })  }  // 子数组进行递归操作  vnode.children.forEach(child => render(child, dom));  return dom;}

32.字符串解析问题

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}

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

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

欢送关注「前端洛霞」微信号获取更多学习材料,继续更新前端相干文章~

咱们是字节跳动互娱研发前端架构团队,目前大规模招人中,北京杭州深圳、社招实习都有地位,欢送加我微信FE-luoxia投简历骚扰~