跳到主要内容

4 篇博文 含有标签「JavaScript」

查看所有标签

· 阅读需 4 分钟
熊滔

思维导图布局实现,如下

简单考虑,只考虑向右展开的布局。

已经条件,知道根节点的位置 (rootX,rootY)(\text{rootX}, \text{rootY}),以及所有节点的大小(宽高),求其他节点的位置 (x,y)(x, y)

首先看子节点的 xx 和父节点 parentX\text{parentX} 的关系,非常简单,子节点在父节点右侧的 marginX\text{marginX} 处,即

childX=parentX+parentWidth+marginX\text{childX} = \text{parentX} + \text{parentWidth} + \text{marginX}

对于子节点的 yy 的计算就比较复杂了,考虑下图

在知道父元素的位置和宽高的情况下以及所有子节点总体高度的情况下,计算所有子节点整体的 yy,在 yy 方向上要满足一个关系,就是父节点和子节点整体是居中对齐的,即满足

parentY+parentHeight2=childrenY+childrenHeight2\text{parentY} + \dfrac{\text{parentHeight}}{2} = \text{childrenY} + \frac{\text{childrenHeight}}{2}

可以得到

childrenY=parentY+parentHeight2childrenHeight2\text{childrenY} = \text{parentY} + \dfrac{\text{parentHeight}}{2} - \frac{\text{childrenHeight}}{2}

在知道总体的一个位置之后,计算单个子节点的位置就很简单了,比如对于第 ii 个子节点,它离顶部的距离就是之前所有节点的实际高度加上所有间距

child[i].childrenTop=index=0i1child[index].actualHeight+(i1)×marginY\text{child[i].childrenTop} = \sum_{\text{index} = 0}^{i - 1}\text{child[index].actualHeight} + (i - 1) \times \text{marginY}

但是这里还需要做一个调整,这里计算的是这个节点占据区域的整体位置,有可能这个节点的高度小于占据的高度,我们还需要将这个节点居中

child.top = child.childrenTop+child.actualHeight - child.height2\text{child.top = child.childrenTop} + \frac{\text{child.actualHeight - child.height}}{2}

上面我们假设 childrenHeight\text{childrenHeight} 是已知的,但实际上是需要计算的,当然很好算,就是所有子节点的占据的高度之和加上所有的间距,注意,这里占据的高度和节点的 height\text{height} 值并不相等,一个节点占据的高度(actualHeight\text{actualHeight})的计算方法是

node.actualHeight=max(node.height,node.childrenHeight))\text{node.actualHeight} = \max(\text{node.height}, \text{node.childrenHeight}))

所有 childrenHeight\text{childrenHeight} 的计算为

node.childrenHeight=inchildren[i].actualHeight+(n1)×marginY\text{node.childrenHeight} = \sum_{i}^{n}\text{children[i].actualHeight} + (n - 1) \times \text{marginY}

需要两次遍历,第一次遍历计算 xxchildrenHeight\text{childrenHeight}actualHeight\text{actualHeight},第二次遍历计算 yy,可以参考以下代码

const MARGIN_X = 36;
const MARGIN_Y = 16;

const dfs = (node: MindNodeElement, {
before,
after,
}, parent: MindNodeElement | null = null, index = 0) => {
before?.(node, parent, index);
node.children.forEach((child, index) => {
this.dfs(child, {
before,
after,
}, node, index);
});
after?.(node, parent, index);
}

const layout = (node: MindNodeElement): MindNodeElement => {
this.dfs(draft, {
before: (node, parent) => {
if (parent) {
node.x = parent.x + parent.width + MARGIN_X;
}
},
after: (node) => {
if (node.children.length > 0) {
node.childrenHeight = node.children.reduce((pre, cur) => {
return pre + cur.actualHeight;
}, 0) + (node.children.length - 1) * MARGIN_Y;
node.actualHeight = Math.max(node.childrenHeight, node.height);
} else {
node.actualHeight = node.height;
node.childrenHeight = 0;
}
}
})

this.dfs(draft, {
before: (node, parent, index) => {
if (!parent) return;

const baseY = parent.y + parent.height / 2 - parent.childrenHeight / 2;
const beforeSiblings = index > 0 ? parent.children.slice(0, index) : [];
const siblingsHeight = beforeSiblings.reduce((pre, cur) => {
return pre + cur.actualHeight;
}, 0) + beforeSiblings.length * MARGIN_Y;

const childrenTop = baseY + siblingsHeight;
node.y = childrenTop + (node.actualHeight - node.height) / 2;
}
})
}

· 阅读需 10 分钟
熊滔

什么是拷贝?对一个变量进行拷贝是指将其表示的数据复制到另一个变量中,大部分情况下的目的是为了避免直接修改原始数据,以便后续进行多种不同的处理或对比。

最简单的拷贝行为,直接将一个变量赋值给另一个变量

let a = 1;
let b = a;
b = 2;
console.log(a, b); // 1, 2
const originalObj = {
a: 1
}
const copyObj = originalObj;
copyObj.a = 2;
console.log(originalObj.a, copyObj.a); // 2, 2

可以看到,这种直接赋值的方式对于基本数据类型和对象是不一样的,对于基本数据类型(number, string, null, undefined, boolean, symbol, BigInt)直接赋值是完全隔离的,对拷贝的修改不会影响原数据,而对于对象(包括数组、日期、函数等特殊对象)直接进行赋值拷贝,拷贝的仅仅是引用,因此对通过拷贝所持有的引用对数据进行的修改也会影响原来的对象。

因此对于对象的拷贝不能通过直接赋值来达到目的,而是需要将引用的数据进行拷贝,根据拷贝方式的不同,分为浅拷贝和深拷贝。

浅拷贝即只复制对象的一层属性,常见的方式有

const originalObj = {
a: 1
}
const copyObj = {};
for (let key in obj1) {
obj2[key] = obj1[key];
}
const originalObj = {
a: 1
}
const copyObj = {
...obj1
}
const originalObj = {
a: 1
}
const copyObj = Object.assign({}, originalObj);

如果对象的属性的值也是一个对象,那么就会存在问题,复制的仍然是一个引用,对该引用的修改同样会影响原对象

const originalObj = {
a: {
b: 1
}
}
const copyObj = Object.assign({}, originalObj);
copyObj.a.b = 2;
console.log(originalObj.a.b, copyObj.a.b); // 2 2

如果希望对拷贝的值的修改完全不影响原对象,这种情况下就需要进行深拷贝了。

信息

大部分情况下不需要使用深拷贝,原因:

  • 浅拷贝够用了
  • 深拷贝性能较低

JSON.parse、JSON.stringify

一种序列化的方式是将对象先序列为字符串,然后根据字符串重新反序列化为对象,该种方式创建的对象是完全开辟一块新的内存空间,和原来的对象没有任何关系:

const originalObj = {
a: {
b: 1
}
}
const copyObj = JSON.parse(JSON.stringify(originalObj));
copyObj.a.b = 2;

console.log(originalObj.a.b, copyObj.a.b); // 1 2

但是该种方式有很多缺点:

  • 只能处理被序列化的对象,对于特殊类型的对象在反序列化时会丢失特定的类型信息

    • Date 会被序列化为字符串

    • RegExpPromise、Map、Set、WeakMap、WeakSet 会被序列化为空对象

    • Function ,DOM 对象会被忽略

    const originalObj = {
    a: 1,
    b: new Date(),
    c: new RegExp('\\d{2}'),
    d: function () {
    return 'hello'
    },
    e: Promise.resolve('world'),
    f: new Map([['a', 1], ['b', 2]]),
    g: new Set([1, 2, 3]),
    h: new WeakMap(),
    i: new WeakSet()
    }
    const copyObj = JSON.parse(JSON.stringify(originalObj));

  • 无法处理循环引用,如果对象中存在循环引用,则 JSON.stringify 会发生错误

    const originalObj = {}
    originalObj.self = originalObj;
    const copyObj = JSON.parse(JSON.stringify(originalObj));

  • undefinedsymbolBigInt 这些值的无法处理,对于值为

    • undefinedsymbol 的属性在序列化会被忽略

      const originalObj = {
      a: 1,
      b: "Hello",
      c: true,
      d: null,
      e: undefined,
      f: Symbol("symbol"),
      }
      const copyObj = JSON.parse(JSON.stringify(originalObj));

    • BigInt 则会报错

      const originalObj = {
      a: 1,
      b: BigInt(123),
      }
      const copyObj = JSON.parse(JSON.stringify(originalObj));

  • NaNInfinity 会被序列化为 null

    const originalObj = {
    a: 1,
    b: NaN,
    c: Infinity,
    }
    const copyObj = JSON.parse(JSON.stringify(originalObj));

structuredClone 和 postMessage

浏览器提供了一个 structuredClone API,它可以对数据进行深拷贝,它会递归的对输入对象进行拷贝,并且在递归的过程中会维护一个存储访问过的对象的表,防止无限递归和解决循环引用的问题。

该 API 较新,可能存在兼容性问题

另外它只能拷贝可序列化的数据,如果存在不可序列化的数据则会报错,下面这些数据是不可序列化的:

  • Symbol

  • Function

  • Promise

  • WeakMap

  • WeakSet

  • DOM 对象

const originalObj = {
a: 1,
b: "Hello",
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: new Date(),
i: /\d{2}/,
j: new Map([['a', 1], ['b', 2]]),
k: new Set([1, 2, 3]),
l: {
a: 1,
}
}
originalObj.self = originalObj;

const copyObj = structuredClone(originalObj);
copyObj.j.set('c', 3);
copyObj.k.add(4);
copyObj.l.a = 2;

也可以使用 MessageChannel 对数据进行拷贝,使用 postMessage 传递数据时,也是深拷贝,其使用的算法和 structuredClone 相同,不同之处在于 MessageChannel 拷贝数据是异步的

const deepCopy = (obj) => {
const channel = new MessageChannel();
return new Promise((resolve) => {
channel.port2.onmessage = (e) => {
resolve(e.data);
}
channel.port1.postMessage(obj);
});
}

手动递归拷贝

除了使用已有的 API,还可以手动递归对象来进行深拷贝。首先实现基本的递归拷贝

const deepCopy = (value) => {
if (typeof value !== 'object' || value === null) {
return value;
}
const newObj = Array.isArray(value) ? [] : {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepCopy(value[key]);
}
}
return newObj;
}
const originalObj = {
a: 1,
b: 'hello',
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: {
a: 1,
},
i: [1, 2, 3]
}

const copyObj = deepCopy(originalObj);
copyObj.h.a = 2;
copyObj.i.push(4);

对于基本的对象和数组拷贝没有问题,但是如果存在循环引用,如果自己引用自己,两个对象之间互相引用等,那么就会陷入无限的递归,直至爆栈

const originalObj = {
a: 1,
b: 'hello',
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: {
a: 1,
},
i: [1, 2, 3]
}
originalObj.self = originalObj;

const copyObj = deepCopy(originalObj);

为了解决这个问题,需要在递归的过程中维护已处理过的对象,当后续访问到相同对象时,直接返回该对象而不用递归处理

const deepCopy = (value, weakMap = new WeakMap()) => {
if (weakMap.has(value)) return weakMap.get(value);
if (typeof value !== 'object' || value === null) {
return value;
}
const newObj = Array.isArray(value) ? [] : {};
weakMap.set(value, newObj);
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepCopy(value[key], weakMap);
}
}
return newObj;
}

接下来需要对一些特殊的对象进行处理,如 FunctionDateMapSet 等,对于不能进行深拷贝的对象当作普通对象处理,如 WeakMapWeakSetPromise

const deepCopy = (value, weakMap = new WeakMap()) => {
if (weakMap.has(value)) return weakMap.get(value);
if (typeof value !== 'object' || value === null) {
return value;
}
let newObj;
if (typeof value === 'function') {
newObj = new Function('return ' + value.toString())();
} else if (Array.isArray(value)) {
newObj = [];
} else if (value instanceof Date) {
newObj = new Date(value);
} else if (value instanceof RegExp) {
const { source, flags } = value;
newObj = new RegExp(source, flags);
// lastIndex 不可遍历
newObj.lastIndex = value.lastIndex;
} else if (value instanceof Map) {
newObj = new Map();
for (const [key, val] of value) {
newObj.set(deepCopy(key, weakMap), deepCopy(val, weakMap));
}
} else if (value instanceof Set) {
newObj = new Set();
for (const val of value) {
newObj.add(deepCopy(val, weakMap));
}
} else {
newObj = {};
}
weakMap.set(value, newObj);
for (const key in value) {
if (value.hasOwnProperty(key)) {
newObj[key] = deepCopy(value[key], weakMap);
}
}
return newObj;
}
const originalObj = {
a: 1,
b: 'hello',
c: true,
d: undefined,
e: null,
f: NaN,
g: Infinity,
h: Symbol("symbol"),
i: BigInt(123),
j: {
a: 1,
},
k: [1, 2, 3],
l: new Date(),
m: /\d{2}/,
n: new Map([['a', 1], ['b', 2]]),
o: new Set([1, 2, 3]),
p: new WeakMap(),
q: new WeakSet(),
r: function () {
return 'Hello'
},
s: Promise.resolve("Hello")
}
originalObj.self = originalObj;

const copyObj = deepCopy(originalObj);
copyObj.j.a = 2;
copyObj.k.push(4);
copyObj.n.set('c', 3);
copyObj.o.add(4);
const value = copyObj.r();
console.log(value); // Hello

参考

· 阅读需 7 分钟
熊滔

在 JavaScript 中有三种方式声明变量:

  • var
  • let
  • const

letconstES6 新引入的变量声明方式,解决 var 声明变量的很多问题,基本上已经不使用 var 来声明变量了。

var

var 声明变量有三个特点:

  • 声明提升

    正常情况下,变量需要先声明才能使用,如果在声明之前使用了还未定义的变量,那么会报错,但是在 JavaScript 中,对于使用 var 声明的变量,可以在声明之前访问,因为 var 声明的变量其作用域会提升到其所在作用域的顶部,不过只有声明会被提升,变量的初始化不会提升,因此在初始化之前访问的值是 undefined,在初始化之后访问才能获取到值

    console.log(a); // undefined,在变量 a 定义之前访问,不会报错,访问到的是 undefined
    var a = 10;
    console.log(a); // 10,在定义之后访问,可以访问到初始的值

    上面的语句相当于

    var a; // 声明提升
    console.log(a); // undefined
    a = 10;
    console.log(a); // 10
  • 无块级作用域

    var 声明的变量的作用域是离其最近的函数主体类静态初始化块,在除此之外的块作用域中,如 ifforwhiletry...catchswitch 等块级作用域中声明的变量可以在其外部访问到

    var a = true;

    if (a) {
    var b = 2;
    }

    console.log(b); // 2,如果 a 是 false,那么这里访问到的就是 undefined
    function func () {
    // 变量 a 的作用域在 func 内,因此在函数外无法访问到 a
    var a = 1;
    }

    func();

    console.log(a); // ReferenceError: a is not defined

    如果不是在函数或类静态初始化块中使用 var 声明变量,分为两种情况:

    • 如果在模块中,那么 var 的作用域就是整个模块

    • 如果是在脚本中,那么就是全局作用域

    如果是全局作用域,声明的变量将作为全局对象的不可配置属性(对于浏览器环境,对应于 windows 属性,对于 Node 环境,就是 global 属性)

    var a = 1;
    console.log(window.a); // 1

    delete window.a; // false,不可配置
  • 重复声明

    在同一个作用域中,var 声明的变量可重复声明,重新声明后值也不会丢失,除非重新进行了初始化

    var a = 1;
    console.log(a); // 1

    var a;
    console.log(a); // 1

    a = 10;
    console.log(a); // 10

    如果 varfunction 在同一作用域声明了同一变量,那么 var 初始化会覆盖 function 的值,因为 function 的声明会被提升到作用域的顶部,随后才是变量的初始化

    var a = 1;

    function a() {
    return 2;
    }

    console.log(a); // 1

let

let 声明的变量与其他语言声明的变量特性一致:

  • 在变量声明之前不能使用,从作用域顶部到变量声明之间的区域称为暂时性死区,在暂时性死区中不能访问变量,否则会报错

    console.log(a); // ReferenceError: Cannot access 'a' before initialization

    let a = 1;
  • 块作用域

    if (true) {
    let a = 1;
    }
    console.log(a) // ReferenceError: a is not defined

    let 声明的变量只在所在的块作用域内生效,作用域之外无法访问,否则会报错

  • 不可重复性声明

    在同一作用域内,不可能声明相同命名的变量

    let a = 1;
    let a = 2; // SyntaxError: Identifier 'a' has already been declared

const

constlet 具有相同的特点,有一个不同的是 let 声明的变量是可变的,可以被重新赋值,而 const 声明的变量是不可变的,不可以被重新赋值

let a = 1;
a = 2;
console.log(a); // 2
const a = 1;
a = 2; // TypeError: Assignment to constant variable.

因为 const 声明的变量不允许重新赋值,因此在声明时就必须初始化

const a; // SyntaxError: Missing initializer in const declaration

const 声明的变量不能被重新赋值,但是不代表值不可以改动

const obj = {
a: 1
}

obj.a = 2;
console.log(obj.a); // 2

变量 obj 指向的内存地址始终没有发生变化,因此上述操作是被允许的。

最佳实践:变量使用 const 定义,当变量需要改变时,在使用 let 定义。

循环中的变量声明

考虑下面的一个循环

const funcs = []
for(var i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}
funcs.forEach(function(func){
func()
})

输出结果为

10
10
10
10
10
10
10
10
10
10

因为变量 i 指向的是全局作用域中的 i,经过十次循环后,i 的值已经变为了 10,所以每次打印的都是 10

如果把 var 声明的变量替换为 letconst,因为每次都会重新声明变量 ifunc 中访问的是闭包捕获的重新声明的 i

const funcs = []
for(var i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}
funcs.forEach(function(func){
func()
})

输出为

0
1
2
3
4
5
6
7
8
9

参考

· 阅读需 28 分钟
熊滔

在字符串的处理中,我们经常要进行字符串的匹配,校验等等操作。比如校验字符串的格式是否符合邮箱,电话号码的格式,校验密码是否符合要求,密码中是否包含数字和字母等等;又或者匹配得到某种规则的字符串。这一些操作如果使用常规的方法进行字符串操作,会花费较大的代价,包括时间和精力。

正则表达式是用来表达字符串的规则,它可以检验字符串是否符合某个特定的规则,或者匹配字符串中符合规则的字符,在一般的使用中,正则表达式一般用来匹配字符串中的字符或者字符串中特定的位置。

正则对象

JavaScript 正则对象的创建有两种常见的方法,一是使用 RegExp 构造函数进行创建,二是使用字面量的方法进行创建,如下

let regex1 = new RegExp('hello', 'g');
let regex2 = /hello/g;

其中 hello 表示字符串的规则,用来匹配字符串中的"hello"g 表示进行全局匹配(global),像这样的标志还有两个,m 表示进行多行匹配(multiline),i 表示忽略大小写(ignoreCase),这三个标志互不冲突,可以同时使用,如

let regex = /hello/igm;