MXOXW

Life always finds a way.

JavaScript 权威指南总结

| Comments

二进制浮点数和四舍五入错误

JavaScript采用了IEEE-754浮点数表示法(几乎所有现代编程语言所采用),这是一种二进制表示法,可以精确地表示分数,比如1/2、1/8和1/1024。遗憾的是,我们常用的分数(特别是在金融计算方面)都是十进制分数1/10、1/100等。二进制浮点数表示法并不能精确表示类似0.1这样简单的数字。

1
2
3
4
5
var x = .3 - .2;        // 30美分减去20美分
var y = .2 - .1; // 20美分减去10美分
x == y // => false:两值不相等!
x == .1 // => false: .3-.2 不等于 .1
y == .1 // => true: .2-.1 等于 .1

JavaScript的未来版本或许会支持十进制数字类型以避免这些舍入问题。在这之前你可能更愿意使用大整数进行重要的金融计算,例如,要使用整数“分”而不要使用小数“元”进行基于货币单位的运算。

null和undefined

null是JavaScript语言的关键字,它表示一个特殊值,常用来描述“空值”。对null执行typeof预算,结果返回字符串“object”,也就是说,可以将null认为是一个特殊的对象值,含义是“非对象”。但实际上,通常认为null是它自有类型的唯一一个成员,它可以表示数字、字符串和对象是“无值”的。大多数编程语言和JavaScript一样含有null:你可能对null或nil很眼熟。

JavaScript还有第二个值来表示值的空缺。用未定义的值表示更深层次的“空值”。它是变量的一种取值,表明变量没有初始化,如果要查询对象属性或数组元素的值时返回undefined则说明这个属性或元素不存在。如果函数没有返回任何值,则返回undefined。引用没有提供实参的函数形参的值也只会得到undefined。undefined是预定义的全局变量(它和null不一样,它不是关键字),它的值就是“未定义”。在ECMAScript 3中,undefined是可读/写的变量,可以给它赋任意值。这个错误在ECMAScript 5中做了修正,undefined在该版本中是只读的。如果使用typeof运算符得到undefined的类型,则返回“undefined”,表明这个值是这个类型的唯一成员。

尽管null和undefined是不同的,但它们都表示“值的空缺”,两者往往可以互换。判断相等运算符“==”认为两者是相等的(要使用严格相等运算符“===”来区分它们)。在希望值是布尔类型的地方它们的值都是假值,和false类似。null和undefined都不包含任何属性和方法。实际上,使用“.”和“[]”来存取这两个值的成员或方法都会产生一个类型错误。

你或许认为undefined是表示系统级的、出乎意料的或类似错误的值的空缺,而null是表示程序级的、正常的或在意料之中的值的空缺。如果你想将它们赋值给变量或者属性,或将它们作为参数传入函数,最佳选择是使用null。

转换和相等性

由于JavaScript可以做灵活的类型转换,因此其“==”相等运算符也随相等的含义灵活多变。例如,如下这些比较结果均是true:

1
2
3
4
null == undefined   // 这两值被认为相等
"0" == 0 // 在比较之前字符串转换成数字
0 == false // 在比较之前布尔值转换成数字
"0" == false // 在比较之前字符串和布尔值都转换成数字

需要特别注意的是,一个值转换为另一个值并不意味着两个值相等。比如,如果在期望使用布尔值的地方使用了undefined,它将会转换为false,但这并不表明undefined == false。JavaScript运算符和语句期望使用多样化的数据类型,并可以相互转换。if语句将undefined转换为false,但“==”运算符从不试图将其操作数转换为布尔值。

严格相等运算符“===”首先计算其操作数的值,然后比较这两个值,比较过程没有任何类型转换:

  • 如果两个值类型不相同,则它们不相等。
  • 如果两个值都是null或者都是undefined,则它们不相等。
  • 如果两个值都是布尔值true或都是布尔值false,则它们相等。
  • 如果其中一个值是NaN,或者两个值都是NaN,则它们不相等。NaN和其他任何值都是不相等的,包括它本身!通过x!==x来判断x是否为NaN,只有在x为NaN的时候,这个表达式的值才为true。
  • 如果两个值为数字且数值相等,则它们相等。如果一个值为0,另一个值为-0,则它们同样相等。
  • 如果两个值为字符串,且所含的对应位上的16位数完全相等,则它们相等。如果它们的长度或内容不同,则它们不等。两个字串可能含义完全一样且所显示出的字符也一样,但具有不同编码的16位值。JavaScript并不对Unicode进行标谁化的转换,因此像这样的字符通过“===”和“==”运算符的比较结果也不相等.第三部分的String.localeCompare()提供了另外一种比较字符串的方法。
  • 如果两个引用值指向同一个对象、数组或函数,则它们是相等的。如果指向不同的对象,则它们是不等的,尽管两个对象具有完全一样的属性。

相等运算符“==”和恒等运算符相似,但相等运算符的比较并不严格。如果两个操作数不是同一类型,那么相等运算符会尝试进行一些类型转换,然后进行比较:

  • 如果两个操作数的类型相同,则和上文所述的严格相等的比较规则一样。如果严格相等,那么比较结果为相等。如果它们不严格相等,则比较结果为不相等。
  • 如果两个操作数类型不同,”==”相等操作符也可能会认为它们相等。检测相等
    将会遵守如下规则和类型转换:
    • 如果一个值是null,另一个是undefined,则它们相等。
    • 如果一个值是数字,另一个是字符串,先将字符串转换为数字,然后使用转换后的值进行比较。
    • 如果其中一个值是true,则将其转换为1再进行比较。如果其中一个值是false,则将其转换为0再进行比较。
    • 如果一个值是对象,另一个值是数字或字符串,则使用3.8.3节所提到的转换规则将对象转换为原始值,然后再进行比较.对象通过tostring()方法或者valueOf()方法转换为原始值。JavaScript语言核心的内置类首先尝试使用valueof(),再尝试使用toString(),除了日期类,日期类只使用toString()转换。那些不是JavascriPt语言核心中的对象则通过各自的实现中定义的方法转换为原始值。
    • 其他不同类型之间的比较均不相等。

对象转换为原始值

JavaScript中的“+”运算符可以进行数学加法和字符串连接操作。如果它的其中一个操作数是对象,则JavaScript将使用特殊的方法将对象转换为原始值,而不是使用其他算术运算符的方法执行对象到数字的转换,“==”相等运算符与此类似。如果将对象和一个原始值比较,则转换将会遵照对象到原始值的转换方式进行。

“+”和“==”应用的对象到原始值的转换包含日期对象的一种特殊情形。日期类是JavaScript语言核心中唯一的预先定义类型,它定义了有意义的向字符串和数字类型的转换。对于所有非日期的对象来说,对象到原始值的转换基本上是对象到数字的转换(首先调用valueOf()),日期对象则使用对象到字符串的转换模式,然而,这里的转换和上文讲述的并不完全一致:通过valueOf或toString()返回的原始值将被直接使用,而不会被强制转换为数字或字符串。

和“==”一样,“<”运算符以及其他关系运算符也会做对象到原始值的转换,但要除去日期对象的特殊情形:任何对象都会首先尝试调用valueOf(),然后调用toString()。不管得到的原始值是否直接使用,它都不会进一步被转换为数字或字符串。

“+”、“==”、“!=”和关系运算符是唯一执行这种特殊的字符串到原始值的转换方式的运算符。其他运算符到特定类型的转换都很明确,而且对日期对象来讲也没有特殊情况。例如“-”(减号)运算符把它的两个操作数都转换为数字。下面的代码展示了日期对象和“+”、“-”、“==”以及“>”的运行结果:

1
2
3
4
5
var now = new Date();   // 创建一个日期对象
typeof (now + 1) // => "string": "+"将日期转换为字符串
typeof (now - 1) // => "number": "-"使用对象到数字的转换
now == now.toString() // => true: 隐式的和显式的字符串转换
now > (now -1) // => true: ">"将日期转换为数字

全局对象——作为属性的变量

当声明一个JavaScript全局变量时,实际上是定义了全局对象的一个属性(见3.5节)。当使用var声明一个变量时,创建的这个属性是不可配置的(见6.7节),也就是说这个变量无法通过delete运算符删除。可能你已经注意到了,如果你没有使用严格模式并给一个未声明的变量赋值的话,JavaScript会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常的可配值属性,并可以删除它们:

1
2
3
4
5
6
var truevar = 1;    // 声明一个不可删除的全局变量
fakevar = 2; // 创建全局对象的一个可删除的属性
this.fakevar2 = 3; // 同上
delete truevar // => false: 变量并没有被删除
delete fakevar // => true: 变量被删除
delete this.fakevar2 // => true: 变量被删除

JavaScript全局变量是全局对象的属性,这是在ECMAScript规范中强制规定的。对于局部变量则没有如此规定,但我们可以想象得到,局部变量当做跟函数调用相关的某个对象的属性。ECMAScript 3规范称该对象为“调用对象”(call object),ECMAScript 5规范称为“声明上下文对象”(declarative environment record)。JavaScript可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。然而,这些局部变量对象存在的观念是非常重要的,我们会在下一节展开讲述。

&&短路

下面代码效果相同

1
2
if (a == b) stop(); // Invoke stop() only if a == b
(a == b) && stop(); // This does the same thing

try/catch/finally

如果finally块使用了return、continue、break或者throw语句使程序发生跳转,或者通过调用了抛出异常的方法改变了程序执行流程,不管这个跳转使程序挂起还是继续执行,解释器都会将其忽略。例如,如果finally从句抛出一个异常,这个异常将替代正在抛出的异常。如果finally从句运行到了return语句,尽管已经抛出了异常且这个抛出的异常还没有处理,这个方法依然会正常返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test(){
var i=0, o={x:1};
try{
console.log('i: ' + i);
console.log('o: ' + o.x);
// console.log('b: ' + b.x);
return [i, JSON.stringify(o)];
}catch(e){
console.log(e);
}finally{
i++;
o.x++;
console.log('finally i: ' + i);
console.log('finally o: ' + o.x);
console.log('b: ' + b.x);
// return [i, JSON.stringify(o)];
}
}
test();
  • finally中的return回覆盖try中的return;
  • finally对try中的返回值修改无效;

use strict

严格代码以严格模式执行。ECMAscript 5中的严格模式是该语言的一个受限制的子集,它修正了语言的重要缺陷,并提供健壮的查错功能和增强的安全机制。严格模式和非严格模式之间的区别如下(前三条尤为重要):

  • 在严格模式中禁止使用with语句。
  • 在严格模式中,所有的变量都要先声明,如果给一个未声明的变量、函数、函数参数、catch从句参数或全局对象的属性赋值,将会抛出一个引用错误异常(在非严格模式中,这种隐式声明的全局变量的方法是给全局对象新添加一个新属性)。
  • 在严格模式中,调用的函数(不是方法)中的一个this值是undefined.(在非严格模式中,调用的函数中的this值总是全局对象)。可以利用这种特性来判断JavaScript实现是否支持严格模式:
    var hasStrictMode=(function(){"use strict";return this===undefined}());
  • 同样,在严格模式中,当通过:call()或apply()来调用函数时,其中的this值就是通过call()或apply()传入的第一个参数(在非严格模式中,null和undefined值被全局对象和转换为对象的非对象值所代替)。
  • 在严格模式中,给只读属性赋值和给不可扩展的对象创建新成员都将抛出一个类型错误异常(在非严格模式中,这些操作只是简单地操作失败,不会报错)。
  • 在严格模式中,传入eval()的代码不能在调用程序所在的上下文中声明变量或定义函数,而在非严格模式中是可以这样做的。相反,变量和函数的定义是在eval()创
    建的新作用域中,这个作用域在eval()返回时就弃用了。
  • 在严格模式中,函数里的arguments对象拥有传人函数值的静态副本。在非严格模式中,arguments对象具有“魔术般”的行为,arguments里的数组
    元素和函数参数都是指向同一个值的引用。
  • 在严格模式中,当delete运算符后跟随非法的标识符(比如变量、函数、函数参数)时,将会抛出一个语法错误异常(在非严格模式中,这种delete表达式什么也没做,并返回false)。
  • 在严格模式中,试图删除一个不可配置的属性将抛出一个类型错误异常(在非严格模式中,delete表达式操作失败,并返回false)。
  • 在严格模式中,在一个对象直接量中定义两个或多个同名属性将产生一个语法错误(在非严格模式中不会报错)。
  • 在严格模式中,函数声明中存在两个或多个同名的参数将产生一个语法错误(在非严格模式中不会报错)。
  • 在严格模式中是不允许使用八进制整数直接量(以0为前缀,而不是0x为前缀)的(在非严格模式中某些实现是允许八进制整数直接量的)。
  • 在严格模式中,标识符eval和arguments当做关键字,它们的值是不能更改的。不能给这些标识符赋值,也不能把它们声明为变量、用做函数名、用做函数参数或用做catch块的标识符。
  • 在严格模式中限制了对调用栈的检测能力,在严格模式的函数中,arguments.caller和arguments.callee都会抛出一个类型错误异常。严格模式的函数同样具有caller和arguments属性,当访问这两个属性时将抛出类型错误异常(有一些JavaScript的实现在非严格模式里定义了这些非标准的属性)。

评论