那些年,前端我们一起踩过的坑
javascript做为一门脚本语言,由于缺乏约束,以及各种自动容错机制和隐式转换,产生了很多容易误解和容易引发问题的地方, 《javascript语言精髓》一书中,有很大一部分篇幅介绍了javascript语言的糟粕和毒瘤部分,相信大部分问题有些人遇到过,有些人则通过学习知晓其原理而完美的躲过,随着ES规范的不断完善与发展,其中的一些问题得到了解决或完善, 本文总结了常见的坑点,前车之鉴,后车之覆,希望大家读了之后能少踩坑,少翻车,也能了解相关问题的根源所在。
1. replace只替换一次
这个是新人最常遇到的问题了,比如下面的例子,我想把空格全部替换掉
"are you kidding me?".replace(" ","-")
结果只替换了第一个字符,必须得用正则治疗
"are you kidding me?".replace(/\s/g,"-")
2. 数组不是基本类型,typeof 数组返回object
其实,数组是用对象模拟出来的,你可以用数组的任意下标赋值,甚至用字符串做下标也没关系,length返回的是值最大的那个索引,原来数组一直在欺骗我们
arr=[];
arr.a="hello";
console.log(arr.length)
arr["100"]="world";
console.log(arr.length)
3. Date 对象的月份从0开始
谁能告诉我,为啥月份是从0开始的,日期却是从1开始的。因为不统一,不符合直觉,一不小心就踩坑。
4. new Date("2019-01-01") 不兼容
new Date("2019-01-01") 在chrome,firefox 下运行是没有问题的,在safari和IE下都无法返回正常的结果,在ie下将日期格式改写为2019-1-1 能够得到正确的结果。大多数人只是凭直觉这么写了,而在chrome下刚好能返回正确的结果,谁知道在别的浏览器下就可能会翻车。
根据mdn文档, new Date创建日期对象 基于 Unix Time Stamp,即自1970年1月1日(UTC)起经过的毫秒数。
最好是老老实实的这样写:
new Date(2019,0,1)
经过实际测试 new Date("2019/01/01") 这样的日期格式也可以正确解析
5. 拥有ID属性的元素,会自动成为window范围内的全局变量
全局变量的泄漏真的不是因为你没用var声明,而是所有具有id的元素都会自动变成一个全局变量。这可能会无意中造成变量冲突。
6. 使用保留字做变量名或对象属性名
var class ={}; // 非法
obj={"class":".on"} ; // 非法
object = {case: value}; // 非法
object.case = value; // 非法
ES规范规定不能用保留字做变量名,使用点号引用属性名时也会出现问题。ES5规范修正了保留 字做属性名问题,所以现在点号引用属性名在chrome中能正常执行了,在IE9以下会产生报错。
7. 使用未声明的变量会报错
js变量不声明就可以赋值使用,使我们经常忽略了用var或let定位变量名,一方面会造成泄露出来很多全局变量,另一方面,有些是代码是if条件赋值,如果因为逻辑问题绕过了赋值的语句,就会产生异常了,例如如下代码,当条件赋值不成立时就会产生异常了
if(false){
UserInfo={userName:"Hello"}
}
if(UserInfo){
console.log("do something")
}
如果没给UserInfo赋值的语句没执行,则会报错 Uncaught ReferenceError: UserInfo is not defined 判断一个变量是否存在正确的姿势是用typeof 判断是否是undefined,如果是全局变量可用加window.变量名
还有一种常见的变量检测场景,如if(result.userInfo.userName)
这样写法,是很不安全的,一但result没有返回,或返回了null值,就会报错了,这绝对是js异常排行榜中稳居前几位的错了,安全的写法 if(result && result.userInfo && result.userInfo.userName)
8. 用for in 遍历会查找原型链引发的潜在问题
给数组或对象原型添加了一些方法之后,会被for in 遍历出来。我曾遇到不小心使用for in遍历数组,功能实现上也没有什么问题,某天之后,使用Array.prototype给数组添加了一 polyfill方法之后,功能出现问题
var obj ={"a":1,"b":2,"c":3};
Object.prototype.method1= function (){} ;
for(var key in obj){
console.log(obj[key]);
}
9. instanceof 在跨iframe页面调用时不起作用
第一次踩这个坑的时候真是排查了很久,属于万万没想到的问题, 一直认为instanceof 是很靠谱的,没想到它在跨iframe 使用时完全失效了。举例来说你在A页面定义了一个公共函数addConfig,如果传入的是对象就添加到配置列表中,如果传入是数组,就把多个配置都添加到配置列表中,很常见的功能场景
var configList=[];
function addConfig(config){
if(config instanceof Array){
configList=configList.concat(config)
}else{
configList.push(config)
}
}
在单个页面中使用是没问题,一旦在A页面中嵌入一个iframe子页面B,B页面使用parent.addConfig调用,就会出现问题,因为不同窗口(window)中的Array构造器函数是不相等的,所以instanceof 总是会返回false。
所以最安全的判断数组方法是用Array.isArray方法,如果是ES3兼容就用Array.prototype.toString判断。
其实上面那个例子有更简便的写法,用ES6延展操作符,连类型判断都省了,这姿势真帅。
function addConfig(config){
configList.push(...config)
}
10. arguments 不是数组
arguments只是和数组长的有点像,你想直接调用数组的那些方法是不行的,要先转化一下成为数组 [].slice.call(arguments);
11. 箭头函数没有arguments
遇到这种情况,乖乖的写普通函数吧,什么?你既想享受 this的便利,又想用arguments,箭头函数:臣妾做不到啊。
12. forEach 无法break和return
如果你的数组很大,想找到符合的项就退出循环,报歉,还是做不到,自己选择的forEach ,含着泪也要执行完。
解决方法
- 用try catch,在要退出循环的地方throw一个Error,不推荐,为了实现功能连异常都用上了,太丧心病狂了。
- 不用forEach,改用every和some 遍历数组 示例,找到3之后退出循环:
result=[1,2,3,4,5].some(function(item){
console.log(item)
if(item==3){
return true
}
})
13. forEach 不支持async
forEach 回调函数是独立的上下文,无法用async/await来实现阻塞外层函数执行的效果,示例代码
function getResponse(url,time){
return new Promise(function (resolve){
setTimeout(function (){
resolve(url);
},time)
});
}
var urlList=["/url1","url2"]
urlList.forEach(async (url)=>{
let res=await getResponse(url,2000);
console.log(res);
});
以上代码,如果用普通的for 循环是串行的,应该是2秒后输出url1 ,4秒后输出url2,但用forEach却是并行的,url1和url2同时输出了
14. 自动插分号,引起函数返回值不正确
如下代码
function test(){
return
{
a: "hello"
}
}
console.log(test()) //输出undefined
return 语句后面自动插分号了,相当于return ; 返回了undefined,这不是我想要的结果
15. 自动插分号,引起含有自执行函数的文件合并后执行不了
//文件1:
(function test1(){
console.log("test1");
})()
//文件2:
(function test2(){
console.log("test2");
})()
两个文件单独执行都没问题,但是用gulp打包合并在一起应会报错了,天啊,谁能想到呢,
Uncaught TypeError: (intermediate value)(...) is not a function
我在第一次遇到的时候真的是很抓狂,查了一整天,明明代码都没有错啊。尤其是打包还会压缩代码,根本看不出是哪里的问题,一开始都以为是编译器压缩代码出问题了,方向都搞错了,没想到浓眉大眼的家伙也会叛变革命了。
原因是js自动插分号,所以记得写分号是个好习惯。有些同学喜欢在文件头加个分号,不要觉得奇葩,就是为了避免这个问题。
16. this 是个任人打扮的小姑娘
obj.getName() 和 fun= obj.getName; fun(); 效果是不一样的, 老生常谈的问题了,当你不了解this的原理时,经常会犯如下的错误
var MyObject = function () {
var self = this;
this.alertMessage = "Javascript rules";
this.OnClick = function() {
alert(self.value);
}
}();
document.getElementById("theText").onclick = MyObject.OnClick
复习一下,this 与函数本身以及上下文都没有关系,只取决于函数是如何被调用的。记住这句话永远不会被this搞晕。
this只有4种情景:
-
纯粹的函数调用,this指向window
-
对象方法调用,必须是a.b()或
a [ "b" ]()
这种形式调用,this指向对象 -
构造函数调用,new Foo() 时,会自动生成一个空对象并返回,this指向这个新生成的空对象
-
call,apply,bind,this可以随意指定。
17. 未声明的变量赋值
变量不需要var声明就可以赋值(在严格模式下已经禁止) 在控制台输入,name="hello"
刷新页面再执行console.log(name)
值在页面刷新后居然还在,是不是很震惊。原因是直接给name赋值相当于给全局变量window.name赋值,window.name是进程级的,不会随窗口刷新消失。这个坑造成的最大问题就是泄露出来很多全局变量。
18. 令人疑惑的+操作符,隐式类型转换
面试题经常会出这些,就是因为很常见,一不小心就会踩坑,所以记一下常用的是很有必要的,加号操作符的两个操作数如果都是值类型,第一优先级是转换为string(如果其中有一个string),第二优先级是转换为number (如果其中有一个number)
// 1
console.log( 1 + 2 ); // 3
console.log( 1 + 2 +"3" ); //33
console.log( "3" + "4" ); // "34"
// 2
console.log( 1 + "3" ); // "13"
console.log( "3" + 1 ); // "31"
// 3
console.log( 1 + null ); //1
console.log( 1 + undefined ); //NaN
console.log( 1 + NaN );//NaN
// 4
console.log( "3" + null ); //3null
console.log( "3" + undefined ); //3undefined
console.log( "3" + NaN );//3NaN
// 5
console.log( 1 + {} ); //1[object Object]
console.log( 1 + [] ); //1
19. if判断检测变量的雷区
if 检测条件会被隐式转换为boolean类型,如下
if(something){
console.log(something)
}
真的要去背一下真值表吗,不然真的很难保证不踩到雷, 总结一下来说,当值为false,null,undefined,"",0,NaN时if条件不成立,其它条件都成立
以下是常见类型的测试:
if(false){
alert('false'); // false
}
if(undefined){
alert('false'); // false
}
if(null){
alert('null'); // false
}
if(0){
alert('0'); // false
}
if(''){
alert(''); // false
}
if(NaN){
alert('NaN'); // false
}
if(' '){
alert(' '); // true
}
if([]){
alert('[]'); // true
}
if({}){
alert('[]'); // true
}
if(new Object()){
alert('object'); // true
}
20. switch 穿透
switch case 如果不写break,会一直往下一个case执行,虽然会带来一些便利,但是会造成逻辑混乱难以读懂,其危害就像臭名昭著的goto语句一样,如下代码:
var type="a";
switch(type){
case "a":
console.log("aaa")
case "b":
console.log("bbb");
case "c":
console.log("cccc")
}
代码的执行结果是aaa,bbb,cccc都会输出, 因此,每写一个case都要先写上一个break,避免忘记。如果真的是有几个case需要执行相同的逻辑,那就封装一个闭包函数来调用。
21. 对象结尾多余的逗号问题
如果你还要兼容IE,这个问题绝对是挥之不去的梦魇
var obj={
a:1,
b:2,
}
多次因为这个问题出现上线后IE9以下浏览器整个挂掉。好在ES5标准已经要求兼容了,IE9以上不会再出现。