我但愿本身尽早知道的 7 个 JavaScript 怪癖(转载oschina)

若是对你来讲JavaScript仍是一门全新的语言,或者你是在最近的开发中才刚刚对它有所了解,那么你可能会有些许挫败 感。任何编程语言都有它本身的怪癖(quirks)——然而,当你从那些强类型的服务器端语言转向JavaScript的时候 ,你会感到很是困惑。我就是这样!当我在几年前作全职JavaScript开发的时候,我多么但愿关于这门语言的许多事情我能尽早地知道。我但愿经过本文中分享的一些怪癖能让你免于遭受我所经历过的那些头疼的日子。本文并不是一个详尽的列表,只是一些取样,目的是抛砖引玉,而且让你明白当你一旦逾越了这些障碍,你会发现JavaScript是多么强大。javascript

咱们会把焦点放在下面这些怪癖上:php

1.) 相等

由于C#的缘故我习惯于用==运算符来作比较。具备相同值的值类型(以及字符串)是相等 的,反之否则。指向相同引用的引用类型是相等的,反之也否则。(固然这是创建在你没有重载==运算符或者GetHashCode方法的前提下)当我知道 JavaScript有==和===两种相等运算符时,令我惊诧不已。我所见过的大多数状况都是使用==,因此我如法炮制。然而,当我运行下面的代码时 JavaScript并无给我想固然的结果:html

1 var x = 1;
2  
3 if(x == "1") {
4     console.log("YAY! They're equal!");
5 }

呃……这是什么黑魔法?整型数1怎么会和字符串”1”相等?java

在JavaScript里有相等(equality ==)和恒等(strict equality ===)。相等运算符会先会先把运算符两边的运算元强制转换为同种类型,而后再进行恒等比较。因此上面例子中的字符串”1”会先被转换成整数1,而后再和 咱们的变量x进行比较。node

恒等不会进行强制类型转换。若是运算元是不一样类型的(就像整型数1和字符串”1”)那么他们就是不相等的:git

01 var x = 1;
02  
03 // 对于恒等,首先类型必须同样
04 if(x === "1") {
05     console.log("Sadly, I'll never write this to the console");
06 }
07  
08 if(x === 1) {
09     console.log("YES! Strict Equality FTW.")
10 }

你可能已经开始为各类不可预知的强制类型转换担心了,它们可能会在你的应用中让真假混乱,致使一些bug,而这些bug你很难从代码中看出来。这并不奇怪,所以,那些有经验的JavaScript开发者建议咱们老是使用恒等运算符。github

2.) 点号 vs 方括号

你可能会对JavaScript中用访问数组元素的方式来访问一个对象的属性这种形式感到诧异,固然,这取决于你以前使用的其余语言:web

1 // getting the "firstName" value from the person object:
2 var name = person.firstName;
3  
4 // getting the 3rd element in an array:
5 var theOneWeWant = myArray[2]; // remember, 0-based index

然而 ,你知道咱们也能用方括号来引用对象的成员吗?例如:正则表达式

1 var name = person["firstName"];

那这有什么用呢?可能大部分时间你仍是使用点号,然而有些为数很少的状况下,方括号给咱们提供了一些点号方式没法完成的捷径。好比,我可能会常常把一些大的switch语句重构成一个调度表(dispatch table),像下面这样:express

01 var doSomething = function(doWhat) {
02     switch(doWhat) {
03         case "doThisThing":
04             // more code...
05         break;
06         case "doThatThing":
07             // more code...
08         break;
09         case "doThisOtherThing":
10             // more code....
11         break;
12         // additional cases here, etc.
13         default:
14             // default behavior
15         break;
16     }
17 }

它们能被转换成下面这样:

01 var thingsWeCanDo = {
02     doThisThing      : function() { /* behavior */ },
03     doThatThing      : function() { /* behavior */ },
04     doThisOtherThing : function() { /* behavior */ },
05     default          function() { /* behavior */ }
06 };
07  
08 var doSomething = function(doWhat) {
09     var thingToDo = thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default"
10     thingsWeCanDo[thingToDo]();
11 }

固然,使用switch自己并无什么错(而且,在大多数状况下,若是你对迭代和性能很在乎的话,switch可能比调度表要好)。然而,调度表提供了一种更好的组织和扩展方式,而且方括号容许你在运行时动态地引用属性。

3.) 函数上下文

已经有不少不错的博客里解释过JavaScript中的this所表明的上下文(而且, 我在本文末尾也添加了这些博文的连接),然而,我仍是明确地决定把它加到我“但愿本身尽早知道的事”的清单里。在代码的任意地方明确this所表明的东西 是什么并不困难——你只须要记住几条规则。然而,我以前读过的那些关于这点的解读只能增添个人困惑,所以,我尝试用一种简单的方式来表述。

第一,开始时假设它是全局的

默认状况下,this引用的是全局对象(global object),直到有缘由让执行上下文发生了改变。在浏览器里它指向的就是window对象(或者在node.js里就是global)。

第二,方法内部的this

若是你有个对象中的某个成员是个function,那么当你从这个对象上调用这个方法的时候this就指向了这个父对象。例如:

01 var marty = {
02     firstName: "Marty",
03     lastName: "McFly",
04     timeTravel: function(year) {
05         console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
06     }
07 }
08  
09 marty.timeTravel(1955);
10 // Marty McFly is time traveling to 1955

你可能已经知道你能够经过建立一个新的对象,来引用marty对象上的timeTravel方法。这确实是JavaScript一个很是强大的特性——能让咱们把函数应用到不止一个目标实例上:

1     var doc = {
2     firstName: "Emmett",
3     lastName: "Brown",
4 }
5  
6 doc.timeTravel = marty.timeTravel;

那么,咱们调用doc.timeTravel(1885)会发生什么事呢?

1 doc.timeTravel(1885);
2 // Emmett Brown is time traveling to 1885

呃……再一次被黑魔法深深地刺伤了。其实事实也并不是如此,还记得咱们前面提到过的当你调用一个方法,那么这个方法中的this将指向调用它的那个父对象。握紧你德罗宁(DeLoreans)跑车的方向盘吧,由于车子变重了。(译注:做者示例代码的参考背景是一部叫《回到将来》 的电影,Marty McFly 是电影里的主角,Emmett Brown 是把DeLoreans跑车改装成时光旅行机的博士,因此marty对象和doc对象分别指代这两人。而此时this指向了doc对象,博士比Marty 重,因此……我必定会看一下这部电影。 )

当咱们保存了一个marty.TimeTravel方法的引用而且经过这个引用调用这个方法时到底发生了什么事呢?咱们来看一下:

1 var getBackInTime = marty.timeTravel;
2 getBackInTime(2014);
3 // undefined undefined is time traveling to 2014

为何是“undefined undefined”?!为何不是“Marty McFly”?

让咱们问一个关键的问题:当咱们调用getBackInTime函数时,它的父/拥有者 对象是谁呢?由于getBackInTime函数是存在于window上的,咱们是把它看成函数(function)调用,而不是某个对象的方 (method)。当咱们像上面这样直接调用一个没有拥有者对象的函数的时候,this将会指向全局对象。David Shariff对此有个很妙的描述:

不管什么时候,当一个函数被调用,咱们必须看方括号或者是圆括号左边紧邻的位置,若是咱们看到一个引用(reference),那么传到function里面的this值就是指向这个方法所属于的那个对象,如若否则,那它就是指向全局对象的。

由于getBackInTime的this是指向window的,而window对象里并无firstName和lastName属性,这就是解释了为何咱们看到的会是“undefined undefined”。

所以,咱们就知道了直接调用一个没有拥有者对象的函数时结果就是其内部的this将会是 全局对象。可是,我也说过咱们的getBackInTime函数是存在于window上的。我是怎么知道的呢?除非我把getBackInTime包裹到 另外一个不一样的做用域中,不然我声明的任何变量都会附加到window上。下面就是从Chrome的控制台中获得的证实:

jsquirgwfwrks_afjq_1

如今是讨论关于this诸多重点之一——绑定事件处理函数——的最佳时机。

第三(其实只是第二点的一个扩展),异步调用的方法内部的this

咱们假设在某个button被点击的时候咱们想调用marty.timeTravel方法:

1 var flux = document.getElementById("flux-capacitor");
2 flux.addEventListener("click", marty.timeTravel);

当咱们点击button的时候,上面的代码会输出“undefined undefined is time traveling to [object MouseEvent]”。什么?!好吧,首先,最显而易见的问题是咱们没有给timeTravel方法提供year参数。反而是把这个方法直接做为一个 事件处理函数,而且,MouseEvent被做为第一个参数传进了事件处理函数中。这个很容易修复,然而真正的问题是咱们又一次看到了 “undefined undefined”。别失望,你已经知道为何会发生这种状况了(即便你没有意识到这一点)。让咱们修改一下timeTravel函数,输出this来 帮助咱们得到一些线索:

1 marty.timeTravel = function(year) {
2     console.log(this.firstName + " " this.lastName + " is time traveling to " + year);
3     console.log(this);
4 };

如今咱们再点击button的时候,应该就能在浏览器控制台中看到相似下面这样的输出:

jsquigwerrks_afjq_2

在方法被调用时第二个console.log输出了this,它其实是咱们绑定的 button元素。感到奇怪么?就像以前咱们把marty.timeTravel赋值给一个getBakInTime的变量引用同样,此时的 marty.timeTravel被保存为咱们事件处理函数的引用,而且被调用了,可是并非从“拥有者”marty对象那里调用的。在这种状况下,它是 被button元素实例中的事件触发接口调用的。

那么,有没有可能让this是咱们想要的东西呢?固然能够!这种状况下,解决方案很是简 单。咱们能够用一个匿名函数代替marty.timeTravel来作事件处理函数,而后在这个匿名函数里调用marty.timeTravel。同时这 样也让咱们有机会修复以前丢失year参数的问题。

1 flux.addEventListener("click"function(e) {
2     marty.timeTravel(someYearValue);
3 });

点击button会看到像下面这样的输出:

jsquisgwegerks_afjq_3

成功了!可是为何成功呢?思考一下咱们是怎么调用timeTravel方法的。第一次 的时候咱们是把这个方法的自己的引用做为事件处理函数,所以它并非从父对象marty上调用的。第二次的时候,咱们的匿名函数中的this是指向 button元素的,然而当咱们调用marty.timeTravel时,咱们是从父对象marty上调用的,因此此时这个方法里的this是 marty。

第四,构造函数里的this

当你用构造函数建立一个对象的实例时,那么构造函数里的this就是你新建的这个实例。例如:

01 var TimeTraveler = function(fName, lName) {
02     this.firstName = fName;
03     this.lastName = lName;
04     // Constructor functions return the
05     // newly created object for us unless
06     // we specifically return something else
07 };
08  
09 var marty = new TimeTraveler("Marty""McFly");
10 console.log(marty.firstName + " " + marty.lastName);
11 // Marty McFly

使用Call,Apply和Bind

从上面给出的例子你可能已经猜到了,经过一些语言级别的特性是容许咱们在调用一个函数的时候指定它在运行时的this的。让你给猜对了。call和apply方法存在于Function的prototype中,它们容许咱们在调用一个方法的时候传入一个this的值。

call方法的签名中先是指定this参数,其后跟着的是方法调用时要用到的参数,这些参数是各自分开的。

1 someFn.call(this, arg1, arg2, arg3);

apply的第一个参数一样也是this的值,而其后跟着的是调用这个函数时的参数的数组。

1 someFn.apply(this, [arg1, arg2, arg3]);

咱们的doc和margy对象本身能进行时光旅行(译注:即对象中有 timeTravel方法),然而爱因斯坦(译注:Einstein,电影中博士的宠物,是一只狗)须要别人的帮助才能进行时光旅行,因此如今让咱们给之 前的doc对象(就是以前把marty.timeTravel赋值给doc.timeTravel的那个版本)添加一个方法,这样doc对象就能帮助 einstein对象进行时光旅行了:

1 doc.timeTravelFor = function(instance, year) {
2     this.timeTravel.call(instance, year);
3     // alternate syntax if you used apply would be
4     // this.timeTravel.apply(instance, [year]);
5 };

如今咱们能够送爱因斯坦上路了:

1 var einstein = {
2     firstName: "Einstein",
3     lastName: "(the dog)"
4 };
5 doc.timeTravelFor(einstein, 1985);
6 // Einstein (the dog) is time traveling to 1985

我知道这个例子让你有些出乎意料,然而这已经足以让你领略到把函数指派给其余对象调用的强大。

这里还有一种咱们还没有探索的可能性。咱们给marty对象加一个goHome的方法,这个方法是个让marty回到将来的捷径,由于它实际上是调用了this.timeTravel(1985):

1 marty.goHome = function() {
2     this.timeTravel(1985);
3 }

咱们已经知道,若是把 marty.goHome 做为事件处理函数绑定到button的click事件上,那么this就是这个button。而且,button对象上也并无timeTravel这个 方法。咱们能够用以前那种匿名函数的办法来绑定事件处理函数,再在匿名函数里调用marty对象上的方法。不过,咱们还有另一个办法,那就是bind函数:

1 flux.addEventListener("click", marty.goHome.bind(marty));

bind函数实际上是返回一个新函数,而这个新函数中的this值正是用bind的参数来指定的。若是你须要支持那些旧的浏览器(好比IE9如下的)你就须要用个bind方法的补丁(或者,若是你使用的是jQuery,那么你能够用$.proxy;另外underscore和lodash库中也提供了_.bind)。

有一件事须要注意,若是你在一个原型方法上使用bind,那它会建立一个实例级别的方法,这样就屏蔽了原型上的同名方法,你应该意识到这并非个错误。关于这个问题的更多细节我在这篇文章里进行了描述。

4.) 函数声明 vs 函数表达式

在JavaScript主要有两种定义函数的方法(而ES6会在这里做介绍):函数声明和函数表达式。

函数声明不须要var关键字。事实上,正如 Angus Croll 所说:“把他看成变量声明的兄弟是颇有帮助的”。例如:

1 function timeTravel(year) {
2     console.log(this.firstName + " " this.lastName + " is time traveling to " + year);
3 }

上例中名叫timeTravel的函数不只仅只在其被声明的做用域内可见,并且对这个函数自身内部也是可见的(这一点对递归函数的调用尤其有用)。函数声明其实就是命名函数,换句话说,上面的函数的name属性就是timeTravel。

函数表达式是定义一个函数并把它赋值给一个变量。通常状况下,它们看起来会是这样:

1 var someFn = function() {
2     console.log("I like to express myself...");
3 };

函数表达式也是能够被命名的,只不过不像函数声明那样,被命名的函数表达式的名字只能在 该函数内部的做用域中访问(译注:上例中的代码,关键字function后面直接跟着圆括号,此时你能够用someFn.name来访问函数名,可是输出 将会是空字符串;而下例中的someFn.name会是”iHazName”,可是你却不能在iHazName这个函数体以外的地方用这个名字来调用此函 数):

1 var someFn = function iHazName() {
2     console.log("I like to express myself...");
3     if(needsMoreExpressing) {
4         iHazName(); // the function's name can be used here
5     }
6 };
7  
8 // you can call someFn() here, but not iHazName()
9 someFn();

函数表达式和函数声明的讨论远不止这些,除此以外至少还有提高(hoisting)。提高是指函数和变量的声明被解释器移动到包含它们的做用域的顶部。虽然咱们在这里没有细说提高,可是务必读一下Ben CherryAngus Croll对它的解读。

5.) 具名和匿名函数

基于咱们刚刚讨论的,你确定猜到所谓的匿名函数就是没有名字的函数。大多数JavaScript开发者都能很快认出下例中第二个参数是一个匿名函数:

1 someElement.addEventListener("click"function(e) {
2     // I'm anonymous!
3 });

而事实上咱们的marty.timeTravel方法也是匿名的:

1 var marty = {
2     firstName: "Marty",
3     lastName: "McFly",
4     timeTravel: function(year) {
5         console.log(this.firstName + " " this.lastName + " is time traveling to " + year);
6     }
7 }

由于函数声明必须有个名字,只有函数表达式才多是匿名的。

6.) 自调用函数表达式

自从咱们开始讨论函数表达以来,有件事我就想立马搞清楚,那就是自调用函数表达式( the Immediately Invoked Function Expression (IIFE))。我会在本文的结尾罗列几篇对IIFE讲解得不错的文章。但简而言之,它就是一个没有赋值给任何变量的函数表达式,它并不等待稍后被调用, 而是在定义的时候就当即执行。下面这些浏览器控制台的截图能帮助咱们理解:

首先让咱们输入一个函数表达式,可是不把它赋值给任何变量,看看会发生什么

jsqduirks_afjq_4

无效的JavaScript语法——它实际上是一个缺乏名字的函数声明。想让它变成一个表达式,咱们只需用一对圆括号把它包裹起来:

jsquirks_afjq_5

当把它变成一个表达式后控制台当即返回给咱们这个匿名函数(咱们并无把这个函数赋值给 其余变量,可是,由于它是个表达式,咱们只是获取到了表达式的值)。然而,这只是实现了“自调用函数表达式”中的“函数表达式”部分。对于“自调用”这部 分,咱们是经过给这个返回的表达式后面加上另一对圆括号来实现的(就像咱们调用任何其余函数同样)。

jsquirks_afjq_6

“可是等等!Jim,我记得我之前在哪看到过把后面的那对圆括号放进表达式括号里面的状况。”你说得对,这种语法彻底正确(由于Douglas Crockford 更喜欢这种语法,才让它变得众所周知):

jsquirks_afjq_7

这两种语法都是可用的,然而我强烈建议你读一下对这两种用法有史以来最好的解释

OK,咱们如今已经知道什么是IIFE了,那为何说它颇有用呢?

它能够帮助咱们控制做用域,这是JavaScript中很重要的一部分!marty对象 一开始是被建立在一个全局做用域里。这意味着window对象(假定咱们运行在浏览器里)里有个marty属性。若是咱们JavaScript代码都照这 个写法,那么很快全局做用域下就会被大量的变量声明给填满,污染了window对象。即便是在最理想的状况下,这都是很差的作法,由于把不少细节暴露给了 全局做用域,那么,当你在声明一个对象时对它命名,而这个名字恰巧又和window对象上已经存在的一个属性同名,那么会发生什么事呢?这个属性会被覆盖 掉!好比,你打算建个“阿梅莉亚·埃尔哈特(Amelia Earhart)”的粉丝网站,你在全局做用域下声明了一个叫navigator的变量,那么咱们来看一下这先后发生了些什么(译注:阿梅莉亚·埃尔哈特 是一位传奇的美国女性飞行员,不幸在1937年,当她尝试全球首次环球飞行时,在飞越太平洋期间失踪。当时和她一块儿在飞机上的导航员 (navigator)就是下面代码中的这位佛莱得·努南(Fred Noonan)):

jsquirks_afjq_8

呃……

显然,污染全局做用域是种很差的作法。JavaScript使用的是函数做用域(而不是 块做用域,若是你是从C#或者Java转过来的,这点必定要当心!)因此,阻止咱们的代码污染全局做用域的办法就是建立一个新做用域,咱们能够用IIFE 来达到这个目的,由于它里面的内容只会在它本身的函数做用域里。下面的例子里,我要先在控制台查看一下window.navigator的值,再用一个 IIFE来包裹起具体的行为和数据,并把他赋值给amelia。这个IIFE返回一个对象做为咱们的“应用程序做用域”。在这个IIFE里我声明了一个 navigator变量,它不会覆盖window.navigator的值。

jsquirks_afjq_9

做为一点额外的福利,咱们上面建立的IIFE实际上是JavaScript模块模式(module pattern)的一个开端。在文章结尾有一些相关的连接,以便你能够继续探索JavaScript的模块模式。

7.) typeof运算符和Object.prototype.toString

终有一天你会遇到与此相似的情形,那就是你须要检测一个函数传进来的值是什么类型。typeof运算符彷佛是不二之选,然而,它并非那么可靠。例如,当咱们对一个对象,一个数组,一个字符串,或者一个正则表达式使用typeof时,会发生什么呢?

jsquirks_afjq_10

好吧,至少它能把字符串从对象,数组和正则表达式中区分出来。幸好咱们还有其它办法能从这些检测的值里获得更多准确的信息。咱们可使用Object.prototype.toString函数而且应用上咱们以前掌握的call方法的知识:

jsquirks_afjq_11

为何咱们要使用Object.prototype上的toString方法呢?由于它可能被第三方的库或者咱们本身的代码中的实例方法给重载掉。而经过Object.prototype咱们能够强制使用原始的toString。

若是你知道typeof会给你返回什么,而且你也不须要知道除此以外的其余信息(例如, 你只须要知道某个值是否是字符串),那么用typeof就再好不过了。然而,若是你想区分数组和对象或者正则表达式和对象等等的,那么就用 Object.prototype.toString吧。

接下来去哪里

我从其余的JavaScript开发者的真知灼见里受益不浅,所以,请访问下面的连接而且感谢一下他们吧。

原文出处: Jim Cowart   译文出处: codingserf

相关文章
相关标签/搜索