10个JavaScript中易犯小错误


Posted in Javascript onFebruary 14, 2016

在今天,JavaScript已经成为了网页编辑的核心。尤其是过去的几年,互联网见证了在SPA开发、图形处理、交互等方面大量JS库的出现。

如果初次打交道,很多人会觉得js很简单。确实,对于很多有经验的工程师,或者甚至是初学者而言,实现基本的js功能几乎毫无障碍。但是JS的真实功能却比很多人想象的要更加多样、复杂。JavaScript的许多细节规定会让你的网页出现很多意想不到的bug,搞懂这些bug,对于成为一位有经验的JS开发者很重要。

10个JavaScript中易犯小错误

常见错误一:对于this关键词的不正确引用

我曾经听一位喜剧演员说过:

“我从未在这里,因为我不清楚这里是哪里,是除了那里之外的地方吗?”

这句话或多或少地暗喻了在js开发中开发者对于this关键字的使用误区。This指代的是什么?它和日常英语口语中的this是一个意思吗?

随着近些年js编程不断地复杂化,功能多样化,对于一个程序结构的内部指引、引用也逐渐变多起来

下面让我们一起来看这一段代码:

Game.prototype.restart = function () {  this.clearLocalStorage(); 

  this.timer = setTimeout(function(){   this.clearBoard();    }, 0);

 };

运行上面的代码将会出现如下错误:

Uncaught TypeError: undefined is not a function

这是为什么?this的调用和它所在的环境密切相关。之所以会出现上面的错误,是因为当你在调用 setTimeout()函数的时候, 你实际调用的是window.setTimeout(). 因此,在 setTimeout() 定义的函数其实是在window背景下定义的,而window中并没有 clearBoard() 这个函数方法。

下面提供两种解决方案。第一种比较简单直接的方法便是,把this存储到一个变量当中,这样他就可以在不同的环境背景中被继承下来:

Game.prototype.restart = function () {  this.clearLocalStorage(); 

  var self = this;

  this.timer = setTimeout(function(){   self.clearBoard();}, 0); };

第二种方法便是用bind()的方法,不过这个相比上一种要复杂一些。

Game.prototype.restart = function () {  this.clearLocalStorage(); 

  this.timer = setTimeout(this.reset.bind(this), 0); };   

Game.prototype.reset = function(){   this.clearBoard();};

上面的例子中,两个this均指代的是Game.prototype。

常见错误二:传统编程语言的生命周期误区

另一种易犯的错误,便是带着其他编程语言的思维,认为在JS中,也存在生命周期这么一说。请看下面的代码:

for (var i = 0; i < 10; i++) {  /* ... */ } console.log(i);

如果你认为在运行console.log() 时肯定会报出 undefined 错误,那么你就大错特错了。我会告诉你其实它会返回 10吗。

当然,在许多其他语言当中,遇到这样的代码,肯定会报错。因为i明显已经超越了它的生命周期。在for中定义的变量在循环结束后,它的生命也就结束了。但是在js中,i的生命还会继续。这种现象叫做 variable hoisting。

而如果我们想要实现和其他语言一样的在特定逻辑模块中具有生命周期的变量,可以用let关键字。

常见错误三:内存泄露

内存泄露在js变成中几乎是一个无法避免的问题。如果不是特别细心的话,在最后的检查过程中,肯定会出现各种内存泄露问题。下面我们就来举例说明一下:

var theThing = null; 

var replaceThing = function () {  

    var priorThing = theThing; 

    var unused = function () { 

         if (priorThing) {    console.log("hi");   }  

    }; 

    theThing = {   longStr: new Array(1000000).join('*'), // 

         someMethod: function () {    console.log(someMessage);   }  
    }; 
};  
setInterval(replaceThing, 1000);

如果运行上面的代码,你会发现你已经造成了大量的内存泄露,每秒泄露1M的内存,显然光靠GC(垃圾回收器)是无法帮助你的了。由上面的代码来看,似乎是longstr在每次replaceThing调用的时候都没有得到回收。这是为什么呢?

每一个theThing结构都含有一个longstr结构列表。每一秒当我们调用 replaceThing, 它就会把当前的指向传递给 priorThing. 但是到这里我们也会看到并没有什么问题,因为 priorThing 每回也是先解开上次函数的指向才会接受新的赋值。并且所有的这一切都是发生在 replaceThing 函数体当中,按常理来说当函数体结束之后,函数中的本地变量也将会被GC回收,也就不会出现内存泄露的问题了,但是为什么会出现上面的错误呢?

这是因为longstr的定义是在一个闭包中进行的,而它又被其他的闭包所引用,js规定,在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。

常见错误四:比较运算符

JavaScript中一个比较便捷的地方,便是它可以给每一个在比较运算的结果变量强行转化成布尔类型。但是从另一方面来考虑,有时候它也会为我们带来很多不便,下面的这些例子便是一些一直困扰很多程序员的代码实例:

console.log(false == '0'); 

console.log(null == undefined); 

console.log(" \t\r\n" == 0); 

console.log('' == 0); // And these do too! 

if ({}) // ... 

if ([]) // ...

最后两行的代码虽然条件判断为空(经常会被人误认为转化为false),但是其实不管是{ }还是[ ]都是一个实体类,而任何的类其实都会转化为true。就像这些例子所展示的那样,其实有些类型强制转化非常模糊。因此很多时候我们更愿意用 === 和 !== 来替代== 和 !=, 以此来避免发生强制类型转化。. ===和!== 的用法和之前的== 和 != 一样,只不过他们不会发生类型强制转换。另外需要注意的一点是,当任何值与 NaN 比较的时候,甚至包括他自己,结果都是false。因此我们不能用简单的比较字符来决定一个值是否为 NaN 。我们可以用内置的 isNaN() 函数来辨别:

console.log(NaN == NaN);  // false 

console.log(NaN === NaN);  // false 

console.log(isNaN(NaN));  // true

常见错误五:低效的DOM操作

js中的DOM基本操作非常简单,但是如何能有效地进行这些操作一直是一个难题。这其中最典型的问题便是批量增加DOM元素。增加一个DOM元素是一步花费很大的操作。而批量增加对系统的花销更是不菲。一个比较好的批量增加的办法便是使用 document fragments :

var div = document.getElementsByTagName("my_div"); 

var fragment = document.createDocumentFragment(); 

 for (var e = 0; e < elems.length; e++) { fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

直接添加DOM元素是一个非常昂贵的操作。但是如果是先把要添加的元素全部创建出来,再把它们全部添加上去就会高效很多。

常见错误六:在for循环中的不正确函数调用

请大家看以下代码:

var elements = document.getElementsByTagName('input');

var n = elements.length; 

for (var i = 0; i < n; i++) {   

elements[i].onclick = function() {     

console.log("This is element #" + i);   }; }

运行以上代码,如果页面上有10个按钮的话,点击每一个按钮都会弹出 “This is element #10”! 。这和我们原先预期的并不一样。这是因为当点击事件被触发的时候,for循环早已执行完毕,i的值也已经从0变成了。

我们可以通过下面这段代码来实现真正正确的效果:

var elements = document.getElementsByTagName('input'); 

var n = elements.length; 

var makeHandler = function(num) { // outer function

   return function() { 

console.log("This is element #" + num);   }; }; 

for (var i = 0; i < n; i++) 

{   elements[i].onclick = makeHandler(i+1); }

在这个版本的代码中, makeHandler 在每回循环的时候都会被立即执行,把i+1传递给变量num。外面的函数返回里面的函数,而点击事件函数便被设置为里面的函数。这样每个触发函数就都能够是用正确的i值了。

常见错误七:原型继承问题

很大一部分的js开发者都不能完全掌握原型的继承问题。下面具一个例子来说明:

BaseObject = function(name) {   

if(typeof name !== "undefined") 

{     this.name = name;   } 

else 

{     this.name = 'default'   } };

这段代码看起来很简单。如果你有name值,则使用它。如果没有,则使用 ‘default':

var firstObj = new BaseObject(); 

var secondObj = new BaseObject('unique'); 

console.log(firstObj.name); // -> 结果是'default' 

console.log(secondObj.name); // -> 结果是 'unique'

但是如果我们执行delete语句呢:

delete secondObj.name;

我们会得到:

console.log(secondObj.name); // -> 结果是 'undefined'

但是如果能够重新回到 ‘default'状态不是更好么? 其实要想达到这样的效果很简单,如果我们能够使用原型继承的话:

BaseObject = function (name) 

{   if(typeof name !== "undefined") 

{     this.name = name;   } }; 

BaseObject.prototype.name = 'default';

在这个版本中, BaseObject 继承了原型中的name 属性, 被设置为了 'default'.。这时,如果构造函数被调用时没有参数,则会自动设置为 default。相同地,如果name 属性被从BaseObject移出,系统将会自动寻找原型链,并且获得 'default'值:

var thirdObj = new BaseObject('unique'); 

 console.log(thirdObj.name); 

 delete thirdObj.name;

 console.log(thirdObj.name); // -> 结果是 'default'

常见错误八:为实例方法创建错误的指引

我们来看下面一段代码:

var MyObject = function() {} 

 MyObject.prototype.whoAmI = function() {   

console.log(this === window ? "window" : "MyObj"); }; 

 var obj = new MyObject();

现在为了方便起见,我们新建一个变量来指引 whoAmI 方法, 因此我们可以直接用 whoAmI() 而不是更长的obj.whoAmI():

var whoAmI = obj.whoAmI;

接下来为了确保一切都如我们所预测的进行,我们可以将 whoAmI 打印出来:

console.log(whoAmI);

结果是:

function () {   console.log(this === window ? "window" : "MyObj"); }

没有错误!

但是现在我们来查看一下两种引用的方法:

obj.whoAmI(); // 输出 "MyObj" (as expected) 

whoAmI();   // 输出 "window" (uh-oh!)

哪里出错了呢?

原理其实和上面的第二个常见错误一样,当我们执行 var whoAmI = obj.whoAmI;的时候,新的变量 whoAmI 是在全局环境下定义的。因此它的this 是指window, 而不是obj!

正确的编码方式应该是:

var MyObject = function() {} 

MyObject.prototype.whoAmI = function() {   

   console.log(this === window ? "window" : "MyObj"); }; 

var obj = new MyObject(); 

obj.w = obj.whoAmI;  // still in the obj namespace obj.whoAmI(); // 输出 "MyObj" (as expected) 

obj.w();    // 输出 "MyObj" (as expected)

常见错误九:用字符串作为setTimeout 或者 setInterval的第一个参数

首先我们要声明,用字符串作为这两个函数的第一个参数并没有什么语法上的错误。但是其实这是一个非常低效的做法。因为从系统的角度来说,当你用字符串的时候,它会被传进构造函数,并且重新调用另一个函数。这样会拖慢程序的进度。

setInterval("logTime()", 1000); 

setTimeout("logMessage('" + msgValue + "')", 1000);

另一种方法是直接将函数作为参数传递进去:

setInterval(logTime, 1000);  

setTimeout(function() { 

logMessage(msgValue); }, 1000);

常见错误十:忽略 “strict mode”的作用

 

“strict mode” 是一种更加严格的代码检查机制,并且会让你的代码更加安全。当然,不选择这个模式并不意味着是一个错误,但是使用这个模式可以确保你的代码更加准确无误。

下面我们总结几条“strict mode”的优势:

1. 让Debug更加容易:在正常模式下很多错误都会被忽视掉,“strict mode”模式会让Debug极致更加严谨。

2. 防止默认的全局变量:在正常模式下,给一个为经过声明的变量命名将会将这个变量自动设置为全局变量。在strict模式下,我们取消了这个默认机制。

3. 取消this的默认转换:在正常模式下,给this关键字指引到null或者undefined会让它自动转换为全局。在strict模式下,我们取消了这个默认机制。

4. 防止重复的变量声明和参数声明:在strict模式下进行重复的变量声明会被抱错,如(e.g., var object = {foo: "bar", foo: "baz"};) 同时,在函数声明中重复使用同一个参数名称也会报错,如 (e.g., function foo(val1, val2, val1){}),

5. 让eval()函数更加安全。

6. 当遇到无效的delete指令的事后报错:delete指令不能对类中未有的属性执行,在正常情况下这种情况只是默默地忽视掉,而在strict模式是会报错的。

正如和其他的技术语言一样,你对JavaScript了解的的越深,知道它是如何运作,为什么这样运作,你才会熟练地掌握并且运用这门语言。相反地,如果你缺少对JS模式的认知的话,你就会碰上很多的问题。了解JS的一些细节上的语法或者功能将会有助于你提高编程的效率,减少变成中遇到的问题。

Javascript 相关文章推荐
JavaScript 继承详解(一)
Jul 13 Javascript
js 全兼容可高亮二级缓冲折叠菜单
Jun 04 Javascript
ie支持function.bind()方法实现代码
Dec 27 Javascript
使用js实现雪花飘落效果
Aug 26 Javascript
jQuery 浮动导航菜单适合购物商品类型的网站
Sep 09 Javascript
jQuery中toggle()函数的使用实例
Apr 17 Javascript
jQuery实现的兼容性浮动层示例
Aug 02 Javascript
原生JS实现的简单小钟表功能示例
Aug 30 Javascript
全面分析JavaScript 继承
May 30 Javascript
JS中的算法与数据结构之集合(Set)实例详解
Aug 20 Javascript
在vue和element-ui的table中实现分页复选功能
Dec 04 Javascript
JavaScript Reflect Metadata实现详解
Dec 12 Javascript
讲解JavaScript的Backbone.js框架的MVC结构设计理念
Feb 14 #Javascript
深入解析JavaScript框架Backbone.js中的事件机制
Feb 14 #Javascript
Node.js 条形码识别程序构建思路详解
Feb 14 #Javascript
jQuery插件支持同一页面被多次调用
Feb 14 #Javascript
JavaScript中通过提示框跳转页面的方法
Feb 14 #Javascript
JavaScript中关联原型链属性特性
Feb 13 #Javascript
JavaScript操作class和style样式代码详解
Feb 13 #Javascript
You might like
如何开始收听短波广播
2021/03/01 无线电
PHPThumb图片处理实例
2014/05/03 PHP
typecho插件编写教程(六):调用接口
2015/05/28 PHP
windows环境下使用Composer安装ThinkPHP5
2018/05/18 PHP
PHP使用PDO实现mysql防注入功能详解
2019/12/20 PHP
Thinkphp 框架扩展之Widget扩展实现方法分析
2020/04/23 PHP
如何在标题栏显示框架内页面的标题
2007/02/03 Javascript
Javascript insertAfter() 实现函数代码
2011/10/12 Javascript
jQuery图片的展开和收缩实现代码
2013/04/16 Javascript
JavaScript在IE和FF下的兼容性问题
2014/05/19 Javascript
jquery的attr方法禁用表单元素禁用输入内容
2014/06/23 Javascript
jquery密码强度校验
2015/12/02 Javascript
用NodeJS实现批量查询地理位置的经纬度接口
2016/08/16 NodeJs
详解使用React全家桶搭建一个后台管理系统
2017/11/04 Javascript
Vue2.0实现组件数据的双向绑定问题
2018/03/06 Javascript
node打造微信个人号机器人的方法示例
2018/04/26 Javascript
浅谈webpack4 图片处理汇总
2018/09/12 Javascript
微信运维交互机器人的示例代码
2018/11/12 Javascript
微信小程序如何使用云开发
2019/05/17 Javascript
微信小程序云开发之数据库操作
2019/05/18 Javascript
详解JavaScript之ES5的继承
2020/07/08 Javascript
vue单应用在ios系统中实现微信分享功能操作
2020/09/07 Javascript
微信小程序开发数据缓存基础知识辨析及运用实例详解
2020/11/06 Javascript
[01:19:54]DOTA2上海特级锦标赛主赛事日 - 2 败者组第二轮#1Alliance VS EHOME
2016/03/03 DOTA
python 从远程服务器下载东西的代码
2013/02/10 Python
windows下安装Python虚拟环境virtualenvwrapper-win
2019/06/14 Python
浅谈keras中的batch_dot,dot方法和TensorFlow的matmul
2020/06/18 Python
Matplotlib自定义坐标轴刻度的实现示例
2020/06/18 Python
css3学习系列之移动属性详解
2017/07/04 HTML / CSS
Css3实现无缝滚动防抖
2020/09/14 HTML / CSS
html5实现图片转圈的动画效果——让页面动起来
2017/10/16 HTML / CSS
测绘工程系学生的自我评价
2013/11/30 职场文书
党支部书记岗位责任制
2014/02/11 职场文书
法定代表人授权委托书
2014/09/19 职场文书
2015学习委员工作总结范文
2015/04/03 职场文书
学雷锋献爱心倡议书
2015/04/27 职场文书