# 基本引用类型

在 ECMAScript 中,引用类型是把数据和功能组织到一起的结构,经常被人错误地称作“类”。

虽然从技术上讲 JavaScript 是一门面向对象语言,但ECMAScript 缺少传统的面向对象编程语言所具备的某些基本结构,包括类和接口。引用类型有时候也被称为对象定义,因为它们描述了自己的对象应有的属性和方法。

注意

引用类型虽然有点像类,但跟类并不是一个概念。为避免混淆,后面不会使用术语“类”。

对象被认为是某个特定引用类型的实例。新对象通过使用 new 操作符后跟一个构造函数(constructor)来创建。构造函数就是用来创建新对象的函数。

let now = new Date(); 
1

这行代码创建了引用类型 Date 的一个新实例,并将它保存在变量 now 中。Date() 在这里就是构造函数,它负责创建一个只有默认属性和方法的简单对象。ECMAScript 提供了很多像 Date 这样的原生引用类型,帮助开发者实现常见的任务。

# Date

ECMAScript 的 Date 类型参考了 Java 早期版本中的 java.util.Date。为此,Date 类型将日期保存为自协调世界时(UTC,Universal Time Coordinated)时间 1970 年 1 月 1 日午夜(零时)至今所经过的毫秒数。这种格式也被叫做时间戳。

要创建日期对象,就使用 new 操作符来调用 Date 构造函数。

let now = new Date();
1

在不给 Date 构造函数传参数的情况下,创建的对象将保存当前日期和时间。要基于其他日期和时间创建日期对象,必须传入其毫秒表示(时间戳)。ECMAScript为此提供了两个辅助方法:Date.parse()Date.UTC()

  • Date.parse()

Date.parse() 方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数

  • “月/日/年”,如 "5/23/2019"
  • “月名 日, 年”,如 "May 23, 2019"
  • “周几 月名 日 年 时:分:秒 时区”,如 "Tue May 23 2019 00:00:00 GMT-0700"
  • ISO 8601 扩展格式 “YYYY-MM-DDTHH:mm:ss.sssZ” ,如 2019-05-23T00:00:00(只适用于兼容 ES5 的实现)。

比如,要创建一个表示“2019 年 5 月 23 日”的日期对象。

let someDate = new Date(Date.parse("May 23, 2019"));
1

如果传给 Date.parse() 的字符串并不表示日期,则该方法会返回 NaN。如果直接把表示日期的字符串传给 Date 构造函数,那么 Date 会在后台调用 Date.parse()。换句话说,下面这行代码跟前面那行代码是等价的。

let someDate = new Date("May 23, 2019");
1
  • Date.UTC()

Date.UTC() 方法也返回日期的毫秒表示,但使用的是跟 Date.parse() 不同的信息来生成这个值。传给 Date.UTC() 的参数是年、零起点月数(1 月是 0,2 月是 1,以此类推)、日(1~31)、时(0~23)、分、秒和毫秒。这些参数中,只有前两个(年和月)是必需的。如果不提供日,那么默认为 1 日。其他参数的默认值都是 0。

// GMT 时间 2000 年 1 月 1 日零点
let y2k = new Date(Date.UTC(2000, 0));

// GMT 时间 2005 年 5 月 5 日下午 5 点 55 分 55 秒
let allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));
1
2
3
4
5

Date.parse() 一样,Date.UTC() 也会被 Date 构造函数隐式调用,但有一个区别:这种情况下创建的是本地日期,不是 GMT 日期。

// 本地时间 2000 年 1 月 1 日零点
let y2k = new Date(2000, 0);

// 本地时间 2005 年 5 月 5 日下午 5 点 55 分 55 秒
let allFives = new Date(2005, 4, 5, 17, 55, 55);
1
2
3
4
5
  • Date.now()

ECMAScript 还提供了 Date.now() 方法,返回表示方法执行时日期和时间的毫秒数。这个方法可以方便地用在代码分析中。

// 起始时间
let start = Date.now();

// 调用函数
doSomething();

// 结束时间
let stop = Date.now(),
result = stop - start;
1
2
3
4
5
6
7
8
9

# 继承的方法

Date 类型重写了 toLocaleString()toString()valueOf() 方法。

Date 类型的 toLocaleString() 方法返回与浏览器运行的本地环境一致的日期和时间。这通常意味着格式中包含针对时间的 AM(上午)或 PM(下午),但不包含时区信息(具体格式可能因浏览器而不同)。toString() 方法通常返回带时区信息的日期和时间,而时间也是以 24 小时制(0~23)表示的。

toLocaleString() - 2/1/2019 12:00:00 AM

toString() - Thu Feb 1 2019 00:00:00 GMT-0800 (Pacific Standard Time)
1
2
3

Date 类型的 valueOf() 方法根本就不返回字符串,这个方法被重写后返回的是日期的毫秒表示。因此,操作符(如小于号和大于号)可以直接使用它返回的值。

let date1 = new Date(2019, 0, 1); // 2019 年 1 月 1 日
let date2 = new Date(2019, 1, 1); // 2019 年 2 月 1 日

console.log(date1 < date2); // true
console.log(date1 > date2); // false 
1
2
3
4
5

# 日期格式化方法

Date 类型有几个专门用于格式化日期的方法,它们都会返回字符串

  • toDateString() 显示日期中的周几、月、日、年(格式特定于实现)
  • toTimeString() 显示日期中的时、分、秒和时区(格式特定于实现)
  • toLocaleDateString() 显示日期中的周几、月、日、年(格式特定于实现和地区)
  • toLocaleTimeString() 显示日期中的时、分、秒(格式特定于实现和地区)
  • toUTCString() 显示完整的 UTC 日期(格式特定于实现)

这些方法的输出与 toLocaleString()toString() 一样,会因浏览器而异。因此不能用于在用户界面上一致地显示日期。

# 日期/时间组件方法

Date 类型剩下的方法(见下表)直接涉及取得或设置日期值的特定部分。注意表中“UTC 日期”,指的是没有时区偏移时的日期。

方法 说明
getTime() 返回日期的毫秒表示;与 valueOf() 相同
setTime(milliseconds) 设置日期的毫秒表示,从而修改整个日期
getFullYear() 返回 4 位数年(即 2019 而不是 19)
getUTCFullYear() 返回 UTC 日期的 4 位数年
setFullYear(year) 设置日期的年(year 必须是 4 位数)
setUTCFullYear(year) 设置 UTC 日期的年(year 必须是 4 位数)
getMonth() 返回日期的月(0 表示 1 月,11 表示 12 月)
getUTCMonth() 返回 UTC 日期的月(0 表示 1 月,11 表示 12 月)
setMonth(month) 设置日期的月(month 为大于 0 的数值,大于 11 加年)
setUTCMonth(month) 设置 UTC 日期的月(month 为大于 0 的数值,大于 11 加年)
getDate() 返回日期中的日(1~31)
getUTCDate() 返回 UTC 日期中的日(1~31)
setDate(date) 设置日期中的日(如果 date 大于该月天数,则加月)
setUTCDate(date) 设置 UTC 日期中的日(如果 date 大于该月天数,则加月)
getDay() 返回日期中表示周几的数值(0 表示周日,6 表示周六)
getUTCDay() 返回 UTC 日期中表示周几的数值(0 表示周日,6 表示周六)
getHours() 返回日期中的时(0~23)
getUTCHours() 返回 UTC 日期中的时(0~23)
setHours(hours) 设置日期中的时(如果 hours 大于 23,则加日)
setUTCHours(hours) 设置 UTC 日期中的时(如果 hours 大于 23,则加日)
getMinutes() 返回日期中的分(0~59)
getUTCMinutes() 返回 UTC 日期中的分(0~59)
setMinutes(minutes) 设置日期中的分(如果 minutes 大于 59,则加时)
setUTCMinutes(minutes) 设置 UTC 日期中的分(如果 minutes 大于 59,则加时)
getSeconds() 返回日期中的秒(0~59)
getUTCSeconds() 返回 UTC 日期中的秒(0~59)
setSeconds(seconds) 设置日期中的秒(如果 seconds 大于 59,则加分)
setUTCSeconds(seconds) 设置 UTC 日期中的秒(如果 seconds 大于 59,则加分)
getMilliseconds() 返回日期中的毫秒
getUTCMilliseconds() 返回 UTC 日期中的毫秒
setMilliseconds(milliseconds) 设置日期中的毫秒
setUTCMilliseconds(milliseconds) 设置 UTC 日期中的毫秒
getTimezoneOffset() 返回以分钟计的 UTC 与本地时区的偏移量(如美国 EST 即“东部标准时间”返回 300,进入夏令时的地区可能有所差异)

# RegExp

ECMAScript 通过 RegExp 类型支持正则表达式。正则表达式使用类似 Perl 的简洁语法来创建。

还没学正则,以后再来看......

# 原始值包装类型

为了方便操作原始值,ECMAScript 提供了 3 种特殊的引用类型:BooleanNumberString。这些类型具有本章介绍的其他引用类型一样的特点,但也具有与各自原始类型对应的特殊行为。

let A = "some text";
let B = A.substring(2); 
1
2

在这里,A 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 A 上调用了 substring() 方法,并把结果保存在 B 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。

具体来说,当第二行访问 A 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:

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

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

let A = new String("some text");
let B = A.substring(2);
A = null;
1
2
3

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

引用类型与原始值包装类型的主要区别在于对象的生命周期。在通过 new 实例化引用类型后,得到的实例会在离开作用域时被销毁,而自动创建的原始值包装对象则只存在于访问它的那行代码执行期间。

这意味着不能在运行时给原始值添加属性和方法。

let A = "some text";
A.color = "red";
console.log(A.color); // undefined
1
2
3

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

可以显式地使用 BooleanNumberString 构造函数创建原始值包装对象。不过应该在确实必要时再这么做,否则容易让开发者疑惑,分不清它们到底是原始值还是引用值。在原始值包装类型的实例上调用 typeof 会返回 "object"

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

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

注意

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

let value = "25";

let number = Number(value);  // 转型函数
console.log(typeof number);  // "number"

let obj = new Number(value); // 构造函数
console.log(typeof obj);     // "object"
1
2
3
4
5
6
7

虽然不推荐显式创建原始值包装类型的实例,但它们对于操作原始值的功能是很重要的。

每个原始值包装类型都有相应的一套方法来方便数据操作。

# Boolean

Boolean 是对应布尔值的引用类型。要创建一个 Boolean 对象,就使用 Boolean 构造函数并传入 truefalse

let booleanObject = new Boolean(true); 
1

啥也不用记

应该没人会用这种东西。知道 new Boolean(true) 后获得的是一个对象就行了。

# Number

Number 是对应数值的引用类型。要创建一个 Number 对象,就使用 Number 构造函数并传入一个数值。

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

let numberObject = new Number(10);

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"
1
2
3
4
5
6
7
8

接下来是两个比较有用的方法。

  1. toFixed() 方法返回包含指定小数点位数的数值字符串
let num = 10.005;
console.log(num.toFixed(2)); // "10.01" 
1
2
  1. toExponential() 返回以科学记数法(也称为指数记数法)表示的数值字符串。
let num = 10;
console.log(num.toExponential(1)); // "1.0e+1"
1
2

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

console.log(Number.isInteger(1));     // true
console.log(Number.isInteger(1.00));  // true
console.log(Number.isInteger(1.01));  // false
1
2
3

IEEE 754 数值格式有一个特殊的数值范围,在这个范围内二进制值可以表示一个整数值。这个数值范围从 Number.MIN_SAFE_INTEGER253+1-2^{53} + 1 )到 Number.MAX_SAFE_INTEGER25312^{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
1
2
3
4
5

# String

正常用法和上面一样,3个继承的方法 valueOf()toLocaleString()toString() 都返回对象的原始字符串值。每个 String 对象都有一个 length 属性,表示字符串中字符的数量。

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

let stringValue = "hello world";
console.log(stringValue.length); // "11"
1
2
3
4

注意

即使字符串中包含双字节字符(而不是单字节的 ASCII 字符),也仍然会按单字符来计数。

  1. JavaScript中的字符

JavaScript 字符串由多个 16 位码元(code unit)组成。对多数字符来说,每 16 位码元对应一个字符。

所以,字符串的 length 属性表示字符串包含多少 16 位码元。

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

此外,charAt() 方法返回给定索引位置的字符,由传给方法的整数参数指定。

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

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

let message = "abcde";

// Unicode "Latin small letter C"的编码是 U+0063
console.log(message.charCodeAt(2)); // 99 
1
2
3
4

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"
1
2
3
4
5
6
7

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

fromCharCode() 方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个 Unicode 笑脸字符。

为正确解析既包含单码元字符又包含代理对字符的字符串,可以使用 codePointAt() 来代替 charCodeAt()

注意,如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点。

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

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

提示

所以下次遇到处理中文以及奇奇怪怪的字符时,回来翻翻就好了。

  1. normalize() 方法

有的时候,字符可能从屏幕显示上的效果来看是一模一样的,但是它对应的值是不一样的。

let A = String.fromCharCode(0x00C5),
    B = String.fromCharCode(0x212B),
    C = String.fromCharCode(0x0041, 0x030A);

console.log(A, B, C); // Å, Å, Å

console.log(A === B); // false
console.log(A === C); // false
console.log(B === C); // false 
1
2
3
4
5
6
7
8
9

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

let A = String.fromCharCode(0x00C5);

// U+00C5 是对 0+212B 进行 NFC/NFKC 规范化之后的结果
console.log(A === A.normalize("NFD"));  // false
console.log(A === A.normalize("NFC"));  // true
console.log(A === A.normalize("NFKD")); // false
console.log(A === A.normalize("NFKC")); // true 

// 略... 
1
2
3
4
5
6
7
8
9
  1. 字符串的操作方法

提示

非常使用且非常重要的部分。

  • concat()

用于将一个或多个字符串拼接成一个新字符串。

let stringValue = "hello";
let result = stringValue.concat("world");

console.log(result);      // "helloworld"
console.log(stringValue); // "hello" 
1
2
3
4
5

在这个例子中,对 stringValue 调 用 concat() 方法的结果是得到 "hello world", 但 stringValue 的值保持不变。

而且 concat() 方法可以接收任意多个参数,因此可以一次性拼接多个字符串。不过正常人应该不会用这个方法,用 + 号更加省事。

let stringValue = "A";
let result = stringValue.concat("B", "C");
console.log(result); // "ABC"
1
2
3
  • 裁剪子串

ECMAScript 提供了 3 个从字符串中提取子字符串的方法:slice()substr()substring()

这3个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。

第一个参数表示子字符串开始的位置,第二个参数表示子字符串结束的位置。

slice()substring() 而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对 substr() 而言,第二个参数表示返回的子字符串数量。

任何情况下,省略第二个参数都意味着提取到字符串末尾。与 concat() 方法一样,slice()substr()substring() 也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。

let stringValue = "ABCDEFGHIJK";

console.log(stringValue.slice(3));        // "DEFGHIJK"
console.log(stringValue.substring(3));    // "DEFGHIJK"
console.log(stringValue.substr(3));       // "DEFGHIJK"
console.log(stringValue.slice(3, 7));     // "DEFG"
console.log(stringValue.substring(3,7));  // "DEFG"
console.log(stringValue.substr(3, 7));    // "DEFGHIJ" 
1
2
3
4
5
6
7
8

当某个参数是负值时,这 3 个方法的行为又有不同。

slice() 方法将所有负值参数都当成字符串长度加上负参数值

substr() 方法将第一个负参数值当成字符串长度加上该值,将第二个负参数值转换为 0

substring() 方法会将所有负参数值都转换为 0

let stringValue = "ABCDEFGHIJK";
console.log(stringValue.slice(-3));        // "IJK"
console.log(stringValue.substring(-3));    // "ABCDEFGHIJK"
console.log(stringValue.substr(-3));       // "IJK"
console.log(stringValue.slice(3, -4));     // "DEFGH"
console.log(stringValue.substring(3, -4)); // "ABC"
console.log(stringValue.substr(3, -4));    // "" (empty string)
1
2
3
4
5
6
7

这里有个坑:调用 substring(3, 0),等价于 substring(0, 3),这是因为这个方法会将较小的参数作为起点,将较大的参数作为终点。

  1. 寻找字串起始下标方法

有两个方法用于在字符串中定位子字符串:indexOf()lastIndexOf()。这两个方法从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回 -1)。两者的区别在于,indexOf() 方法从字符串开头开始查找子字符串,而 lastIndexOf() 方法从字符串末尾开始查找子字符串。

let stringValue = "hello world";
console.log(stringValue.indexOf("o"));     // 4
console.log(stringValue.lastIndexOf("o")); // 7
1
2
3

这两个方法都可以接收可选的第二个参数,表示开始搜索的位置。这意味着,indexOf() 会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;lastIndexOf() 则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。

let stringValue = "hello world";
console.log(stringValue.indexOf("o", 6));     // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4 
1
2
3
  1. 字符串包含方法

ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:startsWith()endsWith()includes()。这些方法都会从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值。

let message = "ABCDEFGHIJK";

console.log(message.startsWith("ABC")); // true
console.log(message.startsWith("DEF")); // false

console.log(message.endsWith("IJK"));   // true
console.log(message.endsWith("DEF"));   // false

console.log(message.includes("DEF"));   // true
console.log(message.includes("AAA"));   // false
1
2
3
4
5
6
7
8
9
10

startsWith()includes() 方法接收可选的第二个参数,表示开始搜索的位置。如果传入第二个参数,则意味着这两个方法会从指定位置向着字符串末尾搜索,忽略该位置之前的所有字符。endsWith() 是逆向的 startsWith(),同理。

  1. 过滤前后空格

trim() 这个方法会创建字符串的一个副本,删除前、后所有空格符。

let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue);        // " hello world "
console.log(trimmedStringValue); // "hello world" 
1
2
3
4

另外,trimLeft()trimRight() 方法分别用于从字符串开始和末尾清理空格符。

  1. 复读机

ECMAScript 在所有字符串上都提供了 repeat() 方法。这个方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。

let stringValue = "na ";
console.log(stringValue.repeat(16) + "batman");
// na na na na na na na na na na na na na na na na batman
1
2
3
  1. 字符串补全

padStart()padEnd() 方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。这两个方法的第一个参数是长度,第二个参数是可选的填充字符串,默认为空格。

let stringValue = "foo";

console.log(stringValue.padStart(6));       // " foo"
console.log(stringValue.padStart(9, "."));  // "......foo"

console.log(stringValue.padEnd(6));         // "foo "
console.log(stringValue.padEnd(9, "."));    // "foo......"
1
2
3
4
5
6
7

可选的第二个参数并不限于一个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度。此外,如果长度小于或等于字符串长度,则会返回原始字符串。

let stringValue = "foo";

console.log(stringValue.padStart(8, "bar")); // "barbafoo"
console.log(stringValue.padStart(2));        // "foo"

console.log(stringValue.padEnd(8, "bar"));   // "foobarba"
console.log(stringValue.padEnd(2));          // "foo" 
1
2
3
4
5
6
7
  1. 字符串迭代与解构

字符串的原型上暴露了一个 @@iterator 方法,表示可以迭代字符串的每个字符。可以像下面这样手动使用迭代器:

let message = "abc";
let stringIterator = message[Symbol.iterator]();

console.log(stringIterator.next()); // {value: "a", done: false}
console.log(stringIterator.next()); // {value: "b", done: false}
console.log(stringIterator.next()); // {value: "c", done: false}
console.log(stringIterator.next()); // {value: undefined, done: true} 
1
2
3
4
5
6
7

在 for-of 循环中可以通过这个迭代器按序访问每个字符:

for (const char of "abcde") {
    console.log(char);
}
1
2
3

有了这个迭代器之后,字符串就可以通过解构操作符来解构了。比如,可以更方便地把字符串分割为字符数组:

let message = "abcde";

console.log([...message]); 
// ["a", "b", "c", "d", "e"] 
1
2
3
4
  1. 字符串大小写转化

大小写转换,包括 4 个方法:toLowerCase()toLocaleLowerCase()toUpperCase()toLocaleUpperCase()

toLowerCase()toUpperCase() 方法是原来就有的方法,与 java.lang.String 中的方法同名。toLocaleLowerCase()toLocaleUpperCase() 方法旨在基于特定地区实现。在很多地区,地区特定的方法与通用的方法是一样的。但在少数语言中(如土耳其语),Unicode 大小写转换需应用特殊规则,要使用地区特定的方法才能实现正确转换。

let stringValue = "hello world";

console.log(stringValue.toLocaleUpperCase()); // "HELLO WORLD"
console.log(stringValue.toUpperCase()); // "HELLO WORLD"

console.log(stringValue.toLocaleLowerCase()); // "hello world"
console.log(stringValue.toLowerCase()); // "hello world"
1
2
3
4
5
6
7
  1. 字符串模式匹配方法

涉及正则的,以后再来补.......

# 单例内置对象

ECMA-262 对内置对象的定义是“任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript程序开始执行时就存在的对象”。

ObjectArrayStringGlobalMath 等都是,这次先介绍两个。

# Global

Global 对象是 ECMAScript 中最特别的对象,因为代码不会显式地访问它。ECMA-262 规定 Global 对象为一种兜底对象,它所针对的是不属于任何对象的属性和方法。

事实上,不存在全局变量或全局函数这种东西。在全局作用域中定义的变量和函数都会变成 Global 对象的属性。前面介绍的函数,包括 isNaN()isFinite()parseInt()parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法。

  1. URL 编码方法

encodeURI()encodeURIComponent() 方法用于编码统一资源标识符(URI),以便传给浏览器。有效的 URI 不能包含某些字符,比如空格。使用 URI 编码方法来编码 URI 可以让浏览器能够理解它们,同时又以特殊的 UTF-8 编码替换掉所有无效字符。

ecnodeURI() 方法用于对整个 URI 进行编码,比如 "www.wrox.com/illegal value.js"。而 encodeURIComponent() 方法用于编码 URI 中单独的组件,比如前面 URL 中的 "illegal value.js"

这两个方法的主要区别是,encodeURI() 不会编码属于 URL 组件的特殊字符,比如冒号、斜杠、问号、井号,而 encodeURIComponent() 会编码它发现的所有非标准字符。

let uri = "http://www.wrox.com/illegal value.js#start";

// "http://www.wrox.com/illegal%20value.js#start"
console.log(encodeURI(uri));

// "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
console.log(encodeURIComponent(uri));
1
2
3
4
5
6
7

这里使用 encodeURI() 编码后,除空格被替换为 %20 之外,没有任何变化。而 encodeURIComponent() 方法将所有非字母字符都替换成了相应的编码形式。

注意

一般来说,使用 encodeURIComponent() 应该比使用 encodeURI() 的频率更高,这是因为编码查询字符串参数比编码基准 URI 的次数更多。

encodeURI()encodeURIComponent() 相对的是 decodeURI()decodeURIComponent()

let uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start";

// http%3A%2F%2Fwww.wrox.com%2Fillegal value.js%23start
console.log(decodeURI(uri));

// http:// www.wrox.com/illegal value.js#start
console.log(decodeURIComponent(uri)); 
1
2
3
4
5
6
7
  1. eval() 方法

最后一个方法可能是整个 ECMAScript 语言中最强大的了,它就是 eval()。这个方法就是一个完整的 ECMAScript 解释器,它接收一个参数,即一个要执行的 ECMAScript(JavaScript)字符串。

当解释器发现 eval() 调用时,会将参数解释为实际的 ECMAScript 语句,然后将其插入到该位置。通过 eval() 执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。这意味着定义在包含上下文中的变量可以在 eval() 调用内部被引用。

let msg = "hello world"; 
eval("console.log(msg)");  // "hello world"
1
2

类似地,可以在 eval() 内部定义一个函数或变量,然后在外部代码中引用。

eval("function sayHi() { console.log('hi'); }"); 
sayHi();
1
2

通过 eval() 定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。它们只是在 eval() 执行的时候才会被创建。 在严格模式下,在 eval() 内部创建的变量和函数无法被外部访问。换句话说,最后两个例子会报错。同样,在严格模式下,赋值给 eval 也会导致错误。

"use strict"; 
eval = "hi";  // 导致错误
1
2

警告

解释代码字符串的能力是非常强大的,但也非常危险。在使用 eval() 的时候必须极为慎重,特别是在解释用户输入的内容时。因为这个方法会对 XSS 利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。

  1. Global 对象属性

Global 对象有很多属性,其中一些前面已经提到过了。像 undefinedNaNInfinity 等特殊值都是 Global 对象的属性。此外,所有原生引用类型构造函数,比如 ObjectFunction,也都是 Global 对象的属性。下表列出了所有这些属性。

  1. window 对象

虽然 ECMA-262 没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性。

var color = "red"; 
 
function sayColor() { 
  console.log(window.color); 
} 
 
window.sayColor(); // "red"
1
2
3
4
5
6
7

# Math

ECMAScript 提供了 Math 对象作为保存数学公式、信息和计算的地方。Math 对象提供了一些辅助计算的属性和方法。

提示

Math 对象上提供的计算要比直接在 JavaScript 实现的快得多,因为 Math 对象上的计算使用了 JavaScript 引擎中更高效的实现和处理器指令。但使用 Math 计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。

  1. Math 对象属性

Math 对象有一些属性,主要用于保存数学中的一些特殊值。

属性 说明
Math.E 自然对数的基数 e 的值
Math.LN10 10 为底的自然对数
Math.LN2 2 为底的自然对数
Math.LOG2E 以 2 为底 e 的对数
Math.LOG10E 以 10 为底 e 的对数
Math.PI π 的值
Math.SQRT1_2 1/2 的平方根
Math.SQRT2 2 的平方根
  1. min()max() 方法

min()max() 方法用于确定一组数值中的最小值和最大值。这两个方法都接收任意多个参数。

let max = Math.max(3, 54, 32, 16); 
console.log(max);  // 54 
 
let min = Math.min(3, 54, 32, 16); 
console.log(min);  // 3
1
2
3
4
5

使用这两个方法可以避免使用额外的循环和 if 语句来确定一组数值的最大最小值。

要知道数组中的最大值和最小值,可以像下面这样使用扩展操作符。

let values = [1, 2, 3, 4, 5, 6, 7, 8]; 
let max = Math.max(...val);
1
2
  1. 舍入方法

接下来是用于把小数值舍入为整数的 4 个方法:Math.ceil()Math.floor()Math.round()Math.fround()

  • Math.ceil() 方法始终向上舍入为最接近的整数。
  • Math.floor() 方法始终向下舍入为最接近的整数。
  • Math.round() 方法执行四舍五入,返回一个整数。
  • Math.fround() 方法返回数值最接近的单精度(32 位)浮点值表示。
console.log(Math.ceil(25.9));   // 26 
console.log(Math.ceil(25.5));   // 26 
console.log(Math.ceil(25.1));   // 26 
 
console.log(Math.floor(25.9));  // 25 
console.log(Math.floor(25.5));  // 25 
console.log(Math.floor(25.1));  // 25

console.log(Math.round(25.9));  // 26 
console.log(Math.round(25.5));  // 26 
console.log(Math.round(25.1));  // 25 

console.log(Math.fround(0.4));  // 0.4000000059604645 
console.log(Math.fround(0.5));  // 0.5 
console.log(Math.fround(25.9)); // 25.899999618530273 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. random() 方法

Math.random() 方法返回一个 0~1 范围内的随机数,其中包含 0 但不包含 1。对于希望显示随机名 言或随机新闻的网页,这个方法是非常方便的。

如果想从 1~10 范围内随机选择一个数,代码就是这样的:

let num = Math.floor(Math.random() * 10 + 1);
1

这样就有 10 个可能的值(1~10),其中最小的值是 1。如果想选择一个 2~10 范围内的值,则代码就要写成这样:

let num = Math.floor(Math.random() * 9 + 2);
1

注意

Math.random() 方法在这里出于演示目的是没有问题的。如果是为了加密而需要生成随机数(传给生成器的输入需要较高的不确定性),那么建议使用 window.crypto.getRandomValues()

  1. 其他方法

Math 对象还有很多涉及各种简单或高阶数运算的方法。

方法 说明
Math.abs(x) 返回 x 的绝对值
Math.exp(x) 返回 Math.E 的 x 次幂
Math.expm1(x) 等于 Math.exp(x) - 1
Math.log(x) 返回 x 的自然对数
Math.log1p(x) 等于 1 + Math.log(x)
Math.pow(x, power) 返回 x 的 power 次幂
Math.hypot(...nums) 返回 nums 中每个数平方和的平方根
Math.clz32(x) 返回 32 位整数 x 的前置零的数量
Math.sign(x) 返回表示 x 符号的 1、0、-0 或-1
Math.trunc(x) 返回 x 的整数部分,删除所有小数
Math.sqrt(x) 返回 x 的平方根
Math.cbrt(x) 返回 x 的立方根
Math.acos(x) 返回 x 的反余弦
Math.acosh(x) 返回 x 的反双曲余弦
Math.asin(x) 返回 x 的反正弦
Math.asinh(x) 返回 x 的反双曲正弦
Math.atan(x) 返回 x 的反正切
Math.atanh(x) 返回 x 的反双曲正切
Math.atan2(y, x) 返回 y/x 的反正切
Math.cos(x) 返回 x 的余弦
Math.sin(x) 返回 x 的正弦
Math.tan(x) 返回 x 的正切

# 小结

JavaScript 中的对象称为引用值,几种内置的引用类型可用于创建特定类型的对象。

  • 引用值与传统面向对象编程语言中的类相似,但实现不同。
  • Date 类型提供关于日期和时间的信息,包括当前日期、时间及相关计算。
  • RegExp 类型是 ECMAScript 支持正则表达式的接口,提供了大多数基础的和部分高级的正则表达式功能。

JavaScript 比较独特的一点是,函数实际上是 Function 类型的实例,也就是说函数也是对象。因为函数也是对象,所以函数也有方法,可以用于增强其能力。

由于原始值包装类型的存在,JavaScript 中的原始值可以被当成对象来使用。有 3 种原始值包装类型:BooleanNumberString。它们都具备如下特点。

  • 每种包装类型都映射到同名的原始类型。
  • 以读模式访问原始值时,后台会实例化一个原始值包装类型的对象,借助这个对象可以操作相应的数据。
  • 涉及原始值的语句执行完毕后,包装对象就会被销毁。

当代码开始执行时,全局上下文中会存在两个内置对象:GlobalMath。其中,Global 对象在大多数 ECMAScript 实现中无法直接访问。不过,浏览器将其实现为 window 对象。所有全局变量和函数都是 Global 对象的属性。Math 对象包含辅助完成复杂计算的属性和方法。