📖ES6 教程 @阮一峰
Set & Map
1. Set
-
解释:类似数组,但是成员值唯一的数据结构。成员值重复判断的算法为 Same-value equality,类似
===
运算符。注:Same-value equality 认为 NaN 等于自身,但是
===
认为 NaN 不等于自身。 -
初始化
-
方法一:创建空
Set
,使用.add()
添加成员1
2const set = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => set.add(x)); -
方法二:接受数组或实现了 Iterable 接口的数据结构作为参数,创建
Set
1
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
-
-
实例属性和方法
-
属性
-
.constructor
构造函数,即 Set 函数。 -
.size
Set 实例的成员总数。
-
-
操作方法
-
.add(value): Set
添加某个值,返回 Set 实例本身,因此可以链式调用。 -
.delete(value): boolean
删除某个值,返回一个布尔值,表示删除是否成功。 -
.has(value): boolean
返回一个布尔值,表示该值是否为 Set 实例的成员。 -
.clear(): void
清除所有成员,没有返回值。
-
-
遍历方法:Set 的遍历顺序就是插入顺序
注-1:
.keys()
方法和.values()
方法的行为完全一致,因为 Set 的键名和键值是相同的。注-2:遍历器可以使用
for...of
遍历。注-3:Set 实例默认可以遍历,其默认遍历器生成函数即其
.values()
方法。注-4:可以对
.key()
、.values()
、.entries()
返回的遍历器使用扩展运算符...
,快速转换为数组。.keys()
返回键名的遍历器。.values()
返回键值的遍历器。.entries()
返回键值对的遍历器。.forEach((value, key, set) => {}, thisArg)
使用回调函数遍历每个成员。
-
-
应用场景:数组去重。
1
2
3let array = [1, 2, 3, 4, 5, 5, 5, 5];
array = [...new Set(array)]; // 方法一(...扩展运算符内部使用了 for...of 循环遍历 Set 结构)
array = Array.from(new Set(array)); // 方法二
2. WeakSet
-
解释:与 Set 类似,但是成员类型只能是对象,且为弱引用,即只要其他对象都不引用该对象,那么 GC 会自动回收该对象所占用的内存,而不考虑该对象是否还存在于 WeakSet 之中。由于 WeakSet 内部有多少个成员,取决于 GC 有没有运行,运行前后很可能成员个数是不一样的,而 GC 何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
-
与 Set 的区别
- 没有
.size
属性。 - 没有
.clear()
方法。 - 没有
.keys()
、.values()
等遍历方法。
- 没有
-
应用场景
-
DOM 节点的存储。当 DOM 节点从文档移除时,不用担心会引发内存泄漏。
-
确保实例方法只能在实例上调用。
1
2
3
4
5
6
7
8
9
10
11const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在 Foo 的实例上调用!');
}
}
}
-
3. Map
-
解释:类似对象,但加强版,允许任意类型的值作为键的数据结构。
注:对于 number、string、boolean 类型的键,Map 需要通过严格相等判断是否为同一个键;true 和 ‘true’ 被认为是两个不同的键;undefined 和 null 被认为是两个不同的键;NaN 及其自身被认为是相同的键。
-
初始化
-
方法一:创建空
Map
,使用.set(key, value)
添加键值对1
2
3const map1 = new Map();
const person = { name: '张三', age: 19 };
map1.set(person, `${person.name}-${person.age}`); -
方法二:接受数组(成员为长度为 2 的数组)或实现了 Iterable 接口的数据结构(成员为长度为 2 的数组)作为参数,创建
Map
1
2
3
4const map2 = new Map([
['name', '张三'],
['age', 19]
]);
-
-
实例属性和方法
-
属性
.constructor
构造函数,即 Map 函数。.size
性返回 Map 结构的成员总数
-
操作方法
.set(key, value): Map
设置键名 key 对应的键值为 value ,然后返回整个 Map 结构,因此可以链式调用。如果 key 已经有值,则键值会被更新,否则就新生成该键。.get(key): any
法读取 key 对应的键值,如果找不到 key ,返回 undefined。.has(key): boolean
返回一个布尔值,表示某个键是否在当前 Map 对象之中。.delete(key): boolean
删除某个键,返回 true。如果删除失败,返回 false。.clear(): void
清除所有成员,没有返回值。
-
遍历方法:Map 的遍历顺序就是插入顺序
注-1:遍历器可以使用
for...of
遍历。注-2:Map 实例默认可以遍历,其默认遍历器生成函数即其
.entries()
方法。注-3:可以对
.key()
、.values()
、.entries()
返回的遍历器使用扩展运算符...
,快速转换为数组。.keys()
返回键名的遍历器。.values()
返回键值的遍历器。.entries()
返回键值对的遍历器。.forEach((value, key, map) => {}, thisArg)
使用回调函数遍历每个成员。
-
4. WeakMap
-
解释:与 Map 类似,但是键的类型只能是对象(null 除外),且为弱引用,即只要所引用的对象的其他引用都被清除,GC 就会释放该对象所占用的内存,WeakMap 里面的键名对象和所对应的键值对会自动消失。
注:WeakMap 的键值为正常引用,不会因为键名被回收而被消除。
-
与 Map 的区别
- 没有
.size
属性。 - 没有
.clear()
方法。 - 没有
.keys()
、.values()
等遍历方法。
- 没有
-
应用场景
-
为 DOM 节点附加描述信息。当 DOM 节点从文档移除时,不用担心会引发内存泄漏。
1
2
3
4
5
6
7let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false); -
部署私有属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
const c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE
-
Symbol
More in 阮一峰 ES6(第三版)
Reflect
1. 简要概述
-
解释:Reflect 是 ES6 引入的一个新的全局对象,提供了一系列用于操作对象的方法。
-
设计目的
-
将一些明显属于语言内部的方法(如
Object.defineProperty
)放到 Reflect 对象上。 -
修改某些 Object 方法的返回结果,使其更合理。如
Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一 个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。 -
让 Object 操作都变成函数行为。如
name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。 -
保持与 Proxy 对象的方法一一对应。Proxy 对象修改默认行为,而 Reflect 对象执行默认行为。
-
2. 静态方法
-
Reflect.get(target, name[, receiver])
-
历史方法
target[name]
-
使用说明
- 查找并返回
target
对象的name
属性,如果没有该属性,则返回undefined
。 receiver
用于指定target
对象中的getter
属性的this
。- 如果第一个参数不是对象,
Reflect.get
方法会报错。
- 查找并返回
-
-
Reflect.set(target, name, value[, receiver])
- 历史方法
target[name] = value
- 使用说明
- 设置
target
对象的name
属性等于value
。 receiver
用于指定target
对象中的setter
属性的this
。- 如果第一个参数不是对象,
Reflect.set
会报错。 Reflect.set
(需要使用receiver
参数)会触发Proxy.defineProperty
拦截。
- 设置
Reflect.has(obj, name): boolean
- 历史方法
name in obj
- 使用说明
- 检查指定的属性是否存在于指定的对象或其原型链中,包括不可枚举属性。
- 如果第一个参数不是对象,
Reflect.has
和in
运算符都会报错。
Reflect.deleteProperty(target, name): boolean
- 历史方法
delete obj[name]
- 使用说明:删除对象的属性,返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回
true
; 删除失败,被删除的属性依然存在,返回false
。
Reflect.construct(target, args)
- 历史方法
new target(...args)
- 使用说明:提供了一种不使用
new
,来调用构造函数的方法。
Reflect.getPrototypeOf(obj)
- 历史方法
Object.getPrototypeOf(obj)
- 使用说明
- 读取对象的
__proto__
属性。 Reflect.getPrototypeOf
和Object.getPrototypeOf
的一个区别是,如果参数不是对象,Object.getPrototypeOf
会将这个参数转为对象,然后再运行, 而Reflect.getPrototypeOf
会报错。
- 读取对象的
Reflect.setPrototypeOf(obj, newProto)
- 历史方法
Object.setPrototypeOf(obj, newProto)
- 使用说明
- 设置对象的
__proto__
属性,返回第一个参数对象。 - 如果第一个参数不是对象,
Object.setPrototypeOf
会返回第一个参数本身, 而Reflect.setPrototypeOf
会报错。 - 如果第一个参数 是
undefined
或null
,Object.setPrototypeOf
和Reflect.setPrototypeOf
都会报错。
- 设置对象的
-
Reflect.apply(func, thisArg, args)
-
历史方法
Function.prototype.apply.call(func, thisArg, args)
注:这里的
args
为传递给func
的参数数组。 -
使用说明
- 绑定
this
,然后执行给定函数。 - 当函数自定义了
apply
方法时,直接调用fn.apply(thisArg, args)
将无法正确执行函数并绑定this
值。此时应使用Function.prototype.apply.call(func, thisArg, args)
来确保调用原始的apply
方法。
- 绑定
-
-
Reflect.defineProperty(target, propertyKey, attributes): boolean
- 历史方法
Object.defineProperty(target, propertyKey, attributes)
- 使用说明
- 为对象定义属性。
- 如果
Reflect.defineProperty
的第一个参数不是对象,就会抛出错误。 Reflect.defineProperty
返回一个布尔值,表示属性是否定义成功。Object.defineProperty
在失败时会抛出异常。
- 历史方法
-
Reflect.getOwnPropertyDescriptor(target, propertyKey)
- 历史方法
Object.getOwnPropertyDescriptor(target, propertyKey)
- 使用说明
- 得到指定属性的描述对象。
- 如果第一个参数不是对象,
Object.getOwnPropertyDescriptor
不报错,返回undefined
,而Reflect.getOwnPropertyDescriptor
会抛出错误,表示参数非法。
- 历史方法
-
Reflect.isExtensible(target): boolean
- 历史方法
Object.isExtensible(target)
- 使用说明
- 返回一个布尔值, 表示当前对象是否可扩展(即是否可以添加新属性)。
- 如果参数不是对象,
Object.isExtensible
会返回false
,因为非对象本来就是不可扩展的,而Reflect.isExtensible
会报错。
- 历史方法
-
Reflect.preventExtensions(target): boolean
- 历史方法
Object.preventExtensions(target)
- 使用说明
- 让一个对象变为不可扩展,返回一个布尔值,表示是否操作成功。
- 如果参数不是对象,
Object.preventExtensions
在 ES5 环境报错,在 ES6 环境返回传入的参数,而Reflect.preventExtensions
会报错。
- 历史方法
-
Reflect.ownKeys(target)
- 历史方法
Object.getOwnPropertyNames
+Object.getOwnPropertySymbols
- 使用说明
- 返回对象的所有属性(包括不可枚举属性)。
- 历史方法
3. 应用:观察者模式
1 | const queuedObservers = new Set(); |
Proxy
1. 简要概述
-
解释:为特定对象设置“拦截”的数据结构,可以对外界对该对象的访问进行过滤或改写。
-
实例化
1
const proxy = new Proxy(target, handler);
注:这里的
target
表示要拦截的目标对象,handler
参数用来定制拦截行为。当对proxy
对象进行操作时,handler
中定义的拦截行为才会生效。
2. 拦截操作
-
get(target, propKey, receiver)
-
解释:拦截对象属性的读取,如
proxy.foo
、proxy['foo']
。 -
使用说明:如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错。
-
应用
-
访问目标对象不存在的属性时抛错。
1
2
3
4
5
6
7
8
9
10var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" do
es not exist.");
}
}
}); -
允许负数索引访问数组元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
} -
实现函数名的链式调用,前一个函数的返回值作为后一个函数的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var pipe = (function () {
return function (value) {
var funcStack = [];
var oproxy = new Proxy(
{},
{
get: function (pipeObject, fnName) {
if (fnName === "get") {
return funcStack.reduce(function (val, fn) {
return fn(val);
}, value);
}
funcStack.push(window[fnName]);
return oproxy;
},
}
);
return oproxy;
};
})();
var double = (n) => n * 2;
var pow = (n) => n * n;
var reverseInt = (n) => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63 -
生成 DOM 结构的通用函数。
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
28const dom = new Proxy({}, {
get(target, property) {
return function(attrs = {}, ...children) {
const el = document.createElement(property);
for (let prop of Object.keys(attrs)) {
el.setAttribute(prop, attrs[prop]);
}
for (let child of children) {
if (typeof child === 'string') {
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el = dom.div({},
'Hello, my name is ',
dom.a({href: '//example.com'}, 'Mark'),
'. I like:',
dom.ul({},
dom.li({}, 'The web'),
dom.li({}, 'Food'),
dom.li({}, '…actually that\'s it')
)
);
document.body.appendChild(el);
-
-
-
set(target, propKey, value, receiver)
-
解释:拦截对象属性的设置,如
proxy.foo = v
、proxy['foo'] = v
。 -
应用
-
限制对象属性的取值范围,不满足要求时报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于 age 以外的属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator); -
监听数据变化,自动更新 DOM。
-
禁止内部属性(如以下划线开头的字段)被读取或修改。
-
-
-
has(target, propKey): boolean
- 解释:拦截
propKey in proxy
(包括不可枚举和原型链上的属性)的操作,返回一个布尔值。 - 使用说明
- 如果原对象不可配置或者禁止扩展,这时
has
拦截会报错。 - 虽然
for...in
循环也用到了in
运算符,但是has
拦截 对for...in
循环不生效。
- 如果原对象不可配置或者禁止扩展,这时
- 解释:拦截
-
deleteProperty(target, propKey): boolean
- 解释:拦截
delete proxy[propKey]
的操作,返回一个布尔值。 - 使用说明:如果这个方法抛出错误或者返回
false
,当前属性就无法被delete
命令删除。
- 解释:拦截
-
ownKeys(target)
-
解释:拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols (proxy)
、Object.keys(proxy)
,返回一个数组。方法 包含字符串属性 包含 Symbol 属性 包含不可枚举属性 Object.getOwnPropertyNames()
✅ ❌ ✅ Object.getOwnPropertySymbols()
❌ ✅ ✅ Object.keys()
✅ ❌ ❌ 注:使用
Object.keys()
时,Symbol
属性、目标对象上不存在的属性,以及不可枚举的属性会被过滤掉。 -
使用说明
ownKeys
方法返回的数组成员,只能是字符串。- 如果目标对象自身包含不可配置的属性,则该属性必须被
ownKeys
方法返回,否则报错。 - 如果目标对象是不可扩展的(non-extensition),这时
ownKeys
方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
-
-
getOwnPropertyDescriptor(target, propKey)
- 解释:拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。
- 解释:拦截
-
defineProperty(target, propKey, propDesc): boolean
- 解释:拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 - 使用说明
- 如果目标对象不可扩展(extensible),则
defineProperty
不能增加目标 对象上不存在的属性,否则会报错。 - 如果目标对象的某个属性不可写 (writable)或不可配置(configurable),则
defineProperty
方法不得改变这两个设置。
- 如果目标对象不可扩展(extensible),则
- 解释:拦截
-
preventExtensions(target): boolean
- 解释:拦截
Object.preventExtensions(proxy)
,返回一个布尔值。 - 使用说明:只有目标对象不可扩展时(即
Object.isExtensible(proxy)
为false
),proxy.preventExtensions
才能返回true
,否则会报错。为了防止出现这个问题,通常要在proxy.preventExtensions
方法里面,调用一 次Object.preventExtensions
。
- 解释:拦截
-
getPrototypeOf(target)
- 解释:拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 - 使用说明:可拦截的情况共包括以下几种情况,
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
- 解释:拦截
-
isExtensible(target): boolean
- 解释:拦截
Object.isExtensible(proxy)
,返回一个布尔值。 - 使用说明:该方法的返回值必须与目标对象的
isExtensible
属性保持 一致,否则就会抛出错误。即Object.isExtensible(proxy) === Object.isExtensible(target)
。
- 解释:拦截
-
setPrototypeOf(target, proto): boolean
- 解释:拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。 - 使用说明:如果目标对象不 可扩展(extensible),
setPrototypeOf
方法不得改变目标对象的原型。
- 解释:拦截
-
apply(target, object, args)
- 解释:拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。
- 解释:拦截 Proxy 实例作为函数调用的操作,比如
-
construct(target, args)
- 解释:拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。 - 使用说明:
construct
方法返回的必须是一个对象,否则会报错。
- 解释:拦截 Proxy 实例作为构造函数调用的操作,比如
3. 可取消的 Proxy
- 解释:
Proxy.revocable()
方法可以用来创建一个可撤销的代理对象。一旦通过调用revoke
函数撤销了代理,该代理将变得不可用,任何后续对它的操作(如读取、设置属性)都会抛出TypeError
。 - 作用:精细控制代理对象的生命周期。
- 语法:
let { proxy, revoke } = Proxy.revocable(target, handler);
proxy
:Proxy 实例,可以基于拦截规则handler
,拦截对目标对象target
的访问。revoke
:一个不带参数的函数,用于取消该proxy
实例。
4. this 问题
-
解释:当通过 Proxy 实例调用目标对象的方法时,方法内部的
this
会指向 Proxy 实例本身,而不是原始的目标对象。这可能导致以下情况:- 如果方法内部依赖
this
来访问原始对象的属性或方法(尤其是那些未被代理拦截的内部属性或原型链上的方法),可能会出现非预期的行为或错误。 - 直接在目标对象上调用相同方法时,
this
指向目标对象,行为可能与通过代理调用时不同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const target = {
m: function() {
console.log('this === proxy:', this === proxy); // 检查 this 是否指向 proxy
console.log('this === target:', this === target); // 检查 this 是否指向 target
}
};
const handler = {};
const proxy = new Proxy(target, handler);
console.log('直接调用 target.m():');
target.m();
// 输出:
// this === proxy: false
// this === target: true
console.log('\n通过 proxy.m() 调用:');
proxy.m();
// 输出:
// this === proxy: true
// this === target: false - 如果方法内部依赖
-
解决:为了确保通过代理调用的方法内部的
this
正确指向原始的目标对象,可以在get
拦截器中进行处理:- 当访问的属性是一个函数(方法)时,使用
Function.prototype.bind()
将该方法的this
绑定到原始的target
对象上。 - 对于其他类型的属性,可以使用
Reflect.get()
来获取属性值,以保持默认行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const handler = {
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver); // 使用 receiver 来正确处理 getter 等情况
if (typeof value === 'function') {
// 确保只绑定定义在 target 自身或其原型链上的函数
// 并且该函数确实是 target 的一部分 (避免绑定不相关的函数)
return value.bind(target); // 将 this 绑定到原始 target
}
return value;
}
};
const targetDate = new Date('2025-10-20');
const proxyDate = new Proxy(targetDate, handler);
console.log(proxyDate.getFullYear()); // 输出: 2025 (this 正确指向 targetDate)
const customTarget = {
value: 42,
getValue() {
return this.value;
}
};
const proxyCustom = new Proxy(customTarget, handler);
console.log(proxyCustom.getValue()); // 输出: 42 (this 正确指向 customTarget) - 当访问的属性是一个函数(方法)时,使用
5. 对象属性检测总结
为了客观准确地对比一系列检测对象属性的方法,这里重点关注以下维度 ① 自有属性 vs. 继承属性(即属性在对象实例上,还是在原型链上)② 可枚举属性 vs. 不可枚举属性(即属性是否会出现在 for...in
或 Object.keys()
的结果中)③ 字符串属性 vs. Symbol 属性(即属性的键是字符串还是 ES6 的 Symbol)④ 安全性与健壮性(即对象通过 Object.create(null)
创建时,检测方法是否仍然可靠;对象自身是否可以重写检测方法,如 hasOwnProperty
)
Object.prototype.hasOwnProperty.call(obj, prop): boolean
关注对象自身独有的属性- 自有属性 ✅;继承属性 ❌
- 可枚举属性 ✅;不可枚举属性 ✅
- 字符串属性 ✅;Symbol 属性 ✅
- 安全性与健壮性 ✅
Reflect.ownKeys(obj).includes(prop)
关注对象自身独有的属性- 自有属性 ✅;继承属性 ❌
- 可枚举属性 ✅;不可枚举属性 ✅
- 字符串属性 ✅;Symbol 属性 ✅
- 安全性与健壮性 ✅
prop in obj
关注可访问的属性- 自有属性 ✅;继承属性 ✅
- 可枚举属性 ✅;不可枚举属性 ✅
- 字符串属性 ✅;Symbol 属性 ✅
- 安全性与健壮性 ✅
Reflect.has(obj, prop)
关注可访问的属性(in
操作符的上位替代)- 自有属性 ✅;继承属性 ✅
- 可枚举属性 ✅;不可枚举属性 ✅
- 字符串属性 ✅;Symbol 属性 ✅
- 安全性与健壮性 ✅
Object.keys(obj).includes(prop)
关注对象自身独有、可枚举的字符串属性- 自有属性 ✅;继承属性 ❌
- 可枚举属性 ✅;不可枚举属性 ❌
- 字符串属性 ✅;Symbol 属性 ❌
- 安全性与健壮性 ✅
Object.getOwnPropertyNames(obj).includes(prop)
关注对象自身独有的字符串属性- 自有属性 ✅;继承属性 ❌
- 可枚举属性 ✅;不可枚举属性 ✅
- 字符串属性 ✅;Symbol 属性 ❌
- 安全性与健壮性 ✅
Object.getOwnPropertyNames(obj).includes(prop)
关注对象自身独有的 Symbol 属性- 自有属性 ✅;继承属性 ❌
- 可枚举属性 ✅;不可枚举属性 ✅
- 字符串属性 ❌;Symbol 属性 ✅
- 安全性与健壮性 ✅
使用建议:
-
检查自有属性:
Object.prototype.hasOwnProperty.call(obj, prop): boolean
-
检查属性的可访问性:
Reflect.has(obj, prop)
-
最常用的场景(对象自有、可枚举、字符串):
Object.keys(obj).includes(prop)
-
检测属性是对象独有的,还是继承自父类的
1
2
3
4
5
6
7
8
9
10
11function isOwnVsInherited(obj, prop): "own" | "inherited" | "unknown" {
if(Object.prototype.hasOwnProperty.call(obj, prop)) {
return "own"; // 对象自由属性
}
if(Reflect.has(obj, prop)) {
return "inherited"; // 对象原型链上的属性
}
return "unknown" // 不存在的属性
}
6. 应用:数据库 ORM
核心思想:利用 Proxy 动态响应不确定的方法名,将其转换为一种具体的、参数化的操作(无论是 API 调用还是 SQL 查询),从而用极少的代码实现强大的动态功能。
1 | // --- 1. 模拟数据库 --- |
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
既是一个操作,也是一个有返回值的表达式。-
暂停并返回值:
yield 123
会暂停 Generator 函数,并把值123
“产出”到函数外部。接收输入值:当外部调用
.next(inputValue)
时,整个yield
表达式(例如yield 123
)会被inputValue
这个值所替代。
-
-
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注:JavaScript 内置的可迭代对象(如 Array, Map, Set)所返回的迭代器,通常也遵循这个模式。
-
在使用
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 }
-
3.3 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
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
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
相同。
4. DataView
More in 阮一峰 ES6(第三版)