# 类型与文法

章节目录:

# 类型

大多数开发者认为,动态语言没有类型。但是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

#

arraystringnumber是任何程序的最基础构建块,但是JavaScript在这些类型上有一些或使你惊喜或使你惊讶的独特性质。下面我们一起来看看如何正确的理解并利用它们的行为。

# Array

和其他强制类型的语言相比,JavaScript的array只是值的容器,而这些值可以是任何类型:stringnumberobject甚至是另一个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进制numberstring值被用作键的话,它会认为你想使用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.

让我们来看另一个例子:翻转一个stringarray拥有一个原地的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.042一样是"整数"。

像大多数现代计算机语言以及几乎所有的脚本语言一样,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

注意,这里返回的是numberstring表现形式,而且会补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.10.2的二进制表示形式是不精确的,所以它们相加时,结果不是精确地0.3。而是非常接近的值:0.30000000000000004

现在的问题是,如果一些number不能被信任为精确的,这不是意味着我们根本不能使用number吗?当然不是

在一些应用程序中你需要多加小心,特别是在对付小数的时候。还有许多应用程序只处理整数,而且最大只处理到几百万到几万亿。这些应用程序使用JS中的数字操作是非常安全的。

要是我们确实需要比较两个number,就像是0.1 + 0.20.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。所以对它们而言,这些文字既是它们的类型也是它们的值。

undefinednull作为"空"值或者"没有"值,经常被认为是可以互换的。另一些开发者偏好于使用微妙的区别将它们区分开。举例来说:

  • 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 0void 1undefined没有区别。

但是在几种其他情况下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"

有趣的是,反向操作(从stringnumber)不会撒谎:

+"-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

很明显,如果你想在你的代码中区分-00,你就不能仅依靠开发者控制台的输出,你必须更聪明一些;

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]

简单值(也叫基本标量)总是通过值拷贝来赋予/传递:nullundefinedstringnumberboolean以及ES6的symbol

复合值(objectfunction)总是在赋值或传递时创建一个引用的拷贝。

引用指向的是值本身而不是变量,你不能使用一个引用来改变另一个引用所指向的值:

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]

引用十分强大,但有时候它们碍你的事儿,而有事你会在它们不存在时需要它们。你唯一可以用来控制引用与值拷贝的东西是值本身的类型,所以你必须通过你选用的值的类型来间接地影响赋值/传递行为。

# 原生类型

在第一和第二章中,我们几次提到了各种内建类型,通常称为"原生类型",比如StringNumber。现在让我们来仔细检视它们。

这是最常用的原生类型的一览:

  • 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]]值对应于关联这个值的内建的原生类型构造器,但事实却不总是这样。

基本类型呢?首先,nullundefined

Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"

你会注意到,不存在NullUndefined原生类型构造器,但不管怎样"Null""Undefined"是被暴露出来的内部[[Class]]值。

但是对于像stringnumberboolean这样的简单基本类型,实际上会启动另一种行为,通常称为"封箱(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]]值。

# 封箱包装器

这些对象包装器服务于一个非常重要的目的。基本类型值没有属性或方法,所以为了访问lengthtoString你需要这个值的对象包装器。值得庆幸的是,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"

# 原生类型作为构造器

对于arrayobjectfunction和正则表达式来说,使用字面形式来创建它们的值几乎总是更好的选择,而且字面形式与构造器形式所创建的值是同一种对象(也就是,没有非包装的值)。

正如我们刚刚在上面看到的其他原生类型,除非你真的知道你需要这些构造器形式,一般来说应当避免使用它们,这主要是因为它们会带来一些你可能不会想要对付的异常和陷阱。

# 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

ObjectFunctionRegExp构造器一般来说也是可选的(除非特别目的,应当避免使用):

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

DateError原生类型构造器要比其它种类的原生类型有用得多,因为它们没有字面量形式。

要创建一个日期对象值,你必须使用new DateDate构造器接受可选参数值来指定要使用的日期/时间,但是如果省略的话,就会使用当前的日期/时间。

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.createSymbol.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.prototypeString.prototype等等。

对于它们特定的对象子类型,这些对象含有独特的行为。

例如,所有的字符串对象和string基本值的扩展(封箱),都可以访问String.prototype对象上的方法。

有赖于原型委托,任何字符串值都可以访问这些方法:

var a = " abc ";

a.indexOf("c"); // 3
a.toUpperCase(); // " ABC "
a.trim(); // "abc"

其它构造器的原型包含也适用于它们类型的行为。所有函数都可以访问applycallbind,因为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强制转换总是得到基本标量值的一种,比如stringnumberboolean。没有强制转换可以得到像objectfunction这样的复杂值。这三章讲解了"封箱",它将一个基本类型标量值包装在它们相应的object中,但是在准确的意义上这不是真正的强制转换。

另一种区别这些术语的常见方法是:"类型转换(type casting/conversion)"发生在静态类型语言的编译时,而"类型强制转换(type coercion)"是动态类型语言的运行时转换。

然而,在JavaScript中,大多数人将所有这些类型的转换都称为强制转换(coericon),所以我偏好的区别方式是使用"隐含强制转换(implicitcoercion)"与"明确强制转换(explicit coercion)"。

其中的区别应当是很明显的:在观察代码时如果一个类型转换明显是有意为之的,那么它就是"明确强制转换",而如果这个类型转换是作为其它操作的不那么明显的副作用发生的,那么它就是"隐含强制转换"。

例如,考虑这两种强制转换的方式:

var a = 42;

var b = a + ""; // 隐含强制转换

var c = String(a); // 明确强制转换

bc都会转换成string类型,c的类型转换是很明显的,而b的转换就比较隐含了。

# 抽象值操作

在我们探究明确隐含强制转换之前,我们需要学习一些基本规则,是它们控制着值如何变成一个stringnumberboolean的。ES5语言规范的第九部分用值的变形规则定义了几种"抽象操作"。我们将特别关注于:ToStringToNumberToBoolean,并稍稍关注一下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不安全的值可能更容易理解一些。一些例子是:undefinedfunctionsymbol和带有循环引用的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,那么它应当是一个stringarray,它的每一个元素指定了允许被包含在这个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强制转换有关联的行为:

  • stringnumberbooleannull值在JSON字符串化时,与它们通过ToString抽象操作的规则强制转化为string值的方式基本上是相同的。
  • 如果传递一个object值给JSON.stringify,儿这个object上拥有一个toJSON方法,那么在字符串化之前,toJSON就会被自动调用来将这个值"强制转化"为JSON安全的。

# ToNumber

如果任何非number值,以一种要求它是number的方式被使用,比如数学操作,就会发生ES5语言规范在9.3部分定义的ToNumber抽象操作。

例如,true变为1false变为0undefined变为NaN,而null变为0

对于一个string值来说,ToNumber工作起来很大程度与数字字面量的规则/语法很相似。如果它失败了,结果将是NaN。一个不同之处的例子是,在这个操作中0前缀的八进制数不会被作为八进制数来处理(而仅作为普通的十进制小数),虽然这样的八进制数作为number字面量是合法的。

对象(以及数组)将会首先被转换为它们的基本类型值的等价物,而后这个结果值(如果它还不是一个number基本类型)会根据刚才提到的ToNumber规则被强制转换为一个number

为了转换为基本类型值的等价物,ToPrimitive抽象操作将会查询这个值(使用内部DefaultValue操作),看它有没有valueOf方法。如果valueOf可用并且它返回一个基本类型值,那么这个值就将用于强制转换。如果不是这样,但toString可用,那么就由它来提供用于强制转换的值。

如果这两种操作都没有提供一个基本类型值,就会抛出一个TypeError

在ES5中,你可以创建一个不可强制转换的对象 —— 没有valueOftoString —— 如果它的[[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实际上拥有truefalse关键字,而且它们的行为正如你所期望的boolean值一样。一个常见的误解是,值10true/false是相同的。虽然这可能在其他语言中是成立的,但在JS中number就是number,而boolean就是boolean。你可以将1强制转换为true。但它们不是相同的。

# Falsy值
最近更新时间: 2023/3/21 19:40:56