第三章:原生类型
原文:
这是最常用的原生类型的一览:
String()
Number()
Boolean()
Array()
Object()
Function()
RegExp()
Date()
Error()
Symbol()
—— 在 ES6 中被加入的!
如你所见,这些原生类型实际上是内建函数。
如果你拥有像 Java 语言那样的背景,JavaScript 的 String()
看起来像是你曾经用来创建字符串值的 String(..)
构造器。所以,你很快就会观察到你可以做这样的事情:
var s = new String("Hello World!");console.log(s.toString()); // "Hello World!"复制代码
这些原生类型的每一种确实可以被用作一个原生类型的构造器。但是被构建的东西可能与你想象的不同:
var a = new String("abc");typeof a; // "object" ... 不是 "String"a instanceof String; // trueObject.prototype.toString.call(a); // "[object String]"复制代码
创建值的构造器形式(new String("abc")
)的结果是一个基本类型值("abc"
)的包装器对象。
重要的是,typeof
显示这些对象不是它们自己的特殊 类型,而是 object
类型的子类型。
1. 内部 [[Class]]
typeof
的结果为 "object"
的值(比如数组)被额外地打上了一个内部的标签属性 [[Class]]
(请把它考虑为一个内部的分类方法,而非与传统的面向对象编码的类有关)。这个属性不能直接地被访问,但通常可以间接地通过在这个值上借用默认的 Object.prototype.toString(..)
方法调用来展示。举例来说:
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"Object.prototype.toString.call(/regex-literal/i); // "[object RegExp]"复制代码
所以,对于这个例子中的数组来说,内部的 [[Class]]
值是 "Array"
,而对于正则表达式,它是 "RegExp"
。在大多数情况下,这个内部的 [[Class]]
值对应于关联这个值的内建的原生类型构造器(见下面的讨论),但事实却不总是这样。
基本类型呢?首先,null
和 undefined
:
Object.prototype.toString.call(null); // "[object Null]"Object.prototype.toString.call(undefined); // "[object Undefined]"复制代码
你会注意到,不存在 Null()
和 Undefined()
原生类型构造器,但不管怎样 "Null"
和 "Undefined"
是被暴露出来的内部 [[Class]]
值。
但是对于像 string
、number
、和 boolean
这样的简单基本类型,实际上会启动另一种行为,通常称为“封箱(boxing)”(见下一节“封箱包装器”):
Object.prototype.toString.call("abc"); // "[object String]"Object.prototype.toString.call(42); // "[object Number]"Object.prototype.toString.call(true); // "[object Boolean]"复制代码
在这个代码段中,每一个简单基本类型都自动地被它们分别对应的对象包装器封箱,这就是为什么 "String"
、"Number"
、和 "Boolean"
分别被显示为内部 [[Class]]
值。
2. 封箱包装器
这些对象包装器服务于一个非常重要的目的。基本类型值没有属性或方法,所以为了访问 .length
或 .toString()
你需要这个值的对象包装器。值得庆幸的是,JS 将会自动地 封箱(也就是包装)基本类型值来满足这样的访问。
var a = "abc";a.length; // 3a.toUpperCase(); // "ABC"复制代码
那么,如果你想以通常的方式访问这些字符串值上的属性/方法,比如一个 for
循环的 i < a.length
条件,这么做看起来很有道理:一开始就得到一个这个值的对象形式,于是 JS 引擎就不需要隐含地为你创建一个。
但事实证明这是一个坏主意。浏览器们长久以来就对 .length
这样的常见情况进行性能优化,这意味着如果你试着直接使用对象形式(它们没有被优化过)进行“提前优化”,那么实际上你的程序将会 变慢。
一般来说,基本上没有理由直接使用对象形式。让封箱在需要的地方隐含地发生会更好。换句话说,永远也不要做 new String("abc")
、new Number(42)
这样的事情 —— 应当总是偏向于使用基本类型字面量 "abc"
和 42
。
3. 开箱
如果你有一个包装器对象,而你想要取出底层的基本类型值,你可以使用 valueOf()
方法:
var a = new String("abc");var b = new Number(42);var c = new Boolean(true);a.valueOf(); // "abc"b.valueOf(); // 42c.valueOf(); // true复制代码
当以一种查询基本类型值的方式使用对象包装器时,开箱也会隐含地发生。这个处理的过程(强制转换)将会在第四章中更详细地讲解,但简单地说:
var a = new String("abc");var b = a + ""; // `b` 拥有开箱后的基本类型值"abc"typeof a; // "object"typeof b; // "string"复制代码
4. 原生类型作为构造器
Array(..)
var a = new Array(1, 2, 3);a; // [1, 2, 3]var b = [1, 2, 3];b; // [1, 2, 3]复制代码
注意: Array(..)
构造器不要求在它前面使用 new
关键字。如果你省略它,它也会像你已经使用了一样动作。所以 Array(1,2,3)
和 new Array(1,2,3)
的结果是一样的。
Array
构造器有一种特殊形式,如果它仅仅被传入一个 number
参数,与将这个值作为数组的 内容 不同,它会被认为是用来“预定数组大小”(嗯,某种意义上)用的长度。
但更重要的是,其实没有预定数组大小这样的东西。你所创建的是一个空数组,并将这个数组的 length
属性设置为那个指定的数字值。
一个数组在它的值槽上没有明确的值,但是有一个 length
属性意味着这些值槽是存在的,在 JS 中这是一个诡异的数据结构,它带有一些非常奇怪且令人困惑的行为。可以创建这样的值的能力,完全源自于老旧的、已经废弃的、仅具有历史意义的功能(比如arguments
这样的“类数组对象”)。
注意: 带有至少一个“空值槽”的数组经常被称为“稀散数组”。
要观察这种不同,试试这段代码:
var a = new Array(3);var b = [undefined, undefined, undefined];var c = [];c.length = 3;a;b;c;复制代码
注意: 正如你在这个例子中看到的 c
,数组中的空值槽可以在数组的创建之后发生。将数组的 length
改变为超过它实际定义的槽值的数目,你就隐含地引入了空值槽。事实上,你甚至可以在上面的代码段中调用 delete b[1]
,而这么做将会在 b
的中间引入一个空值槽。
Object(..)
、Function(..)
和 RegExp(..)
Object(..)
/Function(..)
/RegExp(..)
构造器一般来说也是可选的(因此除非是特别的目的,应当避免使用):
var c = new Object();c.foo = "bar";c; // { foo: "bar" }var d = { foo: "bar" };d; // { foo: "bar" }var e = new Function("a", "return a * 2;");var f = function(a) { return a * 2;};function g(a) { return a * 2;}var h = new RegExp("^a*b+", "g");var i = /^a*b+/g;复制代码
几乎没有理由使用 new Object()
构造器形式,尤其因为它强迫你一个一个地添加属性,而不是像对象的字面形式那样一次添加许多。
Function
构造器仅在最最罕见的情况下有用,也就是你需要动态地定义一个函数的参数和/或它的函数体。不要将 Function(..) 仅仅作为另一种形式的 eval(..)。你几乎永远不会需要用这种方式动态定义一个函数。
用字面量形式(/^a*b+/g
)定义正则表达式是被大力采用的,不仅因为语法简单,而且还有性能的原因 —— JS 引擎会在代码执行前预编译并缓存它们。和我们迄今看到的其他构造器形式不同,RegExp(..)
有一些合理的用途:用来动态定义一个正则表达式的范例。
var name = "Kyle";var namePattern = new RegExp("\\b(?:" + name + ")+\\b", "ig");var matches = someText.match(namePattern);复制代码
这样的场景在 JS 程序中一次又一次地合法出现,所以你有需要使用 new RegExp("pattern","flags")
形式。
Date(..)
和 Error(..)
Date(..)
和 Error(..)
原生类型构造器要比其他种类的原生类型有用得多,因为它们没有字面量形式。
要创建一个日期对象值,你必须使用 new Date()
。Date(..)
构造器接收可选参数值来指定要使用的日期/时间,但是如果省略的话,就会使用当前的日期/时间。
目前你构建一个日期对象的最常见的理由是要得到当前的时间戳(一个有符号整数,从 1970 年 1 月 1 日开始算起的毫秒数)。你可以在一个日期对象实例上调用 getTime()
得到它。
但是在 ES5 中,一个更简单的方法是调用定义为 Date.now()
的静态帮助函数。而且在前 ES5 中填补它很容易:
if (!Date.now) { Date.now = function() { return new Date().getTime(); };}复制代码
Error(..)
构造器(很像上面的 Array()
)在有 new
与没有 new
时的行为是相同的。
你想要创建 error 对象的主要原因是,它会将当前的执行栈上下文捕捉进对象中(在大多数 JS 引擎中,在创建后使用只读的 .stack
属性表示)。这个栈上下文包含函数调用栈和 error 对象被创建时的行号,这使调试这个错误更简单。
典型地,你将与 throw
操作符一起使用这样的 error 对象:
function foo(x) { if (!x) { throw new Error("x wasn't provided"); } // ..}复制代码
Error 对象实例一般拥有至少一个 message
属性,有时还有其他属性(你应当将它们作为只读的),比如 type
。然而,与其检视上面提到的 stack
属性,最好是在 error 对象上调用 toString()
(明确地调用,或者是通过强制转换隐含地调用 —— 见第四章)来得到一个格式友好的错误消息。
提示: 技术上讲,除了一般的 Error(..)
原生类型以外,还有几种特定错误的原生类型:EvalError(..)
、RangeError(..)
、ReferenceError(..)
、SyntaxError(..)
、TypeError(..)
和 URIError(..)
。但是手动使用这些特定错误原生类型十分少见。如果你的程序确实遭受了一个真实的异常,它们是会自动地被使用的(比如引用一个未声明的变量而得到一个 ReferenceError
错误)。
复习
JavaScript 为基本类型提供了对象包装器,被称为原生类型(String
、Number
、Boolean
等等)。这些对象包装器使这些值可以访问每种对象子类型的恰当行为(String#trim()
和 Array#concat(..)
)。
如果你有一个像 "abc"
这样的简单基本类型标量,而且你想要访问它的 length
属性或某些 String.prototype
方法,JS 会自动地“封箱”这个值(用它所对应种类的对象包装器把它包起来),以满足这样的属性/方法访问。