在使用JS进行程序设计时,偶尔会写出结果与预期不符的代码,我将这些代码称为坑点,并记录下来以提高效率避免再出现。
1、Array.prototype.sort(fn)
需要对数组原地稳定排序时,可以使用 arr.sort(fn), 但别忘了指定排序规则函数fn(a,b),否则会按元素的unicode码大小升序排序,如[1,2,10] => [1,10,2]
2、Number类型运算
js中无整数,只有IEEE754标准的双精度64位浮点数,因此做加减乘除运算的时候会很坑:
// 第一个坑点
0.1 + 0.2 不等于 0.3 // 0.30000000000000004
0.3 -0.1 不等于 0.2 // 0.19999999999999998
0.1 * 0.2 不等于0.02 //0.020000000000000004
解决办法1是 parseFloat((5/3).toPrecision(12)) 来解决绝大部分的0.0..01和0.99.。99
解决办法2是 将小数转化成整数,再运算,再转换成小数。
当需要求一个被除数与除数的商和余数时,千万别套用C++的那一套
// C++
int a = 5/3; // 1
int b = 5%3; // 2
// JS
let a = 5/3 // 1.6666666666666667
let b = 5%3 // 2
解决办法是向下取整 let a = Math.floor(5/3); // 适用于求商,其他运算以此类推
补充!!!
当我们需要对一个数进行循环求余、求商的时候,Javascript的Number类型又有一个深坑,举个例子:
(1)将 2 转换为二进制数
(2)将 - 2 转换为二进制数
我们可能会这样写代码(伪代码)
let a = num, b = 0, res = "";
while(a!==0) {
let tmp = Math.floor(a/2);
b = a%2;
res = b + res;
a = tmp;
}
这时,我们发现 正数2
成功转换为了 “10”
, 而 负数-2
报错超时了,我们梳理下:
发现原因是因为 对 -0.xxx
进行向下取整是 -1
而非 0
,所以最终导致超时。
其实这个坑的关键是在我们使用了 Math.floor 来代替 C++ 的 int 取整,所以解决这个问题的办法是封装一个分类讨论的取整函数:
function _int(num) {
if(num >= 0 ) return Math.floor(num);
else return Math.ceil(num);
}
今后用这个 _int
就好了
3、引用类型
拿数组举例,当回溯过程中需要用到数组来返回结果时:
function ruc(arr, res) {
if(end_situation) res.push(arr);
...
ruc(arr);
...
}
如果这样写的话,最后结果毫无意外一定是
[[],[],[],...,[]]
或者
[[same],[same],...,[same]]
所以需要在传递前做一次 仅适合于一维数组的深拷贝
JSON.parse(JSON.stringify(arr));
or
arr.slice();
or
Object.assign([],arr);
4、二维数组
JS是无法像C++一样直接int arr[][] 或 int * arr[] 创建一个二维数组的,会稍微繁琐些,当我想创建一个1到9的3X3矩阵
时:
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
在起初,可能会这样做(错误)
let arr = [], num = 0;
for(let i=0;i<3;i++) {
for(let j=0;j<3;j++) {
arr[i][j] = ++num;
}
}
报错 TypeError: Cannot set property '0' of undefined
很显然,arr[0]是undefined,这种写法不对。
于是改成这样(也是错误)
let arr = [], num = 0;
// -------新增代码段开始-----------
let tmp = [];
for(let i=0;i<3;i++) {
arr.push(tmp);
}
// -------新增代码段结束--------------
for(let i=0;i<3;i++) {
for(let j=0;j<3;j++) {
arr[i][j] = ++num;
}
}
打印结果为[ [ 7, 8, 9 ], [ 7, 8, 9 ], [ 7, 8, 9 ] ]
很显然,又是引用的问题,有两种解决办法
(1)如第四点所说,在arr.push(tmp)
中将tmp深拷贝再传递
(2)改成下面这种写法(正确)
let arr = [], num = 0;
for(let i=0;i<3;i++) {
let tmp = [];
for(let j=0;j<3;j++) {
tmp.push(++num);
}
arr.push(tmp);
}
还可以用(正确)
let arr = new Array(3), num = 0;
for(let i=0;i<3;i++) {
arr[i] = new Array(3);
for(let j=0;j<3;j++) {
arr[i][j]= ++num;
}
}
这种写法和第一种错误写法思路很类似了,使用new Array([size])还能规定长度。
如果想采用ES6来创建,可以这样写:
let matrix = new Array(3).fill(0).map(v=>new Array(3).fill(0));
另外需要留意的是遍历
中会有坑的点:
(1)用arr.length取矩阵的高/行数
, arr[0].length取矩阵的宽/列数
(2)arr[i][j]
表示 第i
个一维数组的第j
个元素; 也表示 第i
行j
列的元素
5、运算符优先级
举个例子: 1与2异或等于3,那么按我们的常规思维来说来说 1^2 > 1 应该是得到布尔值true的,但是实际上返回0
1^2 > 1
等价于
1^(2>1)
这是因为运算符优先级
和左/右关联
导致的,需要时查阅MDN运算符优先级
为了避免运算符这个坑,按自己的思路加小括号()
强制优先运算即可,如(1^2)> 1
另外,有typeof,instanceof的优先级也小于比较运算符。
6、Array.prototype.splice(start, delete_cnt, item1, item2, item3, …)
用splice删除或插入元素时,数组长度会变,在搭配循环使用时需要留意索引的回退等等
for(let i=0;i<arr.length;i++) {
if(condition) {
arr.splice(i, 1);
// 注意回退索引:
i--;
}
}
7、隐式类型转换使用不当
当我们需要用双指针或滑动窗口遍历一个数组时,结束条件往往都是 right指针
超出数组下标范围时结束,因此会有两种写法:
写法一
while(right < nums.length)
这种写法完全没问题
写法二
while(nums[right])
这种写法更简短,但是错的,本意是如果 right指针
越界,取到的值就是 undefined
了,自然就会隐式转换为 false
可是没有考虑到 nums[right] = 0
的情况也会提前终止循环,所以这个是个坑,也得记下来。
另外如果遇到数组内元素本身就可能是 undefined
、null
的情况,需要更加谨慎使用隐式转换,最好使用严格等于 ===
进行判断。
8、二维数组的遍历
在操作一个二维数组时,经常会报数组越界的错误,最后编程5分钟,纠错2小时。
究其原因,是我经常把二维数组放在坐标轴中去思考,但实际上的正确做法是把二维数组当做一个m*n的矩阵来思考,举个例子:
我们定义一个二维数组 arr[ m ][ n ]
, 为了遍历这个数组,我定义当前所遍历到的数组元素为 arr[ x ][ y ]
如果把 arr 放在坐标轴中来看的话,arr[x][y] 就代表 第y行第x列
的元素
而把 arr 看成矩阵的话,arr[x][y] 就代表 第x行第y列
的元素
所以两者是完全反过来的,根据二维数组的定义:一维数组套一维数组,我们明显就可以判断,二维数组就是一个矩阵。
因此,将二维数组视作一个矩阵看待就能解决这个问题,以下是关于 x, y 的范围
m = arr.length;
n = arr[0].length;
有:
0 < x < m
0 < y < n
9、this指向问题
当我们需要抽离一个类/对象的函数出来使用时,需要手动调整函数内部的this指向
如:
class Foo {
constructor(num) {
this.num = num;
}
getNum() {
return this.num;
}
}
let obj = new Foo();
print(obj.getNum);
function print(fn) {
Promise.resolve()
.then(function() {
fn();
resovle();
})
.then(function() {
...
})
}
如果按这么写的话,将会得到一个报错:UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'num' of undefined
, 究其原因,是将 getNum 从对象/类原型函数中抽离出来,此时处在的作用域为 全局, this下无num这是其一,this为undefined是因为node默认采用的严格模式。
因此,改正过来的话 只需要 fn.call(obj)
即可
或者使用闭包进行改造
class Foo {
...
getNum() {
let context = this;
return function() {
return context.num;
}
}
}
10、过度嵌套箭头函数
使用箭头函数的好处就是节省篇幅,代码看起来很简短,在处理简单的逻辑时效率会很高,如 创建一个矩阵,给一个数组设置排序规则等等
// 创建m*n的矩阵
let arr = new Array(m).fill(0).map(v=>new Array(n).fill(0));
// 将nums按升序规则原地稳定排序
nums.sort((a,b) => a-b);
但是逻辑一旦复杂起来时,用箭头函数写出的代码可阅读性极差,让人难以费解,甚至还需要数括号来判断嵌套关系,如:
/*
const N = 5;
let obj = new Scheduler(N);
const timeout = ms => new Promise(res => setTimeout(res, ms));
const LOG = console.log;
*/
const addTask = obj.add((time, order) => timeout(time)).then(() => LOG(order));
以上为被注释的代码令人费解,大致逻辑是 then被add返回的Promise调用,但看的不仔细的话甚至会认为 then 是被 timeout(time) 返回的Promise调用的。
因此,避免这个坑的办法是 箭头函数超过1层嵌套时,就用函数声明把:
function addTask(time, order) {
obj.add(function task(time, order) {
timeout(time);
}).then(function() {
LOG(order);
});
}
但是箭头函数与声明式函数function应该是不能随意来回切换写法,因为箭头函数相较声明式函数function来说,有以下几点区别,为了简便描述,将箭头函数记作 arrow,将声明式函数记为 fn
(1)fn中this指向为调用它的环境,arrow的this指向固定为创建它的环境,call/apply/bind都无法改变
(2)arrow不可用作构造函数,因此无 new.target
(3)arrow无arguments属性,只可用 …params 得到参数列表 params
(4)无原型,无super
因此,应该在合适的环境中使用箭头函数