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仿qq界面的折叠菜单实现代码
Dec 12 Javascript
js实现简单的星级选择器提交效果适用于评论等
Oct 18 Javascript
jQuery 写的简单打字游戏可以提示正确和错误的次数
Jul 01 Javascript
easyui Droppable组件实现放置特效
Aug 19 Javascript
设置jQueryUI DatePicker默认语言为中文
Jun 04 Javascript
jquery配合.NET实现点击指定绑定数据并且能够一键下载
Oct 28 Javascript
js 两个日期比较相差多少天的实例
Oct 19 Javascript
JS处理数据四舍五入(tofixed与round的区别详解)
Oct 26 Javascript
浅谈Vue路由快照实现思路及其问题
Jun 07 Javascript
详解Angular中实现自定义组件的双向绑定的两种方法
Nov 23 Javascript
vue项目中仿element-ui弹框效果的实例代码
Apr 22 Javascript
Vue 3.0 全家桶抢先体验
Apr 28 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
用php获取远程图片并把它保存到本地的代码
2008/04/07 PHP
php实现用于计算执行时间的类实例
2015/04/18 PHP
CodeIgniter表单验证方法实例详解
2016/03/03 PHP
php查询内存信息操作示例
2019/05/09 PHP
通过PHP实现用户注册后邮箱验证激活
2020/11/10 PHP
javascript禁用键盘功能键让右击及其他键无效
2013/10/09 Javascript
jQuery 回车事件enter使用示例
2014/02/18 Javascript
js实现的点击div区域外隐藏div区域
2014/06/30 Javascript
js时间戳转为日期格式的方法
2015/12/28 Javascript
JavaScript 基础函数_深入剖析变量和作用域
2016/05/18 Javascript
详细总结Javascript中的焦点管理
2016/09/17 Javascript
bootstrapfileinput实现文件自动上传
2016/11/08 Javascript
浅析jQuery操作select控件的取值和设值
2016/12/07 Javascript
jQuery实现的简单在线计算器功能
2017/05/11 jQuery
Vue路由跳转问题记录详解
2017/06/15 Javascript
原生JS实现自定义滚动条效果
2020/10/27 Javascript
基于JavaScript中字符串的match与replace方法(详解)
2017/12/04 Javascript
Angular2 自定义表单验证器的实现方法
2018/12/14 Javascript
vue实现微信二次分享以及自定义分享的示例
2019/03/20 Javascript
JS实现省市县三级下拉联动
2020/04/10 Javascript
pycharm安装图文教程
2017/05/02 Python
Python使用plotly绘制数据图表的方法
2017/07/18 Python
pycharm 主题theme设置调整仿sublime的方法
2018/05/23 Python
Python在cmd上打印彩色文字实现过程详解
2019/08/07 Python
Python调用graphviz绘制结构化图形网络示例
2019/11/22 Python
python按顺序重命名文件并分类转移到各个文件夹中的实现代码
2020/07/21 Python
Pandas替换及部分替换(replace)实现流程详解
2020/10/12 Python
HTML5 Canvas绘制五星红旗
2016/05/04 HTML / CSS
英国著名的药妆网站:Escentual
2016/07/29 全球购物
欧舒丹澳洲版:L’OCCITANE
2017/07/17 全球购物
世嘉游戏英国官方商店:SEGA Shop UK
2019/09/20 全球购物
采购员的工作职责
2013/12/26 职场文书
交通事故赔偿协议书范本
2014/04/15 职场文书
小学绿色学校申报材料
2014/08/23 职场文书
查摆问题整改措施
2014/10/24 职场文书
详解Python+OpenCV绘制灰度直方图
2022/03/22 Python