let 和 const 命令
let 命令
不存在变量提升
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined
。
1 | console.log(foo); // 输出 undefined |
let
命令所声明的变量一定要在声明后使用,否则报 ReferenceError
错误。
暂时性死区
只要块级作用域内存在 let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
在代码块内,使用 let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)
1 | var tmp = 'dd'; |
在 let
命令声明变量 tmp
之前,都属于变量 tmp
的“死区”。
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量,因此,不能在函数内部重新声明参数。
1 | function func(arg) { |
块级作用域
ES5 只有全局作用域和函数作用域,ES6 中引入了块级作用域,块级作用域是由一对 {}
包裹的区域,包括条件语句和循环语句以及 try
catch
中的 {}
(不写 {}
则不存在块级作用域)。
块级作用域可以防止:
- 内层变量覆盖外层变量;
- 局部变量泄露为全局变量。
ES6 允许块级作用域的任意嵌套。
内层作用域可以定义外层作用域的同名变量。
ES5 规定,函数只能在全局作用域和函数作用域之中声明,不能在块级作用域声明,ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
ES6 规定,块级作用域之中,函数声明语句的行为类似于 let
,在块级作用域之外不可引用。
ES6 在附录 B 里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
const 命令
const
声明一个只读的常量,一旦声明,常量的值就不能改变,且一旦声明,就必须立即初始化,不能留到以后赋值。
其余特性与 let
相同。
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
如果真的想将对象冻结,应该使用 Object.freeze
方法,且如果对象中的属性有为对象的,应该递归调用 Object.freeze
方法。
变量的解构赋值
数组的解构赋值
解构赋值属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
如果解构不成功,变量的值就等于 undefined
。
如果等号的右边不是可遍历的结构(具备 Iterator 接口),那么将会报错。
对于 Set 结构,也可以使用数组的解构赋值。
1 | let [x, y, z] = new Set(['a', 'b', 'c']); |
只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
1 | function* fibs() { |
默认值
解构赋值允许指定默认值。
1 | let [x, y = 'b'] = ['a']; // x='a', y='b' |
只有当一个数组成员严格等于(===
) undefined
,默认值才会生效。
默认值可以是一个表达式,如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
1 | function f() { |
默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
1 | let [x = 1, y = x] = []; // x = 1; y = 1 |
对象的结构赋值
对象的解构与数组有一个重要的不同,数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
如果解构失败,变量的值等于 undefined
。
如果变量名与属性名不一致,必须写成下面这样。
1 | let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; |
这实际上说明,对象的解构赋值是下面形式的简写。
1 | let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' }; |
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
嵌套结构的对象想要同时获得内层与外层的变量值,写法如下所示。
1 | let obj = { |
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
对象的解构赋值可以取到继承的属性。
1 | const obj1 = { foo: 'bar' }; |
默认值
同数组的解构赋值相同。
注意点
1 | // 错误的写法 |
将一个已经声明的变量用于解构赋值,JavaScript 引擎会将 {x}
理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
1 | // 正确的写法 |
由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构,数组的下标对应对象的键名。
1 | let arr = [1, 2, 3]; |
字符串的结构赋值
字符串进行解构赋值时,其被转换成了一个类似数组的对象。
类似数组的对象都有一个 length
属性,因此还可以对这个属性解构赋值。
1 | const [a, b, c, d, e] = 'hello'; |
数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对应的包装对象。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefined
和 null
无法转为对象,所以对它们进行解构赋值,都会报错。
函数参数的解构赋值
1 | function add([x, y = 2]) { |
1 | function move({ x = 0, y = 0 } = {}) { |
圆括号问题
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
用途
- 交换变量的值
1 | let x = 1; |
- 从函数返回多个值
1 | function example() { |
- 函数参数的定义
1 | // 参数是一组有次序的值 |
- 遍历 Map 结构
1 | const map = new Map(); |
字符串的扩展
字符的 Unicode 表示法
ES6 加强了对 Unicode 的支持,允许采用 \uxxxx
形式表示一个字符,其中 xxxx
表示字符的 Unicode 码点。
这种表示法只限于码点在 \u0000
~ \uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
对于 \u
后面跟着超过 0xFFFF
的数值的值,ES6 做出了改进,只要将码点放入大括号,就能正确解读该字符。
1 | '\uD842\uDFB7'; |
两个码点组成的字符第一个码点要在
0xD800
~0xDBFF
之间,即前 6 位固定为1101 10
。第二个码点要求在0xDC00
~0xDFFF
之间,即前 6 位固定为1101 11
。这样每个码点可以空出 10 位,两个码点可以表示 2^20 个字符。对于编码小于等于0xFFFF
的,一个码点即表示一个字符。对于编码大于0xFFFF
的字符,先减去0x10000
,然后转换为 20 位的二进制数,然后分别填充两个码点空出的 10 位,这就是需要两个码点字符的保存方式。
ES6 新语法规定,对于超出0xFFFF
的编码可以直接用{}
写出,如\u{xxxxx}
,对于\u{1F680}
:0x1F680
-0x10000
=0xF680
=0000 1111 0110 1000 0000
第一个码点是1101 10|00 0011 1101
=0xD83D
第二个码点是1101 11|10 1000 0000
=0xDE80
。
JavaScript 规定有 5 个字符,不能在字符串里面直接使用,只能使用转义形式。
- U+005C:反斜杠(reverse solidus)
- U+000D:回车(carriage return)
- U+2028:行分隔符(line separator)
- U+2029:段分隔符(paragraph separator)
- U+000A:换行符(line feed)
ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符),模板字符串现在就允许直接输入这两个字符。
字符串的遍历器接口
ES6 为字符串添加了遍历器接口,使得字符串可以被 for...of
循环遍历,遍历器可以识别大于 0xFFFF
的码点(传统的 for
循环不行)。
JSON.stringify()的改造
为了确保返回的是合法的 UTF-8 字符,ES2019 改变了 JSON.stringify()
的行为,如果遇到 0xD800
到 0xDFFF
之间的单个码点,或者不存在的配对形式,它会返回转义字符串。
1 | JSON.stringify('\u{D834}'); // ""\\uD834"" |
UTF-8 标准规定,
0xD800
到0xDFFF
之间的码点,不能单独使用,必须配对使用。
模板字符串
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
如果 ${}
中的值不是字符串,将按照一般的规则转为字符串,比如,${}
中是一个对象,将默认调用对象的 toString
方法。
标签模板
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串,这被称为“标签模板”功能(tagged template)。
1 | alert`123`; |
标签模板其实不是模板,而是函数调用的一种特殊形式,“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
1 | let a = 5; |
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
标签模板的另一个应用,就是多语言转换(国际化处理)。
模板处理函数的第一个参数(模板字符串数组),还有一个 raw
属性,其保存的是转义后的原字符串。
1 | tag`First line\nSecond line`; |
ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回 undefined
,而不是报错(这种对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。)。
字符串的新增方法
String.fromCodePoint()
用于从 Unicode 码点返回对应字符,可以识别大于 0xFFFF
的字符,弥补了 ES5 的 String.fromCharCode()
方法的不足。
如果String.fromCodePoint
方法有多个参数,则它们会被合并成一个字符串返回。
1 | String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x🚀y'; |
实例方法:codePointAt()
codePointAt()
方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与 charCodeAt()
方法相同。
codePointAt()
方法返回的是码点的十进制值
1 | let s = '𠮷a'; |
codePointAt()
方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
fromCodePoint
方法定义在 String
对象上,而codePointAt
方法定义在字符串的实例对象上。
String.row()
Iterator
Iterator 的概念
Iterator 是一种接口,为各种不同的数据结构(Array
,Map
,Set
等)提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。
Iterator 的作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费。
Iterator 的遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上就是一个指针对象。
- 第一次调用指针对象的
next
方法,可以将指针指向数据结构的第一个成员。 - 第二次调用指针对象的
next
方法,指针就指向数据结构的第二个成员。 - 不断调用指针对象的
next
方法,直到它指向数据结构的结束位置。
每一次调用 next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value
和 done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。
默认 Iterator 接口
当使用 for...of
循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator
属性,或者说,一个数据结构只要具有 Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。
Symbol.iterator
属性本身是一个函数,就是当前数据结构默认的遍历器生成函数,执行这个函数就会返回一个遍历器对象,该对象的根本特征就是具有 next
方法。每次调用 next
方法,都会返回一个代表当前成员的信息对象,具有 value
和 done
两个属性。
原生具备 Iterator 接口的数据结构:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of
循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在 Symbol.iterator
属性上面部署,这样才会被 for...of
循环遍历。
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
对于类似数组的对象(存在数值键名和 length
属性),部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator
方法直接引用数组的 Iterator 接口。
调用 Iterator 接口的场合
解构赋值
对数组和 Set
结构进行解构赋值时,会默认调用 Symbol.iterator
方法。
1 | let set = new Set() |
扩展运算符
1 | // 例一 |
实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的 Iterator 接口。
1 | let generator = function*() { |
其他场合
由于数组的遍历会调用 Iterator 接口,所以任何接受数组作为参数的场合,其实都调用了 Iterator 接口:
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])
) - Promise.all()
- Promise.race()
Iterator 接口与 Generator 函数
Symbol.iterator
方法的最简单实现,是使用 Generator 函数。
1 | let myIterable = { |
遍历器对象的 return(), throw()
遍历器对象除了具有 next
方法,还可以具有 return
方法和 throw
方法。next
方法是必须部署的,return
方法和 throw
方法是否部署是可选的。
return
方法的使用场合是如果 for...of
循环提前退出(通常是因为出错,或者有 break
语句),就会调用 return
方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return
方法。
1 | function readLinesSync(file) { |
throw
方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。
for…of 循环
一个数据结构只要部署了 Symbol.iterator
属性,就被视为具有 Iterator 接口,就可以用 for...of
循环遍历它的成员,也就是说,for...of
循环内部调用的是数据结构的 Symbol.iterator
方法。
数组
for...of
循环调用 Iterator 接口,数组的 Iterator 接口只返回具有数字索引的属性。
1 | let arr = [3, 5, 7]; |
Set 和 Map 结构
遍历的顺序是按照各个成员被添加进数据结构的顺序。Set
结构遍历时,返回的是一个值,而 Map
结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map
成员的键名和键值。
1 | let map = new Map().set('a', 1).set('b', 2); |
计算生成的数据结构
有些数据结构是在现有数据结构的基础上,计算生成的:
entries()
返回一个遍历器对象,用来遍历[key, value]
组成的数组。对于数组,key
就是索引值;对于Set
,key
与value
相同;Map
结构的 Iterator 接口,默认就是调用entries
方法。keys()
返回一个遍历器对象,用来遍历所有的key
。values()
返回一个遍历器对象,用来遍历所有的value
。
类似数组的对象
并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from
方法将其转为数组。
1 | let arrayLike = { length: 2, 0: 'a', 1: 'b' }; |
Generator 函数
简介
基本概念
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 函数是一个普通函数,但是有两个特征:
function
关键字与函数名之间有一个*
;- 函数体内部使用
yield
表达式,定义不同的内部状态。
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象——遍历器对象(Iterator Object)。必须调用遍历器对象的 next
方法,使得指针移向下一个状态,也就是说,每次调用 next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield
表达式(或 return
语句)为止。换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而 next
方法可以恢复执行。
yield 表达式
遍历器对象的 next
方法的运行逻辑如下:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
需要注意的是,yield
表达式后面的表达式,只有当调用 next
方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
1 | // yield 后面的表达式不会立即求值,只会在 next 方法将指针移到这一句时,才会求值 |
Generator 函数可以不用 yield
表达式,这时就变成了一个单纯的暂缓执行函数。
yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
1 | function* demo () { |
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
1 | function* demo() { |
与 Iterator 接口的关系
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator
属性,执行后返回自身。
next 方法的参数
yield
表达式本身没有返回值,或者说总是返回 undefined
。next
方法可以带一个参数,该参数就会被当作上一个 yield
表达式的返回值。
由于
next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的,V8 引擎将直接忽略第一次使用next
方法时的参数,只有从第二次使用next
方法开始,参数才是有效的。从语义上讲,第一个next
方法用来启动遍历器对象,所以不用带有参数。
for…of 循环
for...of
循环可以自动遍历 Generator 函数运行时生成的遍历器对象,且此时不再需要调用 next
方法。
1 | function* foo() { |
一旦 next
方法的返回对象的 done
属性为 true
,for...of
循环就会中止,且不包含该返回对象,所以上面代码的 return
语句返回的 4,不包括在 for...of
循环之中。
除了 for...of
循环以外,扩展运算符(...
)、解构赋值和 Array.from
方法内部调用的,都是遍历器接口,这意味着,它们都可以将 Generator 函数返回的遍历器对象作为参数。
Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个 throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
1 | let g = function*() { |
throw
方法可以接受一个参数,该参数会被 catch
语句接收,建议抛出 Error
对象的实例。
throw
命令抛出的错误只能被函数体外的catch
语句捕获。
如果 Generator 函数内部没有部署 try...catch
代码块,那么 throw
方法抛出的错误,将被外部 try...catch
代码块捕获。而如果 Generator 函数内部和外部都没有部署 try...catch
代码块,那么程序将报错,直接中断执行。
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next
方法(即启动状态的遍历器对象才可以捕获错误)。
throw
方法被捕获以后,会附带执行下一条 yield
表达式,也就是说,会附带执行一次 next
方法。
1 | let gen = function* gen() { |
只要 Generator 函数内部部署了
try...catch
代码块,那么遍历器的throw
方法抛出的错误不影响下一次遍历。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next
方法,将返回一个 value
属性等于 undefined
、done
属性等于 true
的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
Generator.prototype.return()
Generator 函数返回的遍历器对象的 return
方法可以返回给定的值,并且终结遍历 Generator 函数。
1 | function* gen() { |
如果 return
方法调用时,不提供参数,则返回值的 value
属性为 undefined
。
如果 Generator 函数内部有 try...finally
代码块,且正在执行 try
代码块,那么 return
方法会导致立刻进入 finally
代码块,执行完以后,整个函数才会结束。
1 | function* numbers() { |
yield* 表达式
yield*
表达式用于在一个 Generator 函数里面执行另一个 Generator 函数。
1 | function* foo() { |
从语法角度看,如果 yield
表达式后面跟的是一个遍历器对象,需要在 yield
表达式后面加上 *
,表明它返回的是一个遍历器对象。
yield*
后面的 Generator 函数(没有 return
语句时),等同于在 Generator 函数内部,部署一个 for...of
循环。在有 return
语句时,则需要用 let value = yield* iterator
的形式获取 return
语句的值。
1 | function* foo() { |
如果 yield*
后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。实际上,任何数据结构只要有 Iterator 接口,就可以被 yield*
遍历。
yield*
命令可以很方便地取出嵌套数组的所有成员。
1 | function* iterTree(tree) { |
作为对象属性的 Generator 函数
1 | let obj = { |
Generator 函数的 this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype
对象上的方法。
如果把 Generator 函数当作普通的构造函数,并不会生效,因为 Generator 函数返回的总是遍历器对象,而不是 this
对象。
1 | function* g() { |
Generator 函数也不能跟 new
命令一起用。
让 Generator 函数返回一个正常的对象实例,既可以用 next
方法,又可以获得正常的 this
的方法:
1 | function* gen() { |
Generator 与上下文
JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到 yield
命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
ArrayBuffer
ArrayBuffer
对象、TypedArray
视图和 DataView
视图是 JavaScript 操作二进制数据的一个接口。
ArrayBuffer
对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作,而“视图”则部署了数组接口,可以用数组的方法操作内存。TypedArray
视图:共包括 9 种类型的视图。DataView
视图:可以自定义复合格式的视图,还可以自定义字节序。
TypedArray
视图支持的数据类型一共有 9 种(DataView
视图支持除 Uint8ClampedArray
以外的其他 8 种)。
数据类型 | 字节长度 | 含义 |
---|---|---|
Int8Array | 1 | 8 位带符号整数 |
Uint8Array | 1 | 8 位不带符号整数 |
Uint8ClampedArray | 1 | 8 位不带符号整数(自动过滤溢出) |
Int16Array | 2 | 16 位带符号整数 |
Uint16Array | 2 | 16 位不带符号整数 |
Int32Array | 4 | 32 位带符号整数 |
Uint32Array | 4 | 32 位不带符号整数 |
Float32Array | 4 | 32 位浮点数 |
Float64Array | 8 | 64 位浮点数 |
二进制数组并不是真正的数组,而是类似数组的对象。
ArrayBuffer 对象
ArrayBuffer
对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray
视图和 DataView
视图)以指定格式来读写二进制数据。
调用 ArrayBuffer
构造函数会分配一段可以存放数据的连续内存区域,ArrayBuffer
构造函数的参数是所需要的内存大小(单位字节)。
ArrayBuffer.prototype.byteLength
ArrayBuffer
实例的 byteLength
属性返回所分配的内存区域的字节长度。
如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存)。
ArrayBuffer.prototype.slice()
slice
方法允许将内存区域的一部分,拷贝生成一个新的 ArrayBuffer
对象。
slice
方法包含两步,第一步是先分配一段新内存,第二步是将原来那个 ArrayBuffer
对象拷贝过去。
除了 slice
方法,ArrayBuffer
对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。
ArrayBuffer.isView()
静态方法 isView
返回一个布尔值,表示参数是否为 ArrayBuffer
的视图实例。该方法大致相当于判断参数是否为 TypedArray
实例或 DataView
实例。
TypedArray 视图
TypedArray
很像普通数组,都有 length
属性,都能用方括号运算符([]
)获取单个元素,所有数组的方法,在它们上面都能使用。
普通数组与 TypedArray
数组的差异主要在以下方面:
TypedArray
数组的所有成员都是同一种类型。TypedArray
数组的成员是连续的,不会有空位。TypedArray
数组成员的默认值为0
。TypedArray
数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer
对象之中,要获取底层对象必须使用buffer
属性。
构造函数
TypedArray
构造函数有多种用法。
TypedArray(buffer, byteOffset=0, length?)
视图的构造函数可以接受三个参数:
- buffer(必需):视图对应的底层
ArrayBuffer
对象。 - byteOffset(可选):视图开始的字节序号,默认从 0 开始。
- length(可选):视图包含的数据个数,默认直到本段内存区域结束。
注意,
byteOffset
必须与所要建立的数据类型一致。如果想从任意字节开始解读ArrayBuffer
对象,必须使用DataView
视图。
1 | const buffer = new ArrayBuffer(8); |
TypedArray(length)
视图还可以不通过 ArrayBuffer
对象,直接分配内存而生成,这时,视图构造函数的参数就是 TypedArray
数组成员的个数。
1 | const f64a = new Float64Array(8); |
TypedArray(typedArray)
TypedArray
数组的构造函数,可以接受另一个 TypedArray
实例作为参数,此时生成的新数组只是复制了参数数组的值,对应的底层内存是不一样的,新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
1 | const typedArray = new Int8Array(new Uint8Array(4)); |
如果想基于同一段内存构造不同的视图,可以采用下面的写法。
1 | const x = new Int8Array([1, 1]); |
TypedArray(arrayLikeObject)
构造函数的参数也可以是一个普通数组,然后直接生成 TypedArray
实例,这时 TypedArray
视图会重新开辟内存,不会在原数组的内存上建立视图。
1 | const typedArray = new Uint8Array([1, 2, 3, 4]); |
TypedArray
数组也可以转换回普通数组。
1 | const normalArray = [...typedArray]; |
数组方法
普通数组的操作方法和属性,对 TypedArray
数组完全适用。
注意,
TypedArray
数组没有concat
方法。
另外,TypedArray
数组与普通数组一样,部署了 Iterator 接口,所以可以被遍历。
字节序
字节序指的是数值在内存中的表示方式。
x86 体系的计算机都采用小端字节序(little endian),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,大端字节序则完全相反,将最重要的字节排在前面。
目前,所有个人电脑几乎都是小端字节序,所以 TypedArray
数组内部也采用小端字节序读写数据,或者更准确的说,按照本机操作系统设定的字节序读写数据。
如果一段数据是大端字节序,TypedArray
数组将无法正确解析,因为它只能处理小端字节序。为了解决这个问题,JavaScript 引入了 DataView
对象,它可以设定字节序。
BYTES_PER_ELEMENT 属性
每一种视图的构造函数都有一个 BYTES_PER_ELEMENT
属性,表示这种数据类型占据的字节数。
这个属性在 TypedArray
实例上也能获取。
1 | Int8Array.BYTES_PER_ELEMENT; // 1 |
ArrayBuffer 与字符串的互相转换
使用原生 TextEncoder
和 TextDecoder
方法进行 ArrayBuffer
和字符串的相互转换。
1 | /** |
溢出
TypedArray
数组的溢出处理规则简单来说就是抛弃溢出的位,然后按照视图类型进行解释。
数字在计算机内部采用“补码”表示,正数的“原码”与“补码”相同。。
- 原码求补码:正数不变,负数将其原码除符号位外的所有位取反后加 1。
- 补码求原码:如果补码的符号位为 0,表示是一个正数,其原码就是补码,否则将其除符号位外的所有位取反后加 1。
简单转换规则可以这样表示:
- 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
- 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值的绝对值,再加上 1。
Uint8ClampedArray
视图的溢出规则与其它视图不同。它规定凡是发生正向溢出,该值一律等于当前数据类型的最大值;如果发生负向溢出,该值一律等于当前数据类型的最小值。
TypedArray.prototype.buffer
TypedArray
实例的 buffer
属性返回整段内存区域对应的 ArrayBuffer
对象,只读属性。
TypedArray.prototype.byteLength
byteLength
属性返回 TypedArray
数组占据的内存长度,单位为字节,只读属性。
TypedArray.prototype.byteOffset
byteOffset
属性返回 TypedArray
数组从底层 ArrayBuffer
对象的哪个字节开始,只读属性。
TypedArray.prototype.length
length
属性表示 TypedArray
数组含有多少个成员。
注意区分
length
属性和byteLength
属性。
1 | const a = new Int16Array(8); |
TypedArray.prototype.set()
set
方法用于复制数组(普通数组或 TypedArray
数组),也就是将一段内容完全复制到另一段内存。
set
方法还可以接受第二个参数,表示从调用方的哪一个成员开始复制。
1 | const a = new Uint16Array(8); |
TypedArray.prototype.subarray()
subarray
方法是对于 TypedArray
数组的一部分,再建立一个新的视图。
subarray
方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。
1 | const a = new Uint16Array(8); |
TypedArray.prototype.slice()
TypeArray
实例的 slice
方法,可以返回一个指定位置的新的TypedArray
实例。
slice
方法的参数同普通数组的 slice
方法。
TypedArray.of()
TypedArray
数组的所有构造函数都有一个静态方法 of
,用于将参数转为一个 TypedArray
实例。
下面三种方法都会生成一个成员相同的 TypedArray
数组。
1 | // 方法一 |
TypedArray.from()
静态方法 from
接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的 TypedArray
实例。
该方法还可以将一种 TypedArray
实例,转为另一种。
1 | const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2)); |
from
方法还可以接受一个函数作为第二个参数,用来对每个元素进行遍历,功能类似 map
方法。
from
会将第一个参数指定的 TypedArray
数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式。
1 | Int16Array.from(Int8Array.of(127, 126, 125), (x) => 2 * x); |
复合视图
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
1 | const buffer = new ArrayBuffer(24); |
DataView 视图
如果一段数据包括多种类型,这时除了建立 ArrayBuffer
对象的复合视图以外,还可以通过 DataView
视图进行操作,DataView
视图提供更多操作选项,而且支持设定字节序。
本来,在设计目的上,
ArrayBuffer
对象的各种TypedArray
视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView
视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
DataView
构造函数接受一个 ArrayBuffer
对象作为参数,生成视图。
1 | new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]); |
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var
命令和 function
命令。ES6 添加了 let
命令,const
命令,import
命令和 class
命令,所以截至目前,ES6 一共有 6 种声明变量的方法。
var
命令和 function
命令声明的全局变量,是顶层对象的属性,而 let
命令,const
命令,class
命令声明的全局变量,不属于顶层对象的属性。
- 浏览器里面,顶层对象是
window
,但 Node 和 Web Worker 没有window
。
- 浏览器和 Web Worker 里面,
self
也指向顶层对象,但是 Node 没有self
。 - Node 里面,顶层对象是
global
,但其他环境都不支持。
现在有一个提案,在语言标准的层面,引入
globalThis
作为顶层对象。也就是说,任何环境下,globalThis
都是存在的,都可以从它拿到顶层对象,指向全局环境下的this
。