那些年,我们一起踩过的坑(前端防翻车指南)

6,216 阅读10分钟

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 ,含着泪也要执行完。

解决方法

  1. 用try catch,在要退出循环的地方throw一个Error,不推荐,为了实现功能连异常都用上了,太丧心病狂了。
  2. 不用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种情景:

    1. 纯粹的函数调用,this指向window
    1. 对象方法调用,必须是a.b()或 a [ "b" ]()这种形式调用,this指向对象
    1. 构造函数调用,new Foo() 时,会自动生成一个空对象并返回,this指向这个新生成的空对象
    1. 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以上不会再出现。

总结

本文总结了javascript常见的21个坑,他们有些是语言设计的缺陷,有些是特性本来如此,只是违反我们的直觉,人们总是在踩坑,错误,失败中不断吸取经验教训不断成长,聪明的人会从别人的问题中吸取经验教训,有了前车之鉴,我们可以少走很多弯路,加速成长。

最后,推荐一下我的github开源项目:github.com/windyfancy/…

希望大家多多支持。

参考资料:<javascript语言精髓> Nine Javascript Gotchas