> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/qian-duan-ji-shu/javascript/javascript-zui-jia-shi-jian.md).

# JavaScript最佳实践

## 干净代码（clean code）

### includes优化多条件判断

### replace/match/exec使用分组

replace传递回调函数以获得更细粒度的操作。

### 使用箭头函数

### 使用扩展运算符

### 使用结构赋值

### 使用函数默认值

### 使用模板字符串

### 使用promise和async function处理异步

### 控制反转

## 可读性

### JSdoc

### Eslint

### Typescript

## 稳定性

### 为展开运算符使用默认值

### 使用可选链

### Reduce始终提供初始值

如果我们要合并数组内的元素，例如这样的结构：

```tsx
const arr = [{keywords: [...]}, {keywords: [...]}]
```

很容易想到这样的代码

```tsx
arr.reduce((a,b)=>[...a.keywords, ...b.keywords])
```

但是这隐藏着一个巨大的坑，因为**当数组仅有一个元素（无论位置如何）并且没有提供初始值&#x20;*****initialValue*****，或者有提供&#x20;*****initialValue*****但是数组为空，那么此唯一值将被返回且 `callbackfn`不会被执行。**

因此，当上面的例子中`arr`如果只有一个元素时，那么只能得到那个元素本身，而无法得到我们想要的数组。

```tsx
const arr = [{keywords: [...]}]
arr.reduce((a,b)=>[...a.keywords, ...b.keywords])
// {keywords: [...]}
```

此外，当数组为空时调用`reduce`方法，会触发报错。

```tsx
[].reduce((a,b)=>a>b?a:b)
// Uncaught TypeError: Reduce of empty array with no initial value
```

要避免出现这两个问题，就需要传递一个初始值，可以使用这个方法来解决上个例子中的问题。

```tsx
arr.reduce((a, b) => a.concat(b.keywords), [])
```

提供初始值后，无论是原数组是一个空数组，抑或是数组中只有一个元素，都能得到一个数组结构的返回值，且不会触发异常(原数组是空数组时)。

**因此使用reduce时通常传递初始值更安全。**

### Sort排序传递回调

在JavaScript中，`Array.prototype.sort()`方法用于对数组元素进行排序，**默认行为（即不传递回调函数时）是将元素转换成字符串再进行比较**。这可能会出现以下问题：

1. 排序结果不符合预期：由于默认的排序算法只是将元素转换成字符串后进行比较，并不考虑数字大小或其他特殊情况，因此可能会导致排序结果不符合预期。
2. 排序稳定性不确定：在某些JavaScript引擎中，使用默认排序算法在排序相等元素时可能会导致排序稳定性不确定，即在排序前和排序后两个相等的元素的位置关系可能会发生改变。自 ES10（EcmaScript 2019）起，[规范](https://tc39.es/ecma262/#sec-array.prototype.sort) 要求 `Array.prototype.sort`为稳定排序，但是ES10（EcmaScript 2019）以前没有要求稳定性。

例如：

```tsx
[1, 10, 2].sort()
// [1, 10, 2]
```

如果指明了 `compareFn` ，那么数组会按照调用该函数的返回值排序。即 a 和 b 是两个将要被比较的元素：

* 如果 `compareFn(a, b)` 大于 0，b 会被排列到 a 之前。
* 如果 `compareFn(a, b)` 小于 0，那么 a 会被排列到 b 之前；
* 如果 `compareFn(a, b)` 等于 0，a 和 b 的相对位置不变。备注：ECMAScript 标准并不保证这一行为，而且也不是所有浏览器都会遵守（例如 Mozilla 在 2003 年之前的版本）；
* `compareFn(a, b)` 必须总是对相同的输入返回相同的比较结果，否则排序的结果将是不确定的。

如果要比较数字而非字符串，则可以传递这样的回调函数来实现排序：

```tsx
// 升序排序
[1,10,2].sort((a,b)=> a-b)
// [1, 2, 10]

// 降序排序
[1,10,2].sort((a,b)=> b-a)
// [10, 2, 1]
```

**因此，如果要比较数字大小或其他特殊情况，则建议传入回调函数来确保排序结果的正确性和稳定性。如果必须使用默认排序算法，请确保测试充分，并在使用时注意兼容性问题。**

### Array.prototype.join遇到只有一个元素的数组时

**当数组只有一个元素时，`Array.prototype.join` 方法不会对元素做处理，而是直接返回这个元素，这意味着如果想对数组进行某些处理，那么一定要记得考虑只有一个元素的情况。**

例如你想实现为数组里每个元素都加上一个标记和换行符的效果时，你可能会这么写：

```tsx
[1,2,3].join('(标记)\n')
// '1(标记)\n2(标记)\n3' 
```

但是当数值只有一个元素时，就不会做任何处理：

```tsx
[1].join('(标记)\n')
// 1
```

有些时候join函数不对空数组进行处理可能是更合理的，但是有时候我们却不想这样，哪怕只有一个元素，我们也希望能够处理一遍。

解决的方法很简单，可以判断一下数组的长度，然后分别处理不同的情况。但是有一个更便捷的方法：先利用`map`先处理一遍，然后再利用`join`进行拼接。这样无论数组的长度如何，都可以正常地进行处理。

```tsx
arr.map(i=>i+'(标记)\n').join('')
// 当 arr = [1,2,3]
// '1(标记)\n2(标记)\n3(标记)\n'
// 当arr = [1]
// '1(标记)\n'
```

### 避免使用arguments

`arguments`是类数组，拥有`length`属性且以属性索引以0开始，类数组不拥有数组的任何方法，例如`forEach`、`map`等。

另一个问题是arguments在箭头函数中不存在，在箭头函数中访问`arguments`会返回`undefined`。

使用剩余参数替代，或者扩展运算符/Array.from将其转换成真正的数组是更好的选择，除非是需要在不支持ES6的浏览器上运行。**如果需要统一在函数和箭头函数中都能访问到所有参数，那应该使用剩余参数。**

### 禁用arguments.callee

> 在[严格模式 (en-US)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode)下，第 5 版 ECMAScript (**ES5**) 禁止使用 `arguments.callee()`。当一个函数必须调用自身的时候，避免使用 `arguments.callee()`，通过`要么`给函数表达式一个名字，要么使用一个函数声明。

`arguments.callee`引用当前执行函数本身，当这个函数是一个匿名函数时就很有用了，可以直接通过`arguments.callee` 执行自身，以实现递归调用。

```tsx
(function(arg){
     if(arg === 0) return  // 结束条件
     console.log(arg);
     arguments.callee(arg - 1) // 调用匿名函数自身
})(5)
```

**这是一个非常糟糕的方案，其缺点在于不能实现尾调用、内联，并且每次递归调用会获取到一个不同的 `this` 值。**

```tsx
(function(arg){
    if(arg === 0) return
    console.log(this);
     arguments.callee(arg - 1)
})(5)
// 每次打印的this都不同
```

而解决的方式也很简单，为使用函数名称进行调用即可。

```tsx
(function fn(arg){
    if(arg === 0) return
    console.log(this);
    fn(arg - 1);
})(5)
// window
```

避免使用`arguments.callee` 的优势在于：

* 该函数可以像代码内部的任何其他函数一样被调用
* 它不会在外部作用域中创建一个变量 ([除了 IE 8 及以下](http://kangax.github.io/nfe/#example_1_function_expression_identifier_leaks_into_an_enclosing_scope))
* 它具有比访问 arguments 对象更好的性能

### 为for…in添加hasOwnProperty

### 判断构造函数是否被new调用

JavaScript中的构造函数（或者说类）也可以作为普通函数被调用，虽然可以通过函数首字母是否大写的形式来判断是否是构造函数，但是最好的方法还是应该在代码中硬性规定一定要被new调用，以此避免可能的错误。

```tsx
function F(a){
	this.a = a
}

// 构造函数可以被直接调用，也可以被new调用
F(1)
new F(1)
```

检查是否被new调用可以使用`new.target`。`ES6` 为 `new` 命令引入了一个 `new.target` 属性，该属性一般用在构造函数之中，返回 `new` 命令作用于的那个构造函数。如果构造函数不是通过 `new` 命令或 `Reflect.construct()` 调用的，`new.target` 会返回 `undefined` 。

```tsx
function F(a){
    if(new.target == undefined){
        throw new Error('构造函数F必须要被new调用')
    }
	this.a = a
}
F(1)  
// 报错
new F(1)  
// {a: 1}
```

每次手动写判断方法太过麻烦，ES6的Class是构造函数的语法糖，Class规定必须要被new调用，否则将会报错。

```tsx
class A{}
// Uncaught TypeError: Class constructor A cannot be invoked without 'new'
```

因此可以使用Class语法替代原本的构造函数写法，如果要使用构造函数，那么最好加一层判断，避免被直接调用。

### 冻结对象

### 使用Symbol

Symbol最常用于避免对象冲突，另外一点就是常见的遍历方法不会遍历到symbol类型。

### 属性描述符

### Array.prototype.fill不要传递引用值为参数

使用Array.prototype.fill时不要直接将一个引用值作为参数传入，比较常见的例子是快速创建一个嵌套数组：

`let result = new Array(10).fill([])`

这将导致result中所有的元素都指向同一个数组，当你改变`result`中某个元素时，除非破坏这种引用关系，否则其他的元素都会一起改变。

```tsx
let result = new Array(10).fill([])
result
// [[],[],...,[]]
result[0].push(1)
// [[1],[1],...,[1]]
```

另一种想法是通过`map`来遍历依次赋值：

```tsx
let result = new Array(10).map(i=>([]))
result
// []
result.length
// 10
```

但是map这种ES6遍历方法会跳过数组中所有空的（undefined）元素，因此无法达到我们想要的效果。

有两种解决办法：

```tsx
let result = Array(5).fill(1).map(() => []);
let result = Array.from({length: 5}, () => []);
```

第一种先通过fil填充，再调用map依次赋值，第二种方法是通过Array.from来创建。

### Match和MatchAll使用捕获组

String.prototype.match在开启全局匹配（g）和不开启全局匹配时，返回的结果是不一样的，不开启全局匹配时，返回一个类数组结构，包含`input`、`groups`等字段，而在开启全局匹配时，返回的却是一个包含所有匹配结果的数组结构，此时不包含`input`、`groups`等字段。

**在需要使用捕获组时，建议在不需要开启全局匹配时使用match，在需要开启全局匹配时使用matchAll。**

```jsx
'1-1-1'.match(/(?<number>\\d)/)

[...'-1-1-1'.matchAll(/(?<number>\\d)/g)]
```

需要注意的是，matchAll返回的是一个迭代器，并且matchAll必须开启全局匹配模式（g），否则会报错。

## 技巧

### 获取随机数

### 判断类型
