DFS与BFS遍历
DFS
- 从一个起点开始,沿着一条路径尽可能深地搜索,直到无法继续前进(即到达叶子节点或遇到已访问过的节点),然后回溯到上一个分支点,继续探索其他路径,
- 逆序入栈, 先进后出
1 A
/ \
2 B 5 C
/ \
3 D 4 E
入 A
出 A
入 C B
出 B
入 E D
出 D
出 E
出 C
A B D E C
递归出栈
DFS(A)
DFS(B)
DFS(D)
DFS(E)
DFS(C)
访问顺序:A → B → D → E → C
function DFS(node){//栈 先进先出
let stack=[node]
let ans=[]
while(stack.length){
let item=stack.pop()
ans.push(item)
let children = item.children
for(let i=children.length-1;i>=0;i--){
stack.push(children[i])
}
}
return ans
}
DFS(parent)
BFS
先进先出
1 A
/ \
2 B 5 C
/ \
3 D 4 E
入 A
出 A
入 B C
出 B
入 D E
出 C
出 D
出 E
function BFS(node){//队列 先进先出
let queue=[node]
let ans=[]
while(queue.length){
let item=queue.shift()
ans.push(item)
let children = item.children
for(let i=0;i<children.length;i++){
queue.push(children[i])
}
}
return ans
}
BFS(parent))
<div class="parent">
<div class="child1">
<div class="child1-1"></div>
<div class="child1-2"></div>
</div>
<div class="child2">
<div class="child2-1"></div>
<div class="child2-2"></div>
</div>
<div class="child3">
<div class="child3-1"></div>
<div class="child3-2"></div>
</div>
</div>
发布订阅
核心思想: 发布者和订阅者之间 完全解耦 (Decoupled) 。
发布订阅模式是一种 消息传递模式,其中消息的发送者(称为 发布者)不会直接将消息发送给特定的接收者(称为 订阅者)。相反,发布者将消息发布到 事件通道 (Event Bus) 中介上,订阅者则可以订阅它们感兴趣的事件通道
class EventEmitter {
constructor() {
this.event = {};
}
on(type, cb) {
if (!this.event[type]) {
this.event[type] = [cb];
} else {
this.event[type].push(cb);
}
}
emit(type, ...args) {
if (!this.event[type]) {
return;
} else {
this.event[type].forEach((item) => item(...args));
}
}
off(type, cb) {
if (!this.event[type]) {
return;
} else {
this.event[type] = this.event[type].filter((item) => item != cb);
}
}
}
let mitt = new EventEmitter();
let foo = () => console.log("111");
mitt.on("fn", foo);
mitt.on("fn", () => console.log("object"));
mitt.off("fn", foo);
mitt.emit("fn");
防抖与节流
防抖
在事件被触发后,这n秒内又被触发,则重新计时。
let btn = document.querySelector("#btn");
function foo() {
console.log(this, "已提交");
}
let clear;//污染全局变量
function debounce(fn, time) {
if (clear) clearTimeout(clear);
setTimeout(() => {
fn();
}, time);
}
btn.addEventListener("click", () => debounce(foo, 1000));
- 闭包保存私有变量
- this指向
防抖函数通过
setTimeout
来延迟函数的执行。由于setTimeout
会在全局上下文中执行回调函数,this
指向全局对象(在浏览器中是window
)。事件 :在事件监听器中,
this
指向事件源对象,即触发事件的 DOM 元素。
function foo(arg) {
console.log(this, "已提交", arg);
}
btn.addEventListener("click", debounce(send, 1000, '参数'));
function debounce(fn, time, ...arg) {
let timer = null;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.call(this, ...arg);
}, time);
};
}
节流
私有变量保存上次函数执行时间
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
function throttle(fn, time = 1000, ...args) {
let prevTime = 0;
return function () {
let curTime = Date.now();
if (curTime - prevTime > time) {
fn.call(this, ...args);
prevTime = curTime;
}
};
}
window.addEventListener("scroll", throttle(fn, 2000, "参数"));
function fn() {
console.log("object", arguments);
}
拷贝
简单类型:简单类型的数据存储在
栈内存
中。复杂类型:变量的引用地址存储在栈内存中,实际数据存储在堆内存中
浅拷贝
扩展运算符 slice concat assign
let obj = {
a: 1,
b: {
c: 2,
},
};
let newObj = Object.assign({}, obj);
newObj.a=0
newObj.b.c=3
console.log(obj);
let arr=[1,2,[3,4]]
let newArr=arr.slice(0)
newArr[2][1]=5
console.log(arr);
let arr1=[1,2,[3,4]]
let newArr1=arr1.concat([])
newArr1[2][1]=6
console.log(arr1);
let arr2=[1,2,[3,4]]
let newArr2=[...arr2]
newArr2[2][1]=7
console.log(arr2);
JSON
undefined
与function
symbol
会丢失
let simpleDeepClone = {
a: 1,
b: { name: "owllai" },
c: null, // null
d: undefined, //丢失
e: function () {}, //丢失
f: new Date(), // 变成时间字符串
g: new RegExp(/\d/), //
h: new Error(), // new RegExp 和 new Error 变成空对象
i: NaN,
j: Infinity, //NAN 和 Infinity 会变成null
k: new Map(), //变成空对象
l: new Set(), //变成空对象
m: Symbol("1"),
};
console.log(JSON.parse(JSON.stringify(simpleDeepClone)));
递归
const obj = { a: 1, b: 2 };
console.log(obj.hasOwnProperty('a')); // true
console.log(Object.hasOwn(obj, 'a')); // true
let obj = {
a: [1, 2, 3, 4],
b: { a: 1, b: [1, 2, 3] },
};
function deepClone(obj, map = new Map()) {
//实现深拷贝
if (!(obj instanceof Object)) return obj;
//判断是对象还是数组
let newObj = Array.isArray(obj) ? [] : {};
map.set(obj, newObj);
for (let key in obj) {
//in 会遍历到原型链上的东西
if (Object.hasOwn(obj, key)) {
if (map.has(obj[key])) {
newObj[key] = map.get(obj[key]);
} else {
newObj[key] = deepClone(obj[key], map);
}
}
}
return newObj;
}
console.log(deepClone(obj));
消息通道
无法处理函数、Symbol
let foo={a:1,b:{c:2}}
//缺点:无法处理函数、Symbol 等
function deepClone(obj) {
return new Promise((resolve) => {
let { port1, port2 } = new MessageChannel();
port1.postMessage(obj);
port2.onmessage = (e) => resolve(e.data);
});
}
deepClone(foo).then((res) => console.log(res));
structuredClone
JSON的替代品,
解决JSON不可序列化部分JS属性
无法处理函数、Symbol
const original = {
a: 1,
b: { c: 2 },
d: [3, 4],
e: new Date(),
f: new Map([["key", "value"]]),
};
const clone = structuredClone(original);
console.log(clone);
this
- 默认绑定:当一个函数独立调用,this指向window
- 隐式绑定: 当
函数
被某个对象拥有
, this指向引用它的对象
apply
思路:隐式调用
- 缺点:可能覆盖原属性(symbol)
- 缺点:symbol会被打印出来(
definProperty隐藏
)
foo.apply(obj,[2,3])
//隐式绑定
Function.prototype._apply=function(newThis,arr){
newThis.fn=this
newThis.fn(...arr)
}
Function.prototype._apply = function (newThis, arr = []) {
newThis = newThis || window;
let fn = Symbol();
newThis[fn] = this;
Object.defineProperty(newThis, fn, {
enumerable: false,
});
let ans = newThis[fn](...arr);
delete newThis[fn];
return ans;
};
let arr = [1];
function fn() {
console.log(this);
}
fn._apply(arr);
call
foo.call(obj,2,3)
Function.prototype._call = function(obj, ...arr) {
obj.fn = this
obj.fn(...arr) //隐式绑定
}
Function.prototype._call=function(newThis,...arr){
newThis=newThis||window
let fn=Symbol()
newThis[fn]=this
Object.defineProperty(newThis, fn, { enumerable: false, });
let ans=newThis[fn](...arr)
delete newThis[fn]
return ans
}
let arr = [1];
function fn() {
console.log(this == arr);
}
fn._call(arr);
bind
- 保存将调用的函数
- apply改变将调用的函数的this
Function.prototype._bind = function (newThis, ...args) {
const fn = this;
return function (...args1) {
return fn.apply(newThis, [...args, ...args1]);
};
};
function fn(...args) {
console.log(this, args);
}
let obj = {
myname: "张三",
};
const bindFn = fn._bind(obj);
bindFn(1, 2);
数组 APi
手写Map
数组中每一项都执行函数
array.map(function(item, index, arr){},this)
let ins = {a: 1};
let array = [1, 2, 3];
array.map(function(item, index, arr) {
console.log(item, index, arr);
console.log(this);
}, ins);
Array.prototype._map = function (callback, ins) {
let res = [];
for (let i = 0; i < this.length; i++) {
res[i] = callback.call(ins , this[i], i, this)
}
return res;
};
手写forEach(无返回值)
array.forEach(function(item, index, arr), ins)
Array.prototype._forEach = function(callback, ins) {
for(let i = 0; i < this.length; i++) {
callback.call(ins, this[i], i, this);
}
}
filter
函数处理完的值(满足条件),返回
Array.prototype._filter = function (callback, ins) {
let res = [];
for (let i = 0; i < this.length; i++) {
if (callback.call(ins, this[i], i, this)) {
res.push(this[i])
}
}
return res;
};
let res = [1, 2, 3]._filter(function (item, index, arr) {
console.log(item, index, arr);
console.log(this);
return item > 1;
}, { o: 100 });
console.log(res);
reduce
- reducer 逐个遍历数组元素,每一步都将
当前元素的值
与前一步的结果
相操作
Array.prototype._reduce = function(callback, initialValue) {
let index = initialValue ? 0 : 1;
let res = initialValue ? initialValue : this[0];
for(let i = index; i < this.length; i++) {
res = callback.call(null, res, this[i], i, this);
}
return res;
}
扁平
递归
let arr=[1,[23,[1,2,3]]]
function _flat(arr){
let ans=[]
for(let item of arr){
if(Array.isArray(item)){
ans=ans.concat(_flat(item))//concat不改变原数组
}else{
ans.push(item)
}
}
return ans
}
console.log(_flat(arr));
flat
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.flat(Infinity);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
toString
let arr = [1, [2, [3, 4]]];
function flatten(arr) {
return arr.toString().split(',').map(i=>Number(i));
}
console.log(flatten(arr)); // [1, 2, 3, 4]
判断对象是否相同
===与Object.is()
比较两个对象内存指向是否相同
JSON序列化
JSON.stringify
方法将对象转换为字符串,比较字符串
const obj1 = { b: "hello",a: 1, c: true };
const obj2 = { a: 1, b: "hello", c: true };
const obj3 = { a: 1, b: "hello", c: true };
console.log(JSON.stringify(obj1) === JSON.stringify(obj2)); // false
console.log(JSON.stringify(obj2) === JSON.stringify(obj3)); // true
手动实现
- 判断长度是否一致
- for in 遍历判断每一项
- 两个都是对象就递归判断
let obj={ a: 1, b: 2, c: { a: 1, b: 2 } }
let obj1= { b: 2, a: 1, c: { b: 2, a: 1 ,c:3 } }
function isEqual(obj, obj1) {
const key1 = Object.keys(obj);
const key2 = Object.keys(obj1);
if (key1.length !== key2.length) return false;
for (const k in obj) {
if (obj[k] instanceof Object && obj1[k] instanceof Object) {
if (!isEqual(obj[k], obj1[k])) return false;
} else {
if (obj[k] !== obj1[k]) return false;
}
}
return true;
}
console.log(isEqual(obj,obj1))
对象去重
如何定义重复
const arr = [
{ a: 1, b: 2 },
{ a: 1, b: 2 },
{ a: 1, b: 2, c: { a: 1, b: 2 } },
{ b: 2, a: 1, c: { b: 2, a: 1 } },
];
function isEqual(obj, obj1) {
const key1 = Object.keys(obj);
const key2 = Object.keys(obj1);
if (key1.length !== key2.length) return false;
for (const k in obj) {
if (obj[k] instanceof Object && obj1[k] instanceof Object) {
if (!isEqual(obj[k], obj1[k])) return false;
} else {
if (obj[k] !== obj1[k]) return false;
}
}
return true;
}
function foo(arr) {
return arr.reduce((pre, cur) => {
let is = true;
for (let item of pre) {
if (isEqual(item, cur)) {
is = false;
break;
}
}
if (is) {
pre.push(cur);
}
return pre;
}, []);
}
console.log(foo(arr));
new
- 创建一个空对象
let obj = {}
- 设置对象的原型链,将其指向构建函数的原型
obj.__proto__ = Fun.prototype
- 改变构建函数this指向,将其绑定到新创建的空对象
Fun.apply(obj,args)
- 最后返回对象
function _new(fn, ...args) {
let obj = {};
obj.__proto__ = fn.prototype;
fn.apply(obj, args);
return obj;
}
new.target
new.target
function foo() {
if (new.target !== undefined) {
console.log("被new调用");
} else {
console.log("不是被new调用");
}
}
new foo()
foo()
create
- 给一个新对象设置原型
function create(obj) {
function F() {}
F.prototype = obj
return new F()
}
let obj={a:1}
let obj1=create(obj)
console.log(obj1.a);//1
继承
原型链继承
function parent() {
this.colors = ["red", "blue", "green"];
}
function child() {}
// 继承 parent
child.prototype = new parent();
let instance1 = new child();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new child();
console.log(instance2.colors); // "red,blue,green,black"
缺点
- 父类的prototype会被改变(
内存共享
)
盗用构造函数
function parent(name) {
this.name = name;
this.age = 12;
}
parent.prototype = { a: 1 };
function child(name) {
parent.call(this, name);
}
console.log(new child("yi"));
console.log(new child('yi').a)//undefined
缺点
- 不能访问父类原型上的方法
原型链+构造函数
function parent(name) {
this.name = name;
this.age = 12;
}
parent.prototype = { a: 1 };
function child(name) {
parent.call(this, name);
}
child.prototype = new parent();
console.log(new child("yi"));
console.log(new child("yi").a);
- 父类构造函数被调用两次
create
function object(obj) {
function F() {}
F.prototype = obj;
return new F();
}
盗用构造函数+create
- 盗用构造函数(无父元素的prototype属性)
child.prototype=parent.prototype
function inheritPrototype(child, parent) {
let prototype = Object.create(parent.prototype); // 创建对象
child.prototype = prototype; // 赋值对象
}
function Parent(name) {
this.name = name;
this.friends = ["rose", "lily", "tom"];
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
console.log(this.age);
};
let child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]
let child2 = new Child("yl", 22);
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]
extends(最优)
class Person {
constructor(name) {
this.name = name
}
// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}
class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}
const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法
instanceof
检测构造函数的
prototype
属性是否出现在某个实例对象的原型链上。
symbol
class A {
static [Symbol.hasInstance]() {
return true;
}
}
console.log(null instanceof A); // true
instanceof
function myinstanceof(L, R) {
while (L !== null) {
//通过原型链进行判断
if (L.__proto__ = R.prototype) {
return true
}
L = L.__proto__
}
return false
}
var arr = {}
console.log(myinstanceof(arr, Object))//true
isPrototypeOf
function _instanceof(L,R){
return R.prototype.isPrototypeOf(L)
}
iterator
内置iterator
- 字符串
- 数组
Map
Set
arguments
对象NodeList
等 DOM 集合类型
iterator的应用
for-of
循环- 数组解构
- 扩展操作符
Array.from()
- 创建
Set
- 创建
Map
Promise.all()
接收由Promise
组成的可迭代对象Promise.race()
接收由Promise
组成的可迭代对象yield*
操作符,在生成器中使用
使用
let arr = [1, 2, 3];
let iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
对象解构
let obj = {
name: "tom",
age: 18,
gender: "男",
intro: function () {
console.log("my name is " + this.name);
},
};
obj[Symbol.iterator] = function () {
return Object.values(this)[Symbol.iterator];
};
obj[Symbol.iterator] = function () {
let index = 0;
let values = Object.values(this); //转为数组
return {
next: function () {
return {
value: values[index++],
done: index > values.length,
};
},
};
};
let [name, age, gender] = obj;
console.log(name, objAge, gender);
Generator
函数遇到
yield
的时候会暂停,并把 yield 后面的表达式运行,并将其作为生成器value
手写async
function request(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url);
}, 2000);
});
}
function* getData() {
const res1 = yield request("1");
console.log("res1:", res1);
const res2 = yield request(res1 + "2");
console.log("res2:", res2);
const res3 = yield request(res2 + "3");
console.log("res3:", res3);
}
const generator = getData();
// generator.next().value.then((res1) => {
// generator.next(res1).value.then((res2) => {
// generator.next(res2).value.then((res3) => {
// generator.next(res3);
// });
// });
// });
// 自动化执行生成器函数(了解)
function execGenFn(fn) {
// 1.获取对应函数的generator
const gen = fn();
// 2.定义一个递归函数
function exec(res) {
// result -> { done: true/false, value: 值/undefined }
const result = gen.next(res);
if (result.done) return;
result.value.then((res) => {
exec(res);
});
}
// 3.执行递归函数
exec();
}
execGenFn(getData);
手写PromiseAPI
all
接受一个
迭代器
,返回一个Promise
当所有输入的 Promise 都被兑现时,返回所有兑现值的数组
输入的任何一个 Promise 被拒绝,则返回的 Promise 将被拒绝
Promise._all = function (promises) {
let ans = [];
return new Promise((resolve, reject) => {
promises.forEach((item) => {
Promise.resolve(item).then(
(val) => {
ans.push(val);
if (ans.length == promises.length) {
resolve(ans);
}
},
(err) => {
reject(err);
}
);
});
});
};
let p1 = Promise.resolve("1");
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p2 延时一秒");
}, 2000);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p3 延时一秒");
}, 1000);
});
Promise._all([p1, p2, p3])
.then((res) => console.log(res))
.catch((err) => console.error(err));
race
接受一个
迭代器
,返回一个Promise
返回的 promise 会随着第一个 promise 的敲定而敲定。
Promise._race = function (promiseArray) {
return new Promise((resolve, reject) => {
promiseArray.forEach(p => {
Promise.resolve(p).then((res) => resolve(res), (err) => reject(err));
})
});
}
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p3 延时一秒");
}, 100);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p2 延时一秒");
}, 1000);
});
Promise._race([p3, p2]).then((res) => console.log(res));
any
接受一个
迭代器
,返回一个Promise
当所有输入的 Promise 都被拒绝时,返回所有拒绝值的数组
输入的任何一个 Promise 被接受,则返回的 Promise 将被接受
Promise._any = function (promises) {
let ans = [];
return new Promise((resolve, reject) => {
for (let item of promises) {
Promise.resolve(item).then(
(res) => {
resolve(res);
},
(err) => {
ans.push(err);
if (ans.length == Object.keys(promises).length) {
reject(ans);
}
}
);
}
});
};
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p3 延时一秒");
}, 100);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p2 延时一秒");
}, 1000);
});
Promise._any([p3, p2])
.then((res) => console.log(res))
.catch((err) => {
console.log(err);
});
allsettled
接受一个
迭代器
,返回一个Promise
当全部Promise执行完时,返回包括Promise状态与value
Promise._allsettled = function (promises) {
let ans = [];
return new Promise((resolve) => {
for (let item of promises) {
Promise.resolve(item)
.then(
(res) => {
ans.push({ status: "resolve", res: res });
},
(err) => {
ans.push({ status: "reject", res: err });
}
)
.finally(() => {
if (ans.length == Object.keys(promises).length) {
resolve(ans);
}
});
}
});
};
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p3 延时一秒");
}, 100);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p2 延时一秒");
}, 1000);
});
Promise._allsettled([p3, p2])
.then((res) => console.log(res))
.catch((err) => {
console.log(err);
});
Promise异步题
控制并发
浏览器最多只支持
6个请求
- 并发池
- 请求地址池
- 开始时用url地址请求将并发池填满
- 执行最快的一个(Promise.race)
- 每执行一个,删除并发池中完成的那个,从url地址池拿出一个放入并发池
const URLs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
class PromisePool {
constructor(max, fn) {
this.max = max; // 最大并发数
this.fn = fn; // 自定义的请求函数
this.pool = []; // 并发池
this.urls = []; // 剩余的请求地址
}
start(urls) {
this.urls = urls;
// 循环把并发池塞满
while (this.pool.length < this.max) {
let url = this.urls.shift();
this.setPool(url);
}
// 利用Promise.race 方法来获得并发池中某个任务完成的信号
this.run(Promise.race(this.pool));
}
setPool(url) {
if (!url) return;
let task = this.fn(url);
this.pool.push(task); // 将任务推入pool并发池中
console.log(`${url}开始,当前的并发数:${this.pool.length}`);
task.finally(() => {
// 请求结束将该promise任务从并发池中移除
this.pool.splice(this.pool.indexOf(task), 1);
console.log(`${url}结束,当前的并发数:${this.pool.length}`);
});
}
run(race) {
race.then(() => {
// 每当并发池中完成一个任务,就在塞入一个任务
let url = this.urls.shift();
this.setPool(url);
this.run(Promise.race(this.pool));
});
}
}
// 模拟异步请求函
let fn = (url) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`任务${url}完成`);
}, 1000);
}).then((res)=>{
console.log(res);
})
};
// 并发数为3
const pool = new PromisePool(3, fn);
pool.start(URLs);
红绿灯实现
函数柯里化
核心思想是将一个接受多个参数的函数转化为一系列接受单一参数的函数。
function add() {
let arg = [...arguments];
let fn = function () {
return add.apply(null, arg.concat([...arguments]));
};
fn.toString = () => arg.reduce((a, b) => a + b);
return fn;
}
console.log(+add(1)(2,3))
树
子树添加父ID
let arr1 = [
{
id: 1,
children: [{ id: 11 }, { id: 12, children: [{ id: 121 }] }],
},
];
function foo(arr) {
arr.forEach((item) => {
if (item.children) {
foo(item.children);
item.children.forEach((item1) => {
item1.parent = item.id;
});
}
});
return arr;
}
foo(arr1);
console.log(arr1[0].children[1]);
数组转树
const arr = [
{ id: 1, parentId: null },
{ id: 2, parentId: 1 },
{ id: 3, parentId: 1 },
{ id: 4, parentId: 2 },
{ id: 5, parentId: 2 },
{ id: 6, parentId: 3 },
];
function createNode(id, arr) {
// data中去查找根节点下有哪些子节点
const childArr = arr.filter(({ parentId }) => parentId === id);
// 重写节点
return {
id,
children: childArr.map((item) => {
return createNode(item.id, arr);
}),
};
}