JavaScript最佳实践

干净代码(clean code)

includes优化多条件判断

replace/match/exec使用分组

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

使用箭头函数

使用扩展运算符

使用结构赋值

使用函数默认值

使用模板字符串

使用promise和async function处理异步

控制反转

可读性

JSdoc

Eslint

Typescript

稳定性

为展开运算符使用默认值

使用可选链

Reduce始终提供初始值

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

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

很容易想到这样的代码

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

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

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

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

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

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

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

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

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

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

Sort排序传递回调

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

  1. 排序结果不符合预期:由于默认的排序算法只是将元素转换成字符串后进行比较,并不考虑数字大小或其他特殊情况,因此可能会导致排序结果不符合预期。

  2. 排序稳定性不确定:在某些JavaScript引擎中,使用默认排序算法在排序相等元素时可能会导致排序稳定性不确定,即在排序前和排序后两个相等的元素的位置关系可能会发生改变。自 ES10(EcmaScript 2019)起,规范 要求 Array.prototype.sort为稳定排序,但是ES10(EcmaScript 2019)以前没有要求稳定性。

例如:

[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) 必须总是对相同的输入返回相同的比较结果,否则排序的结果将是不确定的。

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

// 升序排序
[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 方法不会对元素做处理,而是直接返回这个元素,这意味着如果想对数组进行某些处理,那么一定要记得考虑只有一个元素的情况。

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

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

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

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

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

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

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

避免使用arguments

arguments是类数组,拥有length属性且以属性索引以0开始,类数组不拥有数组的任何方法,例如forEachmap等。

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

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

禁用arguments.callee

严格模式 (en-US)下,第 5 版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候,避免使用 arguments.callee(),通过要么给函数表达式一个名字,要么使用一个函数声明。

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

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

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

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

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

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

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

  • 该函数可以像代码内部的任何其他函数一样被调用

  • 它不会在外部作用域中创建一个变量 (除了 IE 8 及以下)

  • 它具有比访问 arguments 对象更好的性能

为for…in添加hasOwnProperty

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

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

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

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

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

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调用,否则将会报错。

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中某个元素时,除非破坏这种引用关系,否则其他的元素都会一起改变。

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

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

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

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

有两种解决办法:

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

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

Match和MatchAll使用捕获组

String.prototype.match在开启全局匹配(g)和不开启全局匹配时,返回的结果是不一样的,不开启全局匹配时,返回一个类数组结构,包含inputgroups等字段,而在开启全局匹配时,返回的却是一个包含所有匹配结果的数组结构,此时不包含inputgroups等字段。

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

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

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

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

技巧

获取随机数

判断类型

最后更新于