原文参见:Javascript语言的10个古怪之处
数据类型和定义
1. Null是一个对象
JavaScript的Null类型有个唯一值null,定义为没有意义的值。确实如此,Null是“没有意义的值”。下面的代码用于对于此说明进行简单验证:
alert(typeof null); //alerts 'object'
typeof值显示null是"object",但并不因此而应认为null就是一个对象实例。(JavaScript中的值都是对象实例:比如数字都是Number对象,对象都是Object对象。)因为null代表“没有值”,从这个角度理解,null不是任何对象的实例,因此,以下结果为false:
alert(null instanceof Object); //evaluates false
2. NaN是一个数值
如果认为“null”是对象是很荒谬的想法,再来看一下NaN(表示not-a-number,即“不是数值”的缩写)在Javascript却是一个数值!而且,更荒谬的是NaN不等于它自己!唉,开始头痛了。
alert(typeof NaN); //alerts 'Number'
alert(NaN === NaN); //evaluates false
实际上NaN和其它任何值都不相等。判断一个变量是不是NaN的唯一方法是使用isNaN().
3. 空数组 == false(关于Truthy和Falsy)
此为JavaScript另一个广为人知的古怪之处:
alert(new Array() == false); //evaluates true
要把这里的事情搞清楚,需要理解truthy(真值)和falsy(假值)的概念。
关于真值/假值的所有解释中,最容易理解的是:JavaScript中,所有非Boolean值都会内置一个boolean标志,当该值需要以boolean方式使用的时候(比如,需要跟 Boolean值进行比较的时候),内置布尔值就会用到。
苹果不能和梨进行比较,当JavaScriptrcn把两个不同类型的值进行比较的时候,首先会强制转化成相同的类型。false, undefined, null, 0, "", NaN都转化成false。转化是暂时的,仅用于正在运行的表达式。示例如下:
var someVar = 0;
alert(someVar == false); //evaluates true
此处把数值0和boolean值false做比较,因两者数据类型不兼容,JavaScript语言透明地统一转化成为truthy和falsy的等同项,这里0等同于false。
是否注意到了,假值列表中并没有空数组? 空数组很奇怪:运行时属于truthy,但当与Boolean类型进行比较的时候,表现得又像falsy。是否有些迷惑不解了?这样的语言行为有其原因。先看一段示例代码:
ar someVar = []; //empty array
alert(someVar == false); //evaluates true
if (someVar) alert('hello'); //alert runs, so someVar evaluates to true
为避免强制转换,可使用强等于(===)代替弱等于(==)。
var someVar = 0;
alert(someVar == false); //evaluates true – zero is a falsy
alert(someVar === false); //evaluates false – zero is a number, not a boolean
关于JavaScript比较两个值时内部发生的事情,想了解更多的话,可参见:section 11.9.3 of the ECMA-262
正则表达式
4. replace()可以接受回调函数
这是JavaScript最鲜为人知的秘密,已发布到v1.3。大多数情况下,replace()用法如下:
alert('10 13 21 48 52'.replace(/d+/g, '*')); //replace all numbers with *
此处进行简单替换,一个字符串,一个星号。但如希望替换时进行更多的控制,包括替换时机及替换逻辑的控制,该怎么处理?比如,如果只想替换30以下的数值怎么办?仅靠正则表达式无法做到这一点(全都涉及字符串操作,而非数学操作)。我们需要在代码调用中转入回调函数对每个匹配值进行计算。
alert('10 13 21 48 52'.replace(/d+/g, function(match) { return parseInt(match) < 30 ? '*' : match; }));
对于每个匹配项,JavaScript都会调用我们的函数,匹配项传递给我们的match参数。然后返回星号(如匹配的数字小于30)或match本身(即不应该进行匹配)。
5. 正则表达式:不仅仅只是match和replace
许多中级JavaScript开发人员仅仅通过通过match和replace和正则表达式打交道。但是JavaScript正则表达式相关方法不止这两个。
值得关注的是test(),其工作方式与match()类似,但并不返回匹配项,只是确认模式是否匹配。从这个意义上讲,test()是更轻量级的计算。
alert(/w{3,}/.test('Hello')); //alerts 'true'
上面的代码查找由三个或更多字母数字字符组成的字符串。字符串Hello满足该要求,因此得到true。结果不会返回实际的匹配项,只是返回true。
还可以使用RegExp对象,可以创建动态正则表达式,而非静态正则表达式。大多数正则表达式都是使用短格式声明的(如上文所述用斜杠括起来的声明模式),也就无法引用变量,因此不可能创建动态模式。有了RegExp对象就可以创建动态正则表达式了。
function findWord(word, string) {
var instancesOfWord = string.match(new RegExp('b'+word+'b', 'ig'));
alert(instancesOfWord);
}
findWord('car', 'Carl went to buy a car but had forgotten his credit card.');
这里,我们基于arguments的值创建了动态模式word。该函数返回word在字符串中单独显示为单词的次数(即不作为其他单词的一部分)。因此,我们的示例将返回car一次,而忽略car单词Carl和card。这里的正则匹配模式通过检查待查找单词两侧的单词边界(b)来进行。
函数和作用域
6. 伪造作用域
执行某些操作的范围定义了可访问的变量的作用域。独立的JavaScript(即不在函数内部运行的JavaScript)在window对象的全局范围内运行,所有对象都可以访问该对象。而函数内部声明的局部变量只能该函数内部访问,不能在外部访问。
var animal = 'dog';
function getAnimal(adjective) {
alert(adjective+' '+this.animal);
}
getAnimal('lovely'); //alerts 'lovely dog';
此处,变量和函数都在全局范围内声明(即window)。因为'this'始终指向当前范围,所以在此示例中,this指向window。函数将查找window.animal变量。目前为止,一切正常。实际上,可以让函数运行在其它的作用域下,而忽视其本身的作用域。为此,可调用Javascript内置call()方法,而非调用函数本身:
var animal = 'dog';
function getAnimal(adjective) {
alert(adjective+' '+this.animal);
};
var myObj = {animal: 'camel'};
getAnimal.call(myObj, 'lovely'); //alerts 'lovely camel'
这里的函数不是在运行在window对象上,而是在运行myObj对象上 -- 被指定为call方法的第一个参数。call()方法中的第一个参数可以伪造函数中的this,因此,这里的this.animal实际上就是myObj.animal,也就是'camel'了。this之后的其它参数继续传递给函数调用。
顺便说一句,apply()功能与call()相同:除了将函数的参数指定为数组而不是单独的参数这一点差别之外。上面的示例使用apply()可以写成下面的代码:
getAnimal.apply(myObj, ['lovely']); //func args sent as array
7. 函数可以执行其本身
下面的代码没有问题:
(function() {
alert('hello');
})(); //alerts 'hello'
语法很简单:声明一个函数并立即调用,就像使用()语法调用其它函数一样。为什么要这么做?看起来有些矛盾:函数通常包含想稍后执行的代码,而不是需要马上执行的代码,否则就不必把代码放在函数中。
自我执行功能(SEF)带来的好处是将变量的当前值绑定到延迟代码中使用,例如事件的回调(callback),超时执行(timeouts)和间隔执行(intervals)等。如下例子:
var someVar ='hello';
setTimeout(function() {
alert(someVar);
}, 1000);
var someVar ='goodbye';
初学者总会问到,为什么alert在timeout弹出goodbye,而不是hello?答案是timeout回调函数就是一个纯粹的回调函数,在someVar运行之前不会计算其值;而到真正运行的时候,someVar的值已赋为goodbye。
SEF为这类问题提供了解决方案。不像上面那样隐式地指定timeout回调,而是直接将someVar值以参数的形式传进去。这种做法很有效果,我们传入并隔离someVar的当前值,之后someVar的实际变量值发生的任何变化都不会影响我们这里传入的值。就像在重新为汽车喷漆之前拍张照片一样,照片将不会使用重新喷涂的颜色进行更新,将永远显示照片拍摄时的汽车颜色。
var someVar = 'hello';
setTimeout((function(someVar) {
return function() { alert(someVar); }
})(someVar), 1000);
var someVar = 'goodbye';
这次,alert显示的是hello,因为使用的是之前隔离版本someVar的值(即传入的函数参数,而不是外部变量)。
浏览器
8. FIREFOX读取并返回RGB而不是十六进制的颜色
我从来没有真正理解过Mozilla为什么这样做。确定的一点是,大多通过JavaScript查询、计算颜色人都对十六进制格式感兴趣,而非RGB。下面的代码可以确认这一点:
Hello, world!
<script>
var ie = navigator.appVersion.indexOf('MSIE') != -1;
var p = document.getElementById('somePara');
alert(ie ? p.currentStyle.color : getComputedStyle(p, null).color);
</script>
虽然大多数浏览器都会发出alert ff9900,但Firefox返回rgb(255, 153, 0)RGB等效值。从而也出现了大量的JavaScript函数用于将RGB转换为十六进制。
杂项
9. 0.1 + 0.2 !== 0.3
真是一个古怪的问题。这个问题不仅出现在JavaScript中,实际上是计算机科学中一遍存在个普的问题,影响了许多语言。其输出为0.30000000000000004。
此问题与称为机器精度的问题有关。当JavaScript尝试执行上面的代码行的时候,会把数值转换为等效的二进制值。
这就是问题所在:0.1不是真正的0.1,而是0.1的二进制等价形式,是一个接近(但不相等)的值。本质上,值一写入精度就丢失了。可能只是想得到简单的两个小数,实际是(根据Chris Pine的注解)进行二进制浮点计算。好比需要把文字翻译成俄语却得到了白俄罗斯语:结果相似,但不相同。
解决此问题的方法是计算机科学和开发人员最喜欢在论坛探讨的主题之一。某种程度上,做出的选择取决于正在执行的计算。各种方法的优缺点不在本文讨论范围之内,但是常见的选择无法是:
-
转换为整数并对其进行计算,然后再转换回十进制;
-
调整逻辑以允许结果以范围表示,而不是一个特定的数值结果。
例如,代码不应该写成下面这样:
var num1 = 0.1, num2 = 0.2, shouldEqual = 0.3;
alert(num1 + num2 == shouldEqual); //false
可以试着写成这样:
alert(num1 + num2 > shouldEqual - 0.001 && num1 + num2 < shouldEqual + 0.001); //true
10. 未定义(undefined)可以被定义(defined)
最后一个古怪之处是看起来有些蠢、也有些无关紧要。听起来有点奇怪,undefined并非JavaScript的保留字,尽管其具有特殊意义,并且是确定变量是否未定义的唯一方法。因此:
var someVar;
alert(someVar == undefined); //evaluates true
目前为止,一切正常。但以下代码:
undefined = "I'm not undefined!";
var someVar;
alert(someVar == undefined); //evaluates false!
可以查看Mozilla JavaScript中所有保留字的列表,进行参考。