七个最有用的JavaScript技巧
如果你是一个JavaScript新手或仅仅最近才在你的开发工作中接触它,你可能感到沮丧。所有的语言都有自己的怪癖(quirks)——但从基于强类型的服务器端语言转U a * e , 7 =移过来的开发人员可能会感到困惑。我就曾经这样,几年前,当我被推到了全职JavaScript开发者的时候,有很多事情我希望我一开始就知道。在这篇文章中,我将分享一些怪癖,希望我 b &=)之_ y S n } ^说。相等运算符将强制转换两边的操作数为相同类型后执行严格相等比较。所以在上面的例子中,字符串”1”会被转换为整数1,这个过程在幕后进行,然后与变量x进行比较。
严格相等q a C不进行类型转换。如果操作数类型不同(如整数和字符串),那么他们不全等(严格相等)。
var x = 1;
// 严格平等,类型必须相同
if(x ===& # \"1\") {
console.log(\"Sadly, I\'ll never write th( h z eis to the console\");
}
if(x === 1) {
console.log(\"YES! Strict Equality FTW.\")
}你可能正在考虑可能发生强制类型转1 ( = g J Q换而引起的各种恐怖问题——假设你的引用中发生了这种转换,可能导致你非常困难找l O k G 4 Q } b到问题出在哪里。这并不奇怪,| J + K O - b这也是为什么经验丰富的JavaScript开发者总是建议使用严格相等。
2.) 点号 vsL G / w h ! ~ M L 括号
这取决于你来自其他什么语言,你可能见过或没见过这种方式(这就是废话)。
// 获取person对象的firstName值
vR u # A a _ _ | xar name = person.firstName;
// 获取数组的第三个元素
va D / R G 2 + Yr th: ~ | L - deOneWeWant = myArray;Q ! 7 { // remember, 0-baseQ B 8 Sd index不要忘了第一个元素的索引是0然而,你知道它也可以使用括号引用对象的成员吗?比如说:
var name = person;为什么会这样有用吗?而你会用点符号的大部分时间,有几个实例的括号使某些方法可能= 8 2 q无法这样做。例如,我会经常重构大开关语句到一个调度表,所以这样的事情:
为什么可以这样用?你以前可能对使用点更熟悉,有几个特例只能用括号表示法。例如,q l z g c .我经常会将swit} V : @ # 0 3 #ch语句重构为查找表+ $ j + 4 . {(速度更快),其实就像这样:
vaB D :r doSomething = function(doWhat) {
switch(doWhat) {
case \"doThisThing\":
/| e % + 2 e 8 4 */ more code...
break;
caseA w : _ p \"doThatThing\":
/c R G 3 T g P/ more code...
break;
case \"doThisOtherThing\":
// more code....
break;
// additional cases here, etc.: 5 ^
default:
// default behavior
break;
}
}可以转化为像下面这样:
var tv L e lhingsWeCanDo = {
doThisThing : function() { /* behavior */ },
doThatThing : function() { /* behavior */ },
doThisOtherThing : functi3 v 8 0 G / 4 zon() { /* behavior */ },
default : function(d W j U) { /* behavior */ }
};
var doSomethin) ) 7 ) O q ! p kg = function(doWhat) {
var th` . y 5 P eingToDo = thingk U , 0 , : WsWeCanDo.hasOwnProperty(doWhat) ? doWhat : \"default\% U n &"
thiE i R m C 1 6 m XngsWeCanDo();
}使用s{ h ^ 6 d U J ewitch并没有错误(并且在许多情况下,如果被迭代多次并且非常关注性能,switch可能比查找表表现更好)。然而查找表提供了一个很好的方法来组织和扩展代码,并且括号允许你的属性延时求值。
3.) 函数上下文
已D O F 9 (经有一些伟大的博客发表了文章,正确理解了JavaScript中的this上下文(在文章的结尾我会给出一些不错的链接),但它确实应该加到“我希望我知道”的列表。它真的困难看懂代码并且自信的知道在任何位置this的值——你仅需要学习一组规则。不幸的是,我早起读到的许多解释只是增加了我的困惑。因此我试图简明扼要v A g的做出解释。
第一——首先考虑全局情况(Global)
默认情况下,直到某些原因改变了执行上下文,否则this的值都指向全局对象。在浏览器中,那将会是window对象(或在node.jsr . O ?中e S ) G s { g ]为global)。
第二—& G 6 z—方法中的this值
当你有一个对象,其有一个函数成员,冲父对象调用这方法,this的值将指向B * A S父对象。例如:
var marty = {
firstName: \"Marty\",
lastName: \"McFly\",
timeTravel: function(year) {
cons? l ) Xole.log(this.firstName +8 / m H Y \] D | k Z" \" + this.lastName + \" is time traveling to \" + year);
}
}
marty.timeTravel(1955);
// Marty McFly is time traveling to 1955你可能已经知道你能引用marty对象的E | T 1 V y ; MtimeTravel方法并且创建一个X ^ 7 7 * s l d F其他对象的新引用。这实际上是JavaScript非常强大的特色——使我们8 r r B r j能够在不同的实例上引用行为(调用函数)。
var doc = {
firstName: \"Emmett\",
laste E K LName: \( ( U"Brown\",
}
doc.timeTravel = maY 5 2 crty.timeTravel;所以——如果我们调用doc.: r m 9timeTravel(1885)将会发生什么?
doc.timeTravel(1885);
// Emmett Brown is time traveling toS u | ! 6 F K 2 1885再次—4 ( % Q ` 1—上演黑暗魔法。嗯,并不是真的。记住,当你调用一个方法的时候,this上下文是被调用函数父的父对象。
当我们保存marty.TimeTravel方法的引用然后调用我们保存的引用时发生了什么?让我们看看:
var getBackInTime = marty.timeTravel;
getBackInTime(2014);
// undefined undefined is time traveling to 2014
为J w p * d什么是“undefined undefined”?!而不是“Matry McFly”?让我们问一个关键的问题:当我们调用我们的getBax ~ 7 % _ %ckInTime) { . h n K函数时父对象/容器对象是什么?当getBackIntTim q 0 qe函数存在于window中时,我们调用它 Q y / X\"flux-capacitor\j U q [ O");
flux.addEventListener(\"click\", marty.timeTravel);
在上面的代码中,当用户点击按钮是,我们会看见| $ 1 v“undefined undefined is time traveling to ”。什么?好——首先,非常明显的问题是我们没有给我们的timeTravel方法提供year参数。反而,我们直接订阅这方法作为事件处理程序,并且MouseEvent参数被作为第一个参数传递个事件处理程序。这是很容易修复的,但真正的问题是我们再次见到( o C | n } : Z ~“undefined undefined”。不要= E # | E m Z k Q无望——你已6 T ( t B ? t c h经知道为什么会发生这种情况(即使你还没意识到)。N B N c N _ v y让我们修改我们的timeTravel函数,输出this,从而t $ S = g w q W r帮助我们搞清事实:
marty.timeTravelG L { t B K N = function(year) {
console.log(this.firstName + \" \" + this.lastName + \" is time traveling to \" + year);
console.log(this);
};现在——当我们点击这按钮,我们将类似下面的输出 在你的浏览器控制台:
当方法被调用时,第二个console.log输出出this上下文——它实际上是我们订阅事件的按钮元素。你感到吃惊吗?就像之前——当我们将marty.timeTravel赋值给getBat x | f 8 G n y DckInTime变量时——对marty.timeTravel的引用被保存到事件处理程序,并被调用,但容器t + 3 Z (对象不再是marty对象。在这种情况下,它将在按钮实例的点击事件中异步调用。
所以——有可能将this设置为我们想要的结果吗?绝对可以!在这个例子里,解决方法非常简单。不在事件处理程序中直接订阅marty6 ~ w.timeTravel,而是使用匿名函数作为事件处理程序,并在匿名函数中调用marty.timeTravel。这也能修复year参数丢失的问题。
flux.addEventListener(\"click\", funp S 0 b J h { 5c 8 3tion(e) {
marty.tK E 6 Y 8 1 J p IimeTravel(someYearValue);
});点击按钮将会在控制台输出类似下面的信息:
成功了!但为什么这样可以?思考我们是如何调用timeTravel方法的。在我们按钮O K Y t ) / f F点击的第一个例子中,我们在事件处理程序中订阅方法自身的引用,所以它没有从父对象marty上, X 0 h M X C调用。在第二个例子中,通过this为按钮元素的匿名函数,并且当我们调用marty.time[ a W L w * $ eTravel时,我们从其父对象marty上调用,所以this为marty。
第四——构造函数中的this值
当你用构造函数创建对象实例时,函数内部的this值就是新创建的对象。例如:
var TimeTraveler = function(fName, lName) {
this.firstName = fName;
this.lastNamq m N r $e = 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.firstNM = F =ame + \" \" + marty.lastName);
// MartyO @ x U 9 ^ % McFlyCall,Apply和BindCall
你可能开始疑惑,上面的@ B ^ | / H例子中,没有语言级别的特性允许我们在运行时指定调用函数的this值吗?你是对的。存在于函数原型上的call和apply方法允许我们调用函数并传递this值。
call方法的第一个参数是this,后面是被调用函数的参数序列:
someFn.call(this, ay P H o p 9 6 e org1, arg2, arg3);apply的第一个参数也是this,后面是其余参r h V R Q数组成的数组:
someFn.apply(this, );我们的doc和marty实例他们自己能时间旅行,但einstein(爱因斯坦)j . L k y G需要他们的帮助才能完成时间旅行。所以让我们给我们的doc实例添加一个方法,以至于doc能帮助einstein完成时间旅行。
doc.timeTravelFor = function(instance, year) {4 q e ) v o ( r v
this.timeTravel.call(instancW e ;e, year);
// 如果你使用apply使用下面_ I B N d的语法
// this.timeTravela : G - { l.apply(instance, );
};现在它可以传送Einstein 了:
var einstein = {
firstName: \"Einstein\",
lastName: \2 3 G"(the dog)\"
};
doc.timeTravelFor(einstein, 1985);
// Einstein (the dog) is time tra1 ? x C ( !veling to 1985我知道这个例子有些牵强,但它足以让H s N ! ] q P f Q你看到应用函数到其他对象的强大之处。
这种方法还s Z 2 8 . o Z有我们没有发现的另一种用处。让我们给我们的marty实例添加一个goHome方法,作为this.timeTravel(1985)的快捷方式。
marty.goHome = function() {
this.timeTravel(1985);
}然而,我们知道如果我们订阅marty.goHome作为按钮的点击事件处理程序,this的值将是按钮——并且不幸的是按钮没有timeTravel方法。我们能用上面的方法解决——用个一匿名函数作为事件处理程序,并在其内部调用上述方法——但我们有另一个选择——bind函数:
flux.addEventListener(\"click\u 1 . K h B j ~", marty.goHome.bind(marty));
bind函数实际上会返回一个新函数,新函数的this值根据你提供的参数设置。如果你需Y 7 $要支持低版本c = S L * c t $ q浏览器(例如:ie9以下版本),你可能需要bind函数的shim(或者,如果你使用jQuerq * 2 # hy你可以用$.proxy代替,underscore和lodash都提供_.bind方法)。
记] G ; ~ w *住重要一点,如果你直接使用原型上S I E Q d的bind方法,它将创建一个6 + Z J P P $ B p实例方法,这将绕过原型方法的优点。这不是错误,做到心里4 3 E 9 ^ 7 G q E清楚就行了。我写了关于这个问题得更多信息在这里。
4.) 函数表达式vs函数声明
函数声明不需要var关键字。事实上,如Angus Croll所说:“把他们想象成变量声明的兄弟有助于理解”。例如:9 j q F e i z
function timeTravel(year) {
console.log(this.firstName + \" \" + this.lastName + \" is time traveling to \" + year);
} 上面例子里的函数名字timeTravel不仅在它声Y ( Y c O J 3 %明的在作用域可见,同时在函数本身内部也是可见的(这对递归函数调用非常有用)。函数声明,本质上说其实就是命名函数。换句话说,上面函数的名称属性是timeTrav= i Q ` P g k f jel。函数表达式定义一个函数并指派给一个变量。典型应用如下:
var someFn = function() {
console.log(\"I like to express myselz 9 W g 6 u Q {f...\");
}; 也可以对函数表达式命名——然而,1 g ( 4 ! X F不像函数声明,命名函数表达式的名字仅在它自身函数体内可访问:
var someFn = function iHazName() {} d m 2 I A A
console.log(\"I like to express myself...\");
if(needsMoreExpressing) {
iHazNa) P V t u %me(); // 函数的名字在这里可以访问
}
};
// 你可以在这里调用someFn(),但不能调用iB 3 [ 6 b c jHazName()
someFn();讨论函数表达式和函数声明不能不提“hoisting(提升)l e $ #”——函u A W q 8数和变量声明被编译器移到作用域的顶部。在这里我们无法详细解释hoisting,但你可以读7 P / i } E h\"click\& ^ j Q 6 6 ", function(e) {
// I\'m anonymous!
});然而,同样的我们的marty.timeTravvel方法也是一个匿名函数:
var marty = {
firstName: \"Marty\",
lastName: \"Mcb 1 ! ! a q uFly\",
timeTra{ @ ^vel: function(year) {
console.log(this.firstName + \" \" + this.lastName + \" is1 5 = 1 _ n r t time traveling to \" + year);
}
}因为函数声明必须有一个唯一的名字,只有函数表达式可以没有名字。
6.) 立即执行函数表达式
因为我们正在谈论函数表达式,有一个东西我希望我早知道:立即执行函数表达式(II, f X Y UFE)。有很多关于IIFE的好文章(我将在文章结尾出列出),但用一句话来形容,函数表达式不是通过} Q V @ 3 K将函_ l 4 Y 4数表达式赋值给一个标量,稍后再执行,而是理解执行。可以在浏览器控制台看这一过程。
首先——让我们先敲入一个函数表达式——但不给它指派变量——看看会发什么:
语法错误——这被认为是函数声明,缺少函数名字。然而,为了使其变为表达式,我们仅需将其包裹在括号内:
让其变为表达式后控制台返回给我们一个匿名函数(记住,我们没有为其指派值,但表达式会有返回值)。所以——我们知道“函数表达式”是“立即调用函数表达式”的一部分。为了等到“立即执行”的特性,我们通过在表达式后面添加另一个括号来调用返回的表达式(就像我们调用其他函数一样):
“但是等一下,Jim!(指作者)我想我以前见过这种调用方式”。 事实上你可能见过——这W T c f是合法的语法(众所周知的是Douglas Crockford的首选语法)
这两种方法都起3 c s Q B N作用,但是我强烈建议你读一读这里。
OK,非常棒——现` Q (在我们已经知道了IIFE是什N $ y B么——以及为什么要用它?
它帮助我们控制作用域——任何JavaSc! o T - Q H x 0 gript教程中非常重要的部分!前面我们看到的许多实例都创建在全局作用域。这意b 1 X a味着window(假~ / ~ v b 5 j设环境是浏览器)e O R 1 G对象将有很多属性。如果我们全部按照这种方式写我们的JavaScript代码,我们会迅速在全局作用域积~ A ` N s f w mindow属性会被重写!
例如,如果你最喜欢的“Amelia Earhart”网站在全局作e } l j S :用域声明了一个navigator变量,下面是设置之前和之后的结果:] R 1 f h
哎呀!
显而易见——全局$ e U x变量被污染是糟糕的。JavaScript使用函数作用{ ] q ] s域(而不是块作用域,如果你来自C#或Java,这点非常重要!),所以保持我们的代码3 } e K # %和全局作用域分离的办法是创建一个新作用域,我们9 } u 7 i : }可以使用IIFE来实现,因为它的? h l M / 6 ) y g内容在它自己的函* X * s r a数$ k & 3 p作用域内。在下q ~ A f ) !面的例子中,我将在控制台向你显示window.navigator的值,然后我常见一个II] ? .FE(立f p P即执行函数表达式)去包裹Amelia Earhart的行为和数据。IIFE结束后返回一个作为我们的“程序命名空间”的对象。我在IIFE内声明的navigator变量将不会重写window.navigator的值。
作为额外好处,我们上面创建的IIFE是JavaScript中模块模式的启蒙。我将在结尾处包括一些我浏览的模块模式的链接。
7.) ‘typeofo y _ Q | P , k D’操作符和’Object.prototypeE a ? ) ).tl F K j , D Y C 1oString’
最终,可能发现在某q u F J 9 G & c [些情况Y l W n ? x下,你需要检查传递给函数参数的类型,或其他类似的东西。typeof运算符会是显而易见的选择,但是,这并不是万能的。例如,当我们对一个对象,数组,字符串或正则表达式,调用typeof运算符时会发生什么?
还好——至少我们可以将字符M G e 7 z串和对; s 6 F # C象,数组,正则表达式区分开,对吗?幸d a H k O 3 J运的是,我们可以得到更准确的类型信息,我们有其他不同的方法。我们将使用Object.prototype.toString方法,并且应用我们前面提到的call方法:
为什么我们要使用Object.prototypes K J C q i q y上的toString方C x v d z T t法?因为第三方库b I % U或你自己的代码可能重写实例的toStrq 4 _ z A *ing方法。通过Object.pr@ Y m x J e J Nototype,我们可以强制实现实例原来的toString行为。
如果你知道0 o 7 .typeof将会返回什么那么你不需要进行多余的检查(例如,你仅需要知道是或不是一个字符串),此时用typeof非常好。然而,如果你需要区分数组和对象,正则表达式和对象,等等,那么使用Object.prototype.toString吧。