📖ES6 教程 @阮一峰
Set & Map
Proxy
Reflect
Promise
Iterator
核心理解:ITERATOR 为数据结构提供了一个统一的访问机制的接口。
1. 简要概述
-
解释:Iterator(遍历器)是一种为各种不同的数据结构提供统一的访问机制的接口。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
-
Iterator 的作用
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据结构的成员能够按某种次序排列
- 为
for...of
循环提供支持
-
Iterator 的遍历原理:Iterator 是一个指针对象,最初指向数据结构的起始位置。它具有一个
next
方法。每次调用next
方法时,指针会移动到下一个成员,并返回一个对象,其中包含两个属性:value
表示当前成员的值,done
是一个布尔值,用于指示遍历是否已结束。 -
Iterator 接口的类型定义
1
2
3
4
5
6
7
8
9
10
11
12interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
interface Iterator<T> {
next(value?: any): IterationResult<T>;
}
interface IterationResult<T> {
value: T;
done: boolean;
}
2. Iterator 接口
2.1 Symbol.iterator
-
解释:只要一个数据结构具有
Symbol.iterator
属性,就可以被视为“可遍历的”(iterable),这意味着该数据结构部署了 Iterator 接口。Symbol.iterator
属性是一个函数,它是该数据结构默认的迭代器生成函数。调用这个函数会返回一个迭代器。 -
遍历方式:对于一个可遍历的数据结构 ITERABLE,有以下两种方式遍历其中的数据成员,
-
for...of
1
2
3for (let x of ITERABLE) {
console.log(x);
} -
while
1
2
3
4
5
6
7var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
var x = $result.value;
console.log(x);
$result = $iterator.next();
}
-
-
原生部署 Iterator 接口的数据结构:Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象
对象没有原生部署 Iterator 接口,因为对象的数据是无序的。因此,给对象部署遍历器接口,就相当于部署一种线性变换。
for...of
遍历字符串时,甚至会正确识别 32 位的 UTF-16 字符。1
2
3for (let x of 'a\uD83D\uDC0A\u9CC4\u9C7C') {
console.log(x); // "a","🐊","鳄","鱼"
}1
2
3
4
5
6const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { "value": 1, "done": false }
console.log(iterator.next()); // { "value": 2, "done": false }
console.log(iterator.next()); // { "value": 3, "done": false }
console.log(iterator.next()); // { "value": undefined, "done": true }
2.2 接口部署
-
解释:为了让数据结构是可遍历的,就要为其部署 Iterator 接口,即在数据结构的
Symbol.iterator
属性上添加遍历器生成方法(原型链上添加也奏效)。 -
Iterator 接口部署示例
-
对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
}
return { value: undefined, done: true };
}
};
}
}; -
类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
} -
类数组对象
1
2
3
4
5
6
7let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};如果一个对象存在数值键名和
length
属性,就称之为类数组对象,此时部署 Iterator 接口时可以直接引用数组的 Iterator 接口。
-
使用说明
- 普通对象部署数组的
Symbol.iterator
方法,并无效果。 - 如果
Symbol.iterator
方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。
- 普通对象部署数组的
2.3 调用场合
-
数组和 Set 的解构赋值
1
2
3
4
5const arr = [1,2];
const [x, y] = arr; // x = 1, y = 2
const set = new Set(['hello', 'world']);
const [a, b] = set; // a = "hello", b = "world" -
扩展运算符
...
(只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组)1
2
3
4
5const str = 'hello world';
console.log([...str]); // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]
const arr = [1, 2, 3];
console.log([0, ...arr, 4]); // [0, 1, 2, 3, 4] -
yield*
1
2
3
4
5
6
7
8
9
10
11
12
13
14const generator = function* () {
yield 1;
yield* [2, 3, 4];
yield 5;
};
const iterator = generator();
console.log(iterator.next()); // { "value": 1, "done": false }
console.log(iterator.next()); // { "value": 2, "done": false }
console.log(iterator.next()); // { "value": 3, "done": false }
console.log(iterator.next()); // { "value": 4, "done": false }
console.log(iterator.next()); // { "value": 5, "done": false }
console.log(iterator.next()); // { "value": undefined, "done": true } -
数组的遍历(包括以数组作为参数的场合)
-
for ... of
循环 -
Array.from()
-
Map()
,Set()
,WeakMap()
,WeakSet()
-
Promise.all()
、Promise.race()
-
······
-
2.4 几个概念
Symbol.iterator
、Iterator 接口、可遍历的、for...of
- 数据结构具有
Symbol.iterator
属性,亦即数据结构部署了 Iterator 接口,亦即数据结构是可遍历的,亦即数据结构可以使用for...of
遍历其数据成员。 for...of
循环内部调用的是数据结构的 Symbol.iterator 方法。
3. Iterator 结构
-
解释:Iterator 本质上是一个对象,必须包含
next()
方法,可选包含return()
和throw()
方法。next()
:用于遍历数据结构中的所有数据成员,返回值是一个对象,其结构为{value: any, done: boolean}
,其中value
表示当前遍历的数据成员,done
表示数据结构是否遍历完毕。return()
:当for...of
循环因为break
或throw
语句提前退出时,就会调用该方法。throw()
:主要配合 Generator 函数使用,详见下述关于 Generator 函数的笔记。
-
示例
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
28
29
30
31
32
33
34
35
36
37
38
39
40const iterableObj = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
const self = this;
let index = 0;
let value: number;
return {
next() {
if (index < self.data.length) {
value = self.data[index++];
console.log(`正在遍历数据 ${value}`);
return { value, done: false };
} else {
console.log('数据结构遍历结束');
return { value: undefined, done: true };
}
},
return() {
console.log(`return 方法在遍历 ${value} 时被调用了`);
return { value }
}
}
}
}
for (let value of iterableObj) {
// "正在遍历数据 1"
console.log(value); // 1
break; // "return 方法在遍历 1 时被调用了"
}
try {
for (let value of iterableObj) {
// "正在遍历数据 1"
console.log(value); // 1
throw new Error('error in for...of'); // "return 方法在遍历 1 时被调用了"
}
} catch (e:any) {
console.log(e.message); // "error in for...of"
}注意:如果在
for...of
循环中,由于throw
语句导致调用了可迭代对象的 Iterator 的return()
方法,那么会先执行return()
方法,然后再抛出错误。
4. 生成数据结构
数组、Set、Map 都具有以下方法,调用后返回一个遍历器,用于遍历生成数据结构的数据成员。
entries()
:返回一个遍历器对象,用于遍历形如[键名,键值]
的数组。对于数组,每次遍历的是[索引值,元素值]
;对于 Set,每次遍历的是[元素值,元素值]
;对于 Map,每次遍历的是[键名,键值]
。keys()
:返回一个遍历器对象,用于遍历所有的键名。values()
:返回一个遍历器对象,用于遍历所有的键值。
5. 遍历语法比较
这里展示其他循环的缺点,与 for...of
循环的优点。
-
for
循环- 写法比较麻烦
-
数组的
forEach
方法- 无法中途跳出
forEach
循环
- 无法中途跳出
-
for...in
循环(用于遍历键名)-
遍历数组时,遍历的键名是索引数字对应的字符串
1
2
3
4const arr = ['boo', 'foo', 'zoo'];
for(let idx in arr){
console.log(idx); // '0', '1', '2'
} -
会遍历原型上的键名
1
2
3
4
5
6
7
8
9const obj1 = {
name:'Jack',
age:19
}
const obj2 = Object.create(obj1);
obj2.gender = 'Male';
for(let key in obj2){
console.log(key); // "gender", "name", "age"
} -
会以任意顺序遍历键名
-
-
for...of
循环(用于遍历可遍历对象的数据成员)- 简洁的语法
- 有序遍历
- 可中途跳出循环(与
break
、continue
配合使用) - 遍历所有数据结构的统一接口
Generator 函数
核心理解:GENERATOR 的执行结果是 ITERATOR,同时这个 ITERATOR 可以看作是 GENERATOR 的实例。
1. 简要概述
-
解释:Generator 函数是 ES6 提供的一种异步编程解决方案,可以从以下两种角度来理解,
- 语法上
- Generator 函数是一个状态机,封装了多个内部状态。
- Generator 函数是一个遍历器生成函数,执行后返回一个遍历器,可以依次遍历 Generator 函数内部的每一个状态。
- 形式上
- Generator 函数使用
function*
来定义,即function
关键字和函数名之间有一个星号*
。 - Generator 函数内部使用
yield
表达式定义不同的内部状态。
- Generator 函数使用
- 语法上
-
Generator 函数的执行流程
-
调用 Generator 函数时,该函数不会立即执行,而是返回一个遍历器对象,该对象是指向函数内部的指针。
-
每次调用遍历器对象的
next()
方法时,Generator 函数会从上次暂停的地方继续执行,直到遇到yield
表达式或return
语句。next()
方法返回一个对象,其结构为{value: any, done: boolean}
。其中,value
表示 Generator 函数当前执行位置的值(即yield
或return
后的表达式的值),而done
是一个布尔值,指示遍历是否已完成。1
2
3
4
5
6
7
8
9
10
11function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next()); // { "value": "hello", "done": false }
console.log(hw.next()); // { "value": "world", "done": false }
console.log(hw.next()); // { "value": "ending", "done": true }
console.log(hw.next()); // { "value": undefined, "done": true }
-
-
yield
表达式:基于上述内容可知,Generator 函数是一种可以暂停执行的函数,而yield
表达式就是暂停标志。-
惰性求值(Lazy Evaluation):
yield
表达式后的表达式只有当调用next()
方法、内部指针指向该语句时才会执行。 -
暂缓执行函数:如果 Generator 函数中不使用
yield
表达式,那么只有调用该函数返回的遍历器对象的next()
方法后,该函数的内部代码才会执行。 -
使用说明
-
yield
表达式只能用在 Generator 函数里面。 -
yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。1
2
3function* demo() {
console.log('Hello' + (yield 123));
} -
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。1
2
3
4function* demo() {
foo(yield 'a', yield 'b');
let input = yield;
}
-
-
-
使用说明
-
可以把 Generator 函数赋值给对象的
Symbol.iterator
属性,从而使得该对象具有 Iterator 接口,使其成为可遍历对象。 -
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有
Symbol.iterator
属性,执行后返回自身。1
2
3
4
5
6
7
8
9function* numbers() {
yield 1;
yield 2;
yield 3;
}
const iterator = numbers();
console.log(iterator[Symbol.iterator]() === iterator); // true -
在使用
for...of
循环遍历 Generator 函数返回的遍历器时,遍历结果不包括 Generator 函数中return
语句返回的值。return
语句仅用于标识迭代的结束,而其返回的值不会被for...of
循环捕获。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function* numbers() {
yield 1;
yield 2;
return 3;
yield 4;
}
console.log([...numbers()]); // [1, 2]
console.log(Array.from(numbers())); // [1, 2]
const [x, y] = numbers();
console.log(x, y); // 1, 2
for (let x of numbers()) {
console.log(x) // 1, 2
}所有与
for...of
遍历原理相同的语法,都会得到相同的结果。 -
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
1
2
3
4
5let obj = {
* myGeneratorMethod() {
···
}
}; -
Generator 函数执行返回的遍历器是 Generator 函数的实例,继承了 Generator 函数的
prototype
对象上的方法。
-
2. next 方法的参数
-
解释:
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。1
2
3
4
5
6
7
8
9
10function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true } -
使用说明
- 通过
next()
方法的参数,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。 - 第一次调用
next()
方法时,传递参数是无效的。
- 通过
3. Generator 函数的原型方法
Generator 函数的执行结果是一个遍历器,可以被视为 Generator 函数的实例对象。因此,下述的 throw()
、return()
方法都是遍历器对象所调用的方法。
3.1 Generator.prototype.throw()
-
解释:在 Generator 函数体外抛出错误,该错误可以在 Generator 函数体内被捕获。
-
语法:
integrator.throw(e)
e
:被抛出的错误,建议为Error
对象的实例。
-
使用说明
-
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法,否则,由于 Generator 函数还没有开始执行,throw
方法抛出的错误只能抛出在函数外部。1
2
3
4
5
6
7
8
9
10
11
12var gen = function* gen() {
try {
yield 1;
yield 2;
} catch (e) {
yield 3;
}
yield 4;
}
var g = gen();
console.log(g.throw(new Error()))// "执行 JavaScript 失败:" -
throw
方法被内部捕获以后,会附带执行到下一条yield
表达式,这种情况下等同于执行一次next
方法。也就是说,throw
方法执行后,也会返回一个结构为{value: any, done: boolean}
的对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var gen = function* gen() {
try {
yield 1;
yield 2;
} catch (e) {
yield 3;
}
yield 4;
}
var g = gen();
console.log(g.next())// { value:1, done:false }
console.log(g.throw(new Error())) // { value:3, done:false }
console.log(g.next()) // { value:4, done:false }
console.log(g.next()) // { value:undefined, done:true }
console.log(g.next()) // { value:undefined, done:true } -
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用
next
方法,将返回一个value
属性等于undefined
、done
属性等于true
的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var gen = function* gen() {
yield 1;
yield 2;
yield 3;
yield 4;
}
var g = gen();
try {
console.log(g.next()) // { value:1, done:false }
console.log(g.throw(new Error()))
} catch (e) {
console.log(g.next()) // { value:undefined, done:true }
console.log(g.next()) // { value:undefined, done:true }
}
-
3.2 Generator.prototype.return()
-
解释:在 Generator 函数体外返回给定的值,并且终结遍历 Generator 函数。
-
语法:
integrator.return(value)
value
:返回的值。如果不提供该参数,则默认value = undefined
。
-
使用说明
-
如果 Generator 函数内部有
try...finally
代码块,且正在执行try
代码块,那么return
方法会导致立刻进入finally
代码块,执行完以后,再返回return
方法指定的返回值,此时整个函数才会结束。 -
与
throw
方法类似,return
方法执行后,也会返回一个结构为{value: any, done: boolean}
的对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var gen = function* gen() {
try {
yield 1;
yield 2;
} catch (e) {
yield 3;
}finally{
yield 5;
yield 6;
}
yield 4;
}
var g = gen();
console.log(g.next())// { value:1, done:false }
console.log(g.throw(new Error())) // { value:3, done:false }
console.log(g.return(100)) // { value:5, done:false }
console.log(g.next()) // { value:6, done:false }
console.log(g.next()) // { value:100, done:true }
console.log(g.next()) // { value:undefined, done:true }
-
next()、throw()、return() 对比
next()
、throw()
、return()
本质上都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield
表达式。假设遍历器对象当前指向 let result = yield x + y
,那么以下方法的执行相当于将 yield
表达式替换为,
next(para)
:let result = para
throw(e)
:let result = throw(e)
return(value)
:let result = return value
4. yield* 表达式
-
解释:在 Generator 函数中使用
yield*
表达式,相当于委托执行另一个遍历器对象或可遍历对象的所有元素。yield*
会自动遍历该对象的所有元素,并对每个元素单独使用yield
表达式进行产出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function* foo() {
yield 'a';
yield 'b';
return 'hello';
}
function* bar() {
yield 'x';
const value = yield* foo();
console.log(value);
yield 'y';
}
const i = bar();
console.log(i.next().value) // "x"
console.log(i.next().value) // "a"
console.log(i.next().value) // "b"
// "hello"
console.log(i.next().value) // "y"
console.log(i.next().value) // undefined -
语法
yield* 遍历器对象
yield* 可遍历对象
-
使用说明
- 如果
yield*
后面跟随的是一个 Generator 函数的遍历器,并且该 Generator 函数包含return
语句,那么可以使用var value = yield* iterator
的形式来获取return
语句返回的值。
- 如果
5. Generator 与协程
- 协程(coroutine):一种程序运行的方式,可以理解成 “协作的线程” 或 “协作的函数”。
- 协程的实现方式
- 单线程实现:特殊的子例程(subroutine)
- 多线程实现:特殊的线程
- 协程 Vs 子例程
- 子例程
- 一个调用栈。
- 堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。
- 协程
- 多个调用栈。
- 多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。
- 子例程
- 协程 Vs 普通线程
- 普通线程:同一时间可以有多个线程处于运行状态。
- 协程:同一时间运行的协程只能有一个,其他协程都处于暂停状态。
- Generator 函数 - 半协程:Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。此外,Generator 使用
yield
表达式交换控制权。 - Generator 函数 - 上下文:Generator 函数执行产生的上下文环境,一旦遇到
yield
命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
Generator 函数的异步应用
async 函数
ArrayBuffer
1. 简要概述
-
解释:
ArrayBuffer
对象代表内存之中的一段二进制数据,开发者可以通过视图(TypedArray
或DataView
)进行操作。由于视图部署了数组接口,因此可以使用数组的方法操作内存。ArrayBuffer
对象、TypedArray
视图和DataView
视图统称为二进制数组。 -
视图:
TypedArray
视图用于读写简单类型的二进制数据;DataView
视图支持自定义复合格式的视图,因此用于读写复杂类型的二进制数据。-
TypedArray
视图:9 种类型,如 Uint8Array 数组视图、Int16Array 数组视图、Float32Array 数组视图等,对应 9 种数据类型。 -
DataView
视图:可以自定义每个字节对应的数据类型,如第一个字节是 Uint8,第二、三个字节是 Int16,第四个字节开始是 Float32等,支持 8 种数据类型(与TypedView
视图相比,不支持 Uint8C 数据类型)。数据类型 字节长度 含义 对应的 C 语言类型 Int8 1 8 位带符号整数 signed char Uint8 1 8 位不带符号整数 unsigned char Uint8C 1 8 位不带符号整数(自动过滤溢出) unsigned char Int16 2 16 位带符号整数 short Uint16 2 16 位不带符号整数 unsigned short Int32 4 32 位带符号整数 int Uint32 4 32 位不带符号的整数 unsigned int Float32 4 32 位浮点数 float Float64 8 64 位浮点数 double
-
2. ArrayBuffer
-
解释:
ArrayBuffer
对象代表内存之中的一段二进制数据,仅支持通过视图进行读写操作。 -
基本使用
-
ArrayBuffer
的创建:指定连续的内存区域大小为byteLength
,同时每个字节的默认值为 0。1
new ArrayBuffer(byteLength)
-
ArrayBuffer
的操作(通过视图)-
DataView
视图:指定ArrayBuffer
实例为参数创建视图实例。1
2const dataView = new DataView(buffer);
console.log(dataView.getInt8(0)); -
TypedArray
视图:指定ArrayBuffer
实例为参数创建视图实例。1
2const int8Array = new Int8Array(buffer);
console.log(int8Array.length);注:与 DataView 视图不同,TypedArray 视图是一组构造函数,代表不同的数据格式
-
TypedArray
视图:指定Array
实例为参数创建视图实例,此时会隐含地创建一个ArrayBuffer
对象。1
2const int8Array2 = new Int8Array([1, 2, 3]);
console.log(int8Array2.length);
-
-
-
ArrayBuffer
的属性和方法-
ArrayBuffer.prototype.byteLength: number
返回 ArrayBuffer 所分配的内存区域的字节长度。内存区域要求连续,因此大内存区域可能会分配失败,因此可以通过如下方法检测内存是否分配成功,1
2
3
4
5if (buffer.byteLength === n) {
// 成功
} else {
// 失败
} -
ArrayBuffer.prototype.slice(start[, end]): ArrayBuffer
创建一个新的ArrayBuffer
对象,其内存区域数据拷贝自原ArrayBuffer
内存区域[start end)
部分的字节数据。这里的start
、end
是字节序号,如果不指定end
,则拷贝原ArrayBuffer
从start
字节开始的所有字节数据。 -
ArrayBuffer.isView(obj): boolean
表示参数obj
是否为ArrayBuffer
的视图实例,即是否为TypedArray
实例或DataView
实例。
-
-
Conversion:
ArrayBuffer
及对应的TypedArray
<–>string
-
ArrayBuffer
及其对应的TypedArray
-->string
:TextDeocder
1
2
3
4// Step1.
const decoder: TextDecoder = new TextDecoder(outputEncoding); // outputEncoding 指定解码的编码格式,默认为 'utf-8'
// Step2.
const output: string = decoder.decode(input); // input 的类型为 ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array -
string
-->ArrayBuffer
及其对应的TypedArray
:TextEncoder
1
2
3
4
5
6// Step1.
const encoder: TextEncoder = new TextEncoder();
// Step2.
const view: Uint8Array = encoder(input); // input 的类型为 string
// Step3.
const buffer: ArrayBuffer = view.buffer;
-
3. TypedArray
3.1 概念理解
-
视图(view):即对
ArrayBuffer
对象对应内存区域的字节数据的解读方式,分为TypedArray
视图(将所有数组成员都解读为同一种数据类型)、DataView
视图(将所有数组成员都解读为不同的数据类型)。 -
TypedArray 视图:共计 9 种,对应 9 种构造函数,9 种数组成员的数据类型。
构造函数 含义 Int8Array 8 位有符号整数,长度 1 个字节。 Uint8Array 8 位无符号整数,长度 1 个字节 Uint8ClampedArray 8 位无符号整数,长度 1 个字节,溢出处理不同。 Int16Array 16 位有符号整数,长度 2 个字节。 Uint16Array 16 位无符号整数,长度 2 个字节。 Int32Array 32 位有符号整数,长度 4 个字节。 Uint32Array 32 位无符号整数,长度 4 个字节。 Float32Array 32 位浮点数,长度 4 个字节。 Float64Array 64 位浮点数,长度 8 个字节。 -
TypedArray Vs. Array
差异点 TypedArray Array 数组成员类型是否相同 是 否 数组成员在内存空间是否连续 是 不一定(密集数组连续,稀疏数组不连续) 数组成员的默认值 0 undefined
(空位)数据存储位置 TypedArray
视图对应的ArrayBuffer
对象(可通过视图的buffer
属性获取)Array
对象自身
3.2 构造函数
注:JavaScript 允许基于同一
ArrayBuffer
对象建立多个视图,此时每个视图可以根据自己的理解去修改相应内存区域的数据,同时一个视图对内存的更改会在其他视图上反映出来。
-
TypedArray(buffer: ArrayBuffer, byteOffset: number=0[, length: number])
表示从buffer
的第byteOffset
个字节开始创建视图,同时确保视图的成员数量为length
。其中byteOffset
是0-based
表示,表示视图开始的字节序号,默认值为 0;length
表示视图的成员数量,因此视图对应内存区域的长度 =length
* 每个成员字节数。1
2
3
4
5
6
7
8
9const buf = new ArrayBuffer(8); // 对应 8 个字节的内存区域(第 0 ~ 7 个字节)
const v1 = new Int32Array(buf); // 可操作第 0 ~ 7 个字节,每 4 个字节被解析为一个 int32 类型的数组成员,因此视图数组长度为 2
const v2 = new Uint8Array(buf, 2); // 可操作第 2 ~ 7 个字节,每 1 个字节被解析为一个 uint8 类型的数组成员,因此视图数组长度为 6
const v3 = new Int16Array(buf, 2, 2); // 可操作第 2 ~ 5(2 + 2 * 2 - 1) 个字节,每 2 个字节被解析为一个 int16 类型的数组成员,此时视图数组长度被指定为 2
console.log(`v1.length=${v1.length}, v2.length=${v2.length}, v3.length=${v3.length}`); // 2, 6, 2
v1[0] = 0x12345678; // 修改第 0 ~ 3 个字节的值为 0x12345678,此时 v2[0] 表示第 2 个字节的值为 0x34, 表示第 2 ~ 3 个字节的值为 0x1234
console.log(buf); // [Uint8Contents]: <78 56 34 12 00 00 00 00>(ArrayBuffer 十六进制表示的每个字节的值)
console.log(`v1[0]=0x${v1[0].toString(16)}, v2[0]=0x${v2[0].toString(16)}, v3[0]=0x${v3[0].toString(16)}`); // 0x12345678, 0x34, 0x1234注:创建视图时,byteOffset 必须与所建立的数据类型一致,即 byteOffset 必须是数组成员的字节数的整数倍,否则会报错。
1
const v4 = new Int16Array(buf, 1, 2); // RangeError: start offset of Int16Array should be a multiple of 2
-
TypedArray(length)
表示创建一个成员数量为length
的视图,此时视图实际上创建了一个大小为length
* 视图成员字节数 的ArrayBuffer
。其中length
表示视图成员数量;视图创建的ArrayBuffer
可以通过视图的buffer
属性获取。1
2
3const f64a = new Float64Array(8); // 视图成员数量为 8,对应的内存区域大小为 8 * 8 = 64 字节
console.log(`f64a.length=${f64a.length}, f64a.buffer.byteLength=${f64a.buffer.byteLength}`); // 8, 64
console.log(f64a.buffer); // [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>(ArrayBuffer 十六进制表示的每个字节的值) -
TypedArray(typedArray)
表示基于给定视图创建一个新的视图,此时新视图对应的ArrayBuffer
与给定视图的ArrayBuffer
不是同一个,可以理解为两个ArrayBuffer
内存区域不同,但是数据相同,即数据发生了拷贝。1
2
3
4const view1 = new Int16Array([1, 2, 3, 4]);
const view2 = new Int16Array(view1);
view1[0] = 123; // 此时 view1 对应的内存区域数据为 123 2 3 4,view2 对应的内存区域的数据为 1 2 3 4,即两个视图的内存区域相互独立互不干扰
console.log(`view1[0]=${view1[0]}, view2[0]=${view2[0]}`); // 123, 1 -
TypedArray(arrayLikeObject)
表示基于给定类数组对象创建一个视图,此时视图实际上开辟了一个新的ArrayBuffer
,与给定类数组对象无关,即数据发生了拷贝。1
2const view = new Int16Array([1, 2, 3, 4]); // 视图的成员数量为给定的类数组对象的成员数量。
console.log(`view.length=${view.length}`); // 4注:TypedArray 视图可以通过以下三种方法转换为普通数组:
[...typedArray]
,Array.from(typedArray)
,Array.prototype.slice.call(typedArray)
。
3.3 数组方法
-
TypedArray 可用的数组方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22TypedArray.prototype.copyWithin(target, start[, end=this.length])
TypedArray.prototype.entries()
TypedArray.prototype.every(callbackfn, thisArg?)
TypedArray.prototype.fill(value, start=0, end=this.length)
TypedArray.prototype.filter(callbackfn, thisArg?)
TypedArray.prototype.find(predicate, thisArg?)
TypedArray.prototype.findIndex(predicate, thisArg?)
TypedArray.prototype.forEach(callbackfn, thisArg?)
TypedArray.prototype.indexOf(searchElement, fromIndex=0)
TypedArray.prototype.join(separator)
TypedArray.prototype.keys()
TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
TypedArray.prototype.map(callbackfn, thisArg?)
TypedArray.prototype.reduce(callbackfn, initialValue?)
TypedArray.prototype.reduceRight(callbackfn, initialValue?)
TypedArray.prototype.reverse()
TypedArray.prototype.slice(start=0, end=this.length)
TypedArray.prototype.some(callbackfn, thisArg?)
TypedArray.prototype.sort(comparefn)
TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
TypedArray.prototype.toString()
TypedArray.prototype.values() -
使用说明
-
TypedArray
数组没有concat
方法,可以通过以下函数实现多个TypedArray
数组的合并操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function concatenate(resultConstructor, ...arrays) {
let totalLength = 0;
for (let arr of arrays) {
totalLength += arr.length;
}
let result = new resultConstructor(totalLength);
let offset = 0;
for (let arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
concatenate(Uint8Array, Uint8Array.of(1, 2), Uint8Array.of(3, 4)注:
TypedArray
可以称之为数组,因为其实现了数组接口;也可称之为视图,因为其可以对ArrayBuffer
对应的内存区域进行操作。 -
TypedArray
数组部署了Iterator
接口,即TypedArray
数组是可迭代对象,可以通过for...of
遍历。1
2
3
4let ui8 = Uint8Array.of(0, 1, 2);
for (let byte of ui8) {
console.log(byte);
}
-
3.4 字节序
-
大端字节序(Big-Endian):重要的字节排在前边,不重要的字节排在后边。换句话说,高位字节排在内存低位,低位字节排在内存高位。
- 示例:
0x12345678
的大端字节序的存储为12
34
56
78
(内存地址从低到高)。 - 场景:网络传输(TCP/IP 协议等)。
- 示例:
-
小端字节序(Little-Endian,默认):不重要的字节排在前边,重要的字节排在后边。换句话说,低位字节在内存低位,高位字节在内存高位。
注:
TypedArray
视图采取小端字节序读写数据,不支持修改字节序。但是,DataView
视图支持以指定的字节序读写数据。- 示例:
0x12345678
的小端字节序的存储为78
56
34
12
(内存地址从低到高)。 - 场景:x86 处理器、Windows 系统。
- 示例:
-
字节序判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
let arr32 = Uint32Array.of(0x12345678);
let arr8 = new Uint8Array(arr32.buffer);
switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8[3])) {
case 0x12345678:
return BIG_ENDIAN;
case 0x78563412:
return LITTLE_ENDIAN;
default:
throw new Error('Unknown endianness');
}
}
3.5 属性和方法
-
静态属性
-
TypedArray.BYTES_PER_ELEMENT
获取指定视图的数组成员所占据的字节数。1
2
3
4
5
6
7
8
9Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Uint8ClampedArray.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8
-
-
实例属性
TypedArray.prototype.BYTES_PER_ELEMENT
与TypedArray.BYTES_PER_ELEMENT
相同。