239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值

--------------- -----

[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

参考官方题解,或者是labuladong

自己的解法,用数组模拟,shift空间复杂度太高了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var maxSlidingWindow = function(nums, k) {
const sw = new SliderWindow()
const rs = []
for(let i=0;i<k;i++){
sw.push(i, nums[i])
}
rs.push(sw.max())
for(let i=k,j=0;i<nums.length;i++,j++){
sw.push(i, nums[i])
sw.shift(j, nums[j])
rs.push(sw.max())
}
return rs
};
class SliderWindow{
constructor(){
this.arr = []
}
max(){
return this.arr[0].value
}
shift(index, value){
if(index === this.arr[0].index){
this.arr.shift()
}
}
push(index, value){
while(this.arr.length!==0 && value > this.arr[this.arr.length-1].value){
this.arr.pop()
}
this.arr.push({
index,
value
})
}
}

改为用双向链表模拟单调队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class ListNode{
constructor(index, value, pre, next) {
this.index = index;
this.value = value;
this.pre = pre || null;
this.next = next || null;
}
}
var maxSlidingWindow = function(nums, k) {
const sw = new SliderWindow()
const rs = []
for(let i=0;i<k;i++){
sw.push(i, nums[i])
}
rs.push(sw.max())
for(let i=k,j=0;i<nums.length;i++,j++){
sw.push(i, nums[i])
sw.shift(j)
rs.push(sw.max())
}
return rs
};
class SliderWindow{
constructor(){
this.dummyHead = new ListNode(-1, -1)
this.dummyTail = new ListNode(-1, -1)
this.dummyTail.pre = this.dummyHead
this.dummyHead.next = this.dummyTail
}
max(){
return this.dummyHead.next.value
}
shift(index){
if(index === this.dummyHead.next.index){
this.dummyHead.next = this.dummyHead.next.next
this.dummyHead.next.pre = this.dummyHead
}
}
push(index, value){
while(this.dummyHead.next !== this.dummyTail && value > this.dummyTail.pre.value){
this.dummyTail.pre.pre.next = this.dummyTail
this.dummyTail.pre = this.dummyTail.pre.pre
}
const newListNode = new ListNode(index, value)
this.dummyTail.pre.next = newListNode
newListNode.pre = this.dummyTail.pre
this.dummyTail.pre = newListNode
newListNode.next = this.dummyTail
}
}

改成链表以后,速度快了非常多,思路还是一样的,就是单调队列用双向链表来实现

image-20211109113118826

76. 最小覆盖子串

自己的代码,根据labuladong这个思路书写的代码,倒数第二个测试用例超时了,原因应该是符合条件的函数需要优化吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
var minWindow = function(s, t) {
let min = Number.MAX_SAFE_INTEGER;
let left = right = 0;
let res = ""
const need = t.split("").reduce((pre, cur) => {
pre[cur] ? pre[cur]++ : pre[cur]=1
return pre
}, {})
const sw = new SliderWindow()
while (right < s.length) {
let flag = false
sw.push(s[right++])

while (sw.isAccord(need)) {
sw.shift(s[left++])
flag = true
}
if (flag && right - left + 1 < min) {
min = right - left + 1
res = s.substring(left-1, right)
}
}
return res
};
class SliderWindow{
constructor(){
this.window = {}
}
push(s){
this.window[s] ? this.window[s]++ : this.window[s] = 1
}
shift(s){
this.window[s] === 1 ? delete this.window[s] : this.window[s]--
}
isAccord(need){
for(let [key, value] of Object.entries(need)){
if(!this.window[key] || this.window[key] < need[key]){
return false
}
}
return true
}
}

优化了匹配符合条件的方式,有很多细节,标注在代码里了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var minWindow = function (s, t) {
// 以防出现整个字符串就是答案的情况,其实也可以只把res=s写上
let min = s.length+1;
let left = right = 0;
let res = ""
let count = 0
const need = t.split("").reduce((pre, cur) => {
if (pre[cur]) {
pre[cur]++
} else {
pre[cur] = 1
count++
}
return pre
}, {})
const sw = new SliderWindow(need, count)
while (right < s.length) {
let flag = false
sw.push(s[right++])

while (sw.isAccord()) {
sw.shift(s[left++])
flag = true
}
if (flag && right - left + 1 < min) {
min = right - left + 1
res = s.substring(left-1, right)
}
}
console.log(res);
return res
};
class SliderWindow{
constructor(need, count) {
this.window = {}
this.need = need
this.needNum = count
this.curNum = 0
}
push(s){
this.window[s] ? this.window[s]++ : this.window[s] = 1
// 用===可以在超出条件后不计算
if (this.need[s] && this.need[s] === this.window[s]) {
this.curNum++
}
}
shift(s){
this.window[s]--
// 同上
if (this.need[s] && (this.need[s] - 1 === this.window[s])) {
this.curNum--
}
}
isAccord(){
return this.curNum === this.needNum
}
}

567. 字符串的排列

给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。

换句话说,s1 的排列之一是 s2 的 子串 。

1
2
3
4
5
6
7
8
9
10
11
12
13
示例 1:

输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:

输入:s1= "ab" s2 = "eidboaoo"
输出:false

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutation-in-string
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

这个题目其实和上边的题目差不多,找到符合76题条件(只要包含就够)的字符串,只要该字符串的长度与s1一致,即是符合条件

438. 找到字符串中所有字母异位词

这个题目和567一样,只不过需要返回所有符合条件的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var findAnagrams = function (s, t) {
// 以防出现整个字符串就是答案的情况,其实也可以只把res=s写上
let left = right = 0;
let res = []
let count = 0
const need = t.split("").reduce((pre, cur) => {
if (pre[cur]) {
pre[cur]++
} else {
pre[cur] = 1
count++
}
return pre
}, {})
const sw = new SliderWindow(need, count)
while (right < s.length) {
let flag = false
sw.push(s[right++])

while (sw.isAccord()) {
sw.shift(s[left++])
flag = true
}
if (flag && right - left + 1 === t.length) {
res.push(left-1)
}
}
return res
};
class SliderWindow{
constructor(need, count) {
this.window = {}
this.need = need
this.needNum = count
this.curNum = 0
}
push(s){
this.window[s] ? this.window[s]++ : this.window[s] = 1
// 用===可以在超出条件后不计算
if (this.need[s] && this.need[s] === this.window[s]) {
this.curNum++
}
}
shift(s){
this.window[s]--
// 同上
if (this.need[s] && (this.need[s] - 1 === this.window[s])) {
this.curNum--
}
}
isAccord(){
return this.curNum === this.needNum
}
}

3. 无重复字符的最长子串 labuladong 题解

难度中等6383收藏分享切换为英文接收动态反馈

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

示例 4:

1
2
输入: s = ""
输出: 0

这道题和上边的题,思路是相差最大的,它需要找到第一次不符合条件的,从而寻找最大值

我的代码,时间复杂度和空间复杂度都不低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var lengthOfLongestSubstring = function(s) {
// 处理 s=” “ 返回 1
if(s&&!s.trim()){
return 1
}
let max = 0;
let left = right = 0;
const sw = new SliderWindow()
while (right < s.length) {
let flag = true
sw.push(s[right++])

while (!sw.isAccord()) {
if (flag) {
flag = false
if (right - 1 - left > max) {
max = right - 1 - left
}
}
sw.shift(s[left++])
flag = true
}
// 处理不会出现不符合情况的字符串 即 s=”a“ s="abc"
if (right - left > max) {
max = right - left
}
}
return max
};
class SliderWindow{
constructor() {
this.window = {}
this.count = 0
}
push(s){
this.window[s] ? this.window[s]++ : this.window[s] = 1
if (this.window[s] === 2) {
this.count++
}
}
shift(s){
this.window[s]--
// 同上
if (this.window[s] === 1) {
this.count--
}
}
isAccord(){
return !this.count
}
}

总结

代码主体部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let left = right = 0
while (right < s.length) {
// 队尾加数据
sw.push(s[right])
right++

while (sw need shink) {
// 对头删除数据
sw.shift(s[left])
left++
// 依据题目在合适的位置进行更新
}
// 依据题目在合适的位置进行更新
}

滑动窗口类

依据条件变更isAccord条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SliderWindow{
constructor(need, count) {
this.window = {}
this.need = need
this.needNum = count
this.curNum = 0
}
push(s){
this.window[s] ? this.window[s]++ : this.window[s] = 1
// 用===可以在超出条件后不计算
if (this.need[s] && this.need[s] === this.window[s]) {
this.curNum++
}
}
shift(s){
this.window[s]--
// 同上
if (this.need[s] && (this.need[s] - 1 === this.window[s])) {
this.curNum--
}
}
isAccord(){
return this.curNum === this.needNum
}
}

git 简记,主要参考掘金小册和以前的视频笔记,构建一个简单的记忆结构

add

  • 提交所有被删除和修改的文件到数据暂存区
1
git add -u 或 git add –update
  • 提交所有修改的和新建的到数据暂存区
1
git add .
  • 提交所有被删除、被替换、被修改和新增的文件到数据暂存区
1
git add -A 或 git add –all

撤销 add

1
2
3
4
5
6

# 恢复暂存区的所有文件到工作区 => vscode git工具中的 取消暂存所有更改

$ git reset HEAD 如果后面什么都不跟的话 就是上一次add 里面的全部撤销了 相当于git restore -staged .

$ git reset HEAD XXX/XXX/XXX.php 就是对某个php文件进行撤销了

commit

将暂存区内容添加到仓库中

1
git commit -m '一些描述'

再简单提一些常见场景, 比如说commit完之后,突然发现一些错别字需要修改,又不想为改几个错别字而新开一个commithistory区,那么就可以使用下面这个命令:

1
git commit --amend

这样就是把错别字的修改和之前的那个commit中的修改合并 并覆盖上次的commit。

撤销 修改 commit

丢弃上次的提交

1
git reset HEAD^

说明:在 Git 中,有两个「偏移符号」: ^~

^ 的用法:在 commit 的后面加一个或多个 ^ 号,可以把 commit 往回偏移,偏移的数量是 ^ 的数量。例如:master^ 表示 master 指向的 commit 之前的那个 commitHEAD^^ 表示 HEAD 所指向的 commit 往前数两个 commit

~ 的用法:在 commit 的后面加上 ~ 号和一个数,可以把 commit 往回偏移,偏移的数量是 ~ 号后面的数。例如:HEAD~5 表示 HEAD 指向的 commit往前数 5 个 commit

如果已经推送到远端

  • 1 确定无风险可以执行,git push -f 可以强制推送,无视冲突,远端的那次commit也会消失

  • 2 安全操作,你希望撤销哪个 commit,就把它填在后面:

    1
    git revert HEAD^

    上面这行代码就会增加一条新的 commit,它的内容和倒数第二个 commit 是相反的,从而和倒数第二个 commit 相互抵消,达到撤销的效果。

    revert 完成之后,把新的 commitpush 上去,这个 commit 的内容就被撤销了。它和前面所介绍的撤销方式相比,最主要的区别是,这次改动只是被「反转」了,并没有在历史中消失掉,你的历史中会存在两条 commit :一个原始 commit ,一个对它的反转 commit

    1
    2
    3
    git revert HEAD                撤销最新一次 commit
    git revert HEAD^ 撤销前一次 commit
    git revert commitid 撤销指定commitid的commit
  • 撤销commit,同时将代码恢复到对应ID的版本(中间的commit都没了)

1
git reset --hard commitId

reset后接参数

  1. –hard:重置位置的同时,清空工作目录和暂存区的所有内容;
  2. –soft:重置位置的同时,保留工作目录和暂存区的内容,并把重置 HEAD 的位置所导致的新的文件差异放进暂存区。
  3. –mixed(默认):重置位置的同时,保留工作目录的内容,把暂存区和重置 HEAD 的位置所导致的新的文件差异都放入工作目录

修改前几次的commit 不新增commit

详见#rebase章节

restore 撤销

git restore --staged <file>... 撤销add,将暂存区的文件从暂存区撤出,但不会更改文件的内容

git restore <file>... 撤销工作区改动,已经在暂存区的改动不会撤销 相当于 git checkout -- filepathname

push

实质上,push 做的事是:把当前 branch 的位置(即它指向哪个 commit)上传到远端仓库,并把它的路径上的 commits 一并上传

也就是每次仅上传当前的分支上的commit

分支

创建一个分支

1
git branch feature1

切换到分支

1
git checkout feature1

切换分支 如果不存在就创建一个分支

1
git checkout -b feature1

删除分支

1
git branch -d feature1 

没有合并到master,删除会失败,强行删除 -d 换成 -D

查看分支

1
2
git branch 列出本地已经存在的分支,并且当前分支会用*标记
git branch -r 查看远程版本库的分支列表

推送远端不存在的分支

1
2
3
4
git checkout feature1
去推送本地分支到远程分支 的同时,为他俩建立联系(默认联系)
以后push这个分支 默认就推送到现在建立联系的远端分支上
git push --set-upstream origin feature1

在 Git 中(2.0 及它之后的版本),默认情况下,你用不加参数的 git push 只能上传那些之前从远端 clone 下来或者 pull 下来的分支,而如果需要 push 你本地的自己创建的分支,则需要手动指定目标仓库和目标分支(并且目标分支的名称必须和本地分支完全相同),就像上面这样

删除远端仓库的分支

1
git push origin -d feature1 # 用 -d 参数把远程仓库的 branch 也删了

找回被删除的分支

1
git reflog 查看删除分支前的commit 

img

从图中可以看出,HEAD 的最后一次移动行为是「从 branch1 移动到 master」。而在这之后,branch1 就被删除了。所以它之前的那个 commit 就是 branch1 被删除之前的位置了,也就是第二行的 c08de9a

所以现在就可以切换回 c08de9a,然后重新创建 branch1

1
2
git checkout c08de9a
git checkout -b branch1

这样,你刚删除的 branch1 就找回来了。

合并分支

两种方式,一种利用merge,一种利用rebase

1
git merge branch

img

1
2
3
4
5
6
1 git checkout branch1
2 git rebase master
在 rebase 之后,记得切回 master 再 merge 一下,把 master 移到最新的 commit
3 git checkout master
4 git merge(rebase) branch1

img

img

merge

merge 的意思是「合并」,它做的事也是合并:指定一个 commit,把它合并到当前的 commit 来。具体来讲,merge 做的事是:

从目标 commit 和当前 commit (即 HEAD 所指向的 commit)分叉的位置起,把目标 commit 的路径上的所有 commit 的内容一并应用到当前 commit,然后自动生成一个新的 commit

merge有时产生冲突:

  • git pull时,远端仓库包含本地没有的 commit,而且本地仓库也包含远端没有的 commit
  • git pull = git fetch + git merge origin/HEAD
  • git merge 合并的时候,文件有冲突

处于冲突解决状态时

解决冲突,解决完冲突后进行commit时,会自动填入commit信息”这是一个merge信息”

放弃解决 git merge --abort输入这行代码,你的 Git 仓库就会回到 merge 前的状态

checkout

git checkout命令用于切换分支或恢复工作树文件,也可以指定HEAD指针的位置。

checkout 本质上的功能其实是:签出( checkout )指定的 commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此命令用来放弃掉所有还没有加入到缓存区(就是 git add 命令)的修改

# 放弃单个文件修改,注意不要忘记中间的"--",不写就成了检出分支了!
git checkout -- filepathname

# 放弃所有的文件修改
git checkout .

# 切换到分支

git checkout feature1

# 切换分支 如果不存在就创建一个分支

git checkout -b feature1

# 签出commit

首先介绍一下HEAD,可以把HEAD理解成一个指针,HEAD指针通常会指向一个分支,如下图所示

e'm

HEAD–>master–>commitID,通常情况下,HEAD会一直跟随着当前分支,并指向分支,而分支则指向当前最新的commit

还有一种 detached HEAD的状态,使用git checkout C3即可以把HEAD指针指向C3

经过我的测试,在工作区和暂存区有改动的情况下,无法进行git checkout commitID or branch

image-20220106112249119

还有一种相对引用的方式,git checkout main^*3 或者 git checkout main^^^ 或者 git checkout main~3 或者使用 git checkout HEAD^^^,这几种方法都是向父节点方向移动

image-20210427144448741

如果此时进行commit,git的返回信息会提示我们,You are in ‘detached HEAD’ state.(你现在处于’分离头’状态)。然后会从C3节点出现一个新的节点,HEAD会指向新的节点,可以简单的理解为匿名分支(图和上边不一样,仅做示意)

image-20210427145825615

我们现在有两个选择,如下:

  • 丢弃这个匿名分支

    • 直接检出到任何一个别的分支,就相当于放弃了这些提交
  • 保留这个匿名分支

    • 创建一个名为newtest的分支来保存这些提交
      • git branch newtest c7 这种方式HEAD并未指向newtest,而是指向了C7,仍是指针分离状态
      • git checkout -b newtest HEAD指向了newtest,newtest指向了C7
  • 与现存分支合并,参考分支命令章节

stash

  • git stash save "save message" : 执行存储时,添加备注,方便查找,只有git stash 也要可以的,但查找时不方便识别。

    • 增加参数可以存储没有追踪的文件 git stash save -u === git stash save --include-untracked
  • git stash list :查看stash了哪些存储

  • git stash show :显示做了哪些改动,默认show第一个存储,如果要显示其他存贮,后面加stash@{$num},比如第二个 git stash show stash@{1}

  • git stash show -p : 显示第一个存储的改动,如果想显示其他存存储,命令:git stash show stash@{$num} -p ,比如第二个:git stash show stash@{1} -p

  • git stash apply:应用某个存储,但不会把存储从存储列表中删除,默认使用第一个存储,即stash@{0},如果要使用其他个,git s tash apply stash@{$num} , 比如第二个:git stash apply stash@{1}

  • git stash pop :命令恢复之前缓存的工作目录,将缓存堆栈中的对应stash删除,并将对应修改应用到当前的工作目录下,默认为第一个stash,即stash@{0},如果要应用并删除其他stash,命令:git stash pop stash@{$num} ,比如应用并删除第二个:git stash pop stash@{1}

  • git stash drop stash@{$num}:丢弃stash@{$num}存储,从列表中删除这个存储192.168.1.110

  • git stash clear:删除所有缓存的stash

查看改动

查看改动内容的方法,大致有这么几类:

  1. 查看历史中的多个commit:log

    1. 查看详细改动: git log -p
    2. 查看大致改动:git log --stat
  2. 查看具体某个commit:show

    1. 要看最新 commit ,直接输入 git show ;要看指定 commit ,输入 git show commit的引用或SHA-1
    2. 如果还要指定文件,在 git show 的最后加上文件名
  3. 查看未提交的内容:diff

    1. 查看暂存区和上一条 commit 的区别:git diff --staged(或 --cached)也就是暂存区 tobe commit的内容

    2. 查看工作目录和暂存区的区别:git diff 不加选项参数

    3. 查看工作目录和上一条 commit 的区别:git diff HEAD(commitID) vscode默认展示的,也就是工作目录和暂存区所有的改动相较于最新commit的区别

git reflog 查看本地所有的所有操作记录(包括分支、包括已经被删除的 commit 记录和 reset 的操作),适合找回

rebase

git rebase 可以理解为嫁接、编辑(包括删除)commit链条,可以完成很多功能,包括但不限于合并分支、编辑任意数量commit节点、删除任意数量的commit节点、插入任意数量的节点

git rebase commitID || branch

没有commit分叉点的话,除了快速移动外应该没有作用

见合并分支一节,如果是commitID的话,效果如下图所示

img

如果在这里执行:

1
git rebase 第3个commit

那么 Git 会自动选取 35 的历史交叉点 2 作为 rebase 的起点,依次将 45 重新提交到 3 的路径上去。

git rebase -i

git rebase -i HEAD^^

可以编辑( 倒数第三个commit, HEAD ] 之间的commit ,按下 i 进行编辑模式,操作命令写的很清楚,一般会用到edit和drop

如果使用了edit,会是HEAD进入游离状态,并指向edit对应的commit

这个时候可以进行各种操作,比如修改工作区并新增一个commit或者直接使用 git commit -amend 来修改这条commit message

操作完成后 使用 git rebase --continue 结束回归正常

也可以放弃 rebase git rebase --abort

建议熟练后在进行骚操作

image-20220106215219131

Git rebase –onto

1
git rebase --onto 目标commit 起点commit(不包括起点) 终点commit

--onto 参数后面有三个附加参数:目标 commit、起点 commit(注意:rebase 的时候会把起点排除在外)、终点 commit

1
git rebase --onto 第3个commit 第4个commit branch1

img

同样的,你也可以用 rebase --onto 来撤销提交:

1
git rebase --onto HEAD^^ HEAD^ branch1

上面这行代码的意思是:以倒数第二个 commit 为起点(起点不包含在 rebase 序列里哟),branch1 为终点,rebase 到倒数第三个 commit 上。

也就是这样:

img

reset

实质上,reset 这个指令虽然可以用来撤销 commit ,但它的实质行为并不是撤销,而是移动 HEAD ,并且「捎带」上 HEAD 所指向的 branch(如果有的话)。

reset后接参数

  1. –hard:重置位置的同时,清空工作目录和暂存区的所有内容;
  2. –soft:重置位置的同时,保留工作目录和暂存区的内容,并把重置 HEAD 的位置所导致的新的文件差异放进暂存区。
  3. –mixed(默认):重置位置的同时,保留工作目录的内容,把暂存区和重置 HEAD 的位置所导致的新的文件差异都放入工作目录
1
git reset --hard C3

Git 的历史只能往回看,不能向未来看,所以把 HEADbranch 往回移动,就能起到撤回 commit 的效果。

在relog里可以回溯哦,前提在git没回收commit节点之前

img

所以同理,reset --hard 不仅可以撤销提交,还可以用来把 HEADbranch 移动到其他的任何地方。

1
git reset --hard branch2

img

tag

git打tag

通常在发布软件的时候打一个tag,tag会记录版本的commit号,方便后期回溯。

列出已有的tag

1
git tag

加上-l命令可以使用通配符来过滤tag

img

新建tag

使用git tag命令跟上tag名字,直接创建一个tag。

1
git tag v1.0

上面创建一个名为v1.0的tag。使用git tag命令可以看到新增加的tag。

还可以加上-a参数来创建一个带备注的tag,备注信息由-m指定。如果你未传入-m则创建过程系统会自动为你打开编辑器让你填写备注信息。

1
git tag -a tagName -m "my tag"

查看tag详细信息

git show命令可以查看tag的详细信息,包括commit号等。

1
git show tagName

image-20220107114423786

tag最重要的是有git commit号,后期我们可以根据这个commit号来回溯代码。

给指定的某个commit号加tag

打tag不必要在head之上,也可在之前的版本上打,这需要你知道某个提交对象的校验和(通过git log获取,取校验和的前几位数字即可)。

1
git tag -a v1.2 9fceb02 -m "my tag"

将tag同步到远程服务器

同提交代码后,使用git push来推送到远程服务器一样,tag也需要进行推送才能到远端服务器。

推送单个分支
1
git push origin v1.0
推送本地所有tag
1
git push origin --tags

切换到某个tag

1
git checkout tagName

跟分支一样,可以直接切换到某个tag去。这个时候不位于任何分支,处于游离状态,可以考虑基于这个tag创建一个分支。

删除某个tag

  • 本地删除
1
git tag -d v0.1.2 
  • 远端删除
    git push origin :refs/tags/
1
git push origin :refs/tags/v0.1.2

cherry-pick

详见阮一峰

git cherry-pick命令的作用,就是将指定的提交(commit)应用于其他分支。

会将指定的提交commitHash,应用于当前分支。这会在当前分支产生一个新的提交,当然它们的哈希值会不一样

举例来说,代码仓库有masterfeature两个分支。

1
2
3
a - b - c - d   Master
\
e - f - g Feature

现在将提交f应用到master分支。

1
2
3
4
5
# 切换到 master 分支
$ git checkout master

# Cherry pick 操作
$ git cherry-pick f

上面的操作完成以后,代码库就变成了下面的样子。

1
2
3
a - b - c - d - f   Master
\
e - f - g Feature

从上面可以看到,master分支的末尾增加了一个提交f

git cherry-pick命令的参数,不一定是提交的哈希值,分支名也是可以的,表示转移该分支的最新提交。

1
git cherry-pick feature

上面代码表示将feature分支的最近一次提交,转移到当前分支。

Cherry pick 支持一次转移多个提交。

1
git cherry-pick <HashA> <HashB>

上面的命令将 A 和 B 两个提交应用到当前分支。这会在当前分支生成两个对应的新提交。

如果想要转移一系列的连续提交,可以使用下面的简便语法。

1
git cherry-pick A..B 

上面的命令可以转移从 A 到 B 的所有提交。它们必须按照正确的顺序放置:提交 A 必须早于提交 B,否则命令将失败,但不会报错。

一定要注意不包括节点 A

git config

查看配置 - git config -l

使用git config -l 可以查看现在的git环境详细配置

查看不同级别的配置文件:

1
2
3
4
5
6
7
8
#查看系统config
git config --system --list
  
#查看当前用户(global)配置
git config --global --list

#查看当前仓库配置信息
git config --local --list

设置用户名与邮箱(用户标识,必要)

当你安装Git后首先要做的事情是设置你的用户名称和e-mail地址。这是非常重要的,因为每次Git提交都会使用该信息。它被永远的嵌入到了你的提交中:

1
2
git config --global user.name "mazhihong"  #名称
git config --global user.email "xxxx@qq.com" #邮箱

添加配置项

1
2
3
4
git config [--local|--global|--system]  section.key value
[--local|--global|--system] #可选的,对应本地,全局,系统不同级别的设置
section.key #区域下的键
value #对应的值

–local 项目级
–global 当前用户级
–system 系统级

删除配置项

1
git config [--local|--global|--system] --unset section.key

tips

checkout 和 reset 的不同

checkoutreset 都可以切换 HEAD 的位置,它们除了有许多细节的差异外,最大的区别在于:reset 在移动 HEAD 时会带着它所指向的 branch 一起移动,而 checkout 不会。当你用 checkout 指向其他地方的时候,HEAD 和 它所指向的 branch 就自动脱离了。

事实上,checkout 有一个专门用来只让 HEADbranch 脱离而不移动 HEAD 的用法:

1
git checkout --detach

执行这行代码,Git 就会把 HEADbranch 脱离,直接指向当前 commit

git checkout --detach

1 有效解决setTimeout跨级传参数

1
2
功能:修改 window.setTimeout,使之可以传递参数和对象参数 
使用方法: setTimeout(回调函数,时间,参数1,...,参数n)

2 单击和双击的冲突问题

在一个对象同时绑定单击和双击事件时,当双击该对象时,事件发生顺序为 单击-单击-双击。
解决该问题可以通过定时器解决

1
2
3
4
5
6
7
8
9
10
11
var TimeFn = null;
$('#box').click(function () {
clearTimeout(TimeFn);
TimeFn = setTimeout(function(){
console.log('click')
},100);
});
$('#box').dblclick(function () {
clearTimeout(TimeFn);
console.log('dbclick');
})

当定时器延迟启动的时间小于300毫秒,第一个单击事件无法被抹消,发生事件为 单击-双击
(第二个单击事件和双击事件触发的时间隔间几乎为0,延迟时间对它没有影响)
当定时器延迟启动的事件大于300毫秒,第一个单击事件可以被抹消,发生事件为 双击

3 jQuery 事件传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script src="js/jquery-2.1.1.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
function Ceshi(){
this.a=1;
this.b=2;
this.c=3;
this.d=4;
}
Ceshi.prototype.aa=function(){
var _this=this;
// $('input').on('click',{e:_this},this.bb) 用此方法可以拿到
$('input').on('click',this.bb).bind(this)
}
Ceshi.prototype.bb=function(evt){
console.log(this.b)// undefined
// console.log(_this.b) // _this is not defined
// console.log(evt.data.e.b) //用此方法可以拿到
// console.log(evt.clientX)
}
var demo=new Ceshi()
demo.aa()
</script>

4 ajax 同步异步

ajax异步请求,不会按上下顺序执行,注意数组操作,以防错位,可以使用同步

5 利用元素记录信息

利用JSON.stringfiy 可以将复杂json数据存入元素中,利用JSON.parse可以拿出

6 窗口大小改变事件

window.onresize以最后一个函数为准, $(window).resize(function(){}),可以同时存在多个

7 判断一个对象是否存在的方法

1
2
3
if (typeof myObj == "undefined") {
 var myObj = { };
}

这是目前使用最广泛的判断javascript对象是否存在的方法。

8 深拷贝函数,可以复制对象中的函数及数组(很全面)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if(! p.hasOwnProperty(i)){
continue;
}
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}

Parent = {name: 'foo', birthPlaces: ['北京','上海','香港']}
var Child = deepCopy(Parent);

9 ‘undefined’ 和 undefined 使用区别

1
2
3
4
5
6
7
8
9
var obj = {
a:1,
}
console.log(obj.b == 'undefined') // false
console.log(obj.b == undefined) // true 判断元素属性是否存在的常用方法
console.log(typeof obj.b =='undefined') //true
console.log(typeof ob) // 'undefined'
console.log(typeof ob == 'undefined') // true 此为最常用的判断元素是否存在的方法
console.log(typeof ob == undefined) // false

10 padding-bottom

占位防止图片加载过程中的抖动 % 定义基于父元素宽度的百分比下内边距

11 siblings() 兄弟元素 且包括它本身

12 撑开父元素宽度超过父元素外层宽度的方法

父元素 white-space: nowrap; 内部元素 display: inline-block;

13 pointer-events

更像是JavaScript,它能够:

  • 阻止用户的点击动作产生任何效果
  • 阻止缺省鼠标指针的显示
  • 阻止CSS里的 hover 和 active 状态的变化触发事件
  • 阻止JavaScript点击动作触发的事件
    pointer-events: none 顾名思义,就是鼠标事件拜拜的意思。元素应用了该 CSS 属性,链接啊,点击啊什么的都变成了 “浮云牌酱油”。pointer-events: none 的作用是让元素实体 “虚化”。例如一个应用 pointer-events: none 的按钮元素,则我们在页面上看到的这个按钮,只是一个虚幻的影子而已,您可以理解为海市蜃楼,幽灵的躯体。当我们用手触碰它的时候可以轻易地没有任何感觉地从中穿过去。

14 switch需要注意的地方,没有break的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function handle(num){
switch (num){
case 1:
console.log(1)
case 2:
console.log(2)
case 3:
console.log(3)
case 4:
console.log(4)
case 5:
console.log(5)
case 6:
console.log(6)
default:
break;
}
}
handle(3) // 3 4 5 6

没有break的情况下,从满足条件开始一直会向下执行,无视case,直至break停止

15 设置position absolute元素的宽高百分比时,并不是依据父元素的,而是依据定位参考的元素

16 js中如何判断属性是对象实例中的属性还是原型中的属性

1
2
3
function hasPrototypeProperty(obj, name) {
return !obj.hasOwnProperty(name) && (name in obj);
}

当属性存在对象实例上的时候,函数返回false,表示该属性不是存在原型上,当属性存在原型上的时候,函数返回true。

17 vue中 dispath(支持promise) 和 commit

1
2
3
4
5
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: '/' }); //登录成功之后重定向到首页
}).catch(err => {
this.$message.error(err); //登录失败提示错误
});
1
2
3
4
5
6
7
8
9
10
11
12
13
LoginByUsername({ commit }, userInfo) {
const username = userInfo.username.trim()
return new Promise((resolve, reject) => {
loginByUsername(username, userInfo.password).then(response => {
const data = response.data
Cookies.set('Token', response.data.token) //登录成功后将token存储在cookie之中
commit('SET_TOKEN', data.token)
resolve()
}).catch(error => {
reject(error)
});
});
}

18 变量提升也有优先级, 函数声明 > arguments > 变量声明

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(c);
function c(a) {
console.log(a);
var a = 3;
}
//function c(){...}
//案例2
console.log(c);
var c = function (a) {
console.log(a);
var a = 3;
}
//undefined

重复声明会被忽略

1
2
3
4
foo(); // 1
var foo;
function foo() { console.log( 1 ); }
foo = function() { console.log( 2 ); };

注意,var foo 尽管出现在 function foo()… 的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。

看一下你所不知道的JavaScript上 40页

19 npx讲解

npx 会帮你执行依赖包里的二进制文件
举个例子:

1
2
3
npm i webpack -D      //非全局安装
//如果要执行 webpack 的命令
./node_modules/.bin/webpack -v

有了 npx 之后

1
2
npm i webpack -D    //非全局安装
npx webpack -v

npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去 PATH 里找。如果依然找不到,就会帮你安装。

使用package.json中的script命令,也会优先使用依赖包里的二进制文件

20 引用图片,两种方式都可以

1
2
const url = require('./bg.jpg')
import url from './bg.jpg'

21 ES6类和TS类的区别记忆

ES6类的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
name = 'aaa' //可以用此方式定义实例属性初始值
constructor(name){
this.name = name //实例属性
}
say(){ //原型方法
return this.name
}
//不需要符号间隔
eat(){
return 'food'
}
//静态方法,不会被实例继承,只能通过Person.destroyed()调用,可以被子类继承
static destroyed() {

}
//类的内部所有定义的方法,与ES5不同,都是不可枚举的(non-enumerable)
}
//仅可以用下述方式添加原型属性
Person.prototype.prop = 'prop'

TS类的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 传统写法
class Person {
public name: string; //实例属性
constructor(name: string) {
this.name = name;
}
say(){ //原型方法
return this.name
}
}
// 简化写法
class Person {
constructor(public name: string) { //实例属性

}
say(){ //原型方法
return this.name
}
}

TS类 有不同的访问类型

  • private, protected, public 访问类型
    • public 完全开放使用
    • private 仅允许在类内被使用
    • protected 仅允许在类内及继承的子类中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
public name: string; //不写constructor 实例属性name压根不存在
constructor(name: string) {
this.name = name;
}
protected sayHi() {
this.name;
console.log('hi');
}
private sayABC() {
this.name;
}
}

ES6类的继承

详细书写在分类继承下

22 TS转义代码中发现的,的骚操作

原代码较为复杂,用下方简化代码示意

1
2
3
4
5
6
7
8
function handle(a, b){
console.log(a+b);
}
var d = 4
var c = ((handle(1, 2)), d)
console.log(c);
//3
//4

程序会先执行逗号之前的语句,然后把逗号之后的语句赋值
必须要加外层的括号

error类无法直观的打印,可以借用下列方法

1
2
3
for (const key of Object.keys(error)) {
console.log(error[key]);
}

vue中事件绑定的this指向

下面是从vue单文件中methods方法中截取的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addPointMove() {
this.$refs["preview-img"].addEventListener("mousedown", this.mouseDownHandle);
document.addEventListener("mouseup", this.mouseUpHandle);
this.$refs["preview-img"].onclick = function () {
console.log(this);
};
this.$refs["preview-img"].addEventListener("click", this.ceshi1);
this.$refs["preview-img"].addEventListener("click", this.ceshi2);
this.$refs["preview-img"].addEventListener("click", this.ceshi3);
},
ceshi1() {
console.log(this);
},
ceshi2: function () {
console.log(this);
},
ceshi3: () => {
console.log(this);
},

当点击click后,控制台输出如下

1
2
3
4
1 this.$refs["preview-img"]
2 vue实例
3 vue实例
4 undefined

async的错误捕捉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function main() {
try {
const val1 = await 1;
const val2 = await 2;
await Promise.reject(new Error("222"))
const val3 = await 3;
console.log('Final: ', val3);
}
catch (err) {
console.error(err);
}
console.log(111);
}
main()

运行至reject,try模块内的代码就停止了,然后运行catch代码块,之后正常运行
Error: 222
111

require 一个图片资源出现的问题

我在项目中书写一个头部组件的时候,需要动态引入背景图片

1
backgroundImage: "url(" + require("../assets/rwkb.png") + ")"

写法是对的,但是死活出不来,我真是百思不得其解、、、后来发现打印出来的资源
资源


然后我抱着试一试的态度多写了一个.default竟然成功了

1
backgroundImage: "url(" + require("../../assets/img/rwkb.png").default + ")"

后来我在另一个项目里打印

1
console.log(require("@/assets/img/401.png"));

控制台输出 /img/401.abca6a5b.png

element ui form表单动态验证

资源
资源
切换某一个选项后,会动态改变表单及需要校验的表单
项目中采用v-if v-else方法改变表单,但是form rules验证会失效,解决方法:给元素添加唯一KEY

产生bug的原因在于diff算法的复用

vue 响应式的注意点

1
2
3
4
5
6
7
8
9
{
data(){
return {
obj1: {
a: 1
}
}
}
}
  • 没有任何方法能在初始化后添加根级别的响应式

    • 结合上方代码即无法添加obj1以外的响应式
  • 向存在的嵌套对象添加新的响应式 property,比如obj1,有下面两种方式

    1. Vue.set(object, propertyName, value)

    2. this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

      注意 Object.assign 第一个参数是 { }

  • Vue.delete( target, propertyName/index )

    • 不能删除根对象 例如obj1 可以删除obj1.xx

element的table组件在flex布局下宽度自适应解决办法

给宽度一个99% 可以很简单的搞定

1
2
3
4
5
6
7
8
<tp-el-table-ex
class="table"
ref="clickTable"
border
highlight-current-row
:loading="tableLoading"
:data="tableData"
style="width: 99%;"

input事件和change事件的区别

  • input事件:

    input事件在输入框输入的时候回实时响应并触发

  • change事件:

    change事件在input失去焦点才会考虑触发,它的缺点是无法实时响应。与blur事件有着相似的功能,但与blur事件不同的是,change事件在输入框的值未改变时并不会触发,当输入框的值和上一次的值不同,并且输入框失去焦点,就会触发change事件。

GC 垃圾回收机制

  • 引用计数 无法解决循环引用 基本上被淘汰了
  • 标记清除 mark and sweep 也存在一定的局限(对象如果有一个属性经常使用,其余属性就无法被回收)

void 0

  • viod 任何东西都会返回 undefined,之所以使用0,是因为void 0 是表达式中最短的
  • undefined可以被重写,所以用 void 0 比较保险
  • 不少 JavaScript 压缩工具在压缩过程中,正是将 undefined 用 void 0 代替掉了

URLSearchParams Object.fromEntries

1
2
3
Object.fromEntries(new URLSearchParams(‘foo=bar&baz=qux’))
// { foo: “bar”, baz: “qux” }

node.contains(otherNode)

node 是否包含otherNode节点, otherNode 是否是node的后代节点
如果 otherNode 是 node 的后代节点或是 node 节点本身.则返回true , 否则返回 false.

getComputedStyle 和 style的区别

用法
let style = window.getComputedStyle(element, [pseudoElt]);

  • element 用于获取计算样式的Element。

  • pseudoElt 可选
    指定一个要匹配的伪元素的字符串,例如’::after’,必须对普通元素省略(或null)。

  • 只读与可写
    getComputedStyle方法是只读的,只能获取样式,不能设置;而element.style能读能写,能屈能伸。

  • 获取的对象范围
    getComputedStyle方法获取的是最终应用在元素上的所有CSS属性对象(即使没有CSS代码,也会把默认的祖宗八代都显示出来);而element.style只能获取元素style属性中的CSS样式。因此对于一个光秃秃的元素

    ,getComputedStyle方法返回对象中length属性值(如果有)就是190+(据我测试FF:192, IE9:195, Chrome:253, 不同环境结果可能有差异), 而element.style就是0。

  • 作用
    getComputedStyle方法有一个很重要的,类似css()方法没有的功能——获取伪类元素样式

  • 兼容性
    getComputedStyle方法IE6~8是不支持的

Math.floor 的另类写法

Math.floor((touch.y2 - touch.y1) / ANCHOR_HEIGHT) === Math.floor((touch.y2 - touch.y1) | 0

取一段区间值的小技巧

如果需要参数范围控制在[min,max]之间,传统方法可能需要三个控制流,借助math函数可以这么写

1
arg = Math.max(min, Math.min(arg, max))

即可以arg控制在[min, max] 范围内

filter 和 backdrop-filter

  • backdrop-filter CSS 属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。 因为它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。
  • filter CSS属性将模糊或颜色偏移等图形效果应用于元素。滤镜通常用于调整图像,背景和边框的渲染。
    详情

需要小范围闭环取值

可以借助取余函数,例如type只有三种值 0 1 2

1
2
3
4
5
6
7
8
let type = 0 
function changeType(){
type = (type + 1) % 3
}
for(var i = 0;i<=100;i++){
changeType()
console.log(type)
}

结果 0、1、2、0、1、2、0、1、2………….
适合闭环取值

console.log 打印彩色

1
2
3
4
5
6
var message = "Hello word";
var color = "red";

console.log("%c " + message , "color:" + color);
console.log("%c " + message , "color:" + "blue");
console.log("%c " + message , "color:" + "yellow");

element 动态rules

form默认会在rules改变后,重新校验,有字段可以设置。需要注意数据变化和rules变化的先后顺序问题,会带来一些不可预见的bug,form有去除校验结果的办法

arguments 和对应参数的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function foo(name, age, sex, hobbit) {

console.log(name, arguments[0]); // name name

// 改变形参
name = 'new name';

console.log(name, arguments[0]); // new name new name

// 改变arguments
arguments[1] = 'new age';

console.log(age, arguments[1]); // new age new age

// 测试未传入的是否会绑定
console.log(sex); // undefined

sex = 'new sex';

console.log(sex, arguments[2]); // new sex undefined

arguments[3] = 'new hobbit';

console.log(hobbit, arguments[3]); // undefined new hobbit

}

foo('name', 'age')

传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享

除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。

JSON.stringify 有第二个参数 replacer

它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function replacer(key, value) {
if (typeof value === "string") {
return undefined;
}
return value;
}

var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);

console.log(jsonString)
// {"week":45,"month":7}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
console.log(JSON.stringify(foo, ['week', 'month']));
// {"week":45,"month":7}

如果一个被序列化的对象拥有 toJSON 方法,那么该 toJSON 方法就会覆盖该对象默认的序列化行为:不是那个对象被序列化,而是调用 toJSON 方法后的返回值会被序列化,例如:

1
2
3
4
5
6
7
8
var obj = {
foo: 'foo',
toJSON: function () {
return 'bar';
}
};
JSON.stringify(obj); // '"bar"'
JSON.stringify({x: obj}); // '{"x":"bar"}'

inject provide 响应式

provide使用函数可以实现响应式,看起来不像是能响应的样子。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 父
data() {
return {
ceshiObj1: {
a: 1
},
}
},
provide () {
return {
getInfo: () => this.ceshiObj1
}
},
methods: {
testInject() {
this.ceshiObj1.a++
},
}
// 子
computed: {
test() {
return this.getInfo().a
}
}

defer和async

js的执行都会阻塞html parsing

wfL82.png

bind函数的参数

直接看代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
function resolve(res) {
console.log(res);
}

function bindHandle(...rest) {
console.log(rest);
}

const after = bindHandle.bind(null, resolve, "promise")

after("res","res2")

[ [Function: resolve], 'promise', 'res', 'res2' ]

进程和线程

启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程

  • 一个进程就是一个程序的运行实例
  • 线程是不能单独存在的,它是由进程来启动和管理的
  • 线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率

image-20210712222146731

总结来说,进程和线程之间的关系有以下 4 个特点。

  • 1. 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
  • 2. 线程之间共享进程中的数据。
  • 3. 当一个进程关闭之后,操作系统会回收进程所占用的内存(包括泄漏的内存)。
  • 4. 进程之间的内容相互隔离。(通信依靠IPC进程间通信)

网络协议

常见网页协议

  • TCP/IP 是互联网相关的各类协议族的总称
  • IP(Internet Protocol)网际协议(网络层协议)IP 协议的作用是把各种数据包传送给对方。而要保证确实传送到对方 那里,则需要满足各类条件。其中两个重要的条件是 IP 地址和 MAC 地址(Media Access Control Address)
  • HTTP 超文本传输协议是一个用于传输超媒体文档(例如 HTML)的应用层协议。它是为 Web 浏览器与 Web 服务器之间的通信而设计的,但也可以用于其他目的
  • TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议
  • UDP(User Data Protocol,用户数据报协议)一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上

TCP/IP 的分层管理

利用 TCP/IP 协议族进行网络通信时,会通过分层顺序与对方进行通信

image-20210714152647756

这些层基本上被分为4层:

  • 应用层

    • 1、超文本传输协议(HTTP):万维网的基本协议
    • 2、文件传输(FTP文件传输协议);
    • 3、远程登录(Telnet),提供远程访问其它主机功能, 它允许用户登录internet主机,并在这台主机上执行命令
    • 4、网络管理(SNMP简单网络管理协议),该协议提供了监控网络设备的方法, 以及配置管理,统计信息收集,性能管理及安全管理等
    • 5、域名系统(DNS),该系统用于在internet中将域名及其公共广播的网络节点转换成IP地址
  • 传输层

    • 1、TCP
    • 2、UDP
  • 网络层

    • 1、Internet协议(IP)
    • 2、Internet控制信息协议(ICMP)
    • 3、地址解析协议(ARP)ARP 是一种用以解析地址的协议,根据通信方的 IP 地址就可以反查出对应的 MAC 地址。
    • 4、反向地址解析协议(RARP)
  • 链路层

    用来处理连接网络的硬件部分。包括控制操作系统、硬件的设备驱动、NIC(Network Interface Card,网络适配器,即网卡),及光纤等物理可见部分(还包括连接器等一切传输媒介)。硬件上的范畴均在链路层的作用范围之内。

Cookies 的属性

在下面这张图里我们可以看到 Cookies 相关的一些属性:

img

这里主要说一些大家可能没有注意的点:

Name/Value

用 JavaScript 操作 Cookie 的时候注意对 Value 进行编码处理。

Expires

Expires 用于设置 Cookie 的过期时间。比如:

1
2
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
复制代码

当 Expires 属性缺省时,表示是会话性 Cookie,像上图 Expires 的值为 Session,表示的就是会话性 Cookie。当为会话性 Cookie 的时候,值保存在客户端内存中,并在用户关闭浏览器时失效。需要注意的是,有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,会话期 Cookie 也会被保留下来,就好像浏览器从来没有关闭一样。

与会话性 Cookie 相对的是持久性 Cookie,持久性 Cookies 会保存在用户的硬盘中,直至过期或者清除 Cookie。这里值得注意的是,设定的日期和时间只与客户端相关,而不是服务端

Max-Age

Max-Age 用于设置在 Cookie 失效之前需要经过的秒数。比如:

1
2
Set-Cookie: id=a3fWa; Max-Age=604800;
复制代码

Max-Age 可以为正数、负数、甚至是 0。

如果 max-Age 属性为正数时,浏览器会将其持久化,即写到对应的 Cookie 文件中。

当 max-Age 属性为负数,则表示该 Cookie 只是一个会话性 Cookie。

当 max-Age 为 0 时,则会立即删除这个 Cookie。

假如 Expires 和 Max-Age 都存在,Max-Age 优先级更高。

Domain

Domain 指定了 Cookie 可以送达的主机名。假如没有指定,那么默认值为当前文档访问地址中的主机部分(但是不包含子域名)。

像淘宝首页设置的 Domain 就是 .taobao.com,这样无论是 a.taobao.com 还是 b.taobao.com 都可以使用 Cookie。

在这里注意的是,不能跨域设置 Cookie,比如阿里域名下的页面把 Domain 设置成百度是无效的:

1
2
Set-Cookie: qwerty=219ffwef9w0f; Domain=baidu.com; Path=/; Expires=Wed, 30 Aug 2020 00:00:00 GMT
复制代码

Path

Path 指定了一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie 首部。比如设置 Path=/docs/docs/Web/ 下的资源会带 Cookie 首部,/test 则不会携带 Cookie 首部。

Domain 和 Path 标识共同定义了 Cookie 的作用域:即 Cookie 应该发送给哪些 URL

1.domain表示的是cookie所在的域,默认为请求的地址,如网址为www.jb51.net/test/test.aspx,那么domain默认为www.jb51.net。而跨域访问,如域A为t1.test.com,域B为t2.test.com,那么在域A生产一个令域A和域B都能访问的cookie就要将该cookie的domain设置为.test.com;如果要在域A生产一个令域A不能访问而域B能访问的cookie就要将该cookie的domain设置为t2.test.com。

2.path表示cookie所在的目录,asp.net默认为/,就是根目录。在同一个服务器上有目录如下:/test/,/test/cd/,/test/dd/,现设一个cookie1的path为/test/,cookie2的path为/test/cd/,那么test下的所有页面都可以访问到cookie1,而/test/和/test/dd/的子页面不能访问cookie2。这是因为cookie能让其path路径下的页面访问。

Secure属性

标记为 Secure 的 Cookie 只应通过被HTTPS协议加密过的请求发送给服务端。使用 HTTPS 安全协议,可以保护 Cookie 在浏览器和 Web 服务器间的传输过程中不被窃取和篡改。

HTTPOnly

设置 HTTPOnly 属性可以防止客户端脚本通过 document.cookie 等方式访问 Cookie,有助于避免 XSS 攻击。

SameSite

Cookie 的SameSite属性用来限制第三方 Cookie,从而减少安全风险。

它可以设置三个值。

  • Strict
  • Lax
  • None

Strict

Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。

1
Set-Cookie: CookieName=CookieValue; SameSite=Strict;

这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。

Lax

Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。

1
Set-Cookie: CookieName=CookieValue; SameSite=Lax;

导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。

请求类型 示例 正常情况 Lax
链接 <a href="..."></a> 发送 Cookie 发送 Cookie
预加载 <link rel="prerender" href="..."/> 发送 Cookie 发送 Cookie
GET 表单 <form method="GET" action="..."> 发送 Cookie 发送 Cookie
POST 表单 <form method="POST" action="..."> 发送 Cookie 不发送
iframe <iframe src="..."></iframe> 发送 Cookie 不发送
AJAX $.get("...") 发送 Cookie 不发送
Image <img src="..."> 发送 Cookie 不发送

设置了StrictLax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。

None

Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。

下面的设置无效。

1
Set-Cookie: widget_session=abc123; SameSite=None

下面的设置有效。

1
Set-Cookie: widget_session=abc123; SameSite=None; Secure

cookie作者:冴羽

链接:https://juejin.cn/post/6844904095711494151

samesite作者:阮一峰

链接:https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html

层叠上下文

image-20211213143326679

BFC

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

下列方式会创建块格式化上下文

  • 根元素(<html>)
  • 浮动元素(元素的 float 不是 none
  • 绝对定位元素(元素的 positionabsolutefixed
  • 行内块元素(元素的 displayinline-block
  • overflow 计算值(Computed)不为 visible 的块元素
  • 弹性元素(displayflexinline-flex元素的直接子元素)
  • contain 值为 layoutcontent或 paint 的元素
  • 前边的是比较常见的
  • 表格单元格(元素的 displaytable-cell,HTML表格单元格默认为该值)
  • 表格标题(元素的 displaytable-caption,HTML表格标题默认为该值)
  • 匿名表格单元格元素(元素的 displaytable、``table-rowtable-row-group、``table-header-group、``table-footer-group(分别是HTML table、row、tbody、thead、tfoot 的默认属性)或 inline-table
  • display 值为 flow-root 的元素
  • 网格元素(displaygridinline-grid 元素的直接子元素)
  • 多列容器(元素的 column-countcolumn-width (en-US) 不为 auto,包括 ``column-count1
  • column-spanall 的元素始终会创建一个新的BFC,即使该元素没有包裹在一个多列容器中(标准变更Chrome bug)。

块格式化上下文包含创建它的元素内部的所有内容.

块格式化上下文对浮动定位(参见 float)与清除浮动(参见 clear)都很重要。浮动定位和清除浮动时只会应用于同一个BFC内的元素。浮动不会影响其它BFC中元素的布局,而清除浮动只能清除同一BFC中在它前面的元素的浮动。外边距折叠(Margin collapsing)也只会发生在属于同一BFC的块级元素之间。

规则:

  • 属于同一个 BFC 的两个相邻 Box 垂直排列
  • 属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
  • BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box的左边相接触 (子元素 absolute 除外)
  • BFC 的区域不会与 float 的元素区域重叠 自适应两栏布局
  • img
  • 计算 BFC 的高度时,浮动子元素也参与计算
  • 文字层不会被浮动层覆盖,环绕于周围

应用:

  • 阻止margin重叠
  • 可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区域之中)
  • 自适应两栏布局
  • 可以阻止元素被浮动元素覆盖

可以参考知乎

同源定义

如果两个 URL 的 protocolport (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html 失败 主机不同

Cookie中的「同站」判断就比较宽松:只要两个 URL 的 eTLD+1 相同即可,不需要考虑协议和端口。其中,eTLD 表示有效顶级域名,注册于 Mozilla 维护的公共后缀列表(Public Suffix List)中,例如,.com、.co.uk、.github.io 等。eTLD+1 则表示,有效顶级域名+二级域名,例如 taobao.com 等。

withCredentials

withCredentialsXMLHttpRequest的一个属性,表示跨域请求是否提供凭据信息(cookie、HTTP认证及客户端SSL证明等)

实际中用途就是跨域请求是要不要携带cookie

samesite默认为laxchrome80+,只设置withCredentials已经没用了

位(bit) & 字节(Byte)

详细

bit

1位二进制数,也就是1bit,有2种可能,可以表示数0,1 也就是开关状态 是计算机的存储基础

2位二进制数,2bit,有4种可能(2x2),可以表示数0,1,2,3

3位二进制数,3bit,有8种可能(2x2x2),可以表示数0,1,2,3,4,5,6,7

Byte

大B,表示字节

1Byte = 8 bit, 2^8是256,1个字节能表示的数就是0-255,共256种可能性。

Unicode编码

摘抄

Unicode(统一码、万国码、单一码)是计算机科学领域里的一项业界标准,包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,又称万国码。UTF-8 用1到6个字节编码Unicode字符。用在网页上可以统一页面显示中文简体繁体及其它语言(如英文,日文,韩文)。

UTF-8是一种非常通用的可变长字符编码方式

像UTF-8里面,ASCII所表示的字符集就是用1 Byte来表示,而大部分汉字则是用3 Byte来表示。

UTF-16

UTF-16 Unicode字符编码五层次模型的第三层:字符编码表(Character Encoding Form,也称为 “storage format”)的一种实现方式。即把Unicode字符集的抽象码位映射为16位长的二进制整数(即码元, 长度为2 Byte)的序列,用于数据存储或传递。Unicode字符的码位,需要1个或者2个16位长的码元(2字节或者4字节)来表示,因此这是一个变长表示。

引用维基百科中对于UTF-16编码的解释我们可以知道,UTF-16最少也会用2 Byte来表示一个字符,因此没有办法兼容ASCII编码(ASCII编码使用1 Byte来进行存储)。

JS中的string

在JavaScript中,所有的string类型(或者被称为DOMString)都是使用UTF-16编码的。

因此,当我们需要转换成二进制与后端进行通信时,需要注意相关的编码方式。

性能指标

在chrome的devtools里有很多性能指标,下面简单介绍一下这些指标

首先是可以在chrome的performance中标识的指标

  • DCL (DOMContentLoaded Event)

    • 当初始的 HTML 文档被完全加载和解析完成之后,**DOMContentLoaded** 事件被触发,而无需等待样式表、图像和子框架的完全加载。(MDN的概念)
    • 更加清晰的结论是,DOMContentLoaded 事件在 html文档加载完毕,并且 html 所引用的内联 js、以及外链 js 的同步代码都执行完毕后触发。
  • L (Onload Event)

    • load 应该仅用于检测一个完全加载的页面 当一个资源及其依赖资源已完成加载时,将触发load事件
    • 更加清晰的结论是,当页面 DOM 结构中的 js、css、图片,以及 js 异步加载的 js、css 、图片都加载完成之后,才会触发 load 事件。

    页面中引用的 js 代码如果有异步加载的 js、css、图片,是会影响 load 事件触发的。
    video、audio、flash 不会影响 load 事件触发。

  • FP (First Paint)

    • 首次绘制: 标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点,简而言之就是浏览器第一次发生变化的时间
  • FCP (First Contentful Paint)

    • 首次内容绘制 标记浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 元素.
  • LCP (Largest Contentful Paint)

    • 最大内容渲染: 代表在viewport中最大的页面元素加载的时间. LCP的数据会通过PerformanceEntry对象记录, 每次出现更大的内容渲染, 则会产生一个新的PerformanceEntry对象.(2019年11月新增)

然后是在性能分析 lighthouse中出现的六个指标,前两个在performance中也存在

  • FCP (First Contentful Paint)
  • LCP (Largest Contentful Paint)
  • SI (Speed Index)
    • 指标用于显示页面可见部分的显示速度, 单位是时间
  • TTI (Time to Interactive)
    • 可交互时间: 指标用于标记应用已进行视觉渲染并能可靠响应用户输入的时间点.
  • TBT (Total Blocking Time)
    • 页面阻塞总时长: TBT汇总所有加载过程中阻塞用户操作的时长,在FCP和TTI之间任何long task中阻塞部分都会被汇总(超过50ms的长任务)
  • CLS (Cumulative Layout Shift)
    • 累积布局偏移: 总结起来就是一个元素初始时和其hidden之间的任何时间如果元素偏移了, 则会被计算进去,说简单点就是用户不期望的元素位置偏移。
    • 根据 Google 的介绍,CLS 问题产生的原因一般包括:
      • 图片没有宽高
      • 无尺寸的广告、嵌入式和iframes
      • 动态注入的内容
      • 导致FOIT/FOUT的Web字体
      • 在更新DOM之前等待网络响应的操作

对象获取属性的方法

方法 不可枚举属性 继承属性 symbol属性 自身属性
in
for…in
JSON.stringfy()
Object.assign()
… 扩展运算符
Object.keys()、Object.values()、Object.entries()
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols(obj)
Reflect.ownKeys(obj)
Object.getOwnPropertyDescriptor(s)

寄生组合继承

避免二次调用父的prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 // 父
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};

// 借助辅助原型函数链接到父prototype
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
// 保证正确的construct,将只prototype链接到辅助原型函数上的实例上
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subType.prototype = prototype; // 赋值对象
}

// 继承
function SubType(name, age) {
// 一
SuperType.call(this, name);
this.age = age;
}
// 二
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function () {
console.log(this.age);
};
var child = new SubType("头疼", 30)

Hot Module Replacement,简称HMR,无需完全刷新整个页面的同时,更新模块。HMR的好处,在日常开发工作中体会颇深:节省宝贵的开发时间、提升开发体验

在这里简单介绍一个 HMR 的原理

HMR 初始化

webpack-dev-server

Webpack-dev-server 的执行过程,其实也是一个 HMR 开启的过程

  • 修改 webpackOptions,添加两个入口文件,一个是 websocket 客户端代码,一个是热更新客户端代码(主要是用于检查更新逻辑)。

  • 启动webpack,生成compiler实例。compiler上有很多方法,比如可以启动 webpack 所有编译工作,以及监听本地文件的变化。

  • 使用express框架启动本地server,让浏览器可以请求本地的静态资源

    • 启动 server 的时候,监听 compiler 的 done 事件,当监听到一次webpack编译结束,就会调用_sendStats方法通过websocket给浏览器发送通知,okhash事件,这样浏览器就可以拿到最新的hash值了,做检查更新逻辑
    • 生成 webpack-dev-middleware 中间件实例,保存在 this.middleware(主要是本地文件的编译输出以及监听
  • 本地server启动之后,再去启动websocket服务,通过websocket,可以建立本地服务和浏览器的双向通信。这样就可以实现当本地文件发生变化,立马告知浏览器可以热更新代码啦!

webpack-dev-middleware

文件相关的操作都抽离到webpack-dev-middleware库了,主要是本地文件的编译输出以及监听

主要流程都在下述代码中

1
2
3
4
5
6
7
8
9
share.setOptions(context.options);
share.setFs(context.compiler);

context.compiler.plugin("done", share.compilerDone);
context.compiler.plugin("invalid", share.compilerInvalid);
context.compiler.plugin("watch-run", share.compilerInvalid);
context.compiler.plugin("run", share.compilerInvalid);

share.startWatch();
  • 初始化配置,利用memory-fs库将文件打包到内存中(访问文件系统中的文件更快,而且也减少了代码写入文件的开销)
  • 注册一系列事件。
  • 开启对本地文件的监听,当文件发生变化,重新编译,编译完成之后继续监听

一次完整的 HMR 流程

  1. 当文件发生变化,就触发重新编译。当监听到一次webpack编译结束,_sendStats方法就通过websoket给浏览器发送通知

    1. hash事件,更新最新一次打包后的hash
    2. ok事件,进行热更新检查
    1
    2
    3
    4
    5
    6
    this.sockWrite(sockets, 'hash', stats.hash);
    if (stats.errors.length > 0) {
    this.sockWrite(sockets, 'errors', stats.errors);
    } else if (stats.warnings.length > 0) {
    this.sockWrite(sockets, 'warnings', stats.warnings);
    } else { this.sockWrite(sockets, 'ok'); }

    image-20220111134320575

  2. 客户端接受到 ws 消息后,hash事件更新当前hash值,ok 事件触发hotEmitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// webpack-dev-server/client/index.js
var socket = require('./socket');
var onSocketMessage = {
hash: function hash(_hash) {
// 更新currentHash值
status.currentHash = _hash;
},
ok: function ok() {
sendMessage('Ok');
// 进行更新检查等操作
reloadApp(options, status);
},
};
// 连接服务地址socketUrl,?http://localhost:8080,本地服务地址
socket(socketUrl, onSocketMessage);

function reloadApp() {
if (hot) {
log.info('[WDS] App hot update...');

// hotEmitter其实就是EventEmitter的实例
var hotEmitter = require('webpack/hot/emitter');
hotEmitter.emit('webpackHotUpdate', currentHash);
}
}
  1. web-dev-server 插入的客户端的另一个入口文件 webpack/hot/dev-server.js,监听hotEmitter事件,进行热更新检查 check

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // webpack/hot/dev-server.js
    var check = function check() {
    // 热更新核心代码
    module.hot.check(true)
    .then(function(updatedModules) {
    // 容错,直接刷新页面
    if (!updatedModules) {
    window.location.reload();
    return;
    }
    // 热更新结束,打印信息
    if (upToDate()) {
    log("info", "[HMR] App is up to date.");
    }
    })
    .catch(function(err) {
    window.location.reload();
    });
    };

    var hotEmitter = require("./emitter");
    hotEmitter.on("webpackHotUpdate", function(currentHash) {
    lastHash = currentHash;
    check();
    });
  2. check代码module.hot.checkHotModuleReplacementPlugin.runtime.js

    hotCheck主要做了三件事

    1. 利用上一次保存的hash值,调用hotDownloadManifest发送xxx/hash.hot-update.jsonajax请求;

    2. 请求结果获取热更新模块,以及下次热更新的Hash 标识,并进入热更新准备阶段。

    3. 调用hotDownloadUpdateChunk发送xxx/hash.hot-update.js 请求,通过JSONP方式。

      img

    4. 返回结果后,要立即执行webpackHotUpdate这个方法。

      1
      2
      3
      window["webpackHotUpdate"] = function (chunkId, moreModules) {
      hotAddUpdateChunk(chunkId, moreModules);
      } ;
    5. hotAddUpdateChunk方法会把更新的模块moreModules赋值给全局全量hotUpdate

    6. hotUpdateDownloaded方法会调用hotApply进行代码的替换。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function hotAddUpdateChunk(chunkId, moreModules) {
      // 更新的模块moreModules赋值给全局全量hotUpdate
      for (var moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      hotUpdate[moduleId] = moreModules[moduleId];
      }
      }
      // 调用hotApply进行模块的替换
      hotUpdateDownloaded();
      }
    7. hotApply 热更新模块替换

      1. 删除过期的模块,就是需要替换的模块

      通过hotUpdate可以找到旧模块

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      var queue = outdatedModules.slice();
      while (queue.length > 0) {
      moduleId = queue.pop();
      // 从缓存中删除过期的模块
      module = installedModules[moduleId];
      // 删除过期的依赖
      delete outdatedDependencies[moduleId];

      // 存储了被删掉的模块id,便于更新代码
      outdatedSelfAcceptedModules.push({
      module: moduleId
      });
      }
      复制代码
      1. 将新的模块添加到 modules 中
      1
      2
      3
      4
      5
      6
      7
      appliedUpdate[moduleId] = hotUpdate[moduleId];
      for (moduleId in appliedUpdate) {
      if (Object.prototype.hasOwnProperty.call(appliedUpdate, moduleId)) {
      modules[moduleId] = appliedUpdate[moduleId];
      }
      }
      复制代码
      1. 通过webpack_require执行相关模块的代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      for (i = 0; i < outdatedSelfAcceptedModules.length; i++) {
      var item = outdatedSelfAcceptedModules[i];
      moduleId = item.module;
      try {
      // 执行最新的代码
      __webpack_require__(moduleId);
      } catch (err) {
      // ...容错处理
      }
      }

转载至 blog
diff 算法 也可以参考掘金小册

目录

  • 前言
  • virtual dom
  • 分析diff
  • 总结

前言

vue2.0加入了virtual dom,有向react靠拢的意思。vue的diff位于patch.js文件中,我的一个小框架aoy也同样使用此算法,该算法来源于snabbdom,复杂度为O(n)。
了解diff过程可以让我们更高效的使用框架。
本文力求以图文并茂的方式来讲明这个diff的过程。

virtual dom

如果不了解virtual dom,要理解diff的过程是比较困难的。虚拟dom对应的是真实dom, 使用document.CreateElementdocument.CreateTextNode创建的就是真实节点。

我们可以做个试验。打印出一个空元素的第一层属性,可以看到标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。

1
2
3
4
var mydiv = document.createElement('div');
for(var k in mydiv ){
console.log(k)
}

virtual dom就是解决这个问题的一个思路,到底什么是virtual dom呢?通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。
举个简单的例子,我们在body里插入一个class为a的div。

1
2
3
var mydiv = document.createElement('div');
mydiv.className = 'a';
document.body.appendChild(mydiv);

对于这个div我们可以用一个简单的对象mydivVirtual代表它,它存储了对应dom的一些重要参数,在改变dom之前,会先比较相应虚拟dom的数据,如果需要改变,才会将改变应用到真实dom上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//伪代码
var mydivVirtual = {
tagName: 'DIV',
className: 'a'
};
var newmydivVirtual = {
tagName: 'DIV',
className: 'b'
}
if(mydivVirtual.tagName !== newmydivVirtual.tagName || mydivVirtual.className !== newmydivVirtual.className){
change(mydiv)
}

// 会执行相应的修改 mydiv.className = 'b';
//最后 <div class='b'></div>

读到这里就会产生一个疑问,为什么不直接修改dom而需要加一层virtual dom呢?

很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,virtual dom的解决方案应运而生,virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。

virtual dom 另一个重大意义就是提供一个中间层,js去写ui,ios安卓之类的负责渲染,就像reactNative一样。

分析diff

一篇相当经典的文章React’s diff algorithm中的图,react的diff其实和vue的diff大同小异。所以这张图能很好的解释过程。比较只会在同层级进行, 不会跨层级比较。

img

举个形象的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 之前 -->
<div> <!-- 层级1 -->
<p> <!-- 层级2 -->
<b> aoy </b> <!-- 层级3 -->
<span>diff</Span>
</P>
</div>

<!-- 之后 -->
<div> <!-- 层级1 -->
<p> <!-- 层级2 -->
<b> aoy </b> <!-- 层级3 -->
</p>
<span>diff</Span>
</div>

我们可能期望将<span>直接移动到<p>的后边,这是最优的操作。但是实际的diff操作是移除<p>里的<span>在创建一个新的<span>插到<p>的后边。
因为新加的<span>在层级2,旧的在层级3,属于不同层级的比较。

源码分析

文中的代码位于aoy-diff中,已经精简了很多代码,留下最核心的部分。

diff的过程就是调用patch函数,就像打补丁一样修改真实dom。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode
}

patch函数有两个参数,vnodeoldVnode,也就是新旧两个虚拟节点。在这之前,我们先了解完整的vnode都有什么属性,举个一个简单的例子:

1
2
3
4
5
6
7
8
9
10
// body下的 <div id="v" class="classA"><div> 对应的 oldVnode 就是

{
el: div //对真实的节点的引用,本例中就是document.querySelector('#id.classA')
tagName: 'DIV', //节点的标签
sel: 'div#v.classA' //节点的选择器
data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
children: [], //存储子节点的数组,每个子节点也是vnode结构
text: null, //如果是文本节点,对应文本节点的textContent,否则为null
}

需要注意的是,el属性引用的是此 virtual dom对应的真实dom,patchvnode参数的el最初是null,因为patch之前它还没有对应的真实dom。

来到patch的第一部分,

1
2
3
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
}

sameVnode函数就是看这两个节点是否值得比较,代码相当简单:

1
2
3
function sameVnode(oldVnode, vnode){
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}

两个vnode的key和sel相同才去比较它们,比如pspandiv.classAdiv.classB都被认为是不同结构而不去比较它们。

如果值得比较会执行patchVnode(oldVnode, vnode),稍后会详细讲patchVnode函数。

当节点不值得比较,进入else中

1
2
3
4
5
6
7
8
9
10
else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}

过程如下:

  • 取得oldvnode.el的父节点,parentEle是真实dom
  • createEle(vnode)会为vnode创建它的真实dom,令vnode.el =真实dom
  • parentEle将新的dom插入,移除旧的dom
    当不值得比较时,新节点直接把老节点整个替换了

最后

1
return vnode

patch最后会返回vnode,vnode和进入patch之前的不同在哪?
没错,就是vnode.el,唯一的改变就是之前vnode.el = null, 而现在它引用的是对应的真实dom。

1
var oldVnode = patch (oldVnode, vnode)

至此完成一个patch过程。

patchVnode

两个节点值得比较时,会调用patchVnode函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}

const el = vnode.el = oldVnode.el 这是很重要的一步,让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。

节点的比较有5种情况

  1. if (oldVnode === vnode),他们的引用一致,可以认为没有变化。
  2. if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用Node.textContent = vnode.text
  3. if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren函数比较子节点,这是diff的核心,后边会讲到。
  4. else if (ch),只有新的节点有子节点,调用createEle(vnode)vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点。
  5. else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。

updateChildren

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { //对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}

代码很密集,为了形象的描述这个过程,可以看看这张图。

img

过程可以概括为:oldChnewCh各有两个头尾的变量StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较。

具体的diff分析

设置key和不设置key的区别:
不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。

diff的遍历过程中,只要是对dom进行的操作都调用api.insertBeforeapi.insertBefore只是原生insertBefore的简单封装。
比较分为两种,一种是有vnode.key的,一种是没有的。但这两种比较对真实dom的操作是一致的。

对于与sameVnode(oldStartVnode, newStartVnode)sameVnode(oldEndVnode,newEndVnode)为true的情况,不需要对dom进行移动。

总结遍历过程,有3种dom操作:

  1. oldStartVnodenewEndVnode值得比较,说明oldStartVnode.el跑到oldEndVnode.el的后边了。

图中假设startIdx遍历到1。

img

  1. oldEndVnodenewStartVnode值得比较,说明 oldEndVnode.el跑到了newStartVnode.el的前边。(这里笔误,应该是“oldEndVnode.el跑到了oldStartVnode.el的前边”,准确的说应该是oldEndVnode.el需要移动到oldStartVnode.el的前边”)

img

  1. newCh中的节点oldCh里没有, 将新节点插入到oldStartVnode.el的前边。

img

在结束时,分为两种情况:

  1. oldStartIdx > oldEndIdx,可以认为oldCh先遍历完。当然也有可能newCh此时也正好完成了遍历,统一都归为此类。此时newStartIdxnewEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边,before很多时候是为null的。addVnodes调用的是insertBefore操作dom节点,我们看看insertBefore的文档:parentElement.insertBefore(newElement, referenceElement)
    如果referenceElement为null则newElement将被插入到子节点的末尾。如果newElement已经在DOM树中,newElement首先会从DOM树中移除。所以before为null,newElement将被插入到子节点的末尾。

img

  1. newStartIdx > newEndIdx,可以认为newCh先遍历完。此时oldStartIdxoldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。

img

下面举个例子,画出diff完整的过程,每一步dom的变化都用不同颜色的线标出

  1. a,b,c,d,e假设是4个不同的元素,我们没有设置key时,b没有复用,而是直接创建新的,删除旧的。

img

  1. 当我们给4个元素加上唯一key时,b得到了的复用。

img

这个例子如果我们使用手工优化,只需要3步就可以达到。

总结

  • 尽量不要跨层级的修改dom
  • 设置key可以最大化的利用节点
  • diff的效率并不是每种情况下都是最优的

Original Repository: ryanmcdermott/clean-code-javascript

JavaScript 风格指南

目录

  1. 介绍
  2. 变量
  3. 函数
  4. 对象和数据结构
  5. 测试
  6. 并发
  7. 错误处理
  8. 格式化
  9. 注释

介绍

作者根据 Robert C. Martin 《代码整洁之道》总结了适用于 JavaScript 的软件工程原则《Clean Code JavaScript》

本文是对其的翻译。

不必严格遵守本文的所有原则,有时少遵守一些效果可能会更好,具体应根据实际情况决定。这是根据《代码整洁之道》作者多年经验整理的代码优化建议,但也仅仅只是一份建议。

软件工程已经发展了 50 多年,至今仍在不断前进。现在,把这些原则当作试金石,尝试将他们作为团队代码质量考核的标准之一吧。

最后你需要知道的是,这些东西不会让你立刻变成一个优秀的工程师,长期奉行他们也并不意味着你能够高枕无忧不再犯错。千里之行,始于足下。我们需要时常和同行们进行代码评审,不断优化自己的代码。不要惧怕改善代码质量所需付出的努力,加油。

变量

使用有意义,可读性好的变量名

反例:

1
var yyyymmdstr = moment().format('YYYY/MM/DD');

正例:

1
var yearMonthDay = moment().format('YYYY/MM/DD');

回到目录

使用 ES6 的 const 定义常量

反例中使用”var”定义的”常量”是可变的。

在声明一个常量时,该常量在整个程序中都应该是不可变的。

反例:

1
var FIRST_US_PRESIDENT = "George Washington";

正例:

1
const FIRST_US_PRESIDENT = "George Washington";

回到目录

对功能类似的变量名采用统一的命名风格

反例:

1
2
3
getUserInfo();
getClientData();
getCustomerRecord();

正例:

1
getUser();

回到目录

使用易于检索名称

我们需要阅读的代码远比自己写的要多,使代码拥有良好的可读性且易于检索非常重要。阅读变量名晦涩难懂的代码对读者来说是一种相当糟糕的体验。
让你的变量名易于检索。

反例:

1
2
3
4
// 525600 是什么?
for (var i = 0; i < 525600; i++) {
runCronJob();
}

正例:

1
2
3
4
5
// Declare them as capitalized `var` globals.
var MINUTES_IN_A_YEAR = 525600;
for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
runCronJob();
}

回到目录

使用说明变量(即有意义的变量名)

反例:

1
2
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
saveCityState(cityStateRegex.match(cityStateRegex)[1], cityStateRegex.match(cityStateRegex)[2]);

正例:

1
2
3
4
5
6
const ADDRESS = 'One Infinite Loop, Cupertino 95014';
var cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
var match = ADDRESS.match(cityStateRegex)
var city = match[1];
var state = match[2];
saveCityState(city, state);

回到目录

不要绕太多的弯子

显式优于隐式。

反例:

1
2
3
4
5
6
7
8
9
10
var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
...
...
...
// l是什么?
dispatch(l);
});

正例:

1
2
3
4
5
6
7
8
9
var locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
...
...
...
dispatch(location);
});

回到目录

避免重复的描述

当类/对象名已经有意义时,对其变量进行命名不需要再次重复。

反例:

1
2
3
4
5
6
7
8
9
var Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};

function paintCar(car) {
car.carColor = 'Red';
}

正例:

1
2
3
4
5
6
7
8
9
var Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};

function paintCar(car) {
car.color = 'Red';
}

回到目录

避免无意义的条件判断

反例:

1
2
3
4
5
6
7
8
function createMicrobrewery(name) {
var breweryName;
if (name) {
breweryName = name;
} else {
breweryName = 'Hipster Brew Co.';
}
}

正例:

1
2
3
function createMicrobrewery(name) {
var breweryName = name || 'Hipster Brew Co.'
}

回到目录

函数

函数参数 (理想情况下应不超过 2 个)

限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。

应避免三个以上参数的函数。通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象。

JS 定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代。

反例:

1
2
3
function createMenu(title, body, buttonText, cancellable) {
...
}

正例:

1
2
3
4
5
6
7
8
9
10
11
var menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}

function createMenu(menuConfig) {
...
}

回到目录

函数功能的单一性

这是软件功能中最重要的原则之一。

功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。

反例:

1
2
3
4
5
6
7
8
function emailClients(clients) {
clients.forEach(client => {
let clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function emailClients(clients) {
clients.forEach(client => {
emailClientIfNeeded(client);
});
}

function emailClientIfNeeded(client) {
if (isClientActive(client)) {
email(client);
}
}

function isClientActive(client) {
let clientRecord = database.lookup(client);
return clientRecord.isActive();
}

回到目录

函数名应明确表明其功能

反例:

1
2
3
4
5
6
7
8
function dateAdd(date, month) {
// ...
}

let date = new Date();

// 很难理解dateAdd(date, 1)是什么意思
dateAdd(date, 1);

正例:

1
2
3
4
5
6
function dateAddMonth(date, month) {
// ...
}

let date = new Date();
dateAddMonth(date, 1);

回到目录

函数应该只做一层抽象

当函数的需要的抽象多于一层时通常意味着函数功能过于复杂,需将其进行分解以提高其可重用性和可测试性。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function parseBetterJSAlternative(code) {
let REGEXES = [
// ...
];

let statements = code.split(' ');
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});

let ast;
tokens.forEach((token) => {
// lex...
});

ast.forEach((node) => {
// parse...
})
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function tokenize(code) {
let REGEXES = [
// ...
];

let statements = code.split(' ');
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});

return tokens;
}

function lexer(tokens) {
let ast;
tokens.forEach((token) => {
// lex...
});

return ast;
}

function parseBetterJSAlternative(code) {
let tokens = tokenize(code);
let ast = lexer(tokens);
ast.forEach((node) => {
// parse...
})
}

回到目录

移除重复的代码

永远、永远、永远不要在任何循环下有重复的代码。

这种做法毫无意义且潜在危险极大。重复的代码意味着逻辑变化时需要对不止一处进行修改。JS 弱类型的特点使得函数拥有更强的普适性。好好利用这一优点吧。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function showDeveloperList(developers) {
developers.forEach(developer => {
var expectedSalary = developer.calculateExpectedSalary();
var experience = developer.getExperience();
var githubLink = developer.getGithubLink();
var data = {
expectedSalary: expectedSalary,
experience: experience,
githubLink: githubLink
};

render(data);
});
}

function showManagerList(managers) {
managers.forEach(manager => {
var expectedSalary = manager.calculateExpectedSalary();
var experience = manager.getExperience();
var portfolio = manager.getMBAProjects();
var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};

render(data);
});
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function showList(employees) {
employees.forEach(employee => {
var expectedSalary = employee.calculateExpectedSalary();
var experience = employee.getExperience();
var portfolio;

if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
} else {
portfolio = employee.getGithubLink();
}

var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};

render(data);
});
}

回到目录

采用默认参数精简代码

反例:

1
2
3
4
5
function writeForumComment(subject, body) {
subject = subject || 'No Subject';
body = body || 'No text';
}

正例:

1
2
3
4
function writeForumComment(subject = 'No subject', body = 'No text') {
...
}

回到目录

使用 Object.assign 设置默认对象

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
}

function createMenu(config) {
config.title = config.title || 'Foo'
config.body = config.body || 'Bar'
config.buttonText = config.buttonText || 'Baz'
config.cancellable = config.cancellable === undefined ? config.cancellable : true;

}

createMenu(menuConfig);

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var menuConfig = {
title: 'Order',
// User did not include 'body' key
buttonText: 'Send',
cancellable: true
}

function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);

// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}

createMenu(menuConfig);

回到目录

不要使用标记(Flag)作为函数参数

这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次划分。

反例:

1
2
3
4
5
6
7
function createFile(name, temp) {
if (temp) {
fs.create('./temp/' + name);
} else {
fs.create(name);
}
}

正例:

1
2
3
4
5
6
7
8
9
10
function createTempFile(name) {
fs.create('./temp/' + name);
}

----------


function createFile(name) {
fs.create(name);
}

回到目录

避免副作用

当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。

程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。

反例:

1
2
3
4
5
6
7
8
9
10
11
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
var name = 'Ryan McDermott';

function splitIntoFirstAndLastName() {
name = name.split(' ');
}

splitIntoFirstAndLastName();

console.log(name); // ['Ryan', 'McDermott'];

正例:

1
2
3
4
5
6
7
8
9
function splitIntoFirstAndLastName(name) {
return name.split(' ');
}

var name = 'Ryan McDermott'
var newName = splitIntoFirstAndLastName(name);

console.log(name); // 'Ryan McDermott';
console.log(newName); // ['Ryan', 'McDermott'];

回到目录

不要写全局函数

在 JS 中污染全局是一个非常不好的实践,这么做可能和其他库起冲突,且调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。

想象以下例子:如果你想扩展 JS 中的 Array,为其添加一个 diff 函数显示两个数组间的差异,此时应如何去做?你可以将 diff 写入 Array.prototype,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff 的需求为比较一个数组中首尾元素间的差异呢?

使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Array.prototype.diff = function(comparisonArray) {
var values = [];
var hash = {};

for (var i of comparisonArray) {
hash[i] = true;
}

for (var i of this) {
if (!hash[i]) {
values.push(i);
}
}

return values;
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SuperArray extends Array {
constructor(...args) {
super(...args);
}

diff(comparisonArray) {
var values = [];
var hash = {};

for (var i of comparisonArray) {
hash[i] = true;
}

for (var i of this) {
if (!hash[i]) {
values.push(i);
}
}

return values;
}
}

回到目录

采用函数式编程

函数式的编程具有更干净且便于测试的特点。尽可能的使用这种风格吧。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

var totalOutput = 0;

for (var i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];

var totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);

回到目录

封装判断条件

反例:

1
2
3
if (fsm.state === 'fetching' && isEmpty(listNode)) {
/// ...
}

正例:

1
2
3
4
5
6
7
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}

回到目录

避免“否定情况”的判断

反例:

1
2
3
4
5
6
7
function isDOMNodeNotPresent(node) {
// ...
}

if (!isDOMNodeNotPresent(node)) {
// ...
}

正例:

1
2
3
4
5
6
7
function isDOMNodePresent(node) {
// ...
}

if (isDOMNodePresent(node)) {
// ...
}

回到目录

避免条件判断

这看起来似乎不太可能。

大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。

第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Airplane {
//...
getCruisingAltitude() {
switch (this.type) {
case '777':
return getMaxAltitude() - getPassengerCount();
case 'Air Force One':
return getMaxAltitude();
case 'Cessna':
return getMaxAltitude() - getFuelExpenditure();
}
}
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Airplane {
//...
}

class Boeing777 extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getPassengerCount();
}
}

class AirForceOne extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude();
}
}

class Cessna extends Airplane {
//...
getCruisingAltitude() {
return getMaxAltitude() - getFuelExpenditure();
}
}

回到目录

避免类型判断(part 1)

JS 是弱类型语言,这意味着函数可接受任意类型的参数。

有时这会对你带来麻烦,你会对参数做一些类型判断。有许多方法可以避免这些情况。

反例:

1
2
3
4
5
6
7
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.peddle(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}

正例:

1
2
3
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}

回到目录

避免类型判断(part 2)

如果需处理的数据为字符串,整型,数组等类型,无法使用多态并仍有必要对其进行类型检测时,可以考虑使用 TypeScript。

反例:

1
2
3
4
5
6
7
8
function combine(val1, val2) {
if (typeof val1 == "number" && typeof val2 == "number" ||
typeof val1 == "string" && typeof val2 == "string") {
return val1 + val2;
} else {
throw new Error('Must be of type String or Number');
}
}

正例:

1
2
3
function combine(val1, val2) {
return val1 + val2;
}

回到目录

避免过度优化

现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。

这里可以找到许多真正需要优化的地方

反例:

1
2
3
4
5
6
7

// 这里使用变量len是因为在老式浏览器中,
// 直接使用正例中的方式会导致每次循环均重复计算list.length的值,
// 而在现代浏览器中会自动完成优化,这一行为是没有必要的
for (var i = 0, len = list.length; i < len; i++) {
// ...
}

正例:

1
2
3
for (var i = 0; i < list.length; i++) {
// ...
}

回到目录

删除无效的代码

不再被调用的代码应及时删除。

反例:

1
2
3
4
5
6
7
8
9
10
11
function oldRequestModule(url) {
// ...
}

function newRequestModule(url) {
// ...
}

var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

正例:

1
2
3
4
5
6
function newRequestModule(url) {
// ...
}

var req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');

回到目录

对象和数据结构

使用 getters 和 setters

JS 没有接口或类型,因此实现这一模式是很困难的,因为我们并没有类似 publicprivate 的关键词。

然而,使用 getters 和 setters 获取对象的数据远比直接使用点操作符具有优势。为什么呢?

  1. 当需要对获取的对象属性执行额外操作时。
  2. 执行 set 时可以增加规则对要变量的合法性进行判断。
  3. 封装了内部逻辑。
  4. 在存取时可以方便的增加日志和错误处理。
  5. 继承该类时可以重载默认行为。
  6. 从服务器获取数据时可以进行懒加载。

反例:

1
2
3
4
5
6
7
8
9
10
class BankAccount {
constructor() {
this.balance = 1000;
}
}

let bankAccount = new BankAccount();

// Buy shoes...
bankAccount.balance = bankAccount.balance - 100;

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BankAccount {
constructor() {
this.balance = 1000;
}

// It doesn't have to be prefixed with `get` or `set` to be a getter/setter
withdraw(amount) {
if (verifyAmountCanBeDeducted(amount)) {
this.balance -= amount;
}
}
}

let bankAccount = new BankAccount();

// Buy shoes...
bankAccount.withdraw(100);

回到目录

让对象拥有私有成员

可以通过闭包完成

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13

var Employee = function(name) {
this.name = name;
}

Employee.prototype.getName = function() {
return this.name;
}

var employee = new Employee('John Doe');
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name: ' + employee.getName()); // Employee name: undefined

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Employee = (function() {
function Employee(name) {
this.getName = function() {
return name;
};
}

return Employee;
}());

var employee = new Employee('John Doe');
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name: ' + employee.getName()); // Employee name: John Doe

回到目录

单一职责原则 (SRP)

如《代码整洁之道》一书中所述,“修改一个类的理由不应该超过一个”。

将多个功能塞进一个类的想法很诱人,但这将导致你的类无法达到概念上的内聚,并经常不得不进行修改。

最小化对一个类需要修改的次数是非常有必要的。如果一个类具有太多太杂的功能,当你对其中一小部分进行修改时,将很难想象到这一修够对代码库中依赖该类的其他模块会带来什么样的影响。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserSettings {
constructor(user) {
this.user = user;
}

changeSettings(settings) {
if (this.verifyCredentials(user)) {
// ...
}
}

verifyCredentials(user) {
// ...
}
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UserAuth {
constructor(user) {
this.user = user;
}

verifyCredentials() {
// ...
}
}


class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user)
}

changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}

回到目录

开/闭原则 (OCP)

“代码实体(类,模块,函数等)应该易于扩展,难于修改。”

这一原则指的是我们应允许用户方便的扩展我们代码模块的功能,而不需要打开 js 文件源码手动对其进行修改。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
class AjaxRequester {
constructor() {
// What if we wanted another HTTP Method, like DELETE? We would have to
// open this file up and modify this and put it in manually.
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}

get(url) {
// ...
}

}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AjaxRequester {
constructor() {
this.HTTP_METHODS = ['POST', 'PUT', 'GET'];
}

get(url) {
// ...
}

addHTTPMethod(method) {
this.HTTP_METHODS.push(method);
}
}

回到目录

利斯科夫替代原则 (LSP)

“子类对象应该能够替换其超类对象被使用”。

也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}

setColor(color) {
// ...
}

render(area) {
// ...
}

setWidth(width) {
this.width = width;
}

setHeight(height) {
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

class Square extends Rectangle {
constructor() {
super();
}

setWidth(width) {
this.width = width;
this.height = width;
}

setHeight(height) {
this.width = height;
this.height = height;
}
}

function renderLargeRectangles(rectangles) {
rectangles.forEach((rectangle) => {
rectangle.setWidth(4);
rectangle.setHeight(5);
let area = rectangle.getArea(); // BAD: Will return 25 for Square. Should be 20.
rectangle.render(area);
})
}

let rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
class Shape {
constructor() {}

setColor(color) {
// ...
}

render(area) {
// ...
}
}

class Rectangle extends Shape {
constructor() {
super();
this.width = 0;
this.height = 0;
}

setWidth(width) {
this.width = width;
}

setHeight(height) {
this.height = height;
}

getArea() {
return this.width * this.height;
}
}

class Square extends Shape {
constructor() {
super();
this.length = 0;
}

setLength(length) {
this.length = length;
}

getArea() {
return this.length * this.length;
}
}

function renderLargeShapes(shapes) {
shapes.forEach((shape) => {
switch (shape.constructor.name) {
case 'Square':
shape.setLength(5);
case 'Rectangle':
shape.setWidth(4);
shape.setHeight(5);
}

let area = shape.getArea();
shape.render(area);
})
}

let shapes = [new Rectangle(), new Rectangle(), new Square()];
renderLargeShapes(shapes);

回到目录

接口隔离原则 (ISP)

“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。”

在 JS 中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}

setup() {
this.rootNode = this.settings.rootNode;
this.animationModule.setup();
}

traverse() {
// ...
}
}

let $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
animationModule: function() {} // Most of the time, we won't need to animate when traversing.
// ...
});

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}

setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}

setupOptions() {
if (this.options.animationModule) {
// ...
}
}

traverse() {
// ...
}
}

let $ = new DOMTraverser({
rootNode: document.getElementsByTagName('body'),
options: {
animationModule: function() {}
}
});

回到目录

依赖反转原则 (DIP)

该原则有两个核心点:

  1. 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。
  2. 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class InventoryTracker {
constructor(items) {
this.items = items;

// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}

requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}

class InventoryRequester {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

let inventoryTracker = new InventoryTracker(['apples', 'bananas']);
inventoryTracker.requestItems();

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}

requestItems() {
this.items.forEach((item) => {
this.requester.requestItem(item);
});
}
}

class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ['HTTP'];
}

requestItem(item) {
// ...
}
}

class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ['WS'];
}

requestItem(item) {
// ...
}
}

// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
let inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2());
inventoryTracker.requestItems();

回到目录

使用 ES6 的 classes 而不是 ES5 的 Function

典型的 ES5 的类(function)在继承、构造和方法定义方面可读性较差。

当需要继承时,优先选用 classes。

但是,当在需要更大更复杂的对象时,最好优先选择更小的 function 而非 classes。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}

this.age = age;
};

Animal.prototype.move = function() {};

var Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}

Animal.call(this, age);
this.furColor = furColor;
};

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function() {};

var Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}

Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};

Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function() {};

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Animal {
constructor(age) {
this.age = age;
}

move() {}
}

class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}

liveBirth() {}
}

class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}

speak() {}
}

回到目录

使用方法链

这里我们的理解与《代码整洁之道》的建议有些不同。

有争论说方法链不够干净且违反了德米特法则,也许这是对的,但这种方法在 JS 及许多库(如 JQuery)中显得非常实用。

因此,我认为在 JS 中使用方法链是非常合适的。在 class 的函数中返回 this,能够方便的将类需要执行的多个方法链接起来。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}

setMake(make) {
this.name = name;
}

setModel(model) {
this.model = model;
}

setColor(color) {
this.color = color;
}

save() {
console.log(this.make, this.model, this.color);
}
}

let car = new Car();
car.setColor('pink');
car.setMake('Ford');
car.setModel('F-150')
car.save();

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Car {
constructor() {
this.make = 'Honda';
this.model = 'Accord';
this.color = 'white';
}

setMake(make) {
this.name = name;
// NOTE: Returning this for chaining
return this;
}

setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}

setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}

save() {
console.log(this.make, this.model, this.color);
}
}

let car = new Car()
.setColor('pink')
.setMake('Ford')
.setModel('F-150')
.save();

回到目录

优先使用组合模式而非继承

在著名的设计模式一书中提到,应多使用组合模式而非继承。

这么做有许多优点,在想要使用继承前,多想想能否通过组合模式满足需求吧。

那么,在什么时候继承具有更大的优势呢?这取决于你的具体需求,但大多情况下,可以遵守以下三点:

  1. 继承关系表现为”是一个”而非”有一个”(如动物->人 和 用户->用户细节)
  2. 可以复用基类的代码(“Human”可以看成是”All animal”的一种)
  3. 希望当基类改变时所有派生类都受到影响(如修改”all animals”移动时的卡路里消耗量)

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}

// ...
}

// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}

// ...
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;

}

setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}

class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}

// ...
}

回到目录

测试

一些好的覆盖工具

一些好的 JS 测试框架

单一的测试每个概念

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const assert = require('assert');

describe('MakeMomentJSGreatAgain', function() {
it('handles date boundaries', function() {
let date;

date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
date.shouldEqual('1/31/2015');

date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);

date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const assert = require('assert');

describe('MakeMomentJSGreatAgain', function() {
it('handles 30-day months', function() {
let date = new MakeMomentJSGreatAgain('1/1/2015');
date.addDays(30);
date.shouldEqual('1/31/2015');
});

it('handles leap year', function() {
let date = new MakeMomentJSGreatAgain('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
});

it('handles non-leap year', function() {
let date = new MakeMomentJSGreatAgain('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});

回到目录

并发

用 Promises 替代回调

回调不够整洁并会造成大量的嵌套。ES6 内嵌了 Promises,使用它吧。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', function(err, response) {
if (err) {
console.error(err);
}
else {
require('fs').writeFile('article.html', response.body, function(err) {
if (err) {
console.error(err);
} else {
console.log('File written');
}
})
}
})

正例:

1
2
3
4
5
6
7
8
9
10
11
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then(function(response) {
return require('fs-promise').writeFile('article.html', response);
})
.then(function() {
console.log('File written');
})
.catch(function(err) {
console.error(err);
})

回到目录

Async/Await 是较 Promises 更好的选择

Promises 是较回调而言更好的一种选择,但 ES7 中的 async 和 await 更胜过 Promises。

在能使用 ES7 特性的情况下可以尽量使用他们替代 Promises。

反例:

1
2
3
4
5
6
7
8
9
10
11
require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
.then(function(response) {
return require('fs-promise').writeFile('article.html', response);
})
.then(function() {
console.log('File written');
})
.catch(function(err) {
console.error(err);
})

正例:

1
2
3
4
5
6
7
8
9
10
11
12
async function getCleanCodeArticle() {
try {
var request = await require('request-promise')
var response = await request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
var fileHandle = await require('fs-promise');

await fileHandle.writeFile('article.html', response);
console.log('File written');
} catch(err) {
console.log(err);
}
}

回到目录

错误处理

错误抛出是个好东西!这使得你能够成功定位运行状态中的程序产生错误的位置。

别忘了捕获错误

对捕获的错误不做任何处理是没有意义的。

代码中 try/catch 的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案。

反例:

1
2
3
4
5
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}

正例:

1
2
3
4
5
6
7
8
9
10
11
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}

不要忽略被拒绝的 promises

理由同 try/catch

反例:

1
2
3
4
5
6
7
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});

回到目录

格式化

格式化是一件主观的事。如同这里的许多规则一样,这里并没有一定/立刻需要遵守的规则。可以在这里完成格式的自动化。

大小写一致

JS 是弱类型语言,合理的采用大小写可以告诉你关于变量/函数等的许多消息。

这些规则是主观定义的,团队可以根据喜欢进行选择。重点在于无论选择何种风格,都需要注意保持一致性。

反例:

1
2
3
4
5
6
7
8
9
10
11
var DAYS_IN_WEEK = 7;
var daysInMonth = 30;

var songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
var Artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restore_database() {}

class animal {}
class Alpaca {}

正例:

1
2
3
4
5
6
7
8
9
10
11
var DAYS_IN_WEEK = 7;
var DAYS_IN_MONTH = 30;

var songs = ['Back In Black', 'Stairway to Heaven', 'Hey Jude'];
var artists = ['ACDC', 'Led Zeppelin', 'The Beatles'];

function eraseDatabase() {}
function restoreDatabase() {}

class Animal {}
class Alpaca {}

回到目录

调用函数的函数和被调函数应放在较近的位置

当函数间存在相互调用的情况时,应将两者置于较近的位置。

理想情况下,应将调用其他函数的函数写在被调用函数的上方。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}

lookupPeers() {
return db.lookup(this.employee, 'peers');
}

lookupMananger() {
return db.lookup(this.employee, 'manager');
}

getPeerReviews() {
let peers = this.lookupPeers();
// ...
}

perfReview() {
getPeerReviews();
getManagerReview();
getSelfReview();
}

getManagerReview() {
let manager = this.lookupManager();
}

getSelfReview() {
// ...
}
}

let review = new PerformanceReview(user);
review.perfReview();

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class PerformanceReview {
constructor(employee) {
this.employee = employee;
}

perfReview() {
getPeerReviews();
getManagerReview();
getSelfReview();
}

getPeerReviews() {
let peers = this.lookupPeers();
// ...
}

lookupPeers() {
return db.lookup(this.employee, 'peers');
}

getManagerReview() {
let manager = this.lookupManager();
}

lookupMananger() {
return db.lookup(this.employee, 'manager');
}

getSelfReview() {
// ...
}
}

let review = new PerformanceReview(employee);
review.perfReview();

回到目录

注释

只对存在一定业务逻辑复杂性的代码进行注释

注释并不是必须的,好的代码是能够让人一目了然,不用过多无谓的注释。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function hashIt(data) {
// The hash
var hash = 0;

// Length of string
var length = data.length;

// Loop through every character in data
for (var i = 0; i < length; i++) {
// Get character code.
var char = data.charCodeAt(i);
// Make the hash
hash = ((hash << 5) - hash) + char;
// Convert to 32-bit integer
hash = hash & hash;
}
}

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

function hashIt(data) {
var hash = 0;
var length = data.length;

for (var i = 0; i < length; i++) {
var char = data.charCodeAt(i);
hash = ((hash << 5) - hash) + char;

// Convert to 32-bit integer
hash = hash & hash;
}
}

回到目录

不要在代码库中遗留被注释掉的代码

版本控制的存在是有原因的。让旧代码存在于你的 history 里吧。

反例:

1
2
3
4
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();

正例:

1
doStuff();

回到目录

不需要版本更新类型注释

记住,我们可以使用版本控制。废代码、被注释的代码及用注释记录代码中的版本更新说明都是没有必要的。

需要时可以使用 git log 获取历史版本。

反例:

1
2
3
4
5
6
7
8
9
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
function combine(a, b) {
return a + b;
}

正例:

1
2
3
function combine(a, b) {
return a + b;
}

回到目录

避免位置标记

这些东西通常只能代码麻烦,采用适当的缩进就可以了。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
let $scope.model = {
menu: 'foo',
nav: 'bar'
};

////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
let actions = function() {
// ...
}

正例:

1
2
3
4
5
6
7
8
let $scope.model = {
menu: 'foo',
nav: 'bar'
};

let actions = function() {
// ...
}

回到目录

避免在源文件中写入法律评论

将你的 LICENSE 文件置于源码目录树的根目录。

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
The MIT License (MIT)

Copyright (c) 2016 Ryan McDermott

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*/

function calculateBill() {
// ...
}

正例:

1
2
3
function calculateBill() {
// ...
}

回到目录

介绍一下commonjs循环加载的处理方式

有循环加载的原理解释,理解起来更加清晰

先介绍下require的特性

​ 1 module 被加载的时候执行,模块的所有语句都会被执行

​ 2 module 加载后缓存,以后使用会在内存中寻找,不会二次加载

​ 3 一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出(下方解释)

脚本文件a.js代码如下。

1
2
3
4
5
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

再看b.js的代码。

1
2
3
4
5
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

a.js已经执行的部分,只有一行。

1
exports.done = false;

因此,对于b.js来说,它从a.js只输入一个变量done,值为false

然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

1
2
3
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运行结果如下。

1
2
3
4
5
6
7
$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

具体执行顺序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// main.js
var a = require('./a.js'); // 1
var b = require('./b.js'); // 12
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done); // 13

// a.js
exports.done = false; // 2
var b = require('./b.js'); // 3
console.log('在 a.js 之中,b.done = %j', b.done); // 9
exports.done = true; // 10
console.log('a.js 执行完毕'); // 11

// b.js
exports.done = false; // 4
var a = require('./a.js'); // 5
console.log('在 b.js 之中,a.done = %j', a.done); // 6
exports.done = true; // 7
console.log('b.js 执行完毕'); // 8

二叉搜索树

image-20210730105124238

常见操作

​ 模拟操作示意网站二叉搜索树

  • 查询

  • 插入

    首先需要搜索是否有重复节点,如果有 count++ ,如果没有,查询到的最后节点也就是插入需要操作的节点

  • 删除

    • 删除 叶子节点(即最底层的节点,仅有一个关联节点)很简单,直接删除就可以,如果是关联节点(有子节点),则会找出该节点右侧最小的节点替换它的位置(即比它大的最小值),需要注意指针的替换和删除

    image-20210730105732099

    image-20210730105831806

时间复杂度

image-20210730105043533

时间复杂度log2 N 也就是 2的X次方 = N x是查询次数

插入 一个数 首先需要搜索是否有重复节点,如果有 count++ ,如果没有,查询到的最后节点也就是插入需要操作的节点

删除 叶子节点(即最底层的节点,仅有一个关联节点)很简单,直接删除就可以,如果是

字符串对象是不可变的,所以字符串对象提供的涉及到字符串“修改”的方法都是返回修改后的新字符串,并不对原始字符串做任何修改,无一例外
JavaScript字符串由 16位码元(code unit)组成。对多数字符来说,每 16位码元对应一个字符。
JavaScript字符串使用了两种 Unicode编码混合的策略:UCS-2和 UTF-16。对于可以采用 16位编码 的字符(U+0000~U+FFFF)16的四次方

charAt()

charAt()方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的 16位码元,并返回该码元对应的字符:

1
2
3
let message = "abcde"; 

console.log(message.charAt(2)); // "c"

charCodeAt()

使用 charCodeAt()方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定

1
2
3
4
let message = "abcde"; 
// Unicode "Latin small letter C"的编码是 U+0063
console.log(message.charCodeAt(2)); // 99

fromCharCode() 原型方法

fromCharCode() 方法用于根据给定的 UTF-16 码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:

1
console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65));  // "abcde" 

对于 U+0000~U+FFFF范围内的字符,length、charAt()、charCodeAt()和 fromCharCode() 返回的结果都跟预期是一样的。
为正确解析既包含单码元字符又包含代理对字符的字符串,与charCodeAt()有对应的 codePointAt()一样,fromCharCode()也有一个对应的 fromCodePoint()

normalize() 非诚不常用

,Unicode提供了4种规范化形式,可以将类似上面的字符规范化为一致的格式,无论 底层字符的代码是什么。这 4种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、 NFKD(Normalization Form KD)和 NFKC(Normalization Form KC)。可以使用 normalize()方法对字 符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串:”NFD”、”NFC”、”NFKD”或”NFKC”。

concat()

首先是 concat(),用于将一个或多个字符串拼接成一个新字符串。concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串。

1
2
3
4
let stringValue = "hello "; 
let result = stringValue.concat("world", "!");
console.log(result); // "hello world!"
console.log(stringValue); // "hello "

虽然 concat()方法可以拼接字符串,但更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加号更方便。

slice()、substr() 和 substring() 截取字符串

这3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。
slice()substring()而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。
substr()而言,第二个参数表示返回的子字符串数量。任何情况下,省略第二个参数都意味着提取到字符串末尾。

1
2
3
4
5
6
7
let stringValue = "hello world"; 
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"

当某个参数是负值时,这3个方法的行为又有不同。
比如,slice()方法将所有负值参数都当成字符串长度加上负参数值。
而substr()方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为 0。
substring()方法会将所有负参数值都转换为 0。

1
2
3
4
5
6
7
let stringValue = "hello world"; 
console.log(stringValue.slice(-3)); // "rld"
console.log(stringValue.substring(-3)); // "hello world"
console.log(stringValue.substr(-3)); // "rld"
console.log(stringValue.slice(3, -4)); // "lo w"
console.log(stringValue.substring(3, -4)); // "hel"
console.log(stringValue.substr(3, -4)); // "" (empty string)

indexOf() 和 lastIndexOf() 字符串位置方法

这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回-1)。两者的区别在于,indexOf()方法 从字符串开头开始查找子字符串,而 lastIndexOf()方法从字符串末尾开始查找子字符串

1
2
3
let stringValue = "hello world"; 
console.log(stringValue.indexOf("o")); // 4
console.log(stringValue.lastIndexOf("o")); // 7

这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,indexOf()会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;lastIndexOf()则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。

1
2
3
let stringValue = "hello world"; 
console.log(stringValue.indexOf("o", 6)); // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4

startsWith()、endsWith() 和 includes() 字符串包含方法

这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值。它们的区别在于,startsWith()检查开始于索引 0 的匹配项,endsWith()检查开始于索引(string.length - substring.length)的匹配项,而 includes()检查整个字符串

1
2
3
4
5
6
7
let message = "foobarbaz"; 
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.endsWith("baz")); // true
console.log(message.endsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false

startsWith()和 includes()方法接收可选的第二个参数,表示开始搜索的位置

1
2
3
4
5
6
7
let message = "foobarbaz"; 

console.log(message.startsWith("foo")); // true
console.log(message.startsWith("foo", 1)); // false

console.log(message.includes("bar")); // true
console.log(message.includes("bar", 4)); // false

endsWith()方法接收可选的第二个参数,表示应该当作字符串末尾的位置

1
2
3
4
let message = "foobarbaz"; 

console.log(message.endsWith("bar")); // false
console.log(message.endsWith("bar", 6)); // true

trim()、 trimLeft() 和 trimRight()

trim()这个方法会创建字符串的一个副本,删除前、 后所有空格符,再返回结果

1
2
3
4
let stringValue = "  hello world  "; 
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"

trimLeft()和 trimRight()方法分别用于从字符串开始和末尾清理空格符。

repeat()

这个方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果

1
2
3
let stringValue = "na "; 
console.log(stringValue.repeat(16) + "batman");
// na na na na na na na na na na na na na na na na batman

padStart()和 padEnd()

padStart()padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格(U+0020)

1
2
3
4
5
6
7
let stringValue = "foo"; 

console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"

console.log(stringValue.padEnd(6)); // "foo "
console.log(stringValue.padEnd(9, ".")); // "foo......"

可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串

1
2
3
4
5
6
7
let stringValue = "foo"; 

console.log(stringValue.padStart(8, "bar")); // "barbafoo"
console.log(stringValue.padStart(2)); // "foo"

console.log(stringValue.padEnd(8, "bar")); // "foobarba"
console.log(stringValue.padEnd(2)); // "foo"

toLowerCase()、toLocaleLowerCase()、toUpperCase() 和 toLocaleUpperCase() 大小写转换

toLowerCase()和toUpperCase()方法是原来就有的方法, 与 java.lang.String 中的方法同名。toLocaleLowerCase()和 toLocaleUpperCase()方法旨在基于特定地区实现

1
2
3
4
5
let stringValue = "hello world"; 
console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD"
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLocaleLowerCase()); // "hello world"
console.log(stringValue.toLowerCase()); // "hello world"

match() search() 和 replace() 字符串模式匹配方法

match()方法本质上跟 RegExp 对象的exec()方法相同。match()方法接收一个参数,可以是一个正则表达式字 符串,也可以是一个 RegExp 对象

1
2
3
4
5
6
7
8
let text = "cat, bat, sat, fat"; 
let pattern = /.at/;

// 等价于 pattern.exec(text)
let matches = text.match(pattern);
console.log(matches.index); // 0
console.log(matches[0]); // "cat"
console.log(pattern.lastIndex); // 0

search()这个方法返回模式第一个匹配的位置索引,如果没找到则返回-1。search()始终从字符串开头向后匹配模式

1
2
3
let text = "cat, bat, sat, fat"; 
let pos = text.search(/at/);
console.log(pos); // 1

replace()方法。这个方法接收两个参数,第一个参数可以是一个 RegExp 对象或一个字符串(这个字符串不会转换为正则表达式),第二个参数可以是一个字符串或一个函数。
如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数必须为正则表达式并且带全局标记

1
2
3
4
5
6
let text = "cat, bat, sat, fat"; 
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"

result = text.replace(/at/g, "ond");
console.log(result); // "cond, bond, sond, fond"

localeCompare()

这个方法比较两个字符串,返回如下 3个值中的一个。

  • 如果按照字母表顺序,字符串应该排在字符串参数前头,则返回负值。(通常是-1,具体还要看与实际值相关的实现。) 
  • 如果字符串与字符串参数相等,则返回 0。 
  • 如果按照字母表顺序,字符串应该排在字符串参数后头,则返回正值。(通常是 1,具体还要看与实际值相关的实现。)
stringValue
1
2
3
console.log(stringValue.localeCompare("brick"));  // 1 
console.log(stringValue.localeCompare("yellow")); // 0
console.log(stringValue.localeCompare("zoo")); // -1

matchAll()

matchAll()方法返回一个正则表达式在当前字符串的所有匹配,详见ES6深入浅出《正则的扩展》的一章。

replaceAll()

历史上,字符串的实例方法replace()只能替换第一个匹配。

1
2
'aabbcc'.replace('b', '_')
// 'aa_bcc'

上面例子中,replace()只将第一个b替换成了下划线。

如果要替换所有的匹配,不得不使用正则表达式的g修饰符。

1
2
'aabbcc'.replace(/b/g, '_')
// 'aa__cc'

正则表达式毕竟不是那么方便和直观,ES2021 引入了replaceAll()方法,可以一次性替换所有匹配。

1
2
'aabbcc'.replaceAll('b', '_')
// 'aa__cc'

它的用法与replace()相同,返回一个新字符串,不会改变原字符串。

更多详细用法见ES6深入浅出 string 方法章节

0%