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指向对象
- 对象方法调用,必须是a.b()或
-
- 构造函数调用,new Foo() 时,会自动生成一个空对象并返回,this指向这个新生成的空对象
-
- call,apply,bind,this可以随意指定。
17. 未声明的变量赋值
变量不需要var声明就可以赋值(在严格模式下已经禁止) 在控制台输入,name="hello"
刷新页面再执行console.log(name)
值在页面刷新后居然还在,是不是很震惊。原因是直接给name赋值相当于给全局变量window.name赋值,window.name是进程级的,不会随窗口刷新消失。这个坑造成的最大问题就是泄露出来很多全局变量。
18. 令人疑惑的+操作符,隐式类型转换
面试题经常会出这些,就是因为很常见,一不小心就会踩坑,所以记一下常用的是很有必要的,加号操作符的两个操作数如果都是值类型,第一优先级是转换为string(如果其中有一个string),第二优先级是转换为number (如果其中有一个number)
// 1console.log( 1 + 2 ); // 3console.log( 1 + 2 +"3" ); //33console.log( "3" + "4" ); // "34"// 2console.log( 1 + "3" ); // "13"console.log( "3" + 1 ); // "31"// 3console.log( 1 + null ); //1console.log( 1 + undefined ); //NaNconsole.log( 1 + NaN );//NaN// 4console.log( "3" + null ); //3nullconsole.log( "3" + undefined ); //3undefinedconsole.log( "3" + NaN );//3NaN// 5console.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开源项目:
希望大家多多支持。
参考资料:<javascript语言精髓> Nine Javascript Gotchas