Javascript踩坑记录

在使用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

解决办法1parseFloat((5/3).toPrecision(12)) 来解决绝大部分的0.0..010.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个元素; 也表示 第ij列的元素

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 的情况也会提前终止循环,所以这个是个坑,也得记下来。

另外如果遇到数组内元素本身就可能是 undefinednull 的情况,需要更加谨慎使用隐式转换,最好使用严格等于 === 进行判断。

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

因此,应该在合适的环境中使用箭头函数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值