【译】JavaScript 10分钟进阶
2015-06-01 09:49
381 查看
1、简介
本指南是为那些已入门Javascript,同时希望了解它的高级特性的人而写的。如果你之前接触过诸如Ruby,Perl,Python,ML,Scheme等等的函数式编程语言,那么本指南对你而言就会相对容易一些,因为我在这里确实没有涉及到太多初级内容的讲解。
Javascript 有9种类型,它们是:
1、空 – null。访问它的任何属性都会失败,例如 null.foo(译注:会抛出类型错误,如 TypeError: Cannot read property ‘foo’ of null)。null无法装箱(译注: 把基本数据类型转换为对应的引用类型的操作称为装箱,把引用类型转换为基本的数据类型称为拆箱)。
2、未定义 – undefined。当访问一个对象中不存在的属性时就会得到一个undefined。例如:document.nonexistent。访问它的任何属性也都会失败。undefined无法装箱。
3、字符串 – 例如:’foo’,”foo”(使用单引号和双引号均可)。字符串在作为String的实例时会进行装箱操作(译注:new String(‘foo’);)。
4、数字 – 例如:5,3e+10(所有数字都是浮点型的,尤其是分数,但是可以用x >>> 0来取出整数位)。数字在作为Number的实例时会进行装箱操作。
5、布尔 – true 和 false。布尔类型在作为Boolean的实例时会进行装箱操作。
6、数组 – 例如:[1, 2, “foo”, [3, 4]]。总是会进行装箱。是Array的实例。
7、对象 – 例如:{foo: ‘bar’, bif: [1, 2]},是真正的哈希表。总是会装箱。是Object的实例。
8、正则表达式 – 例如:/foo\s*([bar]+)/。总是会装箱。RegExp的实例。
9、函数 – 例如:function(x) { return x + 1;}。必定装箱。Function的实例。
在Javascript的运行环境里不会产生null值,除非你在代码中有显式的赋值(通常情况下你得到的会是undefined而不是null,只有一个例外就是document.getElementById,它会在找不到元素的情况下返回null)。有节制的使用undefined来替代null会更容易追踪bug。
函数是最好的词法闭包,就像Ruby中的lambda,Perl中的sub。函数非常好用,它可以做一些很酷的事情,但是有一种情况要格外小心,因为它会带来灾难。
函数的参数总是可变的。出现的参数会被绑定到形参上,不出现的就是undefined了,例如:
我们有个办法可以访问函数中的参数:
关键字arguments只是看上去像一个数组,但不是一个真正的数组!因此,如果你按下面的方式使用它就会出问题:
据我所知,把arguments对象转换成一个数组最好的办法就是 Array.prototype.slice.call (arguments)。
函数内部机制使用的是词法作用域链。也就是说,一个函数内的变量只在这个函数被调用的时候才会解析。我们可以利用这一特性做一些很有意思的事,其中首屈一指的可能就是自我引用(self-reference):
小心:惰性作用域会导致的一个很严重的问题是,允许你将函数关联到(refer
to)根本不存在的变量上。这让Javascript中的bug很难调试。Javascript可以通过toString方法支持句法宏(syntactic macros):
理论上可以利用这一原理进行扩展从而实现真正的结构宏(structural macros),操作符重载,类型系统等等。
人们可能会认为他们可以很简单地弄清楚Javascript中的this指向的是什么,然而这显然是件极具挑战性的事情,甚至是几乎不可能的。在函数外(指是的全局作用域)this这一关键字指向的是全局对象(global object),在浏览器中就是window对象。真正的问题是它在函数内的行为是怎样的,这取决于函数是如何被调用的。下面是它的运行原理:
如果这个函数是单独调用的,例如:foo(5),那么函数内部的this指向的是全局对象。
如果这个函数是被当作对象的方法调用的,例如:x.foo(5),那么函数内部的this指向的就是这个对象,在本例中就是x。
如果这个函数是一个对象的方法,却单独调用:
此时,this又将指向全局对象。没有什么东西会记录下 f 这个方法来自哪里,它完全取决于调用的位置。
如果函数是用apply或是call来调用的。this指向的会是你传参的那个对象(除非你企图传递null或undefined,这种情况下this将会指向全局对象):
鉴于这种不可预测性,大部分Javascript库会提供一个捷径将函数的this绑定(在Javascript内就是函数的绑定)到固定的调用上。最简单的做法就是定义一个函数用apply来代理参数并且指定适当的值(幸运的是,这正是闭包的行为):
call和apply之间的差异是显而易见的:f.call(x, y, z) 和 f.apply(x, [y, z])是一样的,等价于本例中的bind(f,x)(y,z)。函数内部的this会指向call 和 apply 传入的第一个参数,而call 和 apply其余的参数会作为这个函数的参数。通过apply传入到函数的参数是类数组的(arguments对象在这里可以正常工作),而call会把其余的参数原封不动地传入到函数中。
在大多数函数式编程语言中,你能简写一些东西;也就是说如果你有一个类似function(x){return f(x)} 这样的函数,你可以直接使用f 这个函数而不用这么麻烦。然而在Javascript中这种转换并不总是安全的;考虑一下这样的代码:
我们可能会自然而然地想到把它改成下面这种更简洁的方式:
然而后者在Javascript中会导致一个很诡异的错误,这种情况发生的发生是因为Array.push函数发现this指向的是全局对象而不是xs。原因显然是each内的这个函数是被当作独立的函数调用的,而不是对象的方法。事实上这个函数忘记了自己是xs的一个方法。(就像上面提到的第3种情况)
最简单的办法就是把xs.push绑定到xs上:
具体的原因会4.6节中解释,this永远不会被赋假值。如果你尝试给它赋值null或undefined,例如像这样:
事实上它会指向全局的this,在浏览器环境中通常是window。如果你使用原始值,this会指向这个原始值的装箱版本。这里确实有些违反常理,5.4节中会介绍更多的细节。
javascript是一门令人惊叹的语言,就像Perl在编程语言中的地位和Linux在操作系统中的地位一样。如果你掌握如何正确地使用它,那么它将会无微不至地解决你所有的问题(好吧,我承认可能不是所有的)。同时,如果你犯一丁点错,它够让你花上几个小时来找bug的。下面是我收集的javascript这门语言中几乎所有的先天不足之处。
如果你能在每行代码的结尾处都加上分号那就万事大吉了。然而,大部分浏览器并不会强制你这么做,如果你漏掉了它们,就可能会产生一些意想不到的效果。
大多数情况下Javascript能明白你想要干什么。当你用括号作为一行的开头时,它可能并不知道你真正的用意。例如:
Javascript会把这两行连接起来:
我所知道的解决这一问题的唯一办法就是在第一行结尾处加上一个分号。
每个函数都有返回值。如果你没用return语句的话返回值就是undefined;反之就是你返回的值。那些使用Ruby或是Lisp的人常常会在这里犯错;例如:
x的结果是undefined。如果你有可能犯在这里栽跟头,有一个叫做“js2-mode”的Emacs模块会识别那些有副作用或没有返回值的函数,并捕获这些错误。
声明一个变量时要格外小心。如果你不使用var关键字,你声明的变量就会是全局的,因此可能导致一些bug:
据我所知,同样的问题存在于两种类型的for循环中:
不得不说这场灾难很精彩。请看下面的代码:
这三个方法被调用的时候最终会返回什么?你可能希望他们分别返回0、1、2,因为这正是这些函数被创建时的 i 的值。然而它们都将会返回 3。这是因为Javascript的惰性作用域:在创建的时候,每个函数都只接收了一个变量名和用来搜索这个变量的作用域;变量本身的值直到这些方法被调用的时候才会去解析,而那个时候的i是等于3的。
解决这个问题的最简单的方法就是把我们的这部分代码包裹在一个自执行的匿名函数中,引入到另一层作用域中。下面的代码能够正常工作的原因是因为它被包裹在了一个匿名函数里,new_i的值是不会改变的。
顺便说一下,你可能想尝试一下这么做:
它会像我们最早的那个例子一样无法正常运行:j 处于离它最近的封闭函数的作用域中(你应该还记得Javascript的作用域是函数级的而不是块级的吧!),所以它的值会伴随着 i 一起改变。
因为==其实并不是完全相等,在Javascript中下面的都会是true:
所以,除非你真的想要这种效果,否则的话永远不要使用 == 操作符。取而代之应该使用行为更加明确的 === (它的否定式是 !==)。尤其需要注意的是两个操作数不仅仅是值相同,而且类型也要相同。它会用装箱值做引用比较,同时用拆箱值做结构比较。如果做比较的一边是装箱值,而另一边是拆箱值,那么===返回的永远是false。字符串字面量是拆箱值,所以,你可以在下面这种情况下使用它:
有一种情况 == 会比 === 更加有用。如果你想知道一个属性是否存在(即,不是null也不是undefined的情况),最简单的办法是用(x == null) 而不是更详细地用(x === null || x === undefined)。除此之外我再也想不到哪里会经常用到
== 。
小心:事实证明用==来验证真值并不总是稳定的。如果x = 0, y = new Number(0),此时 x == y 为真,然而 !!x 是 false,!!y 是 true。4.6节会详细阐述为什么会发生这种情况。
装箱值永远为真,并且可以添加属性。无法给拆箱值添加属性,但是它们也不会报错;例如:
如何对一个拆箱值进行装箱?你可以这么做:
就像上面那样直接调用构造方法
设置prototype中的一个成员并且在那个方法中引用this(见第5节)
把这个拆箱值作为call 或 apply方法中第一个传入的参数(见3.3.2节)
所有的HTML对象都是装箱值。
Javascript的语法环境非常宽松。下面这些代码都是合法的:
另外还有些非常有用的语句:
除此之外你还要知道当使用+的时候Javascript会把任何东西转换成字符串或是数字。下面的这些表达式全都是字符串:
以下的这些表达式都是数字:
下面的这些是我的收藏:
我最近被一件事情搞得措手不及,就是Javascript的强制类型转换的不确定性。例如:
当你在使用一些操作符操作非数值时你要格外小心。例如,下面的这个函数就无法告诉你一个数组中是否含有真值:
has_truthy_stuff返回false的原因是因为{}做强制类型转换的时候会向数字转换,变成NaN,而在Javascript里NaN为假。使用 |= 操作NaN,就相当于操作0,什么事也不会发生。所以result对数组中所有的值得到的都是0。因此这个方法并不能实现我们想要的效果。
另外,你可以通过(重)定义valueOf方法来改变数值转换的值:
这是值得我们思考的一点,因为它会产生一些有趣的影响。首先,valueOf() 可能会永不终止。例如:
其次,valueOf只是一个常规的Javascript函数,所以它可能会造成安全漏洞。假如你使用了eval() 作为JSON解析器(虽然这不是个好主意),同时也没有先对输入格式进行检查。如果某人传入{valueOf: function () {while (true);}},那么你的应用会在进行第一次对象向数字的强制类型转换时挂起(这种强制类型转换可能是隐式的,比如像上面例子中的
== 5)。
小心:一个数组会转换成什么数值是依赖于这个数组中的内容的:
如果一个数组嵌套的太深,那么数组内建的数值转换就会因为堆栈溢出而失败。例如:
幸运的是,当你在V8引擎中让一个数组包含自身时,数值转换还是可正常运行的;所以下面这个例子看上去有些乏味:
如果你按函数的方式调用了一个非函数的变量,或者你访问了null或undefined的属性,抑或你引用了一个根本不存在的全局变量,那么Javascript会抛出TypeError或ReferenceError的错误。除此之外,当你引用一个不存在的局部变量时会导致ReferenceError错误,因为Javascript会认为你在访问全局变量。
你可以throw很多不同的东西,包括拆箱值。这会带来一些好处,请看下例中的代码:
throw/catch不会维护一份堆栈跟踪,它处理异常的速度要比平常快很多。因此为了便于调试,最好还是抛出一个恰当的错误:
因为它的行为会像下面这样:
在很多情况下,用typeof来做类型检测是一种很蹩脚的方式,更好的做法是使用对象的constructor属性,像这样:
为了防止null和undefined被访问(因为你无法访问它们的constructor),你可能需要依赖它们所代表的布尔值:
但是事实上,这么做可能导致诸如 ’’,0,false,NaN等验证失败。就我所知道的唯一的解决办法是做如下比较:
相反,如果你要检测一个值是不是已给定的类型,你就要用instanceof,它永远不会抛出异常。
一般情况下,instanceof要比typeof有用,但是它只对装箱值起作用。例如,下面的都是false:
然而,这些都为true:
解决上面第一个问题的办法就是包装一下原始值:
一般来说,对于所有的原始值x,(new x.constructor(x) instanceof x.constructor)总会为true。然而无法对null和undefined这么做,因为访问它们的constructor会报错,并且据我所知,永远无法从它们构造函数中返回任何结果(也就是使用new)。
从IE6以后,浏览器对语言核心部分的兼容性还是很好的。然而,IE中有一个String.split的bug:
IE6中还有个更加细微的bug花了我几个小时才找到它,那就是eval()不能返回函数:
我敢肯定有其他相似的bug存在,但通常这些问题最常出现在DOM中。
5、原型
我曾经发表过反对OOP的言论,但是考虑到我偶尔会使用原型,我删除了那条言论。尽管我对Javascript因为市场压力去迎合说是Java给的灵感这种说法嗤之以鼻,然而,基于原型的编程有时还是很有用的。本节内容含有我自己主观的甚至是偏激的观点。
当你定义一个函数的时候,它都能够以两种方式使用。也就是说,就像每个标准的程序员所想的那样,一个函数标准的使用方式是能接收值也能返回值,还有另一种完全不同的方式,那就是生成实例。下面就是一个例子:
这是大多数人期望的。而下面的这种方式只要是个理性的人类就无法想到:
这种情况下,下面的情况都为true:
关键字new是一个右结合的一元运算符,所你可以用它来实例化对象:
如果你准备使用这种不可靠的设计模式,那么就可能想添加一些方法:
你可以在网上找到大量的关于原型编程的信息。
虽然new有一些很酷的特性(很流逼的),但是它也有一个很可怕的缺点。那就是大多数Javascript函数都可以被转发(forwarded),你可以把一个已经存在的函数包裹在一个新的函数里,而这个函数被调用的时候和之前毫无差异。例如:
然而new没有这种机制。一般情况下你不能转发一个构造函数,因为你没有办法对new(译注:作者这里应该是指构造函数(constructor))做像apply这样的事情。(当然也并不是完全没有可能,下一节我会给出一个不错的解决方案。)
不久前,我收到Ondrej Zara的邮件,他指出我之前对new的偏见是毫无根据的,并对我上一节中提到的问题给出一个非常优雅的解决方案,我把他的代码一字不落地搬了过来:
下面是测试用例:
起初我很怀疑这种做法是否会正常运行,然而目前为止我还没有发现一个失败的例子。所以在Javascript里构造函数确实能被转发,虽然这和我之前说的有点矛盾。
如果你需要一个动态调度(派遣)模式,那么原型可能是你最好的选择,你直接使用它们要比你自己搞个套路出来更可行。Google的V8引擎对原型做的专门的优化,不久后发行的Firefox也会这么做。并且,原型更加节省内存;因为只有一个指针指向一个原型,这要比有n个指针指向n个属性更省资源。
另一方面,如果你自己实现了一个继承机制,那么你可能正在犯错。我已经证明了原型编程在Javascript中是一种很高效的方法,但是,继承在Javascript中却是(1)缓慢的,并且(2)Javascript的“一切都是公共的”(everything is public)模式并不具代表性。
你可能会企图尝试去做下面的事情:
而且在运行下面这段代码的时候就会发生这种悲惨而不幸的状况:
这是因为,当你把一个未装箱的值当作对象(例如调用它某个方法),它会暂时变成一个装箱值,目的是为了能调用它的方法。之后,它不会再自动拆箱变回原始值,所以这意味着它失去了它曾经拥有的原始假值(falsity)。当然,根据你正在使用的类型,你可以把它再转换回拆箱值:
Javascript中有一种用法非常重要,然而并不是人人都能意识到。那就是:在所有的情况下 foo[‘bar’] 和 foo.bar是完全等价的。你可以提前在你的代码中放心地使用这种方式,无论是属性值、方法或者是其他什么东西。此外,你还能给一个对象中尚未定义的属性赋值:
当然,你也可以用这种方法来读取属性:
事实上,这正是 for(var … in …) 语句所做的事情:遍历一个对象的属性。例如:
然而,for…in 也有缺点。当你修改prototype时会发生一些怪异的事情。例如:
要解决这个问题,你需要做两件事。第一,永远不要修改Object的prototype,因为一切都是Object的实例(包括数组和其他所有的装箱值);第二,使用hasOwnProperty:
并且,非常重要的一点是,永远不要用for…in 来遍历数组(它只会返回字符串索引而不是数字索引,这会导致一些问题)或是字符串。如果你给Array或是String(或是Object)添加方法在这里都会失败(当然,你不会那么做)。
Javascript几乎可以做任何其他语言能做的事。然而,要想了解这一点似乎并没那么容易。
因为像Ruby这样的语言已经向世界展示了太多过气的for循环,太多有自尊心的程序员不喜欢用for。如果你的项目运行在Firefox上,那么你确实没必要用for,因为Firefox浏览器中Array的prototype已经包含了map和forEach方法。但是,如果你要写一个跨浏览器的代码,同时还不打算用第三方的库,这里有个好办法能实现你需求:
据我所知这(几乎)是写出这些函数的最快途径。我们预先声明了两个变量(i和l)其中一个用来缓存长度(length)。Javascript并不知道this.length在for循环的过程中是不变的,所以,如果我们没有缓存它,那么for会在每次循环中都检测一下。这么做会很耗资源,因为对于一个装箱值,如果我们无法在this中找到length这个属性,那么它会下降到this.__proto__去找到它。然后触发一个方法调用来取回length值。
此外,我们还能做的唯一的优化就是逆向遍历数组(只在each中才会这样,因为我们假定map是正向遍历的):
上面的这种运行速度上比第一种实现要快一点,因为它用一个浮点数的减少作为检测标志(需要用<与非零值比较),因为它内部的运算其实是按位与(and)并且前面的0位会跳过。除非你的Javascript引擎中已经支持上面的函数抑或你真的能确定这么做会影响性能(既然这样我想问的是你为什么还要首选Javascript呢),你可能永远不用考虑小于一个非0值与大于等于0哪个开销更大。
你也可以给对象定义一个遍历器,但是,不要像下面这么做:
更好的办法是单独实现一个keys函数,而不是像上面这样去污染Object的prototype:
只要是个正常人就不会想到使用这种思路。但是,如果你是个疯子或者有人强迫你,那么Google Web Toolkit会告诉你怎么搬起石头砸自己的脚,顺便把它转换成Javascript。
我们可以用不同的方法来实现它,但是最简单的就是下面这样:
此时,metaclass本身就是个元类。我们可以创建一些实例:
这是一个Ruby式的类,你可以定义公共方法和构造函数。例如:
现在,你可以使用这个类了:
使用元类的好处是你可以用这一结构做一些有趣的东西。例如,假设我们想在点(point)中插入跟踪方法以便调试:
现在,当point实例中的任意一个方法被调用的时候trace(trace并不是Javascript内建的,你得自己定义它)也会被调用,而且它会同时访问参数和状态。
默认情况下Javascript并没有对尾调用(tail-call)做优化,这一点确实不太好,因为一些浏览器会造成调用堆栈短路(我知道的最短的是500帧,因此,对于绑定函数和遍历器来说,它会消耗的非常快)。幸运的是,在Javascript中对尾调用编码相当简单:
我们现在可以写一个针对尾调用做过优化的阶乘函数:
前两个函数可以被正常调用:
但是它们也无法运行在连续的堆栈空间中。第三种我们会像下面这样调用:
这种方式采用了尾调用优化的策略,而不是去创建新的堆栈帧:
也不是去创建一个空的堆栈:
我们会在分配新的堆栈之前弹出最后一个堆栈帧(把[function, args]当作一种连续的类型返回):
它不会造成严重的性能损失,为只有两个元素的数组分配指针开销是很小的。
惰性作用允许我们做一些很有意思的事。假如说我们想对变量声明的语法做一个新的定义,比如我们准备把下面的语法:
替换为:
我们可以用正则表达式来实现这个想法,前提是你不介意我们把一半的时间花在给这种表达式排错上:
现在我们能用下面这种语法了:
显然,一个合适的解析器是必要的,因为这样才能保证不会在简单的括号边界处报错。然而,你应该意识到一件重要的事,那就是函数给我们提供了一个引用代码的途径,就像 在Lisp里面:
在Javascript里(假设下面的parse和deparse已经定义过而且实现很复杂)会是下面这样:
这个原理可以扩展到运算符重载上,我们可以用一个方法调用的方式来重写位移运算符:
还记得属性名的标识符是不加限制的吧?所以,就像在Ruby中那样,我可以为数组重载<<运算符:
相比Lisp,在Javascript中想要实现这种事情唯一不幸的地方就是Javascript会把句法结构当作语法,这就导致引入像when这样的新的句法结构并不是件容易的事:
然而,在遵循Javascript的解析树(Javascript parse tree)的前提下,你想做什么事都可以。
我强烈建议您读一下jQuery(http://jquery.com)用业界良心写就的源码。不得不说这是一项辉煌的工程,我从阅读他们代码的过程中学到了很多东西。
道格拉斯·克罗克福德(Douglas Crockford)写过一些非常不错是Javascript参考指南,包括众所周知的《Javascript语言精粹》和不怎么出名但却是免费的Javascript语言指引 http://javascript.crockford.com/survey. html。
我在这里想无耻地提一下我自己写的一个库,Divergence(http: //github.com/spencertipping/divergence),我同样也建议你读一下它。它与jQuery非常不同,更加简练而高效(而且是DOM无关的)。jQuery用了更多的传统方法,然而Divergence趋向使用闭包和函数元程序设计。
如果你使用Lisp和元程序,你可能也对http:// github.com/spencertipping/divergence.rebase 和http://github.com/
spencertipping/caterwaul 感兴趣。这两个项目都用到了函数序列化和eval()来实现在上节中提到的句法扩展。
还有我最近发现的一个网站叫http://wtfjs.com ,它似乎想让Javascript中所有诡异的行为都暴露出来。这个网站确实是非常有趣,也让人深受启迪。一个可以让我们更加深入地了解Javascript中的精华、糟粕和丑陋部分的网站是http://perfectionkills.com ,它是PrototypeJS的一个开发人员写的,它让我意识到自己对Javascript并不是真的那么了解。
本指南是为那些已入门Javascript,同时希望了解它的高级特性的人而写的。如果你之前接触过诸如Ruby,Perl,Python,ML,Scheme等等的函数式编程语言,那么本指南对你而言就会相对容易一些,因为我在这里确实没有涉及到太多初级内容的讲解。
2、类型
Javascript 有9种类型,它们是:1、空 – null。访问它的任何属性都会失败,例如 null.foo(译注:会抛出类型错误,如 TypeError: Cannot read property ‘foo’ of null)。null无法装箱(译注: 把基本数据类型转换为对应的引用类型的操作称为装箱,把引用类型转换为基本的数据类型称为拆箱)。
2、未定义 – undefined。当访问一个对象中不存在的属性时就会得到一个undefined。例如:document.nonexistent。访问它的任何属性也都会失败。undefined无法装箱。
3、字符串 – 例如:’foo’,”foo”(使用单引号和双引号均可)。字符串在作为String的实例时会进行装箱操作(译注:new String(‘foo’);)。
4、数字 – 例如:5,3e+10(所有数字都是浮点型的,尤其是分数,但是可以用x >>> 0来取出整数位)。数字在作为Number的实例时会进行装箱操作。
5、布尔 – true 和 false。布尔类型在作为Boolean的实例时会进行装箱操作。
6、数组 – 例如:[1, 2, “foo”, [3, 4]]。总是会进行装箱。是Array的实例。
7、对象 – 例如:{foo: ‘bar’, bif: [1, 2]},是真正的哈希表。总是会装箱。是Object的实例。
8、正则表达式 – 例如:/foo\s*([bar]+)/。总是会装箱。RegExp的实例。
9、函数 – 例如:function(x) { return x + 1;}。必定装箱。Function的实例。
在Javascript的运行环境里不会产生null值,除非你在代码中有显式的赋值(通常情况下你得到的会是undefined而不是null,只有一个例外就是document.getElementById,它会在找不到元素的情况下返回null)。有节制的使用undefined来替代null会更容易追踪bug。
3、函数
函数是最好的词法闭包,就像Ruby中的lambda,Perl中的sub。函数非常好用,它可以做一些很酷的事情,但是有一种情况要格外小心,因为它会带来灾难。
3.1、可变参数(酷)
函数的参数总是可变的。出现的参数会被绑定到形参上,不出现的就是undefined了,例如:(function (x, y) {return x + y}) (’foo’) // => ’fooundefined’
我们有个办法可以访问函数中的参数:
var f = function () {return arguments[0] + arguments[1]}; var g = function () {return arguments.length}; f (’foo’) // => ’fooundefined’ g (null, false, undefined) // => 3
关键字arguments只是看上去像一个数组,但不是一个真正的数组!因此,如果你按下面的方式使用它就会出问题:
arguments.concat ([1, 2, 3]) [1, 2, 3].concat (arguments) arguments.push (’foo’) arguments.shift ()
据我所知,把arguments对象转换成一个数组最好的办法就是 Array.prototype.slice.call (arguments)。
3.2、惰性作用域(酷)
函数内部机制使用的是词法作用域链。也就是说,一个函数内的变量只在这个函数被调用的时候才会解析。我们可以利用这一特性做一些很有意思的事,其中首屈一指的可能就是自我引用(self-reference):var f = function () {return f}; f () === f // => true
小心:惰性作用域会导致的一个很严重的问题是,允许你将函数关联到(refer
to)根本不存在的变量上。这让Javascript中的bug很难调试。Javascript可以通过toString方法支持句法宏(syntactic macros):
var f = function () {return $0 + $1}; var g = eval (f.toString ().replace (/\$(\d+)/g, function (_, digits) {return ’arguments[’ + digits + ’]’})); g (5, 6) // => 11 (IE除外)
理论上可以利用这一原理进行扩展从而实现真正的结构宏(structural macros),操作符重载,类型系统等等。
3.3、 this的含意(灾难)
人们可能会认为他们可以很简单地弄清楚Javascript中的this指向的是什么,然而这显然是件极具挑战性的事情,甚至是几乎不可能的。在函数外(指是的全局作用域)this这一关键字指向的是全局对象(global object),在浏览器中就是window对象。真正的问题是它在函数内的行为是怎样的,这取决于函数是如何被调用的。下面是它的运行原理:如果这个函数是单独调用的,例如:foo(5),那么函数内部的this指向的是全局对象。
如果这个函数是被当作对象的方法调用的,例如:x.foo(5),那么函数内部的this指向的就是这个对象,在本例中就是x。
如果这个函数是一个对象的方法,却单独调用:
var f = x.foo; f (5);
此时,this又将指向全局对象。没有什么东西会记录下 f 这个方法来自哪里,它完全取决于调用的位置。
如果函数是用apply或是call来调用的。this指向的会是你传参的那个对象(除非你企图传递null或undefined,这种情况下this将会指向全局对象):
var f = function () {return this}; f.call (4) // => 4 f.call (0) // => 0 f.call (false) // => false f.call (null) // => [object global]
鉴于这种不可预测性,大部分Javascript库会提供一个捷径将函数的this绑定(在Javascript内就是函数的绑定)到固定的调用上。最简单的做法就是定义一个函数用apply来代理参数并且指定适当的值(幸运的是,这正是闭包的行为):
var bind = function (f, this_value) { return function () {return f.apply (this_value, arguments)}; };
call和apply之间的差异是显而易见的:f.call(x, y, z) 和 f.apply(x, [y, z])是一样的,等价于本例中的bind(f,x)(y,z)。函数内部的this会指向call 和 apply 传入的第一个参数,而call 和 apply其余的参数会作为这个函数的参数。通过apply传入到函数的参数是类数组的(arguments对象在这里可以正常工作),而call会把其余的参数原封不动地传入到函数中。
3.3.1、 重点:代码简写
在大多数函数式编程语言中,你能简写一些东西;也就是说如果你有一个类似function(x){return f(x)} 这样的函数,你可以直接使用f 这个函数而不用这么麻烦。然而在Javascript中这种转换并不总是安全的;考虑一下这样的代码:Array.prototype.each = function (f) { for (var i = 0, l = this.length; i < l; ++i) f (this[i]); }; var xs = []; some_array.each (function (x) {xs.push (x)});
我们可能会自然而然地想到把它改成下面这种更简洁的方式:
some_array.each (xs.push);
然而后者在Javascript中会导致一个很诡异的错误,这种情况发生的发生是因为Array.push函数发现this指向的是全局对象而不是xs。原因显然是each内的这个函数是被当作独立的函数调用的,而不是对象的方法。事实上这个函数忘记了自己是xs的一个方法。(就像上面提到的第3种情况)
最简单的办法就是把xs.push绑定到xs上:
some_array.each (bind (xs.push, xs));
3.3.2、 奇闻:this永远不会为假值(falsy)
具体的原因会4.6节中解释,this永远不会被赋假值。如果你尝试给它赋值null或undefined,例如像这样:var f = function () { return this; }; f.call (null); //真的会返回null么?
事实上它会指向全局的this,在浏览器环境中通常是window。如果你使用原始值,this会指向这个原始值的装箱版本。这里确实有些违反常理,5.4节中会介绍更多的细节。
4、
陷阱
javascript是一门令人惊叹的语言,就像Perl在编程语言中的地位和Linux在操作系统中的地位一样。如果你掌握如何正确地使用它,那么它将会无微不至地解决你所有的问题(好吧,我承认可能不是所有的)。同时,如果你犯一丁点错,它够让你花上几个小时来找bug的。下面是我收集的javascript这门语言中几乎所有的先天不足之处。
4.1 分号推断
如果你能在每行代码的结尾处都加上分号那就万事大吉了。然而,大部分浏览器并不会强制你这么做,如果你漏掉了它们,就可能会产生一些意想不到的效果。大多数情况下Javascript能明白你想要干什么。当你用括号作为一行的开头时,它可能并不知道你真正的用意。例如:
var x = f (y = x) (5)
Javascript会把这两行连接起来:
var x = f (y = x) (5)
我所知道的解决这一问题的唯一办法就是在第一行结尾处加上一个分号。
4.2、空函数
每个函数都有返回值。如果你没用return语句的话返回值就是undefined;反之就是你返回的值。那些使用Ruby或是Lisp的人常常会在这里犯错;例如:var x = (function (y) {y + 1}) (5);
x的结果是undefined。如果你有可能犯在这里栽跟头,有一个叫做“js2-mode”的Emacs模块会识别那些有副作用或没有返回值的函数,并捕获这些错误。
4.3、var
声明一个变量时要格外小心。如果你不使用var关键字,你声明的变量就会是全局的,因此可能导致一些bug:var f = function () { // 因为f处于顶层,所以是全局的 var x = 5; // x 是f的局部变量 y = 6; // y 是全局变量 };
据我所知,同样的问题存在于两种类型的for循环中:
for (i = 0; i < 10; ++i) // i 是全局的 for(var i=0;i<10;++i) // i是其所处方法中的局部变量 for (k in some_object) // k是全局的 for (var k in some_object) // k 是其所处文中的局部变量
4.4、惰性作用域和不确定性
不得不说这场灾难很精彩。请看下面的代码:var x = []; for (var i = 0; i < 3; ++i) x[i] = function () { return i; }; x[0](); // 会输出什么呢? x[1](); x[2]();
这三个方法被调用的时候最终会返回什么?你可能希望他们分别返回0、1、2,因为这正是这些函数被创建时的 i 的值。然而它们都将会返回 3。这是因为Javascript的惰性作用域:在创建的时候,每个函数都只接收了一个变量名和用来搜索这个变量的作用域;变量本身的值直到这些方法被调用的时候才会去解析,而那个时候的i是等于3的。
解决这个问题的最简单的方法就是把我们的这部分代码包裹在一个自执行的匿名函数中,引入到另一层作用域中。下面的代码能够正常工作的原因是因为它被包裹在了一个匿名函数里,new_i的值是不会改变的。
for (var i = 0; i < 3; ++i) (function (new_i) { x[new_i] = function () { return new_i; }; })(i);
顺便说一下,你可能想尝试一下这么做:
for (var i = 0; i < 3; ++i) { var j = i; x[j] = function () { return j; }; }
它会像我们最早的那个例子一样无法正常运行:j 处于离它最近的封闭函数的作用域中(你应该还记得Javascript的作用域是函数级的而不是块级的吧!),所以它的值会伴随着 i 一起改变。
4.5、相等
因为==其实并不是完全相等,在Javascript中下面的都会是true:null == undefined false == 0 false == ’’ ’’ ==0 true == 1 true == ’1’ ’1’ ==1
所以,除非你真的想要这种效果,否则的话永远不要使用 == 操作符。取而代之应该使用行为更加明确的 === (它的否定式是 !==)。尤其需要注意的是两个操作数不仅仅是值相同,而且类型也要相同。它会用装箱值做引用比较,同时用拆箱值做结构比较。如果做比较的一边是装箱值,而另一边是拆箱值,那么===返回的永远是false。字符串字面量是拆箱值,所以,你可以在下面这种情况下使用它:
’foo’ === ’fo’ + ’o’
有一种情况 == 会比 === 更加有用。如果你想知道一个属性是否存在(即,不是null也不是undefined的情况),最简单的办法是用(x == null) 而不是更详细地用(x === null || x === undefined)。除此之外我再也想不到哪里会经常用到
== 。
小心:事实证明用==来验证真值并不总是稳定的。如果x = 0, y = new Number(0),此时 x == y 为真,然而 !!x 是 false,!!y 是 true。4.6节会详细阐述为什么会发生这种情况。
4.6、装箱 vs 拆箱
装箱值永远为真,并且可以添加属性。无法给拆箱值添加属性,但是它们也不会报错;例如:var x = 5; x.foo = ’bar’; x.foo // => undefined; x 是一个拆箱后的数字 var x = new Number (5); x.foo = ’bar’; x.foo // => ’bar’; x 是一个引用(pointer)
如何对一个拆箱值进行装箱?你可以这么做:
就像上面那样直接调用构造方法
设置prototype中的一个成员并且在那个方法中引用this(见第5节)
把这个拆箱值作为call 或 apply方法中第一个传入的参数(见3.3.2节)
所有的HTML对象都是装箱值。
4.7、不报错的异常
Javascript的语法环境非常宽松。下面这些代码都是合法的:[1, 2, 3].foo // => undefined [1, 2, 3][4] // => undefined 1 / 0 // => Infinity 0 * ’foo’ // => NaN
另外还有些非常有用的语句:
e.nodeType || (e = document.getElementById (e)); options.foo = options.foo || 5;
除此之外你还要知道当使用+的时候Javascript会把任何东西转换成字符串或是数字。下面的这些表达式全都是字符串:
null + [1, 2] // => ’null1,2’ undefined + [1, 2] // => ’undefined1,2’ 3 + {} // => ’3[object Object]’ ’’ + true // => ’true’
以下的这些表达式都是数字:
undefined + undefined // => NaN undefined + null // => NaN null + null // => 0 {} + {} // => NaN true + true // => 2 0 + true // => 1
下面的这些是我的收藏:
null * false + (true * false) + (true * true) // => 1 true << true << true // => 4 true / null // => Infinity [] == [] // => false [] == ![] // => true
4.8、强制数值转换
我最近被一件事情搞得措手不及,就是Javascript的强制类型转换的不确定性。例如:{} // true !!{} // 强制转换为boolean值,true +{} // 强制转换为数字,NaN,false [] // 真 !![] // 强制转换为boolean值,true +[] // 强制转换为数字, 0,false [] == false // true (因为 [] 真的是0或者是其他什么东西) [] == 0 // true [] == ’’ // true (因为 0 == ’’) [] == [] // false (不同的引用,无需强制类型转换) [1] == [1] // false (不同的引用,无需强制类型转换) [1] == +[1] // true (右边是数字,强制类型转换)
当你在使用一些操作符操作非数值时你要格外小心。例如,下面的这个函数就无法告诉你一个数组中是否含有真值:
var has_truthy_stuff = function (xs) { var result = 0; for (var i = 0, l = xs.length; i < l; ++i) result |= xs[i]; return !!result; }; has_truthy_stuff([{}, {}, 0]) // 返回 false
has_truthy_stuff返回false的原因是因为{}做强制类型转换的时候会向数字转换,变成NaN,而在Javascript里NaN为假。使用 |= 操作NaN,就相当于操作0,什么事也不会发生。所以result对数组中所有的值得到的都是0。因此这个方法并不能实现我们想要的效果。
另外,你可以通过(重)定义valueOf方法来改变数值转换的值:
+{valueOf: function () {return 42}} // -> 42 Object.prototype.valueOf = function () { return 15; }; Array.prototype.valueOf = function () { return 91; }; +{} // -> 15 +[] // -> 91 +[1] // -> 91
这是值得我们思考的一点,因为它会产生一些有趣的影响。首先,valueOf() 可能会永不终止。例如:
Object.prototype.valueOf = function () { while (true); }; {} == 5 // 永不返回; {} 被转换成一个数字 +{} // 永不返回 !{} // 返回 false; 会绕过执行 valueOf()
其次,valueOf只是一个常规的Javascript函数,所以它可能会造成安全漏洞。假如你使用了eval() 作为JSON解析器(虽然这不是个好主意),同时也没有先对输入格式进行检查。如果某人传入{valueOf: function () {while (true);}},那么你的应用会在进行第一次对象向数字的强制类型转换时挂起(这种强制类型转换可能是隐式的,比如像上面例子中的
== 5)。
小心:一个数组会转换成什么数值是依赖于这个数组中的内容的:
+[0] //0 +[1] //1 +[2] //2 +[[1]] //1 +[[[[[[[1]]]]]]] // 1 +[1, 2] // NaN +[true] // NaN +[’4’] // 4 +[’0xff’] // 255 +[’ 0xff’] // 255 -[] // 0 -[1] // -1 -[1, 2] // NaN
如果一个数组嵌套的太深,那么数组内建的数值转换就会因为堆栈溢出而失败。例如:
for (var x = [], a = x, tmp, i = 0; i < 100000; ++i) { a.push(tmp = []); a = tmp; } a.push(42); // 这个数组嵌套了100000层,而这个值才是我们真正想要的 x == 5 // 在V8引擎中堆栈会溢出
幸运的是,当你在V8引擎中让一个数组包含自身时,数值转换还是可正常运行的;所以下面这个例子看上去有些乏味:
var a = []; a.push(a); +a // 0
4.9、报错的异常
如果你按函数的方式调用了一个非函数的变量,或者你访问了null或undefined的属性,抑或你引用了一个根本不存在的全局变量,那么Javascript会抛出TypeError或ReferenceError的错误。除此之外,当你引用一个不存在的局部变量时会导致ReferenceError错误,因为Javascript会认为你在访问全局变量。
4.10、抛出异常
你可以throw很多不同的东西,包括拆箱值。这会带来一些好处,请看下例中的代码:try { … throw 3; } catch (n) { // n 没有堆栈跟踪 }
throw/catch不会维护一份堆栈跟踪,它处理异常的速度要比平常快很多。因此为了便于调试,最好还是抛出一个恰当的错误:
try { … throw new Error(3); } catch (e) { // e 有一份堆栈跟踪,这在像Firebug之类的调试器中很有用 }
4.11、当心typeof
因为它的行为会像下面这样:typeof function () {} // => ’function’ typeof [1, 2, 3] // => ’object’ typeof {} // => ’object’ typeof null // => ’object’ typeof typeof // hangs forever in Firefox
在很多情况下,用typeof来做类型检测是一种很蹩脚的方式,更好的做法是使用对象的constructor属性,像这样:
(function () {}).constructor // => Function [1, 2, 3].constructor // => Array ({}).constructor // => Object true.constructor // => Boolean null.constructor // TypeError: null 没有属性
为了防止null和undefined被访问(因为你无法访问它们的constructor),你可能需要依赖它们所代表的布尔值:
x && x.constructor
但是事实上,这么做可能导致诸如 ’’,0,false,NaN等验证失败。就我所知道的唯一的解决办法是做如下比较:
x === null || x === undefined ? x : x.constructor x == null ? x : x.constructor // 同样的效果,但是更简洁
相反,如果你要检测一个值是不是已给定的类型,你就要用instanceof,它永远不会抛出异常。
4.12、也要当心 instanceof
一般情况下,instanceof要比typeof有用,但是它只对装箱值起作用。例如,下面的都是false:3 instanceof Number ’foo’ instanceof String true instanceof Boolean
然而,这些都为true:
[] instanceof Array ({}) instanceof Object [] instanceof Object // Array inherits from Object /foo/ instanceof RegExp // regular expressions are always boxed (function () {}) instanceof Function
解决上面第一个问题的办法就是包装一下原始值:
new Number(3) instanceof Number // true new String(’foo’) instanceof String // also true new Boolean(true) instanceof Boolean // also true
一般来说,对于所有的原始值x,(new x.constructor(x) instanceof x.constructor)总会为true。然而无法对null和undefined这么做,因为访问它们的constructor会报错,并且据我所知,永远无法从它们构造函数中返回任何结果(也就是使用new)。
4.13、浏览器不兼容性
从IE6以后,浏览器对语言核心部分的兼容性还是很好的。然而,IE中有一个String.split的bug:var xs = ’foo bar bif’.split (/(\s+)/); xs // 在标准浏览器中: [’foo’, ’ ’, ’bar’, ’ ’, ’bif’] xs // 在IE中: [’foo’, ’bar’, ’bif’]
IE6中还有个更加细微的bug花了我几个小时才找到它,那就是eval()不能返回函数:
var f = eval(’function() {return 5}’); f() // 在标准浏览器中:5 f() // 在IE6中:报 ’Object expected’ 的错(因为f没有定义)
我敢肯定有其他相似的bug存在,但通常这些问题最常出现在DOM中。
5、原型
我曾经发表过反对OOP的言论,但是考虑到我偶尔会使用原型,我删除了那条言论。尽管我对Javascript因为市场压力去迎合说是Java给的灵感这种说法嗤之以鼻,然而,基于原型的编程有时还是很有用的。本节内容含有我自己主观的甚至是偏激的观点。
当你定义一个函数的时候,它都能够以两种方式使用。也就是说,就像每个标准的程序员所想的那样,一个函数标准的使用方式是能接收值也能返回值,还有另一种完全不同的方式,那就是生成实例。下面就是一个例子:
// 一个标准函数: var f = function (x) {return x + 1}; f (5) // => 6
这是大多数人期望的。而下面的这种方式只要是个理性的人类就无法想到:
// 一个构造函数: var f = function (x) {this.x = x + 1}; // 没有return! var i = new f (5); // i.x = 5
这种情况下,下面的情况都为true:
i.constructor === f i.__proto__ === i.constructor.prototype // 至少在Firefox中总会为true i instanceof f typeof i === ’object’
关键字new是一个右结合的一元运算符,所你可以用它来实例化对象:
var x = 5; new x.constructor (); // 创建一个x的装箱版本, 不论x是什么 new new Function('x', 'this.x = 5');
如果你准备使用这种不可靠的设计模式,那么就可能想添加一些方法:
var f = function (x) {this.x = x}; f.prototype.add_one = function () {++this.x}; var i = new f (5); i.add_one (); i.x // => 6
你可以在网上找到大量的关于原型编程的信息。
5.1、为什么new是可怕的
虽然new有一些很酷的特性(很流逼的),但是它也有一个很可怕的缺点。那就是大多数Javascript函数都可以被转发(forwarded),你可以把一个已经存在的函数包裹在一个新的函数里,而这个函数被调用的时候和之前毫无差异。例如:var to_be_wrapped = function (x) {return x + 1}; var wrapper = function () { return to_be_wrapped.apply (this, arguments); }; // 对于所有的x, wrapper(x) === to_be_wrapped(x)
然而new没有这种机制。一般情况下你不能转发一个构造函数,因为你没有办法对new(译注:作者这里应该是指构造函数(constructor))做像apply这样的事情。(当然也并不是完全没有可能,下一节我会给出一个不错的解决方案。)
5.2、为什么new并不可怕
不久前,我收到Ondrej Zara的邮件,他指出我之前对new的偏见是毫无根据的,并对我上一节中提到的问题给出一个非常优雅的解决方案,我把他的代码一字不落地搬了过来:var Forward = function(ctor /*, args... */) { var tmp = function(){}; tmp.prototype = ctor.prototype; var inst = new tmp(); var args = []; for (var i=1;i<arguments.length;i++) { args.push(arguments[i]); } ctor.apply(inst, args); return inst; }
下面是测试用例:
var Class = function(a, b, c) {} var instance = Forward(Class, a, b, c); instance instanceof Class; // true
起初我很怀疑这种做法是否会正常运行,然而目前为止我还没有发现一个失败的例子。所以在Javascript里构造函数确实能被转发,虽然这和我之前说的有点矛盾。
5.3、为什么应该使用原型
如果你需要一个动态调度(派遣)模式,那么原型可能是你最好的选择,你直接使用它们要比你自己搞个套路出来更可行。Google的V8引擎对原型做的专门的优化,不久后发行的Firefox也会这么做。并且,原型更加节省内存;因为只有一个指针指向一个原型,这要比有n个指针指向n个属性更省资源。另一方面,如果你自己实现了一个继承机制,那么你可能正在犯错。我已经证明了原型编程在Javascript中是一种很高效的方法,但是,继承在Javascript中却是(1)缓慢的,并且(2)Javascript的“一切都是公共的”(everything is public)模式并不具代表性。
5.4、自动装箱
你可能会企图尝试去做下面的事情:Boolean.prototype.xor = function (rhs) {return !! this !== !! rhs};
而且在运行下面这段代码的时候就会发生这种悲惨而不幸的状况:
false.xor (false) // => true
这是因为,当你把一个未装箱的值当作对象(例如调用它某个方法),它会暂时变成一个装箱值,目的是为了能调用它的方法。之后,它不会再自动拆箱变回原始值,所以这意味着它失去了它曾经拥有的原始假值(falsity)。当然,根据你正在使用的类型,你可以把它再转换回拆箱值:
function (rhs) {return !! this.valueOf () !== !! rhs};
6、一个非常棒的等价用法
Javascript中有一种用法非常重要,然而并不是人人都能意识到。那就是:在所有的情况下 foo[‘bar’] 和 foo.bar是完全等价的。你可以提前在你的代码中放心地使用这种方式,无论是属性值、方法或者是其他什么东西。此外,你还能给一个对象中尚未定义的属性赋值:var foo = [1, 2, 3]; foo[’@snorkel!’] = 4; foo[’@snorkel!’] // => 4
当然,你也可以用这种方法来读取属性:
[1, 2, 3][’length’] // => 3 [1, 2, 3][’push’] // => [native function]
事实上,这正是 for(var … in …) 语句所做的事情:遍历一个对象的属性。例如:
var properties = []; for (var k in document) properties.push (k); properties // => 是一组字符串
然而,for…in 也有缺点。当你修改prototype时会发生一些怪异的事情。例如:
Object.prototype.foo = ’bar’; var properties = []; for (var k in {}) properties.push (k); properties // => [’foo’]
要解决这个问题,你需要做两件事。第一,永远不要修改Object的prototype,因为一切都是Object的实例(包括数组和其他所有的装箱值);第二,使用hasOwnProperty:
Object.prototype.foo = ’bar’; var properties = [], obj = {}; for (var k in obj) obj.hasOwnProperty (k) && properties.push (k); properties // => []
并且,非常重要的一点是,永远不要用for…in 来遍历数组(它只会返回字符串索引而不是数字索引,这会导致一些问题)或是字符串。如果你给Array或是String(或是Object)添加方法在这里都会失败(当然,你不会那么做)。
7、如果你有二十分钟……
Javascript几乎可以做任何其他语言能做的事。然而,要想了解这一点似乎并没那么容易。
7.1、常用迭代器
因为像Ruby这样的语言已经向世界展示了太多过气的for循环,太多有自尊心的程序员不喜欢用for。如果你的项目运行在Firefox上,那么你确实没必要用for,因为Firefox浏览器中Array的prototype已经包含了map和forEach方法。但是,如果你要写一个跨浏览器的代码,同时还不打算用第三方的库,这里有个好办法能实现你需求:Array.prototype.each = Array.prototype.forEach || function (f) { for (var i = 0, l = this.length; i < l; ++i) f (this[i]); return this; // 为链式调用提供便利 }; Array.prototype.map = Array.prototype.map || function (f) { var ys = []; for (var i = 0, l = this.length; i < l; ++i) ys.push (f (this[i])); return ys; };
据我所知这(几乎)是写出这些函数的最快途径。我们预先声明了两个变量(i和l)其中一个用来缓存长度(length)。Javascript并不知道this.length在for循环的过程中是不变的,所以,如果我们没有缓存它,那么for会在每次循环中都检测一下。这么做会很耗资源,因为对于一个装箱值,如果我们无法在this中找到length这个属性,那么它会下降到this.__proto__去找到它。然后触发一个方法调用来取回length值。
此外,我们还能做的唯一的优化就是逆向遍历数组(只在each中才会这样,因为我们假定map是正向遍历的):
Array.prototype.each = function (f) { for (var i = this.length - 1; i >= 0; --i) f (this[i]); };
上面的这种运行速度上比第一种实现要快一点,因为它用一个浮点数的减少作为检测标志(需要用<与非零值比较),因为它内部的运算其实是按位与(and)并且前面的0位会跳过。除非你的Javascript引擎中已经支持上面的函数抑或你真的能确定这么做会影响性能(既然这样我想问的是你为什么还要首选Javascript呢),你可能永远不用考虑小于一个非0值与大于等于0哪个开销更大。
你也可以给对象定义一个遍历器,但是,不要像下面这么做:
// 千万别用这种方法 Object.prototype.each = function (f) { for (var k in this) this.hasOwnProperty (k) && f (k); };
更好的办法是单独实现一个keys函数,而不是像上面这样去污染Object的prototype:
var keys = function (o) { var xs = []; for (var k in o) o.hasOwnProperty (k) && xs.push (k); return xs; };
7.2、Java的类和接口
只要是个正常人就不会想到使用这种思路。但是,如果你是个疯子或者有人强迫你,那么Google Web Toolkit会告诉你怎么搬起石头砸自己的脚,顺便把它转换成Javascript。
7.3、递归元类
我们可以用不同的方法来实现它,但是最简单的就是下面这样:var metaclass = {methods: { add_to: function (o) { var t = this; keys (this.methods).each (function (k) { o[k] = bind (t.methods[k], o); // 这里不能用this }); return o}}}; metaclass.methods.add_to.call (metaclass, metaclass);
此时,metaclass本身就是个元类。我们可以创建一些实例:
var regular_class = metaclass.add_to ({methods: {}}); regular_class.methods.def = function (name, value) { this.methods[name] = value; return this; }; regular_class.methods.init = function (o) { var instance = o || {methods: {}}; this.methods.init && this.methods.init.call (instance); return this.add_to (instance); }; regular_class.add_to (regular_class);
这是一个Ruby式的类,你可以定义公共方法和构造函数。例如:
var point = regular_class.init (); point.def (’init’, function () {this.x = this.y = 0}); point.def (’distance’, function () { return Math.sqrt (this.x * this.x + this.y * this.y)});
现在,你可以使用这个类了:
var p = point.init (); p.x = 3, p.y = 4; p.distance () // => 5
使用元类的好处是你可以用这一结构做一些有趣的东西。例如,假设我们想在点(point)中插入跟踪方法以便调试:
keys (point.methods).each (function (k) { var original = point.methods[k]; point.methods[k] = function () { trace (’Calling method ’ + k + ’ with arguments ’ + Array.prototype.join.call (arguments, ’, ’)); return original.apply (this, arguments); }; });
现在,当point实例中的任意一个方法被调用的时候trace(trace并不是Javascript内建的,你得自己定义它)也会被调用,而且它会同时访问参数和状态。
7.4、尾调用
默认情况下Javascript并没有对尾调用(tail-call)做优化,这一点确实不太好,因为一些浏览器会造成调用堆栈短路(我知道的最短的是500帧,因此,对于绑定函数和遍历器来说,它会消耗的非常快)。幸运的是,在Javascript中对尾调用编码相当简单:Function.prototype.tail = function () {return [this, arguments]}; Function.prototype.call_with_tco = function () { var c = [this, arguments]; var escape = arguments[arguments.length - 1]; while (c[0] !== escape) c = c[0].apply (this, c[1]); return escape.apply (this, c[1]); };
我们现在可以写一个针对尾调用做过优化的阶乘函数:
// 标准递归 var fact1 = function (n) { return n > 0 ? n * fact1 (n - 1) : 1; }; // 尾递归 var fact2 = function (n, acc) { return n > 0 ? fact2 (n - 1, acc * n) : acc; }; // 我们刚定义的尾调用 var fact3 = function (n, acc, k) { return n > 0 ? fact3.tail (n - 1, acc * n, k) : k.tail (acc); };
前两个函数可以被正常调用:
fact1 (5) // => 120 fact2 (5, 1) // => 120
但是它们也无法运行在连续的堆栈空间中。第三种我们会像下面这样调用:
var id = function (x) {return x}; fact3.call_with_tco (5, 1, id) // => 120
这种方式采用了尾调用优化的策略,而不是去创建新的堆栈帧:
fact1(5) 5 * fact1(4) 4 * fact1(3) ...
也不是去创建一个空的堆栈:
fact2(5, 1) fact2(4, 5) fact2(3, 20) ...
我们会在分配新的堆栈之前弹出最后一个堆栈帧(把[function, args]当作一种连续的类型返回):
fact3(5, 1, k) -> [fact3, [4, 5, k]] fact3(4, 5, k) -> [fact3, [3, 20, k]] fact3(3, 20, k) ...
它不会造成严重的性能损失,为只有两个元素的数组分配指针开销是很小的。
7.5、句法宏和运算符重载
惰性作用允许我们做一些很有意思的事。假如说我们想对变量声明的语法做一个新的定义,比如我们准备把下面的语法:var f = function () { var y = (function (x) {return x + 1}) (5); ... };
替换为:
var f = function () { var y = (x + 1).where (x = 5); ... };
我们可以用正则表达式来实现这个想法,前提是你不介意我们把一半的时间花在给这种表达式排错上:
var expand_where = function (f) { var s = f.toString (); return eval (s.replace (/\(([ˆ)]+)\)\.where\(([ˆ)])\)/, function (_, body, value) { return ’(function (’ + value.split (’=’)[0] + ’){return ’ + body + ’}) (’ + value.split (’=’, 2)[1] + ’)’; })); };
现在我们能用下面这种语法了:
var f = expand_where (function () { var y = (x + 1).where (x = 5); ... });
显然,一个合适的解析器是必要的,因为这样才能保证不会在简单的括号边界处报错。然而,你应该意识到一件重要的事,那就是函数给我们提供了一个引用代码的途径,就像 在Lisp里面:
(defmacro foo (bar) ...) (foo some-expression)
在Javascript里(假设下面的parse和deparse已经定义过而且实现很复杂)会是下面这样:
var defmacro = function (transform) { return function (f) { return eval (deparse (transform (parse (f.toString ())))); }; }; var foo = defmacro (function (parse_tree) { return ...; }); foo (function () {some-expression});
这个原理可以扩展到运算符重载上,我们可以用一个方法调用的方式来重写位移运算符:
x << y // 重写成 x[’<<’](y)
还记得属性名的标识符是不加限制的吧?所以,就像在Ruby中那样,我可以为数组重载<<运算符:
Array.prototype[’<<’] = function () { for (var i = 0, l = arguments.length; i < l; ++i) this.push (arguments[i]); return this; };
相比Lisp,在Javascript中想要实现这种事情唯一不幸的地方就是Javascript会把句法结构当作语法,这就导致引入像when这样的新的句法结构并不是件容易的事:
expand_when (function () { when (foo) { // 编译错误 ,不期望出现的{ bar (); } });
然而,在遵循Javascript的解析树(Javascript parse tree)的前提下,你想做什么事都可以。
8、扩展阅读
我强烈建议您读一下jQuery(http://jquery.com)用业界良心写就的源码。不得不说这是一项辉煌的工程,我从阅读他们代码的过程中学到了很多东西。道格拉斯·克罗克福德(Douglas Crockford)写过一些非常不错是Javascript参考指南,包括众所周知的《Javascript语言精粹》和不怎么出名但却是免费的Javascript语言指引 http://javascript.crockford.com/survey. html。
我在这里想无耻地提一下我自己写的一个库,Divergence(http: //github.com/spencertipping/divergence),我同样也建议你读一下它。它与jQuery非常不同,更加简练而高效(而且是DOM无关的)。jQuery用了更多的传统方法,然而Divergence趋向使用闭包和函数元程序设计。
如果你使用Lisp和元程序,你可能也对http:// github.com/spencertipping/divergence.rebase 和http://github.com/
spencertipping/caterwaul 感兴趣。这两个项目都用到了函数序列化和eval()来实现在上节中提到的句法扩展。
还有我最近发现的一个网站叫http://wtfjs.com ,它似乎想让Javascript中所有诡异的行为都暴露出来。这个网站确实是非常有趣,也让人深受启迪。一个可以让我们更加深入地了解Javascript中的精华、糟粕和丑陋部分的网站是http://perfectionkills.com ,它是PrototypeJS的一个开发人员写的,它让我意识到自己对Javascript并不是真的那么了解。
相关文章推荐
- javascript客户端检测技术
- js统计文本框内已输入字数
- javascript 用call来继承实例属性
- 浅谈JS DDoS攻击原理与防御
- JavaScript实现把rgb颜色转换成16进制颜色的方法
- JavaScript属性标签
- eval、json.parse()的介绍和使用注意点
- JavaScript实现的简单拖拽效果
- js事件传参
- 一看就懂:jsonp详解
- 十个JavaScript中易犯的小错误,你中了几枪?
- JS数组array元素的添加和删除方法代码实例
- js实现网页新消息标题闪烁提醒
- JS实现HTML静态页传值的方法
- Js 操作 Cookies
- js中cookie的使用详细分析
- JS中注意原型链的“指向”
- 循环中的闭包
- javascript模板引擎原理
- 【优波尔】JS基本内容整理(2)