一、扁平数据和树形数据的相互转化
1、扁平数据
const flatData = [
{ id: 1, name: "节点1", parentId: null },
{ id: 2, name: "节点2", parentId: 1 },
{ id: 3, name: "节点3", parentId: 1 },
{ id: 4, name: "节点4", parentId: 2 },
{ id: 5, name: "节点5", parentId: 2 },
{ id: 6, name: "节点6", parentId: 3 },
];
2、树形数据
const treeData = [
{
id: 1,
name: "节点1",
children: [
{
id: 2,
name: "节点2",
children: [
{ id: 4, name: "节点4" },
{ id: 5, name: "节点5" },
],
},
{
id: 3,
name: "节点3",
children: [{ id: 6, name: "节点6" }],
},
],
},
];
3、相互转化
// 转树
function flatToTree(data, parentId = null) {
let tree = [];
data.forEach((item) => {
if (parentId === item.parentId) {
const children = flatToTree(data, item.id);
if (children.length > 0) {
item.children = children;
}
tree.push(item);
}
});
return tree;
}
// 转平
function treeToFlat(data, parentId = null) {
let flat = [];
data.forEach((item) => {
flat.push({ id: item.id, name: item.name, parentId: parentId });
if (item.children && item.children.length > 0) {
let Ft = treeToFlat(item.children, item.id);
flat.push(...Ft);
}
});
return flat;
}
二、JS去重方法
function deduplicate(data) {
方法一
return Array.from(new Set(data));
return [...new Set(data)];
方法二
return data.filter((item, index) => data.indexOf(item) === index);
方法三
for (let i = 0; i < data.length; i++) {
for (let j = i + 1; j < data.length; j++) {
if (data[i] === data[j]) {
data.splice(j, 1);
j--; // 因为splice方法会改变数组的长度,所以需要将j减一,否则会出错
}
}
}
return data;
方法四
let arr = [];
data.forEach((item) => {
if (arr.indexOf(item) === -1) {
// arr = [...arr, item];
arr.push(item);
}
});
return arr;
方法五
利用对象属性名不能重复这一特点
如果对象中不存在,就可以给 push 进去
let newArr = [];
let obj = {};
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
newArr.push(arr[i]);
obj[arr[i]] = 1;
} else {
obj[arr[i]]++;
}
}
return newArr;
方法六
let newArr = [];
let map = new Map();
for (let i = 0; i < data.length; i++) {
// 如果map里不包含就设置进去
if (!map.has(data[i])) {
map.set(data[i], 1);
newArr.push(data[i]);
}
}
return newArr;
// 方法七
let newArr = [];
return data.reduce((prev, current) => {
return newArr.includes(current) ? newArr : (newArr = [...newArr, current]);
}, []);
let newArr = [];
return data.reduce((prev, current, index, data) => {
// 如果包含,就返回原数据,不包含,就把新数据追加进去
return newArr.includes(current) ? newArr : newArr.push(current);
}, []);
方法八
利用includes 检查新数组是否包含原数组的每一项
如果不包含,就push进去
let newArr = [];
for (let i = 0; i < arr.length; i++) {
newArr.includes(arr[i]) ? newArr : newArr.push(arr[i]);
}
return newArr;
}
三、apply,call,bind
apply与call基本相同,区别是apply的第二个参数是传数组,而call是传一个或多个参数。bind的区别是调用时要加(),它返回的是一个函数。三者的作用都是为方法指定this的值。
// apply
this.x = 9;
const module01 = {
x: 81,
getX: function (y) {
return this.x + ":" + y;
},
};
var module1 = {
x: 91,
};
console.log(module01.getX(10)); // 81:10
var module2 = module01.getX;
console.log(module2(10)); // 9:10
var module3 = module2.apply(module01, [10]);
console.log(module3); // 81:10
var module4 = module2.apply(module1, [10]);
console.log(module4); // 91:10
// bind
this.x = 9;
const module02 = {
x: 81,
getX: function () {
return this.x;
},
};
var module1 = {
x: 91,
};
console.log(module02.getX()); // 81
var module2 = module02.getX;
console.log(module2()); // 9
var module3 = module2.bind(module02);
console.log(module3()); // 81
var module4 = module2.bind(module1);
console.log(module4()); // 91
// call
this.x = 9;
const module = {
x: 81,
getX: function (y) {
return this.x + ":" + y;
},
};
var module1 = {
x: 91,
};
console.log(module.getX(10)); // 81:10
var module2 = module.getX;
console.log(module2(10)); // 9:10
var module3 = module2.call(module, 10, 20);
console.log(module3); // 81:10
var module4 = module2.call(module1, 10, 20);
console.log(module4); // 91:10
四、arguments
arguments是一个类数组对象,第一个参数的属性名是“0”,依次类推,它还有length属性;_proto_是指向Object的,这也说明了他是个类数组对象,而不是一个数组。callee这个属性是存放函数代码的
function arg() {
var a = "这里是代码";
var b = "这是另一段代码";
var c = a + b;
console.log(arguments.callee);
return c;
}
arg();
1、利用arguments实现方法的重载
function add() {
var len = arguments.length,
sum = 0;
for (; len--; ) {
sum += arguments[len];
}
return sum;
}
console.log(add(1, 2, 3)); //6
console.log(add(1, 3)); //4
console.log(add(1, 2, 3, 5, 6, 2, 7)); //26
2、利用arguments.callee实现递归
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
console.log(factorial(10));
五、深浅拷贝
浅拷贝:创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
特征:如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址,即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
1.手写浅拷贝
function shallowClone(obj) {
let newObj = {};
for (let i in obj) {
if (obj.hasOwnProperty(i)) {
newObj[i] = obj[i];
}
}
return newObj;
}
const o = {
name: "zs",
hobby: ["排球", "网球", "乒乓球"],
};
const Obj = shallowClone(o);
Obj.name = "lisi"; //不是修改原数据
Obj.hobby[0] = "篮球"; // 会修改原数据
// Obj的第一层是深拷贝,hobby里面的是浅拷贝
console.log(o);
console.log(Obj);
2.es6的展开语法{...obj}
3.Object.assign({}, obj)进行对象的合并
4.数组和字符串的方法slice
5.数组和字符串的方法concat
const arr = [1, 2, { name: "nordon" }];
const newArr = arr.slice();
console.log(arr);
console.log(newArr);
const arr1 = [1, 2, { name: "nordon" }];
const newArr1 = arr.concat();
console.log(arr1);
console.log(newArr1);
深拷贝:开辟一个新的栈,两个对象属性完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
1.递归实现深拷贝
function deepClone(obj) {
let newObj = obj instanceof Array ? [] : {};
for (let i in obj) {
if (obj[i] && typeof obj[i] == "object") {
// 若对象属性还是引用类型,进行递归
newObj[i] = deepClone(obj[i]);
} else {
// 对象属性是基础引用类型,直接赋值
newObj[i] = obj[i];
}
}
return newObj;
}
2.巧妙运用Object.entries(obj)遍历对象的属性和值
function deepClone(obj) {
let newObj = obj instanceof Array ? [] : {};
for (const [k, v] of Object.entries(obj)) {
newObj[k] = typeof v == "object" ? deepClone(v) : v;
}
return newObj;
}
3.JSON.parse(JSON.stringify(待拷贝对象)
const bj = {
name: "zs",
hobby: ["排球", "网球", "乒乓球"],
};
const ne = JSON.parse(JSON.stringify(bj));
ne.name = "lisi";
ne.hobby[0] = "篮球";
console.log(bj);
console.log(ne);
4.引入loadsh,提供 cloneDeep 实现
vue中使用loash库实现深拷贝的步骤
安装loadsh npm i --save lodash
引入loadsh import _ from 'lodash'
直接调用loadsh库的方法 const newObj = _.cloneDeep(this.obj)
六、JS中的任务
任务分为同步任务和异步任务,异步任务又分为宏任务和微任务
宏任务有:<script>整体代码、setTimeout、setInterval、setImmediate、Ajax、DOM事件
微任务有:process.nextTick、MutationObserver、Promise.then catch finally
setTimeout(() => {
new Promise((resolve) => {
resolve();
}).then(() => {
console.log("test");
});
console.log(4);
}, 3000);
new Promise((resolve) => {
resolve();
console.log(1);
}).then(() => {
console.log(3);
Promise.resolve()
.then(() => {
console.log("before timeout");
})
.then(() => {
Promise.resolve().then(() => {
console.log("also before timeout");
});
});
});
console.log(2);
// 1, 2, 3, before timeout, also before timeout, 4, test
1. 任务队列
任务队列就是一个事件队列,其中最重要的是异步任务事件和定时事件。
异步任务事件:一般我们绑定一个事件,比如点击事件等等,都是在某一个时刻才触发执行的,这个时候就得放到任务队列里面,等待执行,而在某个DOM节点上绑定了事件,就要有相应的回调函数,它们是相辅相成的。
所谓回调函数,就是那些被挂载起来,等待执行的代码,主线程执行任务队列里面的异步任务,其实就是执行这些回调函数。
定时事件:setInterval 和 setTimeout
2. 同步任务和异步任务
同步任务(synchronous):在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务(asynchronous):不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。 (异步任务事件,定时事件,页面渲染请求图片获取异步资源等)
异步执行机制如下:
所有同步任务都在主线程上执行,形成一个执行栈。
主线程之外,还有一个“任务队列”,只要异步任务有了运行结果,就在“任务队列”之中放置一个事件。
一旦“执行栈”中的所有同步任务执行完毕了,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的三步。(事件循环)
3. 宏任务和微任务
不同类型的任务会进入不同的Event Queue,有宏任务的队列和微任务的队列。
这里需要注意的是new Promise是会进入到主线程中立刻执行,而promise.then则属于微任务。
事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后——开始第一轮循环执行完所有微任务——下一轮宏任务…
宏任务(macro-task):整体代码script、setTimeOut、setInterval
微任务(mincro-task):promise.then、process.nextTick(node)
4. Event Loop
1、整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为两部分:“同步任务”、“异步任务”;
2、同步任务会直接进入主线程依次执行;
3、异步任务会再分为宏任务和微任务;
4、宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
5、微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中;
6、当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务;
7、上述过程会不断重复,这就是Event Loop事件循环;