第 8 章 值
目录
购买本书
(广告,请勿屏蔽。)

第 8 章 值

JavaScript 拥有我们期望编程语言拥有的绝大多数值:布尔值、数字、字符串、数组等等。JavaScript 中的所有正常值都具有属性[9] 每个属性都有一个(或名称)和一个。您可以将属性视为记录的字段。您可以使用点 (.) 运算符来访问属性:

> var obj = {}; // create an empty object
> obj.foo = 123;  // write property
123
> obj.foo  // read property
123
> 'abc'.toUpperCase()  // call method
'ABC'

JavaScript 的类型系统

本章概述了 JavaScript 的类型系统。

JavaScript 的类型

根据ECMAScript 语言规范第 8 章的规定,JavaScript只有六种类型

ECMAScript 语言类型对应于 ECMAScript 程序员使用 ECMAScript 语言直接操作的值。ECMAScript 语言类型是

  • Undefined、Null
  • Boolean、String、Number 和
  • Object

因此,从技术上讲,构造函数不会引入新的类型,即使它们被认为具有实例。

静态类型与动态类型

在静态类型语言中,变量、参数和对象成员(JavaScript 将其称为属性)的类型是编译器在编译时知道的。编译器可以使用该信息执行类型检查和优化编译后的代码。

即使在静态类型语言中,变量也有一个动态类型,即变量在运行时给定时间点的值的类型。动态类型可能与静态类型不同。例如(Java)

Object foo = "abc";

foo 的静态类型是 Object;其动态类型是 String

JavaScript 是动态类型的;变量的类型通常在编译时是未知的。

静态类型检查与动态类型检查

如果您有类型信息,则可以检查操作中使用的值(调用函数、应用运算符等)是否具有正确的类型。静态类型检查语言在编译时执行这种检查,而动态类型检查语言在运行时执行这种检查。一种语言可以同时进行静态类型检查和动态类型检查。如果检查失败,您通常会收到某种错误或异常。

JavaScript 执行一种非常有限的动态类型检查

> var foo = null;
> foo.prop
TypeError: Cannot read property 'prop' of null

然而,大多数情况下,事情都会默默地失败或成功。例如,如果您访问一个不存在的属性,您将获得值 undefined

> var bar = {};
> bar.prop
undefined

强制转换

在 JavaScript 中,处理类型不匹配的值的主要方法是将其强制转换为正确的类型。强制转换意味着隐式类型转换。 大多数操作数都会强制转换:

> '3' * '4'
12

JavaScript 的内置转换机制仅支持 BooleanNumberStringObject 类型。没有标准的方法将一个构造函数的实例转换为另一个构造函数的实例。

警告

术语强类型弱类型通常没有意义明确的定义。它们被使用,但通常使用不当。最好使用静态类型静态类型检查等。

原始值与对象

JavaScript对值进行了某种程度上的任意区分:

  • 原始值是布尔值、数字、字符串、nullundefined
  • 所有其他值都是对象

两者之间的一个主要区别在于它们的比较方式;每个对象都有唯一的标识,并且只(严格)等于自身

> var obj1 = {};  // an empty object
> var obj2 = {};  // another empty object
> obj1 === obj2
false

> var obj3 = obj1;
> obj3 === obj1
true

相反,所有编码相同值的原始值都被认为是相同的

> var prim1 = 123;
> var prim2 = 123;
> prim1 === prim2
true

以下两节将更详细地解释原始值和对象。

原始值

以下是所有原始值(简称基元):

基元具有以下特征:

按值比较

比较“内容”

> 3 === 3
true
> 'abc' === 'abc'
true
始终不可变

属性不能更改、添加或删除:

> var str = 'abc';

> str.length = 1; // try to change property `length`
> str.length      // ⇒ no effect
3

> str.foo = 3; // try to create property `foo`
> str.foo      // ⇒ no effect, unknown property
undefined

(读取未知属性始终返回 undefined。)

一组固定的类型
您不能定义自己的原始类型。

对象

所有非原始值都是对象。最常见的对象类型是:

对象具有以下特征

按引用比较

比较标识;每个对象都有自己的标识:

> ({} === {})  // two different empty objects
false

> var obj1 = {};
> var obj2 = obj1;
> obj1 === obj2
true
默认情况下可变

您通常可以自由地更改、添加和删除属性(参见点运算符 (.):通过固定键访问属性

> var obj = {};
> obj.foo = 123; // add property `foo`
> obj.foo
123
用户可扩展
构造函数(参见第 3 层:构造函数——实例的工厂)可以看作是自定义类型的实现(类似于其他语言中的类)。

undefined 和 null

JavaScript有两个“非值”表示缺少信息,undefinednull

undefinednull 是唯一会导致任何类型的属性访问引发异常的值

> function returnFoo(x) { return x.foo }

> returnFoo(true)
undefined
> returnFoo(0)
undefined

> returnFoo(null)
TypeError: Cannot read property 'foo' of null
> returnFoo(undefined)
TypeError: Cannot read property 'foo' of undefined

undefined 有时也被用作表示不存在的元值。相反,null 表示空。例如,JSON 节点访问器(参见通过节点访问器转换数据)返回

  • undefined 以删除对象属性或数组元素
  • null 以将属性或元素设置为 null

undefined 和 null 的出现

在这里,我们回顾了 undefinednull 出现的各种情况。

undefined 的出现

未初始化的变量是 undefined

> var foo;
> foo
undefined

缺少的参数是 undefined

> function f(x) { return x }
> f()
undefined

如果您读取一个不存在的属性,您将获得 undefined

> var obj = {}; // empty object
> obj.foo
undefined

如果未显式返回任何内容,则函数会隐式返回 undefined

> function f() {}
> f()
undefined

> function g() { return; }
> g()
undefined

null 的出现

检查 undefined 或 null

在以下部分中,我们将回顾如何分别检查 undefinednull,或检查两者是否存在。

检查 null

您可以通过严格相等检查 null

if (x === null) ...

检查 undefined

严格相等 (===) 是检查 undefined 的规范方法:

if (x === undefined) ...

您还可以通过 typeof 运算符检查 undefinedtypeof:对基元进行分类),但通常应使用上述方法。

检查 undefined 或 null

大多数函数允许您通过 undefinednull 指示缺少的值。检查这两者的一种方法是通过显式比较:

// Does x have a value?
if (x !== undefined && x !== null) {
    ...
}
// Is x a non-value?
if (x === undefined || x === null) {
    ...
}

另一种方法是利用 undefinednull 都被视为 false 的事实(参见真值和假值

// Does x have a value (is it truthy)?
if (x) {
    ...
}
// Is x falsy?
if (!x) {
    ...
}

警告

false0NaN'' 也被视为 false

undefined 和 null 的历史

单个非值可以同时扮演 undefinednull 的角色。为什么 JavaScript 有两个这样的值?原因是历史原因。

JavaScript 采用了 Java 将值划分为基元和对象的方法。它还使用了 Java 的“非对象”值 null按照 C(但不是 Java)的先例,如果强制转换为数字,则 null 将变为 0:

> Number(null)
0
> 5 + null
5

请记住,第一个版本的 JavaScript 没有异常处理。因此,必须通过值来指示异常情况,例如未初始化的变量和缺少的属性。null 会是一个不错的选择,但 Brendan Eich 在当时想避免两件事

  • 该值不应具有引用的含义,因为它不仅仅是关于对象。
  • 该值不应强制转换为 0,因为这会使错误更难发现。

因此,Eich 将 undefined 作为另一个非值添加到语言中。它会强制转换为 NaN

> Number(undefined)
NaN
> 5 + undefined
NaN

更改 undefined

undefined全局对象(因此也是全局变量;请参阅全局对象)的属性。在 ECMAScript 3 下,读取 undefined 时必须采取预防措施,因为它很容易意外更改其值。在 ECMAScript 5 下,这不是必需的,因为 undefined 是只读的。

为了防止 undefined 被更改,有两种流行的技术(它们仍然与旧的 JavaScript 引擎相关)

技术 1

隐藏全局 undefined(它可能具有错误的值)

(function (undefined) {
    if (x === undefined) ...  // safe now
}());  // don’t hand in a parameter

在前面的代码中,undefined 保证具有正确的值,因为它是一个参数,函数调用没有提供其值。

技术 2

void 0 进行比较,它始终是(正确的)undefined(请参阅void 运算符

if (x === void 0)  // always safe

基本类型的包装对象

三种基本类型 boolean、number 和 string 具有相应的构造函数:BooleanNumberString。它们的实例(所谓的包装对象)包含(包装)基本类型值。构造函数可以通过两种方式使用:

  • 作为构造函数,它们创建的对象与其包装的基本类型值在很大程度上不兼容

    > typeof new String('abc')
    'object'
    > new String('abc') === 'abc'
    false
  • 作为函数,它们将值转换为相应的基元类型(请参阅转换为 Boolean、Number、String 和 Object 的函数)。这是推荐的转换方法

    > String(123)
    '123'

提示

避免使用包装对象被认为是一种最佳实践。您通常不需要它们,因为对象可以做的任何事情,基本类型都可以做到(除了可以被修改之外)。(这与 Java 不同,JavaScript 从 Java 继承了基本类型和对象之间的区别!)

包装对象不同于基本类型

基本类型值(例如 'abc')与包装器实例(例如 new String('abc'))根本不同:

> typeof 'abc'  // a primitive value
'string'
> typeof new String('abc')  // an object
'object'
> 'abc' instanceof String  // never true for primitives
false
> 'abc' === new String('abc')
false

包装器实例是对象,并且在 JavaScript 中无法比较对象,即使通过宽松的相等运算符 == 也不行(请参阅相等运算符:=== 与 ==

> var a = new String('abc');
> var b = new String('abc');
> a == b
false

包装和解包基本类型

包装对象有一个用例:您想向基本类型值添加属性。然后,您可以包装基本类型并将属性添加到包装对象。在使用该值之前,您需要将其解包。

通过调用包装器构造函数来包装基本类型

new Boolean(true)
new Number(123)
new String('abc')

通过调用方法 valueOf() 来解包基本类型。所有对象都有此方法(如转换为基本类型中所述)

> new Boolean(true).valueOf()
true
> new Number(123).valueOf()
123
> new String('abc').valueOf()
'abc'

将包装对象正确转换为基本类型会提取数字和字符串,但不会提取布尔值:

> Boolean(new Boolean(false))  // does not unwrap
true
> Number(new Number(123))  // unwraps
123
> String(new String('abc'))  // unwraps
'abc'

原因在转换为布尔值中进行了说明。

基本类型从包装器借用其方法

基本类型没有自己的方法,而是从包装器借用方法

> 'abc'.charAt === String.prototype.charAt
true

松散模式和严格模式以不同的方式处理这种借用。在松散模式下,基本类型会动态转换为包装器

String.prototype.sloppyMethod = function () {
    console.log(typeof this); // object
    console.log(this instanceof String); // true
};
''.sloppyMethod(); // call the above method

在严格模式下,将透明地使用包装器原型中的方法

String.prototype.strictMethod = function () {
    'use strict';
    console.log(typeof this); // string
    console.log(this instanceof String); // false
};
''.strictMethod(); // call the above method

类型强制转换

类型强制转换是指将一种类型的值隐式转换为另一种类型的值。大多数 JavaScript 运算符、函数和方法都会将操作数和参数强制转换为它们所需的类型。例如,乘法运算符 (*) 的操作数会被强制转换为数字:

> '3' * '4'
12

再举一个例子,如果其中一个操作数是字符串,则加号运算符 (+) 会将另一个操作数转换为字符串

> 3 + ' times'
'3 times'

类型强制转换会隐藏错误

因此,JavaScript 很少会抱怨值的类型错误。例如,程序通常会将用户输入(来自在线表单或 GUI 小窗口)接收为字符串,即使用户输入的是数字也是如此。如果您将数字字符串视为数字,则不会收到警告,只会得到意外的结果。例如

var formData = { width: '100' };

// You think formData.width is a number
// and get unexpected results
var w = formData.width;
var outer = w + 20;

// You expect outer to be 120, but it’s not
console.log(outer === 120);  // false
console.log(outer === '10020');  // true

在上述情况下,您应该尽早转换为适当的类型

var w = Number(formData.width);

转换为 Boolean、Number、String 和 Object 的函数

以下函数是将值转换为布尔值、数字、字符串或对象的推荐方法:

Boolean()(请参阅转换为布尔值

将值转换为布尔值。以下值将转换为 false;它们是所谓的“假值”:

  • undefinednull
  • false
  • 0NaN
  • ''

所有其他值都被视为“真值”并转换为 true(包括所有对象!)。

Number()(请参阅转换为数字

将值转换为数字:

  • undefined 变为 NaN
  • null 变为 0
  • false 变为 0true 变为 1
  • 字符串将被解析。
  • 对象首先转换为基本类型(稍后讨论),然后转换为数字。
String()(请参阅转换为字符串

将值转换为字符串。它对所有基本类型都有明显的结果。例如:

> String(null)
'null'
> String(123.45)
'123.45'
> String(false)
'false'

对象首先转换为基本类型(稍后讨论),然后转换为字符串。

Object()(请参阅将任何值转换为对象

将对象转换为自身,将 undefinednull 转换为空对象,并将基本类型转换为包装的基本类型。例如:

> var obj = { foo: 123 };
> Object(obj) === obj
true

> Object(undefined)
{}
> Object('abc') instanceof String
true

请注意,Boolean()Number()String()Object() 是作为函数调用的。您通常不会将它们用作构造函数。然后,它们会创建自身的实例(请参阅基本类型的包装对象)。

算法:ToPrimitive()—将值转换为基本类型

要将值转换为数字或字符串,首先将其转换为任意基本类型值,然后将其转换为最终类型(如转换为 Boolean、Number、String 和 Object 的函数中所述)。

ECMAScript 规范有一个内部函数 ToPrimitive()(无法从 JavaScript 访问),它执行此转换。了解 ToPrimitive() 使您能够配置如何将对象转换为数字和字符串。它具有以下签名

ToPrimitive(input, PreferredType?)

可选参数 PreferredType 指示转换的最终类型:它是 NumberString,具体取决于 ToPrimitive() 的结果是转换为数字还是字符串。

如果 PreferredTypeNumber,则执行以下步骤

  1. 如果 input 是基本类型,则返回它(无需执行任何操作)。
  2. 否则,input 是一个对象。调用 input.valueOf()。如果结果是基本类型,则返回它。
  3. 否则,调用 input.toString()。如果结果是基本类型,则返回它。
  4. 否则,抛出 TypeError(指示无法将 input 转换为基本类型)。

如果 PreferredTypeString,则步骤 2 和 3 将交换。也可以省略 PreferredType;然后,对于日期,它被视为 String,对于所有其他值,它被视为 Number。这就是运算符 +== 调用 ToPrimitive() 的方式。

示例:ToPrimitive() 的实际应用

valueOf() 的默认实现返回 this,而 toString() 的默认实现返回类型信息:

> var empty = {};
> empty.valueOf() === empty
true
> empty.toString()
'[object Object]'

因此,Number() 会跳过 valueOf() 并将 toString() 的结果转换为数字;也就是说,它会将 '[object Object]' 转换为 NaN

> Number({})
NaN

以下对象自定义了 valueOf(),这会影响 Number(),但不会更改 String() 的任何内容

> var n = { valueOf: function () { return 123 } };
> Number(n)
123
> String(n)
'[object Object]'

以下对象自定义了 toString()。因为结果可以转换为数字,所以 Number() 可以返回一个数字

> var s = { toString: function () { return '7'; } };
> String(s)
'7'
> Number(s)
7



[9] 从技术上讲,基本类型值没有自己的属性,它们是从包装器构造函数借用的。但这发生在幕后,因此您通常看不到它。

下一页:9. 运算符