跳到主要内容

10 篇博文 含有标签「Javascript」

查看所有标签

· 阅读需 4 分钟
Meoo

思路

从最基本的拷贝开始,一步一步处理更复杂的对象、类型。

拷贝对象字面量

先考虑基本类型和层层对象的情况,直接 for-in 遍历 + 递归拷贝

function deepClone(obj) {
// 基本类型直接返回
if (typeof obj !== 'object') return obj;
const newObj = {};
for (let key in obj) {
// 只复制自身的对象
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}

解决循环引用问题

可以用 Map 或 WeakMap 解决,对复杂数据、对象字面量进行深拷贝前保存其引用,如果下次又出现了该值,就不再进行拷贝,直接返回保存的值。

function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object') return obj;
if (hash.has(obj)) return hash.get(obj);

const newObj = {};
hash.set(obj, newObj);

for (let key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key], hash);
}
}
return newObj;
}

现在已经支持复制这样的对象 o 了:

const o = {
name: 'Lee',
attrs: {
degree: 'BA',
year: 2019
}
};
const b = {};
o.reference = b;
b.reference = o;

处理构造出的且可直接赋值的对象

一些构造出来的对象,例如 Array 和 Object 的实例,可能是 [1,2,3] 或者 { a:1, b:2},他们支持 for-in 遍历,而且可以 obj[key] = value 这样赋值,就拿他们的构造函数 new 出一个新对象,然后放在 for-in 遍历里面拷贝每一个值即可。

function deepClone(obj, hash = new WeakMap()) {
if (typeof obj !== 'object') return obj;

if (hash.has(obj)) return hash.get(obj);

// 对象本身没 constructor 属性,但可以访问到,因为会沿着原型链往上找
// 如果是 {} ,就相当于 new Object();如果是数组,就相当于 new Array()
let newObj = new obj.constructor;
hash.set(obj, newObj);

for (let key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key], hash);
}
}
return newObj;
}

处理其他常见类型

考虑 Date、RegExp、Set、Map、null 这些对象,需要用不同的手段来拷贝其值。

当对象是以下情况:

  1. null 对象直接返回 null
  2. Date 和 RegExp 对象,直接用构造函数包裹,new 出来返回即可
  3. Set 和 Map 集合,需要遍历每个值,并对每个值进行递归拷贝,最后返回新的集合

最终代码

function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const o = new Map();
obj.forEach((val, key) => {
o.set(key, deepClone(val, hash));
});
return o;
}
if (obj instanceof Set) {
const s = new Set();
obj.forEach(v => {
s.add(v);
});
return s;
}

// 如果不是对象,直接返回
if (typeof obj !== 'object') return obj;

if (hash.has(obj)) return hash.get(obj); // 3

// obj 是对象字面量或构造出的对象,比如 Array、Object 的实例 ([]和{})
let newObj = new obj.constructor;
// 将对象储存在weakMap中
hash.set(obj, newObj); // 1
// 递归地把原对象属性复制给创建的对象
for (let key in obj) {
if (Object.hasOwn(obj, key)) {
newObj[key] = deepClone(obj[key], hash); // 2
}
}
return newObj;
}

测试对象:

const obj = {
set: new Set([1, 2, 4]),
map: new Map([[1, '123', 2, '456']]),
arr: [1, 3, 5],
attr: {
a: 1, b: 2
},
date: new Date(),
reg: new RegExp(),
fun: function () {
console.log('fn');
}
};

· 阅读需 2 分钟
Meoo

为什么防抖这样写不行?

function debounce(fn, delay) {
let timeout;
return function () {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(fn, delay, ...arguments);
};
}

· 阅读需 5 分钟
Meoo

负责人布置了个任务,写一个过滤 JSON 数据的方法,处理一下模型数据。递归绕的我真晕,脑子内存还是不太够。

样例数据

规则:bosclass 值为 boqs 的是树节点,bosclass 值为 wps 的是纯数据。如果树节点的 children 是空数组,那么把树节点移除掉。树的层级是无法预估的,可能有很多层。以下是过滤后的数据:

[
{
"bosclass": "boqs",
"children": [
{
"bosclass": "boqs",
"children": [
{
"bosclass": "wps",
"guid": "fa01a4a1f1d448a3bfb315dac41e1914"
},
{
"bosclass": "wps",
"guid": "cbf47dc52b2243c1a9d46bb046d8a13d"
}
],
"guid": "f785b4a061714dfe8fea367d12ddd29c"
}
],
"guid": "8bc6c9026bb9472f8a3e23b382f87ac1"
}
]

如何过滤呢?首先想到的方法是递归!

· 阅读需 3 分钟
Meoo

这两天吃饱撑着,在探究为什么我的 Node 程序 Heap Usage 一直居高不下,而且会越来越多。

如何调试 NodeJS 程序

在VSCode中可以直接调试,选择Debug,显示所有自动调试配置,选择要运行的命令,就进入了调试模式,侧栏会显示出调试的信息。可以捕获 Heap 快照,拿到 ChromeDevTool 里面导入分析。

用 Node 的 --inspect 参数搭配 Chrome 实时分析

运行以下命令,进入了调试模式

node --inspect serve.js

在 Chrome 中打开 chrome://inspect,就可以看到 Open dedicated DevTool for Node,点击进入 Node 调试工具,就可以开始分析了。

放大问题

很多时候,人工是难以准确排查出问题的,因为会有很多不可预料的误差,导致每次运行的结果数据不准。这时候,要放大问题,大量测试多次触发漏洞,就能很清楚的发现问题。

k6 对接口进行测试,运行下列测试程序 k6 run test.js

// test.js
import http from 'k6/http';

export let options = {
vus: 100,
duration: '20s',
};

export default function () {
let res = http.post("http://localhost:5000/monitor/start", JSON.stringify({ phone: Math.random()*100000 }), { headers: { 'Content-Type': 'application/json' } })

console.log(res.body);
}

通过对比前后的 Heap Size,确实管用,明显增加了将近 8MB。

接着调用了一下其他的接口,调用 processMap.clear() 把保存子进程的 processMap 清空。内存果然又降回去了,但并没有和最开始一样,而是稍稍高出了一点点。

得出结论,引用计数法回收垃圾是实实在在的,这个例子验证了它。但是仍然困扰我的是,Heap 里的 [compiled code] 这部分会一直增加,无法被回收,增加的量很小...

未解之谜:每次访问接口,即使是访问 / 路径,返回一段文本, [compiled code] 也会不断增加?

· 阅读需 1 分钟
Meoo

每一个函数都有这三个方法,平时也不怎么能用到,整的时间长不看真容易迷糊...

function Fn(){}

函数 Fn,可调用 Fn.call(), Fn.apply(), Fn.bind(),下面细说。

const person = {
name: 'Lee',
say: function (greet) {
console.log(`${greet}, I'm ${this.name}`)
}
}
const dog = { name: 'coco' }

person.say('hi') // hi, I'm Lee

person.say.apply(dog, ['woof']) // woof, I'm coco

person.say.call(dog, 'woof') // woof, I'm coco

let person_dog = person.say.bind(dog, 'woof')
person_dog() // woof, I'm coco

相同之处

将调用此函数的 Fn 的 this 指向传入的第一个参数。

不同之处

  1. call 和 apply 立即执行,bind 返回绑定过的函数,需要手动执行。
  2. call 和 bind 传入参数列表,而 apply 则是要求传入一个参数数组