JavaScript 数据类型

ECMAScript 有 6 种简单数据类型(也称为原始类型):UndefinedNullBooleanNumberStringSymbol。还有一种复杂数据类型叫 Object(对 象)。Object 是一种无序名值对的集合。

UndefinedNull

Undefined 类型只有一个值 undefined,当使用 letvar 声明变量单并没有初始化时,就相当于给变量赋予了 undefined 值。

一般来说不建议显式的给变量赋值 undefined,字面值 undefined 主要是用来比较。

1
2
3
4
5
let v1;
var v2;
console.log(v1); // undefined
console.log(v2); // undefined
console.log(v1 === v2); // true

增加这个特殊值的目的就是为了正式明确空对象指针 null 和未初始化变量的区别。

undefined 值是由 null 值派生而来的,因此 ECMA-262 将它们定义为表面上相等,如下所示:

1
2
console.log(null === undefined); // false
console.log(null == undefined); // true

Null 类型同样只有一个值,即特殊值 null。逻辑上讲,null 值表示一个空对象指针,这也是给 typeof 传一个 null 会返回 “object” 的原因。

1
2
let v3 = null;
console.log(typeof v3); // "object"

在定义将来要保存对象值的变量时,建议使用 null 来初始化,不要使用其他值。这样,只要检查这个变量的值是不是 null 就可以知道这个变量是否在后来被重新赋予了一个对象的引用。

undefinednull 都是假值,如果只是为了检测假值,可以方便的检测它们;如果是明确的想要检测 undefinednull,则需要显式的对其进行判断。

1
2
3
4
5
6
let v4;
let o1 = null;
console.log(!v4); // true
console.log(v4 === undefined); // true
console.log(!o1); // true
console.log(o1 === null); // true

Boolean

Boolean 类型是使用非常频繁的数据类型,它有两个值 truefalse (布尔值字面量是区分大小写的)。

这两个布尔值并不等同于数值:

1
2
3
4
let t1 = true;
let f1 = false;
console.log(t1 === 1); // false
console.log(f1 === 0); // false

虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数。

1
2
3
4
5
6
7
8
9
10
11
console.log(Boolean(true)); // true
console.log(Boolean(false)); // false
console.log(Boolean('true')); // true
console.log(Boolean('false')); // true
console.log(Boolean(0)); // false
console.log(Boolean(-0)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(1)); // true
console.log(Boolean(-1)); // true
console.log(Boolean('')); // false
console.log(Boolean(' ')); // true

Number

Number 类型使用 IEEE 754 格式表示整 数和浮点值(在某些语言中也叫双精度值)。不同的数值类型相应地也有不同的数值字面量格式。

image-20201223142823585

整数可以用八进制、十进制和十六进制字面量表示,但是使用八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值。

1
2
3
4
5
6
let octalNum = 077;
let hexNum = 0xff;
console.log(octalNum); // 63
console.log(hexNum); // 255
let sum = octalNum + hexNum;
console.log(sum); // 318

浮点值

要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但推荐加上。

1
2
let float1 = 1.01;
let float2 = .01;

因为存储浮点值使用的内存空间是存储整数值的两倍,所以 ECMAScript 总是想方设法把值转换为 整数。在小数点后面没有数字的情况下,数值就会变成整数。类似地,如果数值本身就是整数,只是小 数点后面跟着 0(如 1.0),那它也会被转换为整数。

对于非常大或非常小的数值,浮点值可以用科学记数法来表示。科学记数法用于表示一个应该乘以 10 的给定次幂的数值。ECMAScript 中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大 写或小写的字母 e,再加上一个要乘的 10 的多少次幂

1
2
3
4
let float3 = 2.23e9;
console.log(float3); //2230000000
let float4 = 1.01e-5;
console.log(float4); // 0.0000101

浮点值的精确度最高可达 17 位小数,但在算术计算中远不如整数精确。例如,0.1 加 0.2 得到的不 是 0.3,而是 0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。

1
2
3
let float5 = 0.1;
let float6 = 0.2;
console.log(float5 + float6); // 0.30000000000000004

之所以存在这种舍入错误,是因为使用了 IEEE 754 数值,这种错误并非 ECMAScript 所独有。其他使用相同格式的语言也有这个问题。

因此永远不要测试某个特定的浮点值。

使用最小精度值来比较浮点数:

1
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

值的范围

ECMAScript 可以表示的最小 数值保存在 Number.MIN_VALUE 中,这个值在多数浏览器中是 5e-324;可以表示的最大数值保存在 Number.MAX_VALUE 中,这个值在多数浏览器中是 1.7976931348623157e+308。如果某个计算得到的数值结果超出了 JavaScript 可以表示的范围,那么这个数值会被自动转换为一个特殊的 Infinity(无 穷)值。任何无法表示的负数以 -Infinity(负无穷大)表示,任何无法表示的正数以 Infinity(正 无穷大)表示。

1
2
console.log(Number.MIN_VALUE); // 5e-234
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308

如果计算返回正 Infinity 或负 Infinity , 则该值将不能再进一步用于任何计算。 这是因为 Infinity 没有可用于计算的数值表示形式。要确定一个值是不是有限大(即介于 JavaScript 能表示的最小值和最大值之间),可以使用 isFinite() 函数。

1
2
3
4
let num1 = 6e-325;
let num2 = 1.8e308;
console.log(Number.isFinite(num1)); // false
console.log(Number.isFinite(num2)); // false

Number.NEGATIVE_INFINITYNumber.POSITIVE_INFINITY 也可以获取正、负 Infinity。没错,这两个属性包含的值分别就是 -InfinityInfinity

NaN

有一个特殊的数值叫 NaN,意思是 “不是数值”(Not a Number),用于表示本来要返回数值的操作 失败了(而不是抛出错误)。比如,用 0 除任意数值在其他语言中通常都会导致错误,从而中止代码执 行。但在 ECMAScript 中,0、+0 或-0 相除会返回 NaN

1
2
3
4
console.log(Number.parseFloat('f1.f')); // NaN
console.log(1/0); // Infinity
console.log(0/1); // 0
console.log(0/0); // NaN

NaN 有几个独特的属性。首先,任何涉及 NaN 的操作始终返回 NaN(如 NaN/10),在连续多步计算 时这可能是个问题。其次,NaN 不等于包括 NaN 在内的任何值。

1
2
console.log(0/0 === NaN); // false
console.log(NaN === NaN); // false

要判断一个值是不是 NaN, 可以使用 isNaN() 函数。

1
2
console.log(Number.isNaN(0/0)); // true
console.log(Number.isNaN(true)); // false

数值转换

JavaScript 中提供了 Number()parseInt()parseFloat() 三个方法用来将非数值转换为数值。

Number
1
2
3
4
5
6
7
8
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(1000)); // 1000
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number('12')); // 12
console.log(Number(' 12')); // 12
console.log(Number('12-')); // NaN
parseInt()

parseInt() 函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt() 立即 返回 NaN。这意味着空字符串也会返回 NaN(这一点跟 Number() 不一样,它返回 0)。如果第一个字符 是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。

1
2
3
4
5
6
console.log(parseInt(100)); // 100
console.log(parseInt('1000')); // 1000
console.log(parseInt('1000lll')); // 1000
console.log(parseInt(' 1000')); // 1000
console.log(parseInt('001000')); // 1000
console.log(parseInt('a 1000')); // NaN

parseInt() 也接收第二个参数,用于指定底数(进制数)。

1
2
console.log(parseInt('077', 8)); // 63
console.log(parseInt('0xff', 16)); // 255

不传底数参数相当于让 parseInt() 自己决定如何解析,所以为避免解析出错,建议始终传给 它第二个参数。

parseFloat()

parseFloat() 函数的工作方式跟 parseInt() 函数类似,都是从位置 0 开始检测每个字符。同样, 它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。

1
2
3
4
5
6
7
console.log(parseFloat('-1.01')); // -1.01
console.log(parseFloat('1.01')); // 1.01
console.log(parseFloat('1.01.01')); // 1.01
console.log(parseFloat(' 1.01')); // 1.01
console.log(parseFloat('a1.01')); // NaN
console.log(parseFloat('1.01a')); // 1.01
console.log(parseFloat('00001.01a')); // 1.01

String

String(字符串)数据类型表示零或多个 16 位 Unicode 字符序列。

image-20201223160958920

ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量。

把值转换为字符串

使用 toString() 方法

nullundefined 没有 toString() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
let val1 = 12;
let val2= '12';
let val3 = [1,2];
let val4 = new Date();
let val5 = {};

// 12 12 1,2 Fri Dec 25 2020 13:48:14 GMT+0800 (China Standard Time) [object Object]
console.log(val1.toString(), val2.toString(), val3.toString(), val4.toString(), val5.toString());

null.toString(); // Uncaught TypeError: Cannot read property 'toString' of undefined

undefined.toString(); // Uncaught TypeError: Cannot read property 'toString' of null

在对数字类型值调用 toString() 方法时,可传递一个参数指定以什么底数输出数值的字符串表示。

1
2
3
4
5
6
let numVal = 123;

console.log(numVal.toString(2)); // 1111011
console.log(numVal.toString(8)); // 173
console.log(numVal.toString(10)); // 123
console.log(numVal.toString(16)); // 7b
使用 String()

在使用 String() 将值转换为字符串时,如果值有 toString() 方法,则直接调用 toString() 方法并返回结果。对于没有 toString() 方法的 nullundefined ,则会分别返回 “null” 和 “undefiend”。

1
2
console.log(String(null));
console.log(String(undefined));

Symbol

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一不可变的。 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。

1
2
3
4
let s1 = Symbol();
let s2 = Symbol('s2');
console.log(s1);
console.log(s2);

Object

ECMAScript 中的对象其实就是一组数据和功能的集合。对象通过 new 操作符后跟对象类型的名称 来创建。

JavaScript 变量作用域

变量作用域

变量能够被访问的范围被称为变量作用域。

变量声明

ES6 之前所有的变量都使用 var 关键字进行声明,ES6 中新增了 letconst 两种变量声明的关键字。

  • 使用 var 声明变量

    使用 var 声明的变量会被自动添加到最靠近的上下文中,如果变量未经声明就初始化了,会被添加到全局上下文中。

    var 声明的变量会被拿到函数或全局作用域的顶部,这种现象被称为变量提升。提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用。可是在实践中,提 升也会导致合法却奇怪的现象,即在变量声明之前使用变量。

    单需要注意的是:变量的声明会提升,但变量的赋值不会。

  • 使用 let 的块级作用域声明

    ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的,这也是 JavaScript 中的新概念。块级作用域由最近的一对包含花括号 {} 界定。换句话说,if 块、while 块、function 块,甚至连单独 的块也是 let 声明变量的作用域。

    letvar 的另一个不同之处是在同一作用域内不能声明两次。

    重复的 var 声明会被忽略,而重 复的 let 声明会抛出 SyntaxError

  • 使用 const 的常量声明

    ES6 同时还增加了 const 关键字。使用 const 声明的变量必须同时初始化为某个值。 一经声明,在其生命周期的任何时候都不能再重新赋予新值。

    由于 const 声明暗示变量的值是单一类型且不可修改,JavaScript 运行时编译器可以将其所有实例都替换成实际的值,而不会通过查询表进行变量查找。谷歌的 V8 引擎就执行这种优化。

JavaScript delete 操作符

delete 操作符用于删除对象的某个属性,如果没有指向这个属性的引用,它最终将会被释放。

1
delete obj['property'];

对于所有结果都返回 true,除非属性是一个不可配置属性,这种情况,在非严格模式下返回 false,在严格模式下则抛出 TypeError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const originO = {
name: 'Mike',
age: 29,
speak() {
console.log(`I am ${this.name}`);
}
}

Object.defineProperty(originO, 'sex', {
value: '女'
configurable: false
});

console.log(delete originO.sex); // false

'use strict'
console.log(delete originO.sex); // TypeError: Cannot delete property 'sex' of #<Object>
  • 如果要删除的属性不存在,delete 不会有任何作用,但仍返回 true

  • delete 只能删除对象本身上的属性,不会删除原型链上的同名属性(如果有的话)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function originO() {
    this.name = 'originO';
    }

    originO.prototype.name = 'name in prototype';

    let obj = new originO();

    delete obj.name;

    console.log(obj.name); // name in prototype
  • 任何使用 var 声明的属性不能从全局作用域或函数的作用域中删除

    1
    2
    3
    4
    5
    var globalVar = 'XYZ';

    const descriptor = Object.getOwnPropertyDescriptor(window, 'globalVar');

    console.log(descriptor); // { ..., configurable: false, ... }

    可以看到 var 声明的属性不可配置

  • 任何使用 letconst 声明的属性不能从它声明的作用域中被删除

  • delete 删除数组元素

    1
    2
    3
    4
    5
    6
    7
    const arr = [ 1, 2, 3, 4 ];

    delete arr[3];

    console.log(arr); // [ 1, 2, 3, <1 empty item> ]
    console.log(arr.length); // 4
    console.log(arr[3]); // undefined

JavaScript 原型和继承

很多面向对象语言都支持两种继承实现方式:接口继承和实现继承 - 接口继承继承方法签名,实现继承则继承方法的实现。但由于 JavaScript 中没有方法签名,因此只支持实现继承者一种方式,而原型链正是实现它的方式。

原型链

JavaScript 中每个对象都有一个被称为原型的持有其它对象引用的私有属性,而原型对象也有其自己的原型,以此类推,这种链条持续到某个原型对象的原型为 null 。根据定义,null 是没有对象的,被作为原型链的最后一环。

当试图访问一个对象的属性时,不仅会搜索对象本身 - 如果对象本身未找到对应的属性,则会沿着对象的原型链层层向上搜索,直到找到匹配的属性或达到原型链的末尾。

每个构造函数都有一个原型对象 ,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。

1
2
3
4
5
6
7
8
9
10
11
function Point(x, y) {
this.x = x;
this.y = y;
}

console.log(Point.__proto__ === Function.prototype); // true
console.log(Point === Point.prototype.constructor); // true

const somePoint = new Point(0, 0);
console.log(somePoint.__proto__ === Point.prototype); // true

如果原型是另一个类型的实例呢?那就意味着这个原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函 数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SuperType() {
this.superName = 'Super Type';
}

SuperType.prototype.getSuperTypeName = function() { return this.superName; };

function SubType() {
this.subName = 'Sub Type';
}

SubType.prototype = new SuperType();
SubType.prototype.getSubTypeName = function() { return this.subName; };

const aType = new SubType();

console.log(aType.__proto__ === SubType.prototype); // true
console.log(SubType.prototype.__proto__ === SuperType.prototype); // true
console.log(SuperType.prototype.constructor === SuperType); // true

下图展示了子类的实例与两个构造函数及其对应的原型之间的关系:

image-20201202145300965

实际上,原型链中还有一环。默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。 任何函数的默认原型都是一个 Object 的实例, 这意味着这个实例有一个内部指针指向 Object.prototype。这也是为什么自定义类型能够继承包括 toString()valueOf() 在内的所有默认方法的原因。因此前面的例子还有额外一层继承关系:

image-20201202145812496

继承

原型链虽然是实现继承的强大工具,但它也有问题。主要问题出现在原型中包含引用值的时候。前面在谈到原型的问题时也提到过,原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType() {
this.colors = ["red", "blue", "green"];
}

function SubType() {}

// 继承 SuperType SubType.prototype = new SuperType();

let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"

let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"

原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。事实上,我们无法在不影响所有对象实例的情况下把参数传进父类的构造函数。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。

在子类构造函数中调用父类构造函数,因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用 apply()call() 方法以新创建的对象为上下文执行构造函数,这种实现继承的方法被称为盗用构造函数继承

这种方法的问题是必须在构造函数中定义方法,因此函数不能重用,而且子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。

使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性,这种实现继承的方法被称为组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function SuperType(name) { 
this.name = name;
this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() { console.log(this.name); };

function SubType(name, age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}

// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() { console.log(this.age); };

let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29

let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf() 方法识别合成对象的能力。

其它补充

函数的 prototype

每个函数都有 prototype 属性,它默认包含了一个只有 constructor 属性的对象,这个 constructor 属性指向函数自身。

image-20201202160040769
1
2
3
function Foo() {}

console.log(Foo.prototype.constructor === Foo); // true

在使用 new 操作符创建对象时,构造函数的 prototype 被赋值给被创建的对象实例的 [[prototype]] 属性。

image-20201202160157525
1
2
3
4
5
6
7
8
function Foo() {}

const foo = new Foo();

console.log(foo.constructor === Foo); // true
console.log(foo.__proto__ === Foo.prototype); // true

const anotherFoo = new foo.constructor();

我们可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器。

1
2
3
4
5
6
7
function Foo() {}

const foo = new Foo();

const anotherFoo = new foo.constructor();

console.log(anotherFoo.constructor === Foo); // true

JavaScript 迭代器和生成器

ECMAScript 6 规范新增的两个高级特性:生成器和迭代器能够使我们高效、清晰、方便的实现迭代。

ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结构解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。PythonJavaC++,还有其他很多语言都对这个模式提供了完备的支持。

生成器和生成器对象

生成器是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。

需要注意的是:箭头函数不能用来定义生成器函数。(箭头函数的作用域根据当前环境决定)

通过生成器函数可以生成一个遵循可迭代协议(Iterable Protocol)和迭代器协议(Iterator Protocol)的生成器对象。

  • 可迭代协议是指:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。在 ECMAScript 中,这意味着必须暴露一个属性作为 “默认迭代器”,而且这个属性必须使用特殊的 [Symbol.iterator] 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
  • 迭代器协议是指:含有 next() 方法可以在可迭代对象中遍历数据,并返回 { done: Boolean, value: any } 形式的返回数据,done 用来标识迭代是否完成。

yield 关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到 yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用 next() 方法来恢复执行:

1
2
3
4
5
6
7
8
9
10
11
12
function* generatorFun() {
yield 1;
yield 2;
yield 3;
}

const generatorObj = generatorFun(); // {}

console.log(generatorObj.next()); // { value: 1, done: false }
console.log(generatorObj.next()); // { value: 2, done: false }
console.log(generatorObj.next()); // { value: 3, done: false }
console.log(generatorObj.next()); // { value: undefined, done: true }

生成器对象遵循可迭代协议和迭代器协议,因此它可以通过 for...of 进行迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* generatorFun() {
yield 1;
yield 2;
yield 3;
}

const generatorObj = generatorFun();

for (const iterator of generatorObj) {
console.log(iterator);
}

// 1
// 2
// 3

也可以通过 spread 语法输出其值

1
2
3
4
5
6
7
8
9
function* generatorFun() {
yield 1;
yield 2;
yield 3;
}

const generatorObj = generatorFun();

console.log(...generatorObj); // 1 2 3

通过 yield* 可以将执行委托给另一个迭代器,是将一个生成器的流插入到另一个生成的流的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* generateSquence(start, end) {
for (let i = start; i <= end; i++) yield i;
}

function* generatePwdChars() {
yield* generateSquence(48, 57);
yield* generateSquence(65, 90);
yield* generateSquence(97, 122);
}

let pwdChars = '';
for (const char of generatePwdChars()) {
pwdChars += String.fromCharCode(char);
}

console.log(pwdChars); // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

yield 不仅可以向外返回结果,还可以将外部的值传递到生成器内部(第一次调用 next() 方法传递的参数会被忽略)

1
2
3
4
5
6
7
8
9
10
11
12
function* gen() {
// 向外部代码传递一个问题并等待答案
let result = yield "2 + 2 = ?"; // (*)

alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield 返回的 value

generator.next(4); // --> 将结果传递到 generator 中

可以通过 generator.throw() 向生成器内部传递一个错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)

console.log("The execution does not reach here, because the exception is thrown above");
} catch(e) {
console.log(e); // Error: The answer is not found in my database
}
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)

当迭代值是以异步的形式出现时,就需要使用异步迭代,通过为对象添加 [Symbol.asyncIterator] 方法返回异步迭代器对象,异步迭代器对象通过 for await...of 循环来访问迭代值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let range = {
from: 1,
to: 5,

[Symbol.asyncIterator]: async function* () {
for (let start = this.from; start <= this.to; start++) {
value = awiat doSth();
yield value;
}
}
};

(async () => {
for await (let value of range) { // (4)
alert(value); // 1,2,3,4,5
}
})()

迭代器

JavaScript 中的可迭代对象是对数组的泛化,可以通过 for...of 对其进行迭代。

迭代器是一种一次性使用的对象,用于迭代和它关联的可迭代对象。

Symbol.iterator

可以通过为对象添加一个返回迭代器的 [Symbol.iterator] 方法来使得对象成为可迭代的 – 迭代器是指一个拥有 next() 方法的对象。

通过 for...of 对可迭代对象进行迭代时,for...of 希望获得下一个迭代值时就会调用迭代器对象的 next() 方法,next() 方法返回的对象必须是{ value: any: done: Boolean } 形式的值,当 donetrue 时表示迭代结束,否则表示还可以迭代下一个值。

一般来说,迭代器对象和要迭代的对象是不同的,这样可以避免迭代时造成要迭代对象的属性变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const range = {
from: 1,
to: 3,
[Symbol.iterator]: function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};

// for...of 希望获得下一个迭代值时就会调用迭代器对象的 next() 方法
// next() 方法返回的对象必须是 { value: any: done: boolean } 形式的值
for (const iterator of range) {
console.log(iterator);
}
// 1
// 2
// 3

使用 for...of 循环对对象进行迭代时,它会寻找 [Symbol.iterator] 方法,如果没有找到则会报错。

1
2
3
4
5
6
7
8
const range = {
from: 1,
to: 3,
};

for (const iterator of range) { // TypeError: range is not iterable
console.log(iterator);
}

如果需要在迭代过程中做更多的事情,我们可以通过显示调用迭代器来获得比 for...of 跟多的控制权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const range = {
from: 1,
to: 3,
[Symbol.iterator]: function () {
return {
current: this.from,
last: this.to,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};

const generatorObj = range[Symbol.iterator]();

while(true) {
const res = generatorObj.next();
if (res.done) break;
console.log(res.value);
}

JavaScript 中的内建可迭代对象

JavaScript 中很多内建对象都遵循了可迭代协议:

  • 字符串

  • 数组

  • 映射(Map/WeakMap)

  • 集合(Set/WeakSet)

  • arguments 对象

  • NodeListDOM 集合类型

数组和字符串

数组和字符串是使用最广泛的内建可迭代对象。

1
2
3
4
5
6
7
8
9
const string = 'Best wishes for the beautiful world.'
for (let iterator of string) {
console.log(iterator);
}

const arr = [ 'Best', 'wishes', 'for', 'the', 'beautiful', 'world' ];
for (const iterator of arr) {
console.log(iterator);
}

类数组对象和可迭代对象

JavaScript 中的类数组对象是指拥有 length 属性和索引的对象,但它不一定是可迭代的。

1
2
3
4
5
6
7
const arrLikeObj = {
length: 2,
0: '1',
1: '2'
}

for (const iterator of arrLikeObj) {} // TypeError: arrLikeObj is not iterable

JavaScript 中的可迭代对象是指实现了 [Symbol.iterator] 方法的对象。

我们可以通过全局方法 Array.from 将类数组对象转变成真正的数组再进行迭代

1
2
3
4
5
6
7
8
9
const arrLikeObj = {
length: 2,
0: '1',
1: '2'
}

for (const iterator of Array.from(arrLikeObj)) {
console.log(iterator);
}

使用生成器进行迭代

根据上文对生成器的介绍,很容易想到可以使用生成器函数来返回可迭代对象实现对象的迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const range = {
from: 1,
to: 3,
[Symbol.iterator]: function* () {
for (let value = this.from; value <= this.to; value++) {
yield value;
}
}
};

for (const iterator of range) {
console.log(iterator);
}

// 1
// 2
// 3

提前终止迭代器

可选的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以 “关闭” 迭代器。可能的情况包括:

  • for...of 循环通过 breakcontinuereturnthrow 提前退出
  • 解构操作并未消费所有值