当前位置: 代码迷 >> 综合 >> 《JavaScript高级程序设计》(第4版)阅读笔记(二十八)
  详细解决方案

《JavaScript高级程序设计》(第4版)阅读笔记(二十八)

热度:52   发布时间:2024-01-21 18:17:12.0

这篇博客继续分享《高程四》第五章的内容。

 

5.3.2 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 。(建议做小数的运算的时候,可以乘以100或者更大的数,把它们转成整数,进行整数的加减。计算完毕后再除以原来的倍数,还原为小数,因为整数的计算是精确的,小数的计算可能不精确。)

注意 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.00)); // true
console.log(Number.isInteger(1.01)); // false

EEE 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

 

5.3.3 String

 

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

let stringObject = new String("hello world");

String 对象的方法可以在所有字符串原始值上调用。3个继承的方法 valueOf() 、 toLcaleString() 和 toString() 都返回对象的原始字符串值。

每个 String 对象都有一个 length 属性,表示字符串中字符的数量。来看下面的例子:

let stringValue = "hello world";
console.log(stringValue.length); // "11"

这个例子输出了字符串 "hello world" 中包含的字符数量: 11 。注意,即使字符串中包含双字节字符(而不是单字节的ASCII字符),也仍然会按单字符来计数。

String 类型提供了很多方法来解析和操作字符串。
 


1. JavaScript字符

 

JavaScript字符串由16位码元(code unit)组成。对多数字符来说,每16位码元对应一个字符。换句话说,字符串的 length 属性表示字符串包含多少16位码元:

let message = "abcde";
console.log(message.length); // 5

此外, charAt() 方法返回给定索引位置的字符,由传给方法的整数参数指定。具体来说,这个方法查找指定索引位置的16位码元,并返回该码元对应的字符:

let message = "abcde";
console.log(message.charAt(2)); // "c"

JavaScript字符串使用了两种Unicode编码混合的策略:UCS-2和UTF-16。对于可以采用16位编码的字符(U+0000~U+FFFF),这两种编码实际上是一样的。

使用 charCodeAt() 方法可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。比如:

let message = "abcde";// Unicode "Latin small letter C"的编码是U+0063
console.log(message.charCodeAt(2)); // 99
// 十进制99等于十六进制63
console.log(99 === 0x63); // true

fromCharCode() 方法用于根据给定的UTF-16码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:

// Unicode "Latin small letter A"的编码是U+0061
// Unicode "Latin small letter B"的编码是U+0062
// Unicode "Latin small letter C"的编码是U+0063
// Unicode "Latin small letter D"的编码是U+0064
// Unicode "Latin small letter E"的编码是U+0065
console.log(String.fromCharCode(0x61, 0x62, 0x63,0x64, 0x65)); // "abcde"
// 0x0061 === 97
// 0x0062 === 98
// 0x0063 === 99
// 0x0064 === 100
// 0x0065 === 101
console.log(String.fromCharCode(97, 98, 99, 100, 101)); // "abcde"

对于U+0000~U+FFFF范围内的字符, length 、 charAt() 、charCodeAt() 和 fromCharCode() 返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16位表示的,而这几个方法也都基于16位码元完成操作。只要字符编码大小与码元大小一一对应,这些方法就能如期工作。

这个对应关系在扩展到Unicode增补字符平面时就不成立了。问题很简单,即16位只能唯一表示65 536个字符。这对于大多数语言字符集是足够了,在Unicode中称为基本多语言平面(BMP)。为了表示更多的字符,Unicode采用了一个策略,即每个字符使用另外16位去选择一个增补平面。这种每个字符使用两个16位码元的策略称为代理对。在涉及增补平面的字符时,前面讨论的字符串方法就会出问题。比如,下面的例子中使用了一个笑脸表情符号,也就是一个使用代理对编码的字符:

// "smiling face with smiling eyes" 表情符号的编码是U+1F60A
// 0x1F60A === 128522
let message = "ab?de";
console.log(message.length); // 6
console.log(message.charAt(1)); // b
console.log(message.charAt(2)); // <?>
console.log(message.charAt(3)); // <?>
console.log(message.charAt(4)); // d
console.log(message.charCodeAt(1)); // 98
console.log(message.charCodeAt(2)); // 55357
console.log(message.charCodeAt(3)); // 56842
console.log(message.charCodeAt(4)); // 100
console.log(String.fromCodePoint(0x1F60A)); // ?
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab?de

这些方法仍然将16位码元当作一个字符,事实上索引2和索引3对应的码元应该被看成一个代理对,只对应一个字符。 fromCharCode() 方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个Unicode笑脸字符。为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用codePointAt() 来代替 charCodeAt() 。跟使用charCodeAt() 时类似, codePointAt() 接收16位码元的索引并返回该索引位置上的码点(code point)。码点是Unicode中一个字符的完整标识。比如, "c" 的码点是0x0063,而"?"的码点是0x1F60A。码点可能是16位,也可能是32位,而 codePointAt() 方法可以从指定码元位置识别完整的码点。

let message = "ab?de";
console.log(message.codePointAt(1)); // 98
console.log(message.codePointAt(2)); // 128522
console.log(message.codePointAt(3)); // 56842
console.log(message.codePointAt(4)); // 100

注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。(因为笑脸的字符是一个代理对,位置为2,3。3并不是代理对的开头,所以这里得到了错误的码点56822)这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点:

console.log([..."ab?de"]); // ["a", "b", "?", "d", "e"]

与 charCodeAt() 有对应的 codePointAt() 一样,fromCharCode() 也有一个对应的 fromCodePoint() 。这个方法接收任意数量的码点,返回对应字符拼接起来的字符串:

console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab?de
console.log(String.fromCodePoint(97, 98, 128522, 100, 101)); // ab?de

 

2. normalize() 方法
 

 

某些Unicode字符可以有多种编码方式。有的字符既可以通过一个BMP字符表示,也可以通过一个代理对表示。比如:

// U+00C5:上面带圆圈的大写拉丁字母A
console.log(String.fromCharCode(0x00C5));
// ?
// U+212B:长度单位“埃”
console.log(String.fromCharCode(0x212B));
// ?
// U+004:大写拉丁字母A
// U+030A:上面加个圆圈
console.log(String.fromCharCode(0x0041, 0x030A));
// ?

它们编码不同,但是看起来都一样。比较操作符不在乎字符看起来是什么样的,因此这3个字符互不相等。

let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1, a2, a3); // ?, ?, ?
console.log(a1 === a2); // false
console.log(a1 === a3); // false
console.log(a2 === a3); // false

为解决这个问题,Unicode提供了4种规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。这4种规范化形式是:NFD(Normalization Form D)、NFC(Normalization Form C)、NFKD(Normalization Form KD)和NFKC(Normalization Form KC)。可以使用 normalize() 方法对字符串应用上述规范化形式,使用时需要传入表示哪种形式的字符串: "NFD" 、 "NFC" 、 "NFKD" 或 "NFKC" 。
通过比较字符串与其调用 normalize() 的返回值,就可以知道该字符串是否已经规范化了:

let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
// U+00C5是对0+212B进行NFC/NFKC规范化之后的结果
console.log(a1 === a1.normalize("NFD")); //false
console.log(a1 === a1.normalize("NFC")); // true
console.log(a1 === a1.normalize("NFKD")); //false
console.log(a1 === a1.normalize("NFKC")); // true// U+212B是未规范化的
console.log(a2 === a2.normalize("NFD")); //false
console.log(a2 === a2.normalize("NFC")); //false
console.log(a2 === a2.normalize("NFKD")); //false
console.log(a2 === a2.normalize("NFKC")); //false
// U+0041/U+030A是对0+212B进行NFD/NFKD规范化之后的结果 
console.log(a3 === a3.normalize("NFD")); // true
console.log(a3 === a3.normalize("NFC")); //false
console.log(a3 === a3.normalize("NFKD")); // true
console.log(a3 === a3.normalize("NFKC")); //false

选择同一种规范化形式可以让比较操作符返回正确的结果:

let a1 = String.fromCharCode(0x00C5),
a2 = String.fromCharCode(0x212B),
a3 = String.fromCharCode(0x0041, 0x030A);
console.log(a1.normalize("NFD") === a2.normalize("NFD")); // true
console.log(a2.normalize("NFKC") === a3.normalize("NFKC")); // true
console.log(a1.normalize("NFC") === a3.normalize("NFC")); // true


 

 

 

 



 

  相关解决方案