# 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），否则会报错。

## 技巧

### 获取随机数

### 判断类型


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://1425816423.gitbook.io/my-knowledge-base/qian-duan-ji-shu/javascript/javascript-zui-jia-shi-jian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
