# 类型与文法
章节目录:
# 类型
大多数开发者认为,动态语言没有类型。但是ES5.1规范是这么说的:
在本语言规范中的算法所操作的每一个值都有一种关联的类型。可能的值的类型就是那些在本条款中定义的类型。类型还进一步被分为ECMAScript语言类型和语言规范类型
一个ECMAScript语言类型对应ECMAScript程序员使用ECMAScript语言直接操作的值。ECMAScript语言类型有Undefined、Null、Boolean、String、Number和Object。
现在,如果你是强类型(静态类型)语言的爱好者,你可以会反对『类型』一次的用法。在那些语言中,『类型』的含义要比它在JS这里的含义丰富得多。
一个粗糙的定义:一个类型是一组固有的,内建的性质,对于引擎和开发者来说,它独一无二地标识了一个特定的值的行为,并将它与其他值区分开来。
# 类型的重要意义
抛开学术上关于定义的分歧,为什么JavaScript有或者没有类型那么重要?
对每一种类型和它的固有行为有一个正确的理解,对于理解如何正确和准确地转换两个不同类型的值来说是绝对必要的。几乎每一个被编写过的JS程序都需要以某种形式处理类型的强制转换。
对强制转换的困惑可能是JavaScript开发者所经历的最深刻的挫败感之一,它曾经总是因为如此危险而为人所诟病,被认为是语言设计上的缺点而应当被回避。
带着对JavaScript类型的全面理解,我们将要阐明为什么强制转换的坏名声是言过其实,而且是有些冤枉的——以此反转你的视角,来看清强制转换的力量和用处。但首先,我们必须更好地把握值和类型。
# 内建类型
JavaScript定义了七种内建类型:
null
undefined
boolean
number
string
object
symbol
注意:除了Object
,其他类型都称为『基本类型』。
typeof
操作符可以检测给定值的类型,而且总是返回七种字符串的值的一种——令人吃惊的是,对于我们刚刚列出的七种内建类型,它没有一个恰好的一对一匹配。
typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof {life: 42} === "object"; // true
// 在ES6中被加入
typeof Symbol() === "symbol"; // true
如上所示,你可以发现剔除了null
,它是特殊的——特殊在于它与typeof
操作符组合时有Bug。
typeof null === "object"; // true
这个Bug已经存在20年了,应该永远不会修复了,因为有太多的web的内容依存着这个Bug的行为,修复Bug将会制造更多的Bug。
如果你想使用null
类型来测试null
值,你需要一个复合条件:
var a = null;
(!a && typeof a === "object"); // true
null
是唯一一个falsy
,但是在typeof
检测中返回"object"的基本类型。
那么typeof
可以返回的第七种字符串值是什么?
typeof function a(){} === "function"; // true
很容易让人以为JS中function
是一种顶层的内建类型,特别是看到typeof
操作符的这种行为时。然而,如果你阅读语言规范,你会看到它实际上是对象(object)的子类型。特别地,一个函数(function)被称为可调用对象——一个拥有[[Call]]
内部属性、允许被调用的对象。
函数实际上是对象这一事实十分有用。最重要的是,它们可以拥有属性。例如:
function a(b, c) {
/* */
}
这个函数对象拥有一个length
属性,它被设置为函数被声明时的形式参数的数量。
a.length; // 2
那么数组呢?它们是JS原生的,所以它们是一个特殊的类型吗?
typeof [1, 2, 3] === "object"; // true
不,它们仅仅是对象。考虑它们最恰当的方法是,它们是对象的子类型,带有被数字索引的附加性质,并维护一个自动更新的.length
属性。
# 值作为类型
在JavaScript中,变量没有类型——值才有类型。变量可以在任何时候,持有任何值。
另一种考虑JS类型的方式是,JS没有『类型强制』,也就是引擎不坚持认为一个变量总是持有与它开始存在时相同的初始类型的值。
# undefined
vs undeclared
当前还不拥有值的变量,实际上拥有undefined
值。对这样的变量调用typeof
将会返回undefined
:
var a;
typeof a; // undefined
var b = 42;
var c;
// 稍后
b = c;
typeof b; // undefined
typeof c; // undefined
大多数开发者考虑undefined
这个词的方式会诱使他们认为它是"undeclared"(未声明)的同义词。然而在JS中,这两个概念十分不同。
一个undefined
变量是在可访问的作用域中已经被声明过的,但是在这个时刻它里面没有任何值。相比之下,一个undeclared变量是在可访问的作用域中还没有被正式声明的。
考虑这段代码:
var a;
a; // undefined
b; // ReferenceError: b is not defined
比较困扰的反馈is not defined
,实际上指is not declared
。因为typeof
的保护机制,即使是declared
也会返回undefined
。
# 值
array
、string
和number
是任何程序的最基础构建块,但是JavaScript在这些类型上有一些或使你惊喜或使你惊讶的独特性质。下面我们一起来看看如何正确的理解并利用它们的行为。
# Array
和其他强制类型的语言相比,JavaScript的array
只是值的容器,而这些值可以是任何类型:string
、number
、object
甚至是另一个array
。
你不需要预先指定array
的大小,你可以仅声明它们并加入你觉得适合的值:
var a = [];
a.length; // 0
a[0] = 1;
a[1] = "2";
a[2] = [3];
a.length; // 3
警告
array
值使用delete
仅会删除值,但是不会更新length
属性。
小心创建『稀散』的array
(留下或创建空的/丢失的值槽):
var a = [ ];
a[0] = 1;
// 这里没有设置值槽a[1]
a[2] = [3];
a[1]; // undefined
a.length; // 3
虽然它可以工作,但是留下的『空值槽』会导致令人困惑的行为。虽然这样的值槽看起来拥有undefined
值,但是它不会像明确设置(a[1] = undefined
)的值槽那样动作。
array
是被数字索引的,但微妙的是它们也是对象,可以在它们上面添加string
键/属性(但属性不会计算在length
中):
var a = [ ];
a[0] = 1;
a["footer"] = 2;
a.length; // 1
a["footer"]; // 2
a.foobar; // 2
然而,一个需要小心的坑是,如果一个可以被强制转换为10进制number
的string
值被用作键的话,它会认为你想使用number
索引而不是string
键!
var a = [ ];
a["13"] = 42;
a.length; // 14
一般来说,向array
添加string
键/属性不是一个好主意。最好使用object
来持有键/属性形式的值,而array
专用于严格地数字索引的值。
# 类Array
有时你需要将一个类array
值(一个数字索引的值集合)转换为一个真正的array
,这样就可以调用数组的工具函数。
举个例子,DOM查询操作会返回DOM元素的列表、函数参数arguments
对象等。
一个常见的转换方法是使用slice工具:
function foo() {
var arr = Array.prototype.slice.call(arguments);
arr.push("bam");
console.log(arr);
}
foo("bar", "baz"); // ["bar", "baz", "bam"]
如果slice没有其他参数,会默认使它具有复制这个array
的效果。
在ES6中,可以通过Array.from()
执行相同的任务:
var arr = Array.from(arguments);
注意:Array.from
拥有其他几种强大的能力,之后会涵盖它的细节。
# String
一个很常见的想法是,string
实质上是字符的array
。虽然内部的实现可能是也可能不是array
,但重要的是理解JavaScript的string
与字符的array
确实不一样,它们的相似性几乎只是表面上的。
举个例子,我们考虑如下两个值:
var a = "foo";
var b = ["f", "o", "o"];
String确实与array
有很肤浅的相似性--也就是上面说的,类array
--举例来说,它们都有length
属性,一个indexOf
方法和一个concat
方法。
a.length; // 3
b.length; // 3
a.indexOf("o"); // 1
b.indexOf("o"); // 1
var c = a.concat("bar"); // foobar
var d = b.concat(["b", "a", "r"]); // ["f","o","o","b","a","r"]
a === c; // false
b === d; // false
a; // "foo"
b; // ["f","o","o"]
那么,它们基本上都仅仅是"字符的数组",对吧?不确切:
a[1] = "0";
b[1] = "0";
a; // foo
b; // ["f","o","o"]
JavaScript的string
是不可变的,而array
是可变的。另外,在JavaScript中用位置访问字符的a[1]
形式不总是广泛合法的,例如低版本的IE就不支持这种语法,而需要使用a.charAt(1)
。
string
不可变性的进一步后果是,string
上没有一个方法是可以原地修改它的内容的,而是创建并返回一个新的string
。与之相对的是,许多改变array
内容的方法实际上是原地修改的。
c = a.toUpperCase();
a === c; // false
a; // foo
c; // FOO
b.push("!");
b; // ["f","O","o","!"]
另外,许多array
方法在处理string
时非常有用,虽然这些方法不属于string
,但是我们可以针对我们的string
"借用"非变化的array
方法:
a.join; // undefined
a.map; // undefined
var c = Array.prototype.join.call(a, "-");
var d = Array.prototype.map.call(a, function(v) {
return v.toUpperCase() + '.';
}).join("");
c; // f-o-o
d; // F.O.O.
让我们来看另一个例子:翻转一个string
。array
拥有一个原地的reserver
修改器方法,但是string
没有:
a.reverse; // undefined
b.reverse(); // ["!","o","O","f"]
b; // ["!","o","O","f"]
不幸的是,这种"借用"array
修改器不起作用,因为string
是不可变的吗,因此它不能被原地修改:
Array.prototype.reverse.call(a);
// 仍然返回一个"foo"的String对象包装器
另一种迂回的做法是,将string
转换为一个array
,实施我们想做的操作,然后将它转回string
。
var c = a.split("").reverse().join("");
c; // oof
# Number
JavaScript只有一种数字类型:number
。这种类型包含"整数"值和小数值。我说"整数"时加了引号,因为JS的一个长久以来为人诟病的原因是和其他语言不同,JS没有真正的整数。这可能在未来某个时候会改变,但是目前,我们只有number
可用。
所以,在JS中,一个"整数"只是一个没有小数部分的小数值。也就是说,42.0
和42
一样是"整数"。
像大多数现代计算机语言以及几乎所有的脚本语言一样,JavaScript的number
的实现基于"IEEE 754"标准,通常被称为"浮点"。JavaScript明确地使用了这个标准的"双精度"(也就是64位二进制)格式。
# 数字的语法
在JavaScript中字面数字一般用十进制小数表达。例如:
var a = 42;
var b = 42.3;
小数的整数部分如果是0
,是可选的:
var a = 0.42;
var b = .42;
相似地,一个小数在.
之后的小数部分如果是0
,是可选的:
var a = 42.0;
var b = 42.; // 尽管它是合法的,但是会让阅读者感到困惑,因此尽量不要这样书写。
默认情况下,大多数number
将会以十进制小数的形式输出,并去掉末尾小数的部分的0
。所以:
var a = 42.300;
var b = 42.0;
a; // 42.3
b; // 42
非常大或非常小的number
将默认以指数形式输出,与toExponential
方法的输出一样,比如:
var a = 5E10;
a; // 50000000000
a.toExponential(); // 5e+10
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
因为number
值可以用Number
对象包装器封装,所以number
值可以访问内建在Number.prototype
的方法,举个例子,toFixed
方法允许你指定一个值在被表示时,带有多少位小数:
var a = 42.59;
a.toFixed(0); // 43
a.toFixed(1); // 42.6
a.toFixed(2); // 42.59
a.toFixed(3); // 42.590
a.toFixed(4); // 42.5900
注意,这里返回的是number
的string
表现形式,而且会补0.
toPrecision
很相似,但它指定的是有多少有效数字用来表示这个值:
var a = 42.59;
a.toPrecision(1); // 4e+1
a.toPrecision(2); // 43
a.toPrecision(3); // 42.6
a.toPrecision(4); // 42.59
a.toPrecision(5); // 42.590
a.toPrecision(6); // 42.5900
你可以直接在number
上访问这些方法,但你得小心.
操作符。因为.
是一个合法数字字符,如果有可能的话,它会首先被翻译为number
字面的一部分,而不是被翻译为属性访问操作符。
// 不合法的语法:
42.toFixed(3); // SyntaxError
// 合法:
(42).toFixed(3); // 42.000
0.42.toFixed(3); // 0.420
42..toFixed(3); // 42.000
number
还可以使用科学计数法的形式指定,这在表示很大的number
时很常见,比如:
var onethousand = 1E3; // 代表 1 * 10 ^ 3
var onemilliononehundredthousand = 1.1E6; // 代表 1.1 * 10 ^ 6
number
字面量还可以使用其他进制表达,比如二进制,八进制和十六进制。
Oxf3; // 十六进制 243
OXf3; // 同上
0363; // 八进制 243
# 小数值
使用二进制浮点数的最出名的副作用是(对所有使用IEEE 754语言都成立的):
0.1 + 0.2 === 0.3; // false
从数学意义上,我们知道这个等式应当成立,但为什么它是false
呢?
简单地说,0.1
和0.2
的二进制表示形式是不精确的,所以它们相加时,结果不是精确地0.3
。而是非常接近的值:0.30000000000000004
。
现在的问题是,如果一些number
不能被信任为精确的,这不是意味着我们根本不能使用number
吗?当然不是。
在一些应用程序中你需要多加小心,特别是在对付小数的时候。还有许多应用程序只处理整数,而且最大只处理到几百万到几万亿。这些应用程序使用JS中的数字操作是非常安全的。
要是我们确实需要比较两个number
,就像是0.1 + 0.2
与0.3
,而且知道这个简单的相等测试会失败呢?
可以接受的最常见的做法是使用一个很小的"错误舍入"值作为比较的容差。这个很小的值经常被称为"机械极小值(machine epsilon)",对于JavaScript来说这种number
通常为2^-52
。
在ES6中,使用这个容差值预定义了Number.EPSILON
,所以你将会使用它,你也可以在ES6中安全的填补这个定义:
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2, -52);
}
我们可以使用这个Number.EPSILON
来比较两个number
的"等价性"(带有错误舍入的容差):
function numbersCloseEnoughToEqual(n1, n2) {
return Math.abs(n1 - n2) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual(a, b); // true
numbersCloseEnoughToEqual(0.0000001, 0.0000002); // false
可以被表示的最大的浮点值大概是1.798e+308
,它为你预定义为Number.MAX_VALUE
,在极小的一端,Number.MIN_VALUE
大概是5e-324
,它不是负数但是非常接近于0!
# 安全整数范围
由于number
的表示方式,对完全是number
的整数而言有一个安全的值的范围,而且它要比Number.MAX_VALUE
小得多。
可以『安全地』被表示的最大整数是2^53 - 1
,也就是9007199254740991
,刚好9万亿左右,还是挺大的。
在ES6中这个值实际上是自动预定义的,它是Number.MAX_SAFE_INTEGER
。意料之中的是,还有一个最小值-9007199254740991
,它在ES6中定义为Number.MIN_SAFE_INTEGER
。
JS程序面临处理这样大的数字的主要情况是,处理数据库中的64位ID等等。64位数字不能使用number
类型准确表达,所以在JavaScript中必须使用string
表现形式存储(和传递)。
# 测试整数
测试一个值是否是整数,你可以使用ES6定义的Number.isInteger
:
Number.isInteger(42); // true
Number.isInteger(42.000); // true
Number.isInteger(42.3); // false
为ES6前填补Number.isInteger
:
if (!Number.isInteger) {
Number.isInteger = function (num) {
return typeof num == "number" && num % 1 == 0;
};
}
要测试一个值是否为安全整数,使用ES6定义的Number.isSafeInteger
:
Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true
Number.isSafeInteger(Math.pow(2, 53)); // false
Number.isSafeInteger(Math.pow(2, 53) - 1); // true
为ES6前填补Number.isSafeInteger
:
if (!Number.isSafeInteger) {
Number.isSafeInteger = function (num) {
return Number.isIntefer(num)
&& Math.abs(num) <= Number.MAX_SAFE_INTEGER;
};
}
# 32位(有符号)整数
虽然整数可以安全地最大达到约九万亿,但有一些数字操作是仅仅为32位number
定义的。
要强制a
中的number
值是32位有符号整数,使用a|0
,这可以工作是因为|
位操作符仅仅对32位值起作用。而且,和0进行"或"的位操作实质上是什么也不做。
# 特殊值
在各种类型中散布着一些特殊值,需要开发者警惕并正确使用。
# 不是值的值
对于undefined
类型来说,有且仅有一个值:undefined
。对于null
类型来说,有且仅有一个值:null
。所以对它们而言,这些文字既是它们的类型也是它们的值。
undefined
和null
作为"空"值或者"没有"值,经常被认为是可以互换的。另一些开发者偏好于使用微妙的区别将它们区分开。举例来说:
null
是一个空值。undefined
是一个丢失的值。
或者:
undefined
还没有值。null
曾经有过值但现在没有。
不管你选择如何"定义"和使用这两个值,null
是一个特殊的关键字,不是一个标识符,因此你不能将它作为一个变量对待来给它赋值。然而,undefined
是一个标识符。
# Undefined
在非strict
模式下,给在全局上提供的undefined
标识符赋值是可能的:
function foo() {
undefined = 2; // 这。。。这。。
}
foo();
function foo() {
"use strict";
undefined = 2; // TypeError!
}
foo();
但是,在非strict
模式和strict
模式下,你可以创建名为undefiend
的局部变量。
function foo() {
"use strict";
var undefined = 2;
console.log(undefined); // 2
}
foo();
朋友永远不让朋友覆盖undefined
。
# void操作符
另一个能得到undefined
值的方法是void
操作符。
表达式void __
会"躲开"任何值,所以这个表达式的结果总是undefined
。它不会修改任何已经存在的值;只是确保不会有值从操作符表达式中返回来。
var a = 42;
console.log(void a, a); // undefined 42
从惯例上讲,要通过使用void
来独立表现值undefined
,你可以使用void 0
。实际上void 0
、void 1
和undefined
没有区别。
但是在几种其他情况下void
操作符可以十分有用:如果你需要确保一个表达式没有结果值(即便它有副作用)。举个例子:
function doSomething() {
// 注意:APP.ready 是由我们的应用程序提供的
if (!APP.ready) {
// 稍后再试一次
return void setTimeout(doSomething, 100);
}
var result;
// 做一些其他事情
return result;
}
// 我们能立即执行吗?
if (doSomething()) {
// 马上处理其他任务
}
这里,setTimeout
函数返回一个数字值,但是我们想void
它,这样函数就只会返回undefined
了。
许多开发者宁愿将这些动作分开,功能相同但可以不使用void
操作符:
if (!APP.ready) {
// 稍后再试一次
setTimeout(doSomething, 100);
return;
}
# 特殊的数字
number
类型包含几种特殊值。我们将会仔细考察每一种。
# 不是数字的数字
如果你不使用同为number
的两个操作数进行任何算数操作,那么操作的结果将失败而产生一个不合法的number
,这种情况下就会获得NaN
值。
NaN
在字面上代表"不是一个number
(Not a Number)",但是正如我们即将看到的,这种文字描述十分失败而且容易误导人。将NaN
考虑为"不合法数字","失败的数字"甚至是"坏掉的数字"都要比"不是一个数字"准确得多。举例来说:
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
换句话说:"'不是一个数字'的类型是'数字'"。。。。。。。。。
NaN
是一种"哨兵值"(一个被赋予了特殊意义的普通的值),它代表number
集合内的一种特殊的错误情况。这种错误情况实质上是:"我试着进行数学操作但是失败了,而这就是失败的number
结果"。
那么,如果你有一个值存在某个变量中,而且你想要测试它是否是这个特殊的失败数字NaN
,你也许认为可以直接与NaN
本身比较,但事实不可以。
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
NaN
是一个非常特殊的值,它是唯一一个不具有反射性的值,所以NaN !== NaN
。
那我们如何去测试它呢?
var a = 2 / "foo";
isNaN(a); // true
够简单吧,通过全局isNaN
内建工具,来告诉它是否是NaN
,问题解决了!
别高兴得太早。
isNaN
工具有一个重大缺陷。它的工作原理是——测试这个传进来的东西是否不是一个number
或者是一个number
。但这不准确。
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; // "foo"
window.isNaN(a); // true
window.isNaN(b); // true
很明显,"foo"
根本不是一个number
,但它也不是NaN
值!这个Bug从最开始的时候就存在于JS中了。
在ES6中,终于找到了替代的工具:Number.isNaN
,有一个简单的填补支持ES6前。
if (!Number.isNaN) {
Number.isNaN = function (n) {
return n !== n;
};
}
怪吧?但是好用!
# 无穷
来自于像C这样的传统编译型语言的开发者,可能习惯于看到编译器错误或者是运行时异常,比如对这样一个操作给出的"除数为0":
var a = 1 / 0;
然而在JS中,这个操作是明确定义的,而且它的结果是值Infinity
(也就是`Number.POSITIVE_INFINITY)。意料之中的是:
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
如你所见,-Infinity
是从任一个被除数为负的除0操作得来的。
# 零
虽然这可能使有数学头脑的读者困惑,但JavaScript拥有-0
和+0
。在我们讲解为什么-0
存在之前,我们应该考察JS如何处理它,因为它可能十分令人困惑。
除了使用字面量-0
指定,负的零还可以从特定的数学操作中得出。比如:
var a = 0 / -3; // -0
var b = 0 * -3; // -0
加法和减法无法得出负零。
在开发者控制台中考察一个负的零,经常显示为-0
,然而直到最近这才是一个常见情况,所以一些你可能遇到的老版本浏览器也许依然将它报告为0
。
但是根据语言规范,如果你试着将一个负零转换为字符串,它将总会被报告为"0"
。
var a = 0 / -3;
// 至少(有些浏览器)控制台是对的
a; // -0
// 但是语言规范坚持要向你撒谎
a.toString(); // "0"
a + ""; // "0"
String(a); // "0"
// 奇怪的是,就连JSON也加入了骗局之中
JSON.stringify(a); // "0"
有趣的是,反向操作(从string
到number
)不会撒谎:
+"-0"; // -0
Number("-0"); // -0
JSON.parse("-0"); // -0
除了一个负零的字符串化会欺骗性地隐藏它实际的值外,比较操作符也被设定为(有意地)要说慌。
var a = 0;
var b = 0 / -3;
a == b; // true
-0 == 0; // true
a === b; // true
-0 === 0; // true
0 > -0; // false
a > b; // false
很明显,如果你想在你的代码中区分-0
和0
,你就不能仅依靠开发者控制台的输出,你必须更聪明一些;
function isNegZero(n) {
n = Number(n);
return (n === 0) && (1 / n === -Infinity);
}
isNegZero(-0); // true
isNegZero(0 / -3); // true
isNegZero(0); // false
那么,除了学院派的细节以外,我们为什么需要一个负零呢?
在一些应用程序中,开发者使用值的大小来表示一部分信息(比如动画中每一帧的速度),而这个number
的符号来表示另一部分信息(比如移动的方向)。在这些应用程序中,举例来说,如果一个变量的值变成了0,而它丢失了符号,那么你就丢失了它是从哪个方向移动到0的信息。保留0的符号避免了潜在的意外信息丢失。
# 特殊等价
正如我们上面看到的,当使用等价性比较时,值NaN
和值-0
拥有特殊的行为。NaN
永远不会和自己相等,所以你不得不使用ES6的Number.isNaN
。相似地,-0
撒谎并假装它和+0
相等,所以你不得不使用上面isNegZero
工具来比较。
在ES6中,有一个新工具可以测试两个值的绝对等价性,它称为Object.is
:
var a = 2 / "foo";
var b = -3 * 0;
Object.is(a, NaN); // true
Object.is(b, -0); // true
Object.is(b, 0); //false
对于ES6前的填补:
if (!Object.is) {
Object.is = function (v1, v2) {
// 测试 -0
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 测试 NaN
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情况
return v1 === v2;
};
}
Object.is
主要是处理这些特殊的等价情况。
# 值与引用
在其他许多语言中,根据你使用的语法,值可以通过值拷贝,也可以通过引用拷贝来赋予/传递。
比如,在C++中如果你想要把一个number
变量传递进一个函数,并使这个变量的值被更新,你可以用int& myNum
这样的东西来声明函数参数,当你传入一个变量x
时,myNum
将是一个指向x
的引用;引用就像一个特殊形式的指针,你得到的是一个指向另一个变量的指针(像别名alias)。如果你没有声明一个引用参数,被传入的值将总是被拷贝的,就算它使一个复杂的对象。
在JavaScript中,没有指针,并且引用的工作方式有一点儿不同。你不能拥有从一个JS变量到另一个JS变量的引用。
JS中的引用指向一个(共享的)值,所以即便你有十个不同的引用,它们仍都共享同一个值。
在JavaScript中,没有语法上的提示可以控制值和引用的赋值/传递。取而代之的是,值的类型用来唯一控制值是通过值拷贝,还是引用拷贝来赋予。
让我们来展示一下:
var a = 2;
var b = a; // b总是a中的值的拷贝
b++;
a; // 2
b; // 3
var c = [1, 2, 3];
var d = c; // d是共享值[1, 2, 3]的引用
d.push(4);
c; // [1,2,3,4]
d; // [1,2,3,4]
简单值(也叫基本标量)总是通过值拷贝来赋予/传递:null
、undefined
、string
、number
、boolean
以及ES6的symbol
。
复合值(object
和function
)总是在赋值或传递时创建一个引用的拷贝。
引用指向的是值本身而不是变量,你不能使用一个引用来改变另一个引用所指向的值:
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// 稍后
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
当我们做赋值操作b = [4, 5, 6]
时,是绝对不会影响到a
指向的位置。
这样的困惑常见于函数参数:
function foo(x) {
x.push(4);
x; // [1,2,3,4]
// 稍后 ---- 这里容易混淆,因为赋值了,导致引用断掉了
x = [4,5,6];
x.push(7);
x; // [4,5,6,7]
}
var a = [1,2,3];
foo(a);
a; // [1,2,3,4] 不是 [4,5,6,7]
引用十分强大,但有时候它们碍你的事儿,而有事你会在它们不存在时需要它们。你唯一可以用来控制引用与值拷贝的东西是值本身的类型,所以你必须通过你选用的值的类型来间接地影响赋值/传递行为。
# 原生类型
在第一和第二章中,我们几次提到了各种内建类型,通常称为"原生类型",比如String
和Number
。现在让我们来仔细检视它们。
这是最常用的原生类型的一览:
String()
Number()
Boolean()
Array()
Object()
Function()
RegExp()
Date()
Error()
Symbol()
如你所见,这些原生类型实际上是内建函数。
如果你熟悉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; // true
Object.prototype.toString.call(a); // "[object String]"
创建值的构造器形式(new String("abc")
)的结果是一个基本类型值("abc"
)的包装器对象。
重要的是,typeof
显示这些对象不是它们自己的特殊类型,而是object
类型的子类型。
这个包装器对象可以被进一步观察,像这样:
console.log(a);
这个语句的输出会根据你使用的浏览器变化,开发者控制台可以自由选择它认为合适的方式来序列化对象。
# 内部[[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]]
值。
# 封箱包装器
这些对象包装器服务于一个非常重要的目的。基本类型值没有属性或方法,所以为了访问length
或toString
你需要这个值的对象包装器。值得庆幸的是,JS将会自动地封箱(也就是包装)基本类型值来满足这样的访问。
var a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
那么,如果你想以通常的方式访问这些字符串值上的属性/方法,比如一个for
循环的i < a.length
条件,这么做看起来很有道理:一开始就得到一个这个值的对象形式,于是JS引擎就不需要隐含地为你创建一个。
但事实证明这是一个坏主意。浏览器长久以来就对length
这样的常见情况进行性能优化,这意味着如果你试着直接使用对象形式(它们没有被优化过)进行"提前优化",那么实际上你的程序将会变慢。
一般来说,基本上没有理由直接使用对象形式。让封箱在需要的地方隐含地发生会更好。换句话说,永远也不要做new String("abc")
、new Number(42)
这样的事情 —— 应当总是偏向于使用基本类型字面量"abc"
和42
。
# 对象包装器的坑
如果你确实选择要直接使用对象包装器,那么有几个坑你应该注意。
举个例子,考虑Boolean
包装的值:
var a = new Boolean(false);
if (!a) {
console.log("Oops"); // 永远不会运行
}
这里的问题是,虽然你为值false
创建了一个对象包装器,但是对象本身是"truthy",所以使用对象的效果与使用底层的值false
本身是相反的,这与通常的期望十分不同。
如果你想手动封箱一个基本类型值,你可以使用Object
函数(没有new
关键字):
var a = "abc";
var b = new String(a);
var c = Object(a);
typeof a; // "string"
typeof b; // "object"
typeof c; // "object"
b instanceof String; // true
c instanceof String; // true
Object.prototype.toString.call(b); // "[object String]"
Object.prototype.toString.call(c); // "[object String]"
再说一遍,通常不鼓励直接使用封箱的包装器对象。
# 开箱
如果你有一个包装器对象,而你想要取出底层的基本类型值,你可以使用valueOf
方法:
var a = new String("abc");
var b = new Number(42);
var c = new Boolean(true);
a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true
当以一种查询基本类型值的方式使用对象包装器时,开箱也会隐含地发生。举例:
var a = new String("abc");
var b = a + ""; // b拥有开箱后的基本类型值 "abc"
typeof a; // "object"
typeof b; // "string"
# 原生类型作为构造器
对于array
、object
、function
和正则表达式来说,使用字面形式来创建它们的值几乎总是更好的选择,而且字面形式与构造器形式所创建的值是同一种对象(也就是,没有非包装的值)。
正如我们刚刚在上面看到的其他原生类型,除非你真的知道你需要这些构造器形式,一般来说应当避免使用它们,这主要是因为它们会带来一些你可能不会想要对付的异常和陷阱。
# Array
var a = new Array(1, 2, 3);
a; // [1, 2, 3]
var b = [1, 2, 3];
b; // [1, 2, 3]
Array
构造器有一种特殊形式,如果它仅仅被传入一个number
参数,与将这个值作为数组的内容不同,它会被认为是用来"预定数组大小"的长度。
这是个可怕的主意。首先,你会意外地用错这种形式,因为它很容易忘记。
但更重要的是,其实没有预定数组大小这样的东西。你所创建的是一个空数组,并将这个数组的length
属性设置为那个指定的数字值。
一个数组在它的值槽上没有明确的值,但是有一个length
属性意味着这些值槽是存在的,在JS中这是一个诡异的数据结构,它带有一些非常奇怪且令人困惑的行为。可以创建这样的值的能力,完全源于老旧的、已经废弃的、仅具有历史意义的功能。
注意
带有至少一个"空值槽"的数组经常被称为"稀散数组"。
这里另外一个例子,展示浏览器的开发者控制台在如何表示这样的对象上有所不同,它产生了更多的困惑。
举例来说:
var a = new Array(3);
a.length; // 3
a; // [empty * 3]
在任何情况下,永远不要创建并使用诡异的空值槽数组。
# 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
仅仅作为另一种形式的evel
方法。你几乎永远不会需要用这种方式动态定义一个函数。
用字面量形式(/^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
构造器接受可选参数值来指定要使用的日期/时间,但是如果省略的话,就会使用当前的日期/时间。
Error
无论是否由new
来构造,其行为都是相同的。
创建error对象的主要原因是,它会将当前的执行栈上下文捕捉进对象中。这个栈上下文包含函数调用栈和error对象被创建时的行号,这使调试这个错误更简单。
典型地,你将与throw
操作符一起使用这样的error对象:
function foo(x) {
if (!x) {
throw new Error("x wasn't provided");
}
}
Error对象实例一般拥有至少一个message
属性,有时还有其他属性(你应该将它们作为只读的),比如type
。然而,与其检视上面提到的stack
属性,最好是在error对象上调用toString
来得到一个格式友好的错误信息。
# Symbol
在ES6中,新增了一个基本值类型,称为"Symbol"。Symbol是一种特殊的"独一无二"的值,可以作为对象上的属性使用而几乎不必担心任何冲突。它们主要是为特殊的ES6结构的内建行为设计的,但你也可以定义你自己的symbol。
Symbol可以用做属性名,但是你不能从你的程序中看到或访问一个symbol的实际值,从开发者控制台也不行。例如,如果你在开发者控制台中对一个Symbol求值,将会显示Symbol(Symbol.create)
之类的东西。
在ES6中有几种预定义的Symbol,作为Symbol
函数对象的静态属性访问,比如Symbol.create
,Symbol.iterator
等等。要使用它们,可以这样做:
obj[Symbol.iterator] = function() {/* .. */}
要定义你自己的Symbol,使用Symbol
原生类型。Symbol
原生类型"构造器"很独特,因为它不允许你将new
与它一起使用,这么做会抛出一个错误。
var mysym = Symbol("my own symbol");
mysym; // Symbol(my own symbol)
mysym.toString(); // "Symbol(my own symbol)"
typeof mySym; // "symbol"
var a = {};
a[mysym] = "foobar";
Object.getOwnPropertySymbols(a); // [Symbol(my own symbol)]
虽然Symbol实际上不是私有的(在对象上使用Object.getOwnPropertySymbols
)反射,揭示了Symbol其实是相当公开的,但是它们的主要用途可能是私有属性,或者类似的特殊属性。对于大多数开发者,他们也许会在属性名上加入_
下划线前缀,这在惯例上表示:"这是一个私有的/特殊的/内部的属性,别碰!"
注意:Symbol
不是object
,它们是简单的基本标量。
# 原生类型原型
每一个内建的原生构造器都拥有它自己的prototype
对象 —— Array.prototype
,String.prototype
等等。
对于它们特定的对象子类型,这些对象含有独特的行为。
例如,所有的字符串对象和string
基本值的扩展(封箱),都可以访问String.prototype
对象上的方法。
有赖于原型委托,任何字符串值都可以访问这些方法:
var a = " abc ";
a.indexOf("c"); // 3
a.toUpperCase(); // " ABC "
a.trim(); // "abc"
其它构造器的原型包含也适用于它们类型的行为。所有函数都可以访问apply
、call
、bind
,因为Function.prototype
定义了它们。
但是,一些原生类型的原型不仅仅是单纯的对象:
typeof Function.prototype; // "function"
Function.prototype(); // 它是一个空函数!
RegExp.prototype.toString(); // "/(?:)/" —— 空的正则表达式
"abc".match(RegExp.prototype); // [""]
一个特别差劲儿的主意是,你甚至可以修改这些原生类型的原型:
Array.isArray(Array.prototype); // true
Array.prototype.push(1, 2, 3); // 3
Array.prototype; // [1, 2, 3]
// 记得重置,否则坑哭你
Array.prototype.length = 0;
如你所见,Function.prototype
是一个函数,RegExp.prototype
是一个正则表达式,而Array.prototype
是一个数组。
# 原型作为默认值
Function.prototype
是一个空函数,RegExp.prototype
是一个"空"正则表达式,而Array.prototype
是一个空数组,这使它们成了可以赋值给变量的,很好的"默认"值 —— 如果这些类型的变量还没有值。
例如:
function isThisCool(vals, fn, rx) {
vals = vals || Array.prototype;
fn = fn || Function.prototype;
rx = rx || RegExp.prototype;
return rx.test(vals.map(fn).join(""));
}
isThisCool(); // true
isThisCool(
["a", "b", "c"],
function (v) {return v.toUpperCase();},
/D/
); // false
这个方式的一个微小的副作用是,prototype
已经被创建了,而且是内建的,因此它仅被创建一次。相比之下,使用[]
、function (){}
和/(?:)/
这些值本身作为默认值,将会在每次调用isThisCool
时重新创建这些值。这可能会消耗内存/CPU。
另外,要非常小心不要对后续要被修改的值使用Array.prototype
作为默认值。在这个例子中,vals
是只读的,但如果你要在原地对vals
进行修改,那你实际上修改的是Array.prototype
本身,这将把你引到刚才提到的坑里!
# 强制转换
现在我们更全面地了解了JavaScript的类型和值,我们将注意力转向一个极具争议的话题:强制转换。
正如我们在第一章中提到的,关于强制转换到底是一个有用的特性,还是一个语言设计上的缺陷(或介于两者之间),早就开始争论不休了。如果你读过关于JS的其它书籍,你就会知道流行在市面上那种淹没一切的声音:强制转换是魔法,是邪恶的,令人困惑的,彻头彻尾的坏主意。
本着这个系列丛书的总体精神,你应当直面你不理解的东西并设法更全面地搞懂它。而不是因为大家都这样做,或者你曾经被一些怪东西咬到就逃避强制转换。
我们的目标是全面地探索强制转换的优点和缺点,这样你就能在程序中对它是否合适做出明智的决定。
# 转换值
将一个值从一个类型明确地转换到另一个类型通常称为"类型转换(type casting)",当这个操作隐含的完成时称为"强制转换(coercion)"。
注意
这可能不明显,但是JavaScript强制转换总是得到基本标量值的一种,比如string
、number
或boolean
。没有强制转换可以得到像object
和function
这样的复杂值。这三章讲解了"封箱",它将一个基本类型标量值包装在它们相应的object
中,但是在准确的意义上这不是真正的强制转换。
另一种区别这些术语的常见方法是:"类型转换(type casting/conversion)"发生在静态类型语言的编译时,而"类型强制转换(type coercion)"是动态类型语言的运行时转换。
然而,在JavaScript中,大多数人将所有这些类型的转换都称为强制转换(coericon),所以我偏好的区别方式是使用"隐含强制转换(implicitcoercion)"与"明确强制转换(explicit coercion)"。
其中的区别应当是很明显的:在观察代码时如果一个类型转换明显是有意为之的,那么它就是"明确强制转换",而如果这个类型转换是作为其它操作的不那么明显的副作用发生的,那么它就是"隐含强制转换"。
例如,考虑这两种强制转换的方式:
var a = 42;
var b = a + ""; // 隐含强制转换
var c = String(a); // 明确强制转换
b
和c
都会转换成string
类型,c
的类型转换是很明显的,而b
的转换就比较隐含了。
# 抽象值操作
在我们探究明确与隐含强制转换之前,我们需要学习一些基本规则,是它们控制着值如何变成一个string
、number
或boolean
的。ES5语言规范的第九部分用值的变形规则定义了几种"抽象操作"。我们将特别关注于:ToString
、ToNumber
和ToBoolean
,并稍稍关注一下ToPrimitive
。
# ToString
当任何一个非string
值被强制转换为一个string
表现形式时,这个转换的过程是由语言规范的9.8部分的ToString
抽象操作处理的。
内建的基本类型值拥有自然的字符串化形式:null
变为"null"
,undefined
变为"undefined"
,true
变为"true"
。number
一般会以你期望的自然方式表达,但正如我们在第二章中讨论的,非常小或非常大的number
将会以指数形式表达:
// 1.07 乘以 1000,7次
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 7次乘以3位 => 21位
a.toString(); // 1.07e21
对于普通的对象,默认的toString
将返回内部[[Class]]
,例如"[object Object]"
。
但正如早先所展示的,如果一个对象上拥有它自己的toString
方法,而你又以一种类似string
的方式使用这个对象,那么它的toString
将会被自动调用,而这个调用的string
结果将被使用。
注意
技术上讲,一个对象被强制转换为一个string
要通过ToPrimitive
抽象操作,但是那其中的微妙细节将会在本章稍后的ToNumber
部门中讲解,所以我们在这里先跳过它。
数组拥有一个覆盖版本的默认toString
,将数组字符串化为它所有的值的连接,并用,
分割每个值。
var a = [1, 2, 3];
a.toString(); "1,2,3"
重申一次,toString
可以明确地被调用,也可以通过在一个需要string
的上下文环境中使用一个非string
来自动地调用。
# JSON字符串化
另一种看起来与ToString
密切相关的操作是,使用JSON.stringify
工具将一个值序列化为一个JSON兼容的string
值。
重要的是要注意,这种字符串化与强制转化并不完全是同一种东西。但是因为它与上面讲的ToString
规则有关联,我们将在这里稍微转移一下话题,来讲解JSON字符串化行为。
对于最简单的值,JSON字符串化行为基本上和toString
转换是相同的,除了序列化的结果总是一个string:
JSON.stringify(42); // "42"
JSON.stringify( "42" ); // ""42"" 一个包含双引号的字符串
JSON.stringify(null); // "null"
JSON.stringify(true); // "true"
任何JSON安全的值都可以被JSON.stringify
字符串化。
JSON不安全的值可能更容易理解一些。一些例子是:undefined
、function
、symbol
和带有循环引用的object
(一个对象结构中的属性相互引用而造成一个循环)。如果在一个object
的属性中遇到这样的值,这个属性会被简单的剔除掉。
考虑下面的代码:
JSON.stringify(undefined); // undefined
JSON.stringify(function (){}); // undefined
JSON.stringify([1, undefined, function(){}, 4]); // "[1,null,null,4]"
JSON.stringify({a: 2, b:function(){}}); // "{"a": 2}"
但如果你尝试用JSON.stringify
格式化一个带有循环引用的object
,就会抛出一个错误。
JSON字符串化有一个特殊行为,如果一个object
值定义了一个toJSON
方法,这个方法将会被首先调用,以取得用于序列化的值。
如果你打算JSON字符串化一个可能含有非法JSON值对象,或者如果这个对象中正好有不适于序列化的值,那么你就应当为它定义一个toJSON
方法,返回这个object
的一个JSON安全版本。
例如:
var o = {};
var a = {
b: 42,
c: o,
d: function (){}
};
// 在a内部制造一个循环引用
o.e = a;
// 这会因循环引用而抛出一个错误
// JSON.stringify(a);
// 自定义一个JSON值序列化
a.toJSON = function () {
// 序列化仅包含属性b
return {b: this.b};
};
JSON.stringify(a); // "{b:42}"
一个很常见的误解是,toJSON
应当返回一个JSON字符串化的表现形式。这可能是不正确的,除非你事实上想要字符串化string
本身。toJSON
应当返回合适的实际普通值(无论什么类型),而JSON.stringify
自己会处理字符串化。
换句话说,toJSON
应当被翻译为:"变为一个适用于字符串化的JSON安全的值",而不是像许多开发者错误的认为那样,"变成一个JSON字符串"。
考虑下面的代码:
var a = {
val: [1, 2, 3],
// 可能正确
toJSON: function() {
return this.val.slice(1);
}
};
JSON.stringify(a); // "[2,3]"
JSON.stringify(b); // ""[2,3]""
在第二个调用中,我们字符串化了返回的string
而不是array
本身,这可能不是我们想要做的。
既然我们说到了JSON.stringify
,那么就让我们来讨论一些不那么广为人知,但是仍然很有用的功能吧。
JSON.stringify
的第二个参数值是可选的,它成为替换器(replacer)。这个参数值既可以是一个array
也可以是一个function
。与toJSON
为序列化准备一个值的方式类似,它提供一种过滤机制,指出一个object
的哪一个属性应该或不应该被包含在序列化形式中,来自定义这个object
的递归序列化行为。
如果替换器是一个array
,那么它应当是一个string
的array
,它的每一个元素指定了允许被包含在这个object
的序列化形式中的属性名称。如果一个属性不存在于这个列表中,那么它就会被跳过。
如果替换器是一个function
,那么它会为object
本身而被调用一次,并且为这个object
中的每个属性都被调用一次,而且每次都被传入两个参数值,key和value。要在序列化中跳过一个key,可以返回undefined
。否则,就返回被提供的value。
var a = {
b: 42,
c: "42",
d: [1, 2, 3]
};
JSON.stringify(a, ["b", "c"]); // "{"b": 42, "c": "42"}"
JSON.stringify(a, function(k, v) {
if (k !== "c") return v;
});
// "{"b": 42, "d": [1, 2, 3]}"
JSON.stringify
还可以接收第三个可选参数,称为填充符,在对人类友好的输出中它被用作缩进。填充符可以是一个正整数,用来指示每一级缩进中应当使用多少个空格字符。或者,填充符可以是一个string
,这时每一级缩进将会使用它的前十个字符。
var a = {
b: 42,
c: "42",
d: [1, 2, 3]
};
JSON.stringify(a, null, 3);
// "{
// "b": 42,
// "c": "42",
// "d": [
// 1,
// 2,
// 3
// ]
// }"
JSON.stringify(a, null, "-----");
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"
记住,JSON.stringify
并不直接是一种强制转换的形式。但是,我们在这里讨论它,是由于两个与ToString
强制转换有关联的行为:
string
、number
、boolean
和null
值在JSON字符串化时,与它们通过ToString
抽象操作的规则强制转化为string
值的方式基本上是相同的。- 如果传递一个
object
值给JSON.stringify
,儿这个object
上拥有一个toJSON
方法,那么在字符串化之前,toJSON
就会被自动调用来将这个值"强制转化"为JSON安全的。
# ToNumber
如果任何非number
值,以一种要求它是number
的方式被使用,比如数学操作,就会发生ES5语言规范在9.3部分定义的ToNumber
抽象操作。
例如,true
变为1
而false
变为0
。undefined
变为NaN
,而null
变为0
。
对于一个string
值来说,ToNumber
工作起来很大程度与数字字面量的规则/语法很相似。如果它失败了,结果将是NaN
。一个不同之处的例子是,在这个操作中0
前缀的八进制数不会被作为八进制数来处理(而仅作为普通的十进制小数),虽然这样的八进制数作为number
字面量是合法的。
对象(以及数组)将会首先被转换为它们的基本类型值的等价物,而后这个结果值(如果它还不是一个number
基本类型)会根据刚才提到的ToNumber
规则被强制转换为一个number
。
为了转换为基本类型值的等价物,ToPrimitive
抽象操作将会查询这个值(使用内部DefaultValue
操作),看它有没有valueOf
方法。如果valueOf
可用并且它返回一个基本类型值,那么这个值就将用于强制转换。如果不是这样,但toString
可用,那么就由它来提供用于强制转换的值。
如果这两种操作都没有提供一个基本类型值,就会抛出一个TypeError
。
在ES5中,你可以创建一个不可强制转换的对象 —— 没有valueOf
和toString
—— 如果它的[[Prototype]]
的值为null
,这通常是通过Object.create(null)
来创建的。
考虑如下代码:
var a = {
valueOf: function() {
return "42";
}
};
var b = {
toString: function() {
return "42"
}
};
var c = [4, 2];
c.toString = function() {
return this.join(""); // "42"
};
Number(a); // 42
Number(b); // 42
Number(c); // 42
Number(""); // 0
Number([]); // 0
Number(["abc"]); // NaN
# ToBoolean
下面,让我们聊一聊在JS中boolean
如何动作。世面上关于这个话题有许多的困惑和误解,所以集中注意力!
首先最重要的是,JS实际上拥有true
和false
关键字,而且它们的行为正如你所期望的boolean
值一样。一个常见的误解是,值1
和0
与true/false
是相同的。虽然这可能在其他语言中是成立的,但在JS中number
就是number
,而boolean
就是boolean
。你可以将1
强制转换为true
。但它们不是相同的。