面试官:这些JavaScript手写题你都会吗?

357 阅读5分钟

Call

Function.prototype.tcCall = function (innerthis, ...args) {
  console.log(this, innerthis, args);
  //获取被调用的函数foo,也就是this
  let fn = this;
  //获取绑定的对象,判断类型
  let callThis =
    innerthis !== null || innerthis !== undefined ? Object(innerthis) : window;
  //调用函数
  callThis.fn = fn;
  let result = callThis.fn(...args);
  delete callThis.fn;
  return result;
};
function foo(...args) {
  console.log(this.name + '是我');
  console.log(args + '参数是我');
}
let testfoo = foo.tcCall({ name: 'thunder' }, 1, 2, 3);
console.log(testfoo);

Apply

Function.prototype.tcApply = function (applythis, args) {
  let fn = this;
  let innerapplythis =
    applythis !== null && applythis !== undefined ? Object(applythis) : window;
  //调用函数
  innerapplythis.fn = fn;

  let result = null;
  //判断是否传参
  if (!args) {
    result = innerapplythis.fn();
  } else {
    result = innerapplythis.fn(...args);
  }
  delete innerapplythis.fn;
  return result;
};
function foo(arg1, arg2) {
  console.log(this.age + '岁');
  console.log(arg1, arg2 + '参数是我');
}
let testfoo = foo.tcApply({ age: '18' }, [1, 2]);
console.log(testfoo);

Bind

Function.prototype.tcbind = function (bindthis, ...outArgs) {
  let fn = this;

  let innerBindThis =
    bindthis !== null && bindthis !== undefined ? bindthis : window;

  return function (...innerArgs) {
    innerBindThis.fn = fn;
    let newArr = outArgs.concat(innerArgs);
    let result = innerBindThis.fn(newArr);
    delete innerArgs.fn;
    return result;
  };
};

function foo(args) {
  console.log(this.name, ...args);
}
foo.tcbind({ name: 'thunder' }, 123)(123);

Promise

const PROMISE_STATUS_PENDING = 'pending';
const PROMISE_STATUS_FULFILLED = 'fulfilled';
const PROMISE_STATUS_REJECTED = 'rejected';

function execFunctionWithCatchError(execFn, value, resolve, reject) {
  try {
    const result = execFn(value);
    resolve(result);
  } catch (error) {
    reject(error);
  }
}

class TCPromise {
  constructor(executor) {
    console.log(executor);
    this.status = PROMISE_STATUS_PENDING;
    //保存回调值
    this.value = undefined;
    this.reason = undefined;
    //调用的所有回调
    this.onfullfilledFns = [];
    this.onrejectedFns = [];
    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        queueMicrotask(() => {
          //防止初始化时resolve 和 reject 同时调用
          if (this.status !== PROMISE_STATUS_PENDING) return;
          this.status = PROMISE_STATUS_FULFILLED;
          this.value = value;
          this.onfullfilledFns.forEach((fn) => {
            fn(this.value);
          });
        });
      }
    };
    const reject = (reason) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        //添加微任务
        queueMicrotask(() => {
          if (this.status !== PROMISE_STATUS_PENDING) return;
          this.status = PROMISE_STATUS_REJECTED;
          this.reason = reason;
          this.onrejectedFns.forEach((fn) => {
            fn(this.reason);
          });
        });
      }
    };

    try {
      // 执行函数
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onfullfilled, onrejected) {
    const defineOnRejected = (err) => {
      throw err;
    };
    onrejected = onrejected || defineOnRejected;

    const defalutOnFulfilled = (value) => {
      return value;
    };
    onfullfilled = onfullfilled || defalutOnFulfilled;
    //链式调用
    return new TCPromise((resolve, reject) => {
      //如果在then函数调用的时候,状态已经确定下来,
      if (this.status === PROMISE_STATUS_FULFILLED && onfullfilled) {
        execFunctionWithCatchError(onfullfilled, this.value, resolve, reject);
      }
      if (this.status === PROMISE_STATUS_REJECTED && onrejected) {
        execFunctionWithCatchError(onrejected, this.reason, resolve, reject);
      }
      //将成功回调和失败回调放入到数组中
      if (this.status === PROMISE_STATUS_PENDING) {
        if (onfullfilled) {
          this.onfullfilledFns.push(() => {
            execFunctionWithCatchError(
              onfullfilled,
              this.value,
              resolve,
              reject,
            );
          });
        }

        if (onrejected) {
          this.onrejectedFns.push(() => {
            execFunctionWithCatchError(
              onrejected,
              this.reason,
              resolve,
              reject,
            );
          });
        }
      }
    });
  }
  catch(onrejected) {
    return this.then(undefined, onrejected);
  }
  finally(onfinally) {
    this.then(
      () => {
        onfinally();
      },
      () => {
        onfinally();
      },
    );
  }

  static resolve(value) {
    return new TCPromise((resolve) => resolve(value));
  }
  static reject(reason) {
    return new TCPromise((resovle, reject) => reject(reason));
  }
  static all(promises) {
    return new TCPromise((resolve, reject) => {
      const values = [];
      promises.forEach((promise) => {
        promise.then(
          (res) => {
            values.push(res);
            if (values.length === promises.length) {
              resolve(values);
            }
          },
          (err) => {
            reject(err);
          },
        );
      });
    });
  }

  static allSettled(promises) {
    return new TCPromise((resolve) => {
      const results = [];
      promises.forEach((promise) => {
        promise.then(
          (res) => {
            results.push({ status: PROMISE_STATUS_FULFILLED, value: res });
            if (results.length === promises.length) {
              resolve(results);
            }
          },
          (err) => {
            results.push({ status: PROMISE_STATUS_REJECTED, value: err });

            if (results.length === promises.length) {
              resovle(results);
            }
          },
        );
      });
    });
  }
  static race(promises) {
    return new TCPromise((resolve, reject) => {
      promises.forEach((promise) => {
        promise.then(
          (res) => {
            resolve(res);
          },
          (err) => {
            reject(err);
          },
        );
      });
    });
  }
  static any(promises) {
    const reasons = [];

    return new TCPromise((resolve, reject) => {
      promises.forEach(
        (promise) => {
          promise.then(resolve);
        },
        (err) => {
          reasons.push(err);
          if (reasons.length === promises.length) {
            reject(new AggregateError(reasons));
          }
        },
      );
    });
  }
}

Vue响应式原理

class TCVue {
  //获取options.data
  constructor() {
    this.$options = options;
    this.$data = options.data;
    this.$el = options.el;

    //数据响应式Observer

    //每个data属性对应这一个Dep对象
    //初次的时候Dep.target 为 null
    new Observer(this.$data);
    //处理el
    //此时
    this.$mount(this.$el);
  }

  $mount(el) {
    //添加订阅者
    const updateView = (_) => {
      let innerHtml = document.querySelector(el).innerHtml;
      let key = innerHtml.match(/{(\w+)}/)[1];
      document.querySelector(el).innerHtml = this.options.data[key];
    };
    new Watcher(updateView, true);
  }
}

class Observer {
  constructor(data) {
    //遍历data
    if (
      !data ||
      !Object.prototype.toString.call(data).indexOf('Object') ||
      Object.getOwnPropertyDescriptor(obj).configurable === false
    ) {
      throw new Error('data错误');
    }
    if (Array.isArray(data)) {
      Object.setPrototypeOf(data,arrayProto)

    } else {
      this.walk(data);
    }
  }
  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      //defineReactive :真实的数据响应式
      defineReactive(obj, keys[i]);
    }
  }
}
const defineReactive = function (obj, val) {
  //判断obj是否符合存取属性描述
  //添加响应之前添加dep依赖, 因为每个keys[i]会被多个watcher使用

  const dep = new Dep();
  Object.defineProperty(obj, val, {
    configurable: true,
    enumerbale: true,
    get() {
      if (Dep.target) {
        dep.depend(Dep.target);
      }
      return val;
    },
    set(nv) {
      if (nv === val) {
        return;
      }
      val = nv;
      dep.notify()
    },
  });
};
//依赖
let uuid = 0;
class Dep {
  constructor() {
    this.id = uuid++;
    this.subs = [];
  }
  //添加依赖
  depend(sub) {
    this.subs.push(sub);
  }
  //派发更新
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}
Dep.target = null;

//watcher 订阅数据变化,绑定更新函数
//挂载的时候,get函数触发,解析el中的模板中的指令
//此时 getter函数,会触发$data中属性的get操作,
//将订阅者Watcher添加到Dep中
class Watcher {
  constructor(expOrFn, isRenderWatcher) {
    this.getter = expOrFn;
    //更新状态
    this.get();
  }
  get() {
    //每个
    Dep.target = this;
    this.getter();
    Dep.target = null;
  }
  update() {
    this.get();
  }
}

//数组
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
//重写原型方法
methodsToPatch.forEach(item => {
    switch (method) {
      case 'push':
        arrayMethods['push'].apply(this.arguments)
        break;
      default:
        break;
    }
    //派发更新
    const ob = this.__ob__
    ob.dep.notify()
})


同时也可以参考Vue响应式原理

深拷贝

function isObject(objtype) {
  let obj = typeof objtype;
  return obj !== null && obj === 'object';
}

function deepClone(val, map = new WeakMap()) {
  //对特殊类型进行判断
  //map  set
  if (val instanceof Map) {
    return new Map([...val]);
  }

  if (val instanceof Set) {
    return new Set([...val]);
  }
  //判断是否是symbol
  if (typeof val === 'symobl') {
    return Symbol(val.description);
  }
  //判断是否是函数
  if (typeof val === 'function') {
    return val;
  }

  //判断val 是否是对象 , 如果是基本类型,除了symbol外,会直接作为值进行返回。
  if (!isObject(val)) {
    console.log(val);
    return val;
  }
  //判断是否重复
  if (map.has(val)) {
    return map.get(val);
  }

  //判断传入的对象是否是数组还是对象
  let newObj = Array.isArray(val) ? [] : {};
  // 保留deep
  map.set(val, newObj);
  //深拷贝
  for (const key in val) {
    if (Object.hasOwnProperty.call(val, key)) {
      const el = val[key];
      newObj[key] = deepClone(el, map);
    }
  }

  //因为symbol 遍历不会出现在for...in、for...of循环中
  let symbolKeys = Object.getOwnPropertySymbols(val);
  for (const key of symbolKeys) {
    newObj[key] = deepClone(val[key], map);
  }

  return newObj;
}

防抖节流

防抖

//防抖
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false;
  const _debounce = function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    if (immediate && !isInvoke) {
      fn.call(this, args);
      isInvoke = true;
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
        timer = null;
      }, delay);
    }
  };
  //添加取消功能
  _debounce.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      isInvoke = false;
      timer = null;
    }
  };

  return _debounce
}

节流

function throttle(
  fn,
  interval,
  options = {
    //头部执行,还是尾部执行
    leading: true, //头部
    trailing: false, //尾部
  },
) {
  const { leading, trailing } = options;
  //保留上次触发的时间
  let lastTime = 0;
  let timer = null;

  const _throttle = function (...args) {
    //获取当前时间
    let currentTime = new Date().getTime();
    if (!leading && !lastTime) {
      //默认的时间
      lastTime = currentTime;
    }
    //保留的时间
    //第一次为负数,肯定是立即执行的
    let remainTime = interval - (currentTime - lastTime);
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(this, args);
      lastTime = currentTime;
      return;
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null;
        lastTime = !leading ? 0 : new Date().getTime();
        fn();
      }, remainTime);
    }
  };

  return _throttle;
}

diff算法(Vue2 / Vue3)

Vue2双端Diff

function patchChildren(n1, n2, container) {
  if (typeof n2.children === 'string') {
    //省略代码
  } else if (Array.isArray(n2.children)) {
    //封装函数 patchKeyedArray 函数处理两组子节点
  } else {
    //省略代码
  }
}
function patchKeyedChildren(n1, n2, container) {
  const oldChildren = n1.children;
  const newChildren = n2.children;

  //四个索引值
  let oldStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newChildren.length - 1;

  //四个索引值对应的vnode节点
  let oldStartVnode = oldChildren[oldStartIdx];
  let oldEndVnode = oldChildren[oldEndIdx];
  let newStartVnode = newChildren[newStartIdx];
  let newEndVnode = newChildren[newEndIdx];

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    //增加两个判断分支,如果头尾部为undefined, 则说明该节点被处理过,直接跳到下一个位置
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIdx];
    } else if (!oldEndVnode) {
      oldEndVnode = newChildren[--oldEndVnode];
    } else if (oldStartVnode.key === newStartVnode.key) {
      //第一步
      //只需要补丁
      patch(oldStartVnode, newStartVnode, container);
      oldStartVnode = oldChildren[++oldStartIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else if (oldStartVnode.key === newEndVnode.key) {
      //第二步
      //调用 patch函数 在oldstartVnode 和 newStartVnode 之间打补丁
      patch(oldStartVnode, newEndVnode, container);

      insert(oldStartVnode.el, container, oldEndVnode.el.nextSibling);

      oldStartVnode = oldChildren[++oldStartIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (oldEndVnode.key === newEndVnode.key) {
      //第三步
      //节点在新的顺序中仍然处于尾部,不需要移动,但需要打补丁
      patch(oldEndVnode, newEndVnode, container);
      //更新索引和头尾节点变量
      oldEndVnode = oldChildren[--oldEndIdx];
      newEndVnode = newChildren[--newEndIdx];
    } else if (oldEndVnode.key === newStartVnode.key) {
      //第四步
      //首先需要调用patch函数进行补丁
      patch(oldEndVnode, newStartVnode, container);
      //移动DOM操作
      // oldEndVnode.el 移动到oldStartVnode 前面
      insert(oldEndVnode.el, container, oldStartVnode);

      oldEndVnode = oldChildren[--oldEndIdx];
      newStartVnode = newChildren[++newStartIdx];
    } else {
      //遍历旧的一组子节点,视图寻找于newStartVnode拥有相同Key值的节点
      //idxInOld 就是新的一组子节点的头部节点在旧的一组节点中的索引
      const idxInOld = oldChildren.findIndex(
        (node) => node.key === newStartVnode.key,
      );
      //如果idxInOld大于0 说明已经找到了可以复用的节点, 并且需要将其对应的DOM移动到头部
      if (indxInOld > 0) {
        const vnodeToMove = oldChildren[idxInOld];
        //不要忘记移除移动操作外还应该打补丁
        patch(vnodeToMove, newStartVnode, container);
        //将vnodetoMove。el 移动到头部节点oldStartVnode.el之前 因此需要后者作为描点
        insert(vnodeToMove.el, container, oldStartVnode.el);

        oldChildren[idxInOld] = undefined;
        // //更新 newStartIdx到下一个位置
        // newStartVnode = newChildren[++newStartIdx]
      } else {
        //添加元素
        // 将newStartVnode作为新的节点挂载到头部,使用当前头部节点oldStartVnode.el作为描点
        patch(null, newStartVnode, container, oldStartVnode.el);
      }
      //更新 newStartIdx到下一个位置
      newStartVnode = newChildren[++newStartIdx];
    }
  }
  //添加新节点
  if (oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
    //如果满足条件说明有新增的节点遗漏,需要挂载他们
    for (let i = newStartIdx; i < newEndIdx; i++) {
      patch(null, newChildren[i], container, oldStartVnode.el);
    }
  } else if (newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
    //删除操作
    for (let i = oldStartIdx; i < oldEndIdx; i++) {
      unmount(oldChildren[i]);
    }
  }
}

Vue3快速diff

function patchKeyedChildren(n1, n2, container) {
  const newChildren = n2.children;
  const oldChildren = n1.children;

  //处理相同的前置节点
  //索引 j 指向新旧两组子节点的开头
  let j = 0;
  let oldVnode = oldChildren[j];
  let newVnode = newChildren[j];
  //while循环向后遍历, 直到遇到拥有不同key值的节点为止
  while (oldVnode.key === newVnode.key) {
    //调用patch进行更新
    patch(oldVnode, newVnode, container);
    //更新J ,让其递增
    j++;
    oldVnode = oldChildren[j];
    newVnode = newChildren[j];
  }

  //更新相同的后置节点
  let oldEnd = oldChildren.length - 1;
  let newEnd = newChildren.length - 1;
  oldVnode = oldChildren[oldEnd];
  newVnode = newChildren[newEnd];
  //while 循环从后往前遍历, 直到遇到不同key值为止
  while (oldVnode.key === newVnode.key) {
    //调用patch进行更新
    patch(oldVnode, newVnode, container);
    //递减
    oldEnd--;
    newEnd--;
    oldVnode = oldChildren[oldEnd];
    newVnode = newChildren[newEnd];
  }
  //预处理完毕之后, 如果满足如下条件, 则说明 j --> nextEnd之间的节点应作为新节点插入
  if (j > oldEnd && j <= newEnd) {
    //描点的索引
    const anchorIndex = newEnd + 1;
    const anchor =
      anchorIndex < newChildren.length ? newChildren[anchorIndex].el : null;
    //采用 while循环,调用patch函数 逐个挂载新增节点
    while (j <= newEnd) {
      patch(null, newChildren[j++], container, anchor);
    }
  } else if (j > newEnd && j <= oldEnd) {
    // j => oldEnd之间的节点应该被卸载
    while (j <= oldEnd) {
      unmount(oldChildren[j++]);
    }
  } else {
    //处理 非理想情况
    // 构建 source 数组
    // 新的一组子节点中剩余未处理节点的数量
    const count = newEnd - j + 1;
    const source = new Array(count);
    source.fill(-1);
    //oldStart  newStart 分别为其实索引,即 j
    const oldStart = j;
    const newStart = j;
    //新增两个变量
    let moved = false;
    let pos = 0; // 遍历过程中遇到的最大索引值key

    // 构建索引表
    const keyIndex = {};
    for (let i = newStart; i <= newEnd; i++) {
      keyIndex[newChildren[i].key] = i;
    }
    //新增 patched 变量,代表更新过的节点数量
    let patched = 0;
    //遍历旧的子节点
    for (let i = oldStart; i <= oldEnd; i++) {
      const oldVnode = oldChildren[i];
      if (patched <= count) {
        //通过索引表快速找到新的一组子节点具有相同key值的节点位置
        const k = keyIndex[oldVnode.key];
        if (typeof k !== 'undefined') {
          const newVnode = newChildren[k];
          patch(oldVnode, newVnode, container);
          //没更新一个节点 都将patched 变量 + 1
          patched++;
          source[k - newStart] = i;
          //判断节点是否需要移动
          if (k < pos) {
            moved = true;
          } else {
            pos = k;
          }
        } else {
          //没找到
          unmount(oldVnode);
        }
      } else {
        //如果更新过的节点数量 大于需要更新的节点数量 则卸载多余的节点
        unmount(oldVnode);
      }
    }
    if (moved) {
      //如果moved 为true 则需要移动dom
      const seq = lis(source);
      //S 指向 最长递归子序列的最后一个元素
      let s = seq.length - 1;

      //i 指向新的一个子节点的最后一个元素
      let i = count - 1;
      //for 循环使得I递减,
      for (i; i >= 0; i--) {
        if (source[i] === -1) {
          //说明索引为I的节点为全新的节点, 应该将其挂载
          //获取该节点在新的children中的真实位置索引
          const pos = i + newStart;
          const newVnode = newChildren[pos];
          //该节点的下一个节点的位置索引
          const nextPos = pos + 1;

          //描点
          const anchor =
            nextPos < newChildren.length ? newChildren[nextPos].el : null;
          //挂载
          putch(null, newVnode, container, anchor);
        } else if (i !== seq[s]) {
          //如果节点的索引 i,不等于seq[s] 的值,说明该节点需要移动
          // 获取新的节点的真实位置
          const pos = i+newStart
          const newVnode = newChildren[pos]
          //该节点的下一个节点的位置索引
          const nextPos = pos + 1
          //描点
          const anchor = nextPos < newChildren.length?newChildren[nextPos].el : null
          // 移动
          insert(newVnode.el,container,anchor)

        } else {
          //如果节点的索引 i,等于seq[s] 的值,说明该节点不需要移动
          //只需要让S指向下一个指针
          s--;
        }
      }
    }
  }
}

封装一个axios吧

import axios from 'axios'

class MyRequest {
  constructor(options) {
    this.config = options
    this.interceptorHooks = options.interceptorHooks
    this.showLoading = options.showLoading
    this.instance = axios.create(options)
    this.setUpInterceptor()
  }
  //设置拦截器
  setUpInterceptor() {
    this.instance.interceptors.request.use(
      this.interceptorHooks.requestInterceptor,
      this.interceptorHooks.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptorHooks.responseInterceptor,
      this.interceptorHooks.responseInterceptorCatch
    )

    this.instance.interceptors.request.use((config) => {
      return config
    })
    this.instance.interceptors.response.use(
      (res) => {
        return res
      },
      (err) => {
        return err
      }
    )
  }
  //设置request
  request(config) {
    return new Promise((resolve, reject) => {
      this.instance
        .request(config)
        .then((res) => {
          resolve(res)
        })
        .catch((err) => {
          reject(err)
        })
    })
  }

  get(config) {
    return this.request({ ...config, method: 'get' })
  }
  post(config) {
    return this.request({ ...config, method: 'post' })
  }
}

const itcRequest = new MyRequest({
  baseURL: '',
  timeout: 1000,
  interceptorHooks: {
    requestInterceptor: (config) => {
      const token = localStorage.getCache('token')
      return config
    },
    requestInterceptorCatch: (err) => {
      return err
    },
    responseInterceptor: (res) => {
      return res.data
    },
    responseInterceptorCatch: (err) => {
      return err
    }
  }
})

export function getAmountList() {
  return itcRequest.get({})
}

树级菜单递归

var data = [
  { id: 1, name: '用户管理', pid: 0, child: null },
  { id: 2, name: '菜单申请', pid: 1, child: null },
  { id: 3, name: '信息申请', pid: 1, child: null },
  { id: 4, name: '模块记录', pid: 2, child: null },
  { id: 5, name: '系统设置', pid: 0, child: null },
  { id: 6, name: '权限管理', pid: 5, child: null },
  { id: 7, name: '用户角色', pid: 6, child: null },
  { id: 8, name: '菜单设置', pid: 6, child: null },
];

function arrayToTree (items) {
    const result = []; // 存放结果集
    const itemMap = {}; //
    for (let i = 0; i < items.length; i++) {
        const item = items[i];
        let id = item.id;
        let pid = item.pid;
        //首先以id为键值添加到itemMap中
        if (!itemMap[id]) {
            itemMap[id] = {
                child: []
            }
        }
        itemMap[id] = {
            ...item,
            child: itemMap[id]['child']
        }

        //将itemMap[id]添加到 result

        const treeItem = itemMap[id];
        if (pid === 0) {
            result.push(treeItem);
        } else {
            if (!itemMap[pid]) {
                itemMap[pid] = {
                    child: [],
                };
            }
            //根据对象的特性,修改itemMap会直接修改result中的元素
            itemMap[pid].child.push(treeItem);
        }
    }
    return result;
}


//递归实现
function arrayToTree(data, parentId = 0) {
    const tree = [];
  
    data.forEach(item => {
      if (item.pid === parentId) {
        const children = arrayToTree(data, item.id);
        if (children.length) {
          item.children = children;
        }
        tree.push(item);
      }
    });
  
    return tree;
  }
  
let at = arrayToTree(data);
console.log(at);