原始值包装类型

​ 为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型:Boolean、Number 和 String。 这些类型具有本章介绍的其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,从而暴露操作原始值的各种方法。来看下面的例子:

let s1 = "some text";
let s2 = s1.substring(2);

​ 在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring() 方法,并把结果保存在 s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:

 1. **创建**一个 String 类型的**实例**;
 2. **调用**实例上的**特定方法**;
 3. **销毁**实例。

​ 可以把这 3 步想象成执行了如下 3 行 ECMAScript 代码:

let s1 = new String("some text"); 
let s2 = s1.substring(2);
s1 = null;

​ 这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上 3 步也会在后台发生,只不过使用的是 Boolean 和 Number 包装类型而已。

引用类型原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象只存在访问它的那行代码执行期间。这意味着不能在运行时给原始值添加属性和方法。比如下面的例子:

let s1 = "some text";
s1.color = "red"; 
console.log(s1.color); // undefined

​ 这里的第二行代码尝试给字符串 s1 添加了一个 color 属性。可是,第三行代码访问 color 属性时, 它却不见了。原因就是第二行代码运行时会临时创建一个 String 对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的 String 对象,但这个对象没有 color 属性

​ 可以显式地使用 Boolean、Number 和 String 构造函数创建原始值包装对象。不过应该在确实必要时再这么做,否则容易让开发者疑惑分不清它们到底是原始值还是引用值。在原始值包装类型的实例调用 typeof 会返回”object”,所有原始值包装对象都会转换为布尔值 true

​ 另外,Object 构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。比如:

let obj = new Object("some text"); 
console.log(obj instanceof String); // true

​ 如果传给 Object 的是字符串,则会创建一个 String 的实例。如果是数值,则会创建 Number 的 实例。布尔值则会得到 Boolean 的实例。

​ 注意,使用 new 调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:

let value = "25";
let number = Number(value); // 转型函数
console.log(typeof number);  // "number"
let obj = new Number(value);  // 构造函数
console.log(typeof obj); // "object"

​ 在这个例子中,变量 number 中保存的是一个值为 25 的原始数值,而变量 obj 中保存的是一个 Number 的实例

​ 虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。每个原始值包装类型都有相应的一套方法方便数据操作

Boolean

​ Boolean 是对应布尔值的引用类型。要创建一个 Boolean 对象,就使用 Boolean 构造函数并传入 true 或 false,如下例所示:

let booleanObject = new Boolean(true);

​ Boolean 的实例会重写 valueOf()方法,返回一个原始值 true 或 falsetoString()方法被调用时也会被覆盖,返回字符串”true”或”false”。不过,Boolean 对象在 ECMAScript 中用得很少。 不仅如此,它们还容易引起误会,尤其是在布尔表达式中使用 Boolean 对象时,比如:

let falseObject = new Boolean(false);
let result = falseObject && true;
console.log(result); // true
let falseValue = false;
result = falseValue && true;
console.log(result); // false

​ 在这段代码中,我们创建一个值为 false 的 Boolean 对象。然后,在一个布尔表达式中通过&&操作将这个对象与一个原始值 true 组合起来。在布尔算术中,false && true 等于 false。可是,这 个表达式是对 falseObject 对象而不是对它表示的值(false)求值。前面刚刚说过,所有对象在布尔表达式中都会自动转换为 true,因此 falseObject 在这个表达式里实际上表示一个 true 值。那么 true && true 当然是 true。

​ 除此之外,原始值和引用值(Boolean 对象)还有几个区别。首先,typeof 操作符对原始值返回 “boolean”,但对引用值返回”object”。同样,Boolean 对象是 Boolean 类型的实例,在使用 instaceof 操作符时返回 true,但对原始值则返回 false,如下所示:

console.log(typeof falseObject); // object
console.log(typeof falseValue); // boolean
console.log(falseObject instanceof Boolean); // true
console.log(falseValue instanceof Boolean); // false

​ 理解原始布尔值Boolean 对象之间的区别非常重要,强烈建议永远不要使用后者

Number

​ Number 是对应数值的引用类型。要创建一个 Number 对象,就使用 Number 构造函数并传入一个 数值,如下例所示:

let numberObject = new Number(10);

​ 与 Boolean 类型一样,Number 类型重写了 valueOf()、toLocaleString()和 toString()方 法。valueOf()方法返回 Number 对象表示的原始数值,另外两个方法返回数值字符串。toString() 方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:

let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

​ 除了继承的方法,Number 类型还提供了几个用于将数值格式化为字符串的方法。

​ toFixed()方法返回包含指定小数点位数的数值字符串,如:

let num = 10;
console.log(num.toFixed(2)); // "10.00"

​ 这里的 toFixed()方法接收了参数 2,表示返回的数值字符串要包含两位小数。结果返回值为 “10.00”,小数位填充了 0。如果数值本身的小数位超过了参数指定的位数,则四舍五入到最接近的小数位:

let num = 10.005;
console.log(num.toFixed(2)); // "10.01"

​ toFixed()自动舍入的特点可以用于处理货币。不过要注意的是,多个浮点数值的数学计算不一定 得到精确的结果。比如,0.1 + 0.2 = 0.30000000000000004。

注意 toFixed()方法可以表示有 0~20 个小数位的数值。某些浏览器可能支持更大的范围,但这是通常被支持的范围。

​ 另一个用于格式化数值的方法是 toExponential(),返回以科学记数法(也称为指数记数法)表示的数值字符串。与 toFixed()一样,toExponential()也接收一个参数,表示结果中小数的位数。 来看下面的例子:

let num = 10;
console.log(num.toExponential(1)); // "1.0e+1"

​ 这段代码的输出为”1.0e+1”。一般来说,这么小的数不用表示为科学记数法形式。如果想得到数值最适当的形式,那么可以使用 toPrecision()。

toPrecision()方法会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。来看几个例子:

let num = 99;
console.log(num.toPrecision(1)); // "1e+2" 
console.log(num.toPrecision(2)); // "99" 
console.log(num.toPrecision(3)); // "99.0"

​ 在这个例子中,首先要用 1 位数字表示数值 99,得到”1e+2”,也就是 100。因为 99 不能只用 1 位数字来精确表示,所以这个方法就将它舍入为 100,这样就可以只用 1 位数字(及其科学记数法形式) 来表示了。用 2 位数字表示 99 得到”99”,用 3 位数字则是”99.0”。本质上,toPrecision()方法会根据数值和精度来决定调用 toFixed()还是 toExponential()。为了以正确的小数位精确表示数值, 这 3 个方法都会向上或向下舍入

注意 toPrecision()方法可以表示带 1~21 个小数位的数值。某些浏览器可能支持更大的范围,但这是通常被支持的范围。

​ 与 Boolean 对象类似,Number 对象也为数值提供了重要能力。但是,考虑到两者存在同样的潜在问题,因此并不建议直接实例化 Number 对象。在处理原始数值和引用数值时,typeof 和 instacnceof 操作符会返回不同的结果,如下所示:

let numberObject = new Number(10);
let numberValue = 10;
console.log(typeof numberObject); // "object"
console.log(typeof numberValue); // "number"
console.log(numberObject instanceof Number); // true
console.log(numberValue instanceof Number); // false

​ 原始数值在调用 typeof 时始终返回”number”,而 Number 对象则返回”object”。类似地,Number 对象是 Number 类型的实例,而原始数值不是

​ **isInteger()**方法与安全整数

​ ES6 新增了 Number.isInteger()方法,用于辨别一个数值是否保存为整数。有时候,小数位的 0可能会让人误以为数值是一个浮点值:

console.log(Number.isInteger(1)); // true
console.log(Number.isInteger(1.0)); // true
console.log(Number.isInteger(1.01)); // false

​ IEEE 754 数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个数值范围从 Number.MIN_SAFE_INTEGER(-2^53 + 1)到 Number.MAX_SAFE_INTEGER(2^53- 1)。对超出这个范围的数值,即使尝试保存为整数,IEEE 754 编码格式也意味着二进制值可能会表示一个完全不同的数值。为了鉴别整数是否在这个范围内,可以使用 Number.isSafeInteger()方法:

console.log(Number.isSafeInteger(-1 * 2 ** 53)); // false
console.log(Number.isSafeInteger(-1 * 2 ** 53 + 1)); // true
console.log(Number.isSafeInteger(2 ** 53)); // false
console.log(Number.isSafeInteger(2 ** 53 - 1)); // true