正如我们在第一章中宣称的,JavaScript拥有带类型的值,没有带类型的变量。下面是可用的内建类型:

  • string
  • number
  • boolean
  • nullundefined
  • object
  • symbol (ES6新增类型)

JavaScript提供了一个typeof操作符,它可以检查一个值并告诉你它的类型是什么:

  1. var a;
  2. typeof a; // "undefined"
  3. a = "hello world";
  4. typeof a; // "string"
  5. a = 42;
  6. typeof a; // "number"
  7. a = true;
  8. typeof a; // "boolean"
  9. a = null;
  10. typeof a; // "object" -- 奇怪的bug
  11. a = undefined;
  12. typeof a; // "undefined"
  13. a = { b: "c" };
  14. typeof a; // "object"

来自typeof的返回值总是六个(ES6中是七个! —— “symbol”类型)字符串值之一。也就是,typeof "abc"返回"string",不是string

注意在这个代码段中变量a是如何持有每种不同类型的值的,而且尽管表面上看起来很像,但是typeof a并不是在询问“a的类型”,而是“当前a中的值的类型”。在JavaScript中只有值拥有类型;变量只是这些值的简单容器。

typeof null是一个有趣的例子,因为当你期望它返回"null"时,它错误地返回了"object"

警告: 这是JS中一直存在的一个bug,但是看起来它永远都不会被修复了。在网络上有太多的代码依存于这个bug,因此修复它将会导致更多的bug!

另外,注意a = undefined。我们明确地将a设置为值undefined,但是在行为上这与一个还没有被设定值的变量没有区别,比如在这个代码段顶部的var a;。一个变量可以用好几种不同的方式得到这样的“undefined”值状态,包括没有返回值的函数和使用void操作符。

对象

object类型指的是一种复合值,你可以在它上面设定属性(带名称的位置),每个属性持有各自的任意类型的值。它也许是JavaScript中最有用的类型之一。

  1. var obj = {
  2. a: "hello world",
  3. b: 42,
  4. c: true
  5. };
  6. obj.a; // "hello world"
  7. obj.b; // 42
  8. obj.c; // true
  9. obj["a"]; // "hello world"
  10. obj["b"]; // 42
  11. obj["c"]; // true

可视化地考虑这个obj值可能会有所帮助:

值与类型 - 图1

属性既可以使用 点号标记法(例如,obj.a) 访问,也可以使用 方括号标记法(例如,obj["a"]) 访问。点号标记法更短而且一般来说更易于阅读,因此在可能的情况下它都是首选。

如果你有一个名称中含有特殊字符的属性名称,方括号标记法就很有用,比如obj["hello world!"] —— 当通过方括号标记法访问时,这样的属性经常被称为 [ ]标记法要求一个变量(下一节讲解)或者一个string 字面量(它需要包装进" .. "' .. ')。

当然,如果你想访问一个属性/键,但是它的名称被存储在另一个变量中时,方括号标记法也很有用。例如:

  1. var obj = {
  2. a: "hello world",
  3. b: 42
  4. };
  5. var b = "a";
  6. obj[b]; // "hello world"
  7. obj["b"]; // 42

注意: 更多关于JavaScript的object的信息,请参见本系列的 this与对象原型,特别是第三章。

在JavaScript程序中有另外两种你将会经常打交道的值类型:数组函数。但与其说它们是内建类型,这些类型应当被认为更像是子类型 —— object类型的特化版本。

数组

一个数组是一个object,它不使用特殊的带名称的属性/键持有(任意类型的)值,而是使用数字索引的位置。例如:

  1. var arr = [
  2. "hello world",
  3. 42,
  4. true
  5. ];
  6. arr[0]; // "hello world"
  7. arr[1]; // 42
  8. arr[2]; // true
  9. arr.length; // 3
  10. typeof arr; // "object"

注意: 从零开始计数的语言,比如JS,在数组中使用0作为第一个元素的索引。

可视化地考虑arr很能会有所帮助:

值与类型 - 图2

因为数组是一种特殊的对象(正如typeof所暗示的),所以它们可以拥有属性,包括一个可以自动被更新的length属性。

理论上你可以使用你自己的命名属性将一个数组用作一个普通对象,或者你可以使用一个object但是给它类似于数组的数字属性(01,等等)。然而,这么做一般被认为是分别误用了这两种类型。

最好且最自然的方法是为数字定位的值使用数组,而为命名属性使用object

函数

另一个你将在JS程序中到处使用的object子类型是函数:

  1. function foo() {
  2. return 42;
  3. }
  4. foo.bar = "hello world";
  5. typeof foo; // "function"
  6. typeof foo(); // "number"
  7. typeof foo.bar; // "string"

同样地,函数也是object的子类型 —— typeof返回"function",这暗示着"function"是一种主要类型 —— 因此也可以拥有属性,但是你一般仅会在有限情况下才使用函数对象属性(比如foo.bar)。

注意: 更多关于JS的值和它们的类型的信息,参见本系列的 类型与文法 的前两章。

内建类型的方法

我们刚刚讨论的内建类型和子类型拥有十分强大和有用的行为,它们作为属性和方法暴露出来。

例如:

  1. var a = "hello world";
  2. var b = 3.14159;
  3. a.length; // 11
  4. a.toUpperCase(); // "HELLO WORLD"
  5. b.toFixed(4); // "3.1416"

使调用a.toUpperCase()成为可能的原因,要比这个值上存在这个方法的说法复杂一些。

简而言之,有一个StringS大写)对象包装器形式,通常被称为“原生类型”,与string基本类型配成一对儿;正是这个对象包装器的原型上定义了toUpperCase()方法。

当你通过引用一个属性或方法(例如,前一个代码段中的a.toUpperCase())将一个像"hello world"这样的基本类型值当做一个object来使用时,JS自动地将这个值“封箱”为它对应的对象包装器(这个操作是隐藏在幕后的)。

一个string值可以被包装为一个String对象,一个number可以被包装为一个Number对象,而一个boolean可以被包装为一个Boolean对象。在大多数情况下,你不担心或者直接使用这些值的对象包装器形式 —— 在所有实际情况中首选基本类型值形式,而JavaScript会帮你搞定剩下的一切。

注意: 关于JS原生类型和“封箱”的更多信息,参见本系列的 类型与文法 的第三章。要更好地理解对象原型,参见本系列的 this与对象原型 的第五章。

值的比较

在你的JS程序中你将需要进行两种主要的值的比较:等价不等价。任何比较的结果都是严格的boolean值(truefalse),无论被比较的值的类型是什么。

强制转换

在第一章中我们简单地谈了一下强制转换,我们在此回顾它。

在JavaScript中强制转换有两种形式:明确的隐含的。明确的强制转换比较简单,因为你可以在代码中明显地看到一个类型转换到另一个类型将会发生,而隐含的强制转换更像是另外一些操作的不明显的副作用引发的类型转换。

你可能听到过像“强制转换是邪恶的”这样情绪化的观点,这是因为一个清楚的事实 —— 强制转换在某些地方会产生一些令人吃惊的结果。也许没有什么能比当一个语言吓到开发者时更能唤起他们的沮丧心情了。

强制转换并不邪恶,它也不一定是令人吃惊的。事实上,你使用类型强制转换构建的绝大部分情况是十分合理和可理解的,而且它甚至可以用来 增强 你代码的可读性。但我们不会在这个话题上过度深入 —— 本系列的 类型与文法 的第四章将会进行全面讲解。

这是一个 明确 强制转换的例子:

  1. var a = "42";
  2. var b = Number( a );
  3. a; // "42"
  4. b; // 42 -- 数字!

而这是一个 隐含 强制转换的例子:

  1. var a = "42";
  2. var b = a * 1; // 这里 "42" 被隐含地强制转换为 42
  3. a; // "42"
  4. b; // 42 -- 数字!

Truthy 与 Falsy

在第一章中,我们简要地提到了值的“truthy”和“falsy”性质:当一个非boolean值被强制转换为一个boolean时,它是变成true还是false

在JavaScript中“falsy”的明确列表如下:

  • "" (空字符串)
  • 0, -0, NaN (非法的number
  • null, undefined
  • false

任何不在这个“falsy”列表中的值都是“truthy”。这是其中的一些例子:

  • "hello"
  • 42
  • true
  • [ ], [ 1, "2", 3 ] (数组)
  • { }, { a: 42 } (对象)
  • function foo() { .. } (函数)

重要的是要记住,一个非boolean值仅在实际上被强制转换为一个boolean时才遵循这个“truthy”/“falsy”强制转换。把你搞糊涂并不困难 —— 当一个场景看起来像是将一个值强制转换为boolean,可其实它不是。

等价性

有四种等价性操作符:=====!=,和!==!形式当然是与它们相对应操作符平行的“不等”版本;不等(non-equality) 不应当与 不等价性(inequality) 相混淆。

=====之间的不同通常被描述为,==检查值的等价性而===检查值和类型两者的等价性。然而,这是不准确的。描述它们的合理方式是,==在允许强制转换的条件下检查值的等价性,而===是在不允许强制转换的条件下检查值的等价性;因此===常被称为“严格等价”。

考虑这个隐含强制转换,它在==宽松等价性比较中允许,而===严格等价性比较中不允许:

  1. var a = "42";
  2. var b = 42;
  3. a == b; // true
  4. a === b; // false

a == b的比较中,JS注意到类型不匹配,于是它经过一系列有顺序的步骤将一个值或者它们两者强制转换为一个不同的类型,直到类型匹配为止,然后就可以检查一个简单的值等价性。

如果你仔细想一想,通过强制转换a == b可以有两种方式给出true。这个比较要么最终成为42 == 42,要么成为"42" == "42"。那么是哪一种呢?

答案:"42"变成42,于是比较成为42 == 42。在一个这样简单的例子中,只要最终结果是一样的,处理的过程走哪一条路看起来并不重要。但在一些更复杂的情况下,这不仅对比较的最终结果很重要,而且对你 如何 得到这个结果也很重要。

a === b产生false,因为强制转换是不允许的,所以简单值的比较很明显将会失败。许多开发者感觉===更可靠,所以他们提倡一直使用这种形式而远离==。我认为这种观点是非常短视的。我相信==是一种可以改进程序的强大工具,如果你花时间去学习它的工作方式

我们不会详细地讲解强制转换在==比较中是如何工作的。它的大部分都是相当合理的,但是有一些重要的极端用例要小心。你可以阅读ES5语言规范的11.9.3部分(http://www.ecma-international.org/ecma-262/5.1/)来了解确切的规则,而且与围绕这种机制的所有负面炒作比起来,你会对这它是多么的直白而感到吃惊。

为了将这许多细节归纳为一个简单的包装,并帮助你在各种情况下判断是否使用=====,这是我的简单规则:

  • 如果一个比较的两个值之一可能是truefalse值,避免==而使用===
  • 如果一个比较的两个值之一可能是这些具体的值(0"",或[] —— 空数组),避免==而使用===
  • 所有 其他情况下,你使用==是安全的。它不仅安全,而且在许多情况下它可以简化你的代码并改善可读性。

这些规则归纳出来的东西要求你严谨地考虑你的代码:什么样的值可能通过这个被比较等价性的变量。如果你可以确定这些值,那么==就是安全的,使用它!如果你不能确定这些值,就使用===。就这么简单。

!=不等价形式对应于==,而!==形式对应于===。我们刚刚讨论的所有规则和注意点对这些非等价比较都是平行适用的。

如果你在比较两个非基本类型值,比如object(包括functionarray),那么你应当特别小心=====的比较规则。因为这些值实际上是通过引用持有的,=====比较都将简单地检查这个引用是否相同,而不是它们底层的值。

例如,array默认情况下会通过使用逗号(,)连接所有值来被强制转换为string。你可能认为两个内容相同的array将是==相等的,但它们不是:

  1. var a = [1,2,3];
  2. var b = [1,2,3];
  3. var c = "1,2,3";
  4. a == c; // true
  5. b == c; // true
  6. a == b; // false

注意: 更多关于==等价性比较规则的信息,参见ES5语言规范(11.9.3部分),和本系列的 类型与文法 的第四章;更多关于值和引用的信息,参见它的第二章。

不等价性

<><=,和>=操作符用于不等价性比较,在语言规范中被称为“关系比较”。一般来说它们将与number这样的可比较有序值一起使用。3 < 4是很容易理解的。

但是JavaScriptstring值也可进行不等价性比较,它使用典型的字母顺序规则("bar" < "foo")。

那么强制转换呢?与==比较相似的规则(虽然不是完全相同!)也适用于不等价操作符。要注意的是,没有像===严格等价操作符那样不允许强制转换的“严格不等价”操作符。

考虑如下代码:

  1. var a = 41;
  2. var b = "42";
  3. var c = "43";
  4. a < b; // true
  5. b < c; // true

这里发生了什么?在ES5语言规范的11.8.5部分中,它说如果<比较的两个值都是string,就像b < c,那么这个比较将会以字典顺序(也就是像字典中字母的排列顺序)进行。但如果两个值之一不是string,就像a < b,那么两个值就将被强制转换成number,并进行一般的数字比较。

在可能不同类型的值之间进行比较时,你可能遇到的最大的坑 —— 记住,没有“严格不等价”可用 —— 是其中一个值不能转换为合法的数字,例如:

  1. var a = 42;
  2. var b = "foo";
  3. a < b; // false
  4. a > b; // false
  5. a == b; // false

等一下,这三个比较怎么可能都是false?因为在<>的比较中,值b被强制转换为了“非法的数字值”,而且语言规范说NaN既不大于其他值,也不小于其他值。

==比较失败于不同的原因。如果a == b被解释为42 == NaN或者"42" == "foo"都会失败 —— 正如我们前面讲过的,这里是前一种情况。

注意: 关于不等价比较规则的更多信息,参见ES5语言规范的11.8.5部分,和本系列的 类型与文法 第四章。