我但愿我知道的七个JavaScript技巧

若是你是一个JavaScript新手或仅仅最近才在你的开发工做中接触它,你可能感到沮丧。全部的语言都有本身的怪癖(quirks)——但从基于强类型的服务器端语言转移过来的开发人员可能会感到困惑。我就曾经这样,几年前,当我被推到了全职JavaScript开发者的时候,有不少事情我但愿我一开始就知道。在这篇文章中,我将分享一些怪癖,但愿我能分享给你一些曾经令我头痛不已的经验。这不是一个完整列表——仅仅是一部分——但但愿它让你看清这门语言的强大之处,可能曾经被你认为是障碍的东西。javascript

咱们将看下列技巧:

  1. 相等
  2. 点好vs括号
  3. 函数上下文
  4. 函数声明vs函数表达式
  5. 命名vs匿名函数
  6. 当即执行函数表达式
  7. typeof vs Object.prototype.toString

1.) 相等

C#出身的我很是熟悉==比较运算符。值类型(或字符串)当有相同值是是相等的。引用类型相等须要有相同的引用。(咱们假设你没有重载==运算符,或实现你本身的等值运算和GetHashCode方法)我很惊讶为何JavaScript有两个等值运算符:==和===。最初个人大部分代码都是用的==,因此我并不知道当我运行以下代码的时候JavaScript为我作了什么:html

var x = 1;

if(x == "1") {
    console.log("YAY! They're equal!");
}

这是黑暗魔法吗?整数1是如何和字符串"1"相等的?java

在JavaScript中,有相等(==)和严格相等(===)之说。相等运算符将强制转换两边的操做数为相同类型后执行严格相等比较。因此在上面的例子中,字符串"1"会被转换为整数1,这个过程在幕后进行,而后与变量x进行比较。node

严格相等不进行类型转换。若是操做数类型不一样(如整数和字符串),那么他们不全等(严格相等)。git

var x = 1;

// 严格平等,类型必须相同
if(x === "1") {
    console.log("Sadly, I'll never write this to the console");
}

if(x === 1) {
    console.log("YES! Strict Equality FTW.")
}

你可能正在考虑可能发生强制类型转换而引发的各类恐怖问题——假设你的引用中发生了这种转换,可能致使你很是困难找到问题出在哪里。这并不奇怪,这也是为何经验丰富的JavaScript开发者老是建议使用严格相等。github

2.) 点号 vs 括号

这取决于你来自其余什么语言,你可能见过或没见过这种方式(这就是废话)。web

// 获取person对象的firstName值
var name = person.firstName;

// 获取数组的第三个元素
var theOneWeWant = myArray[2]; // remember, 0-based index不要忘了第一个元素的索引是0

然而,你知道它也可使用括号引用对象的成员吗?好比说:正则表达式

var name = person["firstName"];

为何会这样有用吗?而你会用点符号的大部分时间,有几个实例的括号使某些方法可能没法这样作。例如,我会常常重构大开关语句到一个调度表,因此这样的事情:express

为何能够这样用?你之前可能对使用点更熟悉,有几个特例只能用括号表示法。例如,我常常会将switch语句重构为查找表(速度更快),其实就像这样:后端

var doSomething = function(doWhat) {
    switch(doWhat) {
        case "doThisThing":
            // more code...
        break;
        case "doThatThing":
            // more code...
        break;
        case "doThisOtherThing":
            // more code....
        break;
        // additional cases here, etc.
        default:
            // default behavior
        break;
    }
}

能够转化为像下面这样:

var thingsWeCanDo = {
    doThisThing      : function() { /* behavior */ },
    doThatThing      : function() { /* behavior */ },
    doThisOtherThing : function() { /* behavior */ },
    default          : function() { /* behavior */ }
};

var doSomething = function(doWhat) {
    var thingToDo = thingsWeCanDo.hasOwnProperty(doWhat) ? doWhat : "default"
    thingsWeCanDo[thingToDo]();
}

使用switch并无错误(而且在许多状况下,若是被迭代屡次而且很是关注性能,switch可能比查找表表现更好)。然而查找表提供了一个很好的方法来组织和扩展代码,而且括号容许你的属性延时求值。

3.) 函数上下文

已经有一些伟大的博客发表了文章,正确理解了JavaScript中的this上下文(在文章的结尾我会给出一些不错的连接),但它确实应该加到“我但愿我知道”的列表。它真的困难看懂代码而且自信的知道在任何位置this的值——你仅须要学习一组规则。不幸的是,我早起读到的许多解释只是增长了个人困惑。所以我试图简明扼要的作出解释。

第一——首先考虑全局状况(Global)

默认状况下,直到某些缘由改变了执行上下文,不然this的值都指向全局对象。在浏览器中,那将会是window对象(或在node.js中为global)。

第二——方法中的this值

当你有一个对象,其有一个函数成员,冲父对象调用这方法,this的值将指向父对象。例如:

var marty = {
    firstName: "Marty",
    lastName: "McFly",
    timeTravel: function(year) {
        console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
    }
}

marty.timeTravel(1955);
// Marty McFly is time traveling to 1955

你可能已经知道你能引用marty对象的timeTravel方法而且建立一个其余对象的新引用。这其实是JavaScript很是强大的特点——使咱们可以在不一样的实例上引用行为(调用函数)。

var doc = {
    firstName: "Emmett",
    lastName: "Brown",
}

doc.timeTravel = marty.timeTravel;

因此——若是咱们调用doc.timeTravel(1885)将会发生什么?

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

再次——上演黑暗魔法。嗯,并非真的。记住,当你调用一个方法的时候,this上下文是被调用函数父的父对象。

当咱们保存marty.TimeTravel方法的引用而后调用咱们保存的引用时发生了什么?让咱们看看:

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

为何是“undefined undefined”?!而不是“Matry McFly”?

让咱们问一个关键的问题:当咱们调用咱们的getBackInTime函数时父对象/容器对象是什么?当getBackIntTime函数存在于window中时,咱们调用它做为一个函数,而不是一个对象的方法。当咱们像这样调用一个函数——没有容器对象——this上下文将是全局对象。David Shariff有一个伟大的描述关于这:

不管什么时候调用一个函数,咱们必须马上查看括号的左边。若是在括号的左边存在一个引用,那么被传递个调用函数的this值肯定为引用所属的对象,不然是全绝对象。

因为getBackInTime的this上下文是window——没有firstName和lastName属性——这解释了为何咱们看见“undefined undefined”。

所以咱们知道直接调用一个函数——没有容器对象——this上下文的结果是全局对象。然而我也说我早就知道咱们的getBackInTime函数存在于window上。我是如何知道的?好的,不像上面我包裹getBackInTime在不一样的上下文(咱们探讨当即执行函数表达式的时候),我声明的任何变量都被添加的window。来自Chrome控制台的验证:

jsquirks_afjq_1

是时候讨论下this的主要用武之地之一了:订阅事件处理。

第三(仅仅是#2的扩展)——异步调用方法中的this值

因此,让咱们伪装咱们想调用咱们的marty.timeTravel方法当有人点击一个按钮时:

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

在上面的代码中,当用户点击按钮是,咱们会看见“undefined undefined is time traveling to [object MouseEvent]”。什么?好——首先,很是明显的问题是咱们没有给咱们的timeTravel方法提供year参数。反而,咱们直接订阅这方法做为事件处理程序,而且MouseEvent参数被做为第一个参数传递个事件处理程序。这是很容易修复的,但真正的问题是咱们再次见到“undefined undefined”。不要无望——你已经知道为何会发生这种状况(即便你还没意识到)。让咱们修改咱们的timeTravel函数,输出this,从而帮助咱们搞清事实:

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

如今——当咱们点击这按钮,咱们将相似下面的输出 在你的浏览器控制台:

jsquirks_afjq_2

当方法被调用时,第二个console.log输出出this上下文——它其实是咱们订阅事件的按钮元素。你感到吃惊吗?就像以前——当咱们将marty.timeTravel赋值给getBackInTime变量时——对marty.timeTravel的引用被保存到事件处理程序,并被调用,但容器对象再也不是marty对象。在这种状况下,它将在按钮实例的点击事件中异步调用。

因此——有可能将this设置为咱们想要的结果吗?绝对能够!在这个例子里,解决方法很是简单。不在事件处理程序中直接订阅marty.timeTravel,而是使用匿名函数做为事件处理程序,并在匿名函数中调用marty.timeTravel。这也能修复year参数丢失的问题。

flux.addEventListener("click", function(e) {
    marty.timeTravel(someYearValue); 
});

点击按钮将会在控制台输出相似下面的信息:

jsquicks_afjq_3

成功了!但为何这样能够?思考咱们是如何调用timeTravel方法的。在咱们按钮点击的第一个例子中,咱们在事件处理程序中订阅方法自身的引用,因此它没有从父对象marty上调用。在第二个例子中,经过this为按钮元素的匿名函数,而且当咱们调用marty.timeTravel时,咱们从其父对象marty上调用,因此this为marty。

第四——构造函数中的this值

当你用构造函数建立对象实例时,函数内部的this值就是新建立的对象。例如:

var TimeTraveler = function(fName, lName) {
    this.firstName = fName;
    this.lastName = lName;
    // Constructor functions return the
    // newly created object for us unless
    // we specifically return something else
};

var marty = new TimeTraveler("Marty", "McFly");
console.log(marty.firstName + " " + marty.lastName);
// Marty McFly

Call,Apply和BindCall

你可能开始疑惑,上面的例子中,没有语言级别的特性容许咱们在运行时指定调用函数的this值吗?你是对的。存在于函数原型上的call和apply方法容许咱们调用函数并传递this值。

call方法的第一个参数是this,后面是被调用函数的参数序列:

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

apply的第一个参数也是this,后面是其他参数组成的数组:

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

咱们的doc和marty实例他们本身能时间旅行,但einstein(爱因斯坦)须要他们的帮助才能完成时间旅行。因此让咱们给咱们的doc实例添加一个方法,以致于doc能帮助einstein完成时间旅行。

doc.timeTravelFor = function(instance, year) {
    this.timeTravel.call(instance, year);
    // 若是你使用apply使用下面的语法
    // this.timeTravel.apply(instance, [year]);
};

如今它能够传送Einstein 了:

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

我知道这个例子有些牵强,但它足以让你看到应用函数到其余对象的强大之处。

这种方法还有咱们没有发现的另外一种用处。让咱们给咱们的marty实例添加一个goHome方法,做为this.timeTravel(1985)的快捷方式。

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

然而,咱们知道若是咱们订阅marty.goHome做为按钮的点击事件处理程序,this的值将是按钮——而且不幸的是按钮没有timeTravel方法。咱们能用上面的方法解决——用个一匿名函数做为事件处理程序,并在其内部调用上述方法——但咱们有另外一个选择——bind函数:

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

bind函数实际上会返回一个新函数,新函数的this值根据你提供的参数设置。若是你须要支持低版本浏览器(例如:ie9如下版本),你可能须要bind函数的shim(或者,若是你使用jQuery你能够用$.proxy代替,underscore和lodash都提供_.bind方法)。

记住重要一点,若是你直接使用原型上的bind方法,它将建立一个实例方法,这将绕过原型方法的优势。这不是错误,作到内心清楚就好了。我写了关于这个问题得更多信息在这里

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

函数声明不须要var关键字。事实上,如Angus Croll所说:“把他们想象成变量声明的兄弟有助于理解”。例如:

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

上面例子里的函数名字timeTravel不只在它声明的在做用域可见,同时在函数自己内部也是可见的(这对递归函数调用很是有用)。函数声明,本质上说其实就是命名函数。换句话说,上面函数的名称属性是timeTravel。

函数表达式定义一个函数并指派给一个变量。典型应用以下:

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

也能够对函数表达式命名——然而,不像函数声明,命名函数表达式的名字仅在它自身函数体内可访问:

var someFn = function iHazName() {
    console.log("I like to express myself...");
    if(needsMoreExpressing) {
        iHazName(); // 函数的名字在这里能够访问
    }
};

// 你能够在这里调用someFn(),但不能调用iHazName()
someFn();

 

讨论函数表达式和函数声明不能不提“hoisting(提高)”——函数和变量声明被编译器移到做用域的顶部。在这里咱们没法详细解释hoisting,但你能够读Ben CherryAngus Croll两我的的伟大解释。

5.) 命名vs匿名函数

基于咱们刚才的讨论,你可能一进猜到“匿名”函数其实就是一个没有名字的函数。大多数JavaScript开发者能迅速识别瞎买年第一个参数为匿名函数:

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

然而,一样的咱们的marty.timeTravvel方法也是一个匿名函数:

var marty = {
    firstName: "Marty",
    lastName: "McFly",
    timeTravel: function(year) {
        console.log(this.firstName + " " + this.lastName + " is time traveling to " + year);
    }
}

由于函数声明必须有一个惟一的名字,只有函数表达式能够没有名字。

6.) 当即执行函数表达式

由于咱们正在谈论函数表达式,有一个东西我但愿我早知道:当即执行函数表达式(IIFE)。有不少关于IIFE的好文章(我将在文章结尾出列出),但用一句话来形容,函数表达式不是经过将函数表达式赋值给一个标量,稍后再执行,而是理解执行。能够在浏览器控制台看这一过程。

首先——让咱们先敲入一个函数表达式——但不给它指派变量——看看会发什么:

jsquirks_afjq_4

语法错误——这被认为是函数声明,缺乏函数名字。然而,为了使其变为表达式,咱们仅需将其包裹在括号内:

 jsquirks_afjq_5

让其变为表达式后控制台返回给咱们一个匿名函数(记住,咱们没有为其指派值,但表达式会有返回值)。因此——咱们知道“函数表达式”是“当即调用函数表达式”的一部分。为了等到“当即执行”的特性,咱们经过在表达式后面添加另外一个括号来调用返回的表达式(就像咱们调用其余函数同样):

jsquirks_afjq_6

“可是等一下,Jim!(指做者)我想我之前见过这种调用方式”。 事实上你可能见过——这是合法的语法(众所周知的是Douglas Crockford的首选语法)

jsquirks_afjq_7

这两种方法都起做用,可是我强烈建议你读一读这里

OK,很是棒——如今咱们已经知道了IIFE是什么——以及为何要用它?

它帮助咱们控制做用域——任何JavaScript教程中很是重要的部分!前面咱们看到的许多实例都建立在全局做用域。这意味着window(假设环境是浏览器)对象将有不少属性。若是咱们所有按照这种方式写咱们的JavaScript代码,咱们会迅速在全局做用域积累一吨(夸张)变量声明,window代码会被污染。即便在最好的状况下,在全局变量暴漏许多细节是糟糕的建议,但当变量的名字和已经存在的window属性名字相同时会发生什么呢?window属性会被重写!

例如,若是你最喜欢的“Amelia Earhart”网站在全局做用域声明了一个navigator变量,下面是设置以前和以后的结果:

jsquirks_afjq_8

哎呀!

显而易见——全局变量被污染是糟糕的。JavaScript使用函数做用域(而不是块做用域,若是你来自C#或Java,这点很是重要!),因此保持咱们的代码和全局做用域分离的办法是建立一个新做用域,咱们可使用IIFE来实现,由于它的内容在它本身的函数做用域内。在下面的例子中,我将在控制台向你显示window.navigator的值,而后我常见一个IIFE(当即执行函数表达式)去包裹Amelia Earhart的行为和数据。IIFE结束后返回一个做为咱们的“程序命名空间”的对象。我在IIFE内声明的navigator变量将不会重写window.navigator的值。

jsquirks_afjq_9

做为额外好处,咱们上面建立的IIFE是JavaScript中模块模式的启蒙。我将在结尾处包括一些我浏览的模块模式的连接。

7.) 'typeof'操做符和'Object.prototype.toString'

最终,可能发如今某些状况下,你须要检查传递给函数参数的类型,或其余相似的东西。typeof运算符会是显而易见的选择,可是,这并非万能的。例如,当咱们对一个对象,数组,字符串或正则表达式,调用typeof运算符时会发生什么?

jsquirks_afjq_10

还好——至少咱们能够将字符串和对象,数组,正则表达式区分开,对吗?幸运的是,咱们能够获得更准确的类型信息,咱们有其余不一样的方法。咱们将使用Object.prototype.toString方法,而且应用咱们前面提到的call方法:

jsquirks_afjq_11

为何咱们要使用Object.prototype上的toString方法?由于第三方库或你本身的代码可能重写实例的toString方法。经过Object.prototype,咱们能够强制实现实例原来的toString行为。

若是你知道typeof将会返回什么那么你不须要进行多余的检查(例如,你仅须要知道是或不是一个字符串),此时用typeof很是好。然而,若是你须要区分数组和对象,正则表达式和对象,等等,那么使用Object.prototype.toString吧。

接下来呢

我已经从其余JavaScript开发者的看法中收益颇多,因此请看看下面的这些连接,并给这些人一些鼓励,他们给予了咱们谆谆教诲。

英文:http://developer.telerik.com/featured/seven-javascript-quirks-i-wish-id-known-about/

Q群推荐

CSS家园 188275051,Web开发者(先后端)的天堂,欢迎有兴趣的同窗加入

GitHub家园 225932282,Git/GitHub爱好者的天堂,欢迎有兴趣的同窗加入

码农之家  203145707,码农们的天堂,欢迎有兴趣的同窗加入
相关文章
相关标签/搜索