Set & Map

1. Set

  1. 解释:类似数组,但是成员值唯一的数据结构。成员值重复判断的算法为 Same-value equality,类似 === 运算符。

    注:Same-value equality 认为 NaN 等于自身,但是 === 认为 NaN 不等于自身。

  2. 初始化

    • 方法一:创建空 Set,使用 .add() 添加成员

      1
      2
      const 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]);
  3. 实例属性和方法

    • 属性

      • .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) 使用回调函数遍历每个成员。
  4. 应用场景:数组去重。

    1
    2
    3
    let array = [1, 2, 3, 4, 5, 5, 5, 5];
    array = [...new Set(array)]; // 方法一(...扩展运算符内部使用了 for...of 循环遍历 Set 结构)
    array = Array.from(new Set(array)); // 方法二

2. WeakSet

  1. 解释:与 Set 类似,但是成员类型只能是对象,且为弱引用,即只要其他对象都不引用该对象,那么 GC 会自动回收该对象所占用的内存,而不考虑该对象是否还存在于 WeakSet 之中。由于 WeakSet 内部有多少个成员,取决于 GC 有没有运行,运行前后很可能成员个数是不一样的,而 GC 何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历

  2. 与 Set 的区别

    • 没有 .size 属性。
    • 没有 .clear() 方法。
    • 没有 .keys().values() 等遍历方法。
  3. 应用场景

    • DOM 节点的存储。当 DOM 节点从文档移除时,不用担心会引发内存泄漏。

    • 确保实例方法只能在实例上调用。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      const foos = new WeakSet()
      class Foo {
      constructor() {
      foos.add(this)
      }
      method () {
      if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在 Foo 的实例上调用!');
      }
      }
      }

3. Map

  1. 解释:类似对象,但加强版,允许任意类型的值作为键的数据结构。

    注:对于 number、string、boolean 类型的键,Map 需要通过严格相等判断是否为同一个键;true 和 ‘true’ 被认为是两个不同的键;undefined 和 null 被认为是两个不同的键;NaN 及其自身被认为是相同的键。

  2. 初始化

    • 方法一:创建空 Map,使用 .set(key, value) 添加键值对

      1
      2
      3
      const map1 = new Map();
      const person = { name: '张三', age: 19 };
      map1.set(person, `${person.name}-${person.age}`);
    • 方法二:接受数组(成员为长度为 2 的数组)或实现了 Iterable 接口的数据结构(成员为长度为 2 的数组)作为参数,创建 Map

      1
      2
      3
      4
      const map2 = new Map([
      ['name', '张三'],
      ['age', 19]
      ]);
  3. 实例属性和方法

    • 属性

      • .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

  1. 解释:与 Map 类似,但是键的类型只能是对象(null 除外),且为弱引用,即只要所引用的对象的其他引用都被清除,GC 就会释放该对象所占用的内存,WeakMap 里面的键名对象和所对应的键值对会自动消失。

    注:WeakMap 的键值为正常引用,不会因为键名被回收而被消除。

  2. 与 Map 的区别

    • 没有 .size 属性。
    • 没有 .clear() 方法。
    • 没有 .keys().values() 等遍历方法。
  3. 应用场景

    • 为 DOM 节点附加描述信息。当 DOM 节点从文档移除时,不用担心会引发内存泄漏。

      1
      2
      3
      4
      5
      6
      7
      let 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
      21
      const _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. 简要概述

  1. 解释:Reflect 是 ES6 引入的一个新的全局对象,提供了一系列用于操作对象的方法。

  2. 设计目的

    • 将一些明显属于语言内部的方法(如 Object.defineProperty)放到 Reflect 对象上。

    • 修改某些 Object 方法的返回结果,使其更合理。如 Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一 个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false

    • 让 Object 操作都变成函数行为。如 name in objdelete obj[name] ,而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为。

    • 保持与 Proxy 对象的方法一一对应。Proxy 对象修改默认行为,而 Reflect 对象执行默认行为。

2. 静态方法

  1. Reflect.get(target, name[, receiver])

    • 历史方法 target[name]

    • 使用说明

      • 查找并返回 target 对象的 name 属性,如果没有该属性,则返回 undefined
      • receiver 用于指定 target 对象中的 getter 属性的 this
      • 如果第一个参数不是对象, Reflect.get 方法会报错。
  2. Reflect.set(target, name, value[, receiver])

  • 历史方法 target[name] = value
  • 使用说明
    • 设置 target 对象的 name 属性等于 value
    • receiver 用于指定 target 对象中的 setter 属性的 this
    • 如果第一个参数不是对象, Reflect.set 会报错。
    • Reflect.set(需要使用 receiver 参数)会触发 Proxy.defineProperty 拦截。
  1. Reflect.has(obj, name): boolean
  • 历史方法 name in obj
  • 使用说明
    • 检查指定的属性是否存在于指定的对象或其原型链中,包括不可枚举属性。
    • 如果第一个参数不是对象, Reflect.hasin 运算符都会报错。
  1. Reflect.deleteProperty(target, name): boolean
  • 历史方法 delete obj[name]
  • 使用说明:删除对象的属性,返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回 true ; 删除失败,被删除的属性依然存在,返回 false
  1. Reflect.construct(target, args)
  • 历史方法 new target(...args)
  • 使用说明:提供了一种不使用 new ,来调用构造函数的方法。
  1. Reflect.getPrototypeOf(obj)
  • 历史方法 Object.getPrototypeOf(obj)
  • 使用说明
    • 读取对象的 __proto__ 属性。
    • Reflect.getPrototypeOfObject.getPrototypeOf 的一个区别是,如果参数不是对象, Object.getPrototypeOf 会将这个参数转为对象,然后再运行, 而 Reflect.getPrototypeOf 会报错。
  1. Reflect.setPrototypeOf(obj, newProto)
  • 历史方法 Object.setPrototypeOf(obj, newProto)
  • 使用说明
    • 设置对象的 __proto__ 属性,返回第一个参数对象。
    • 如果第一个参数不是对象, Object.setPrototypeOf 会返回第一个参数本身, 而 Reflect.setPrototypeOf 会报错。
    • 如果第一个参数 是 undefinednullObject.setPrototypeOfReflect.setPrototypeOf 都会报错。
  1. 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 方法。
  2. Reflect.defineProperty(target, propertyKey, attributes): boolean

    • 历史方法 Object.defineProperty(target, propertyKey, attributes)
    • 使用说明
      • 为对象定义属性。
      • 如果 Reflect.defineProperty 的第一个参数不是对象,就会抛出错误。
      • Reflect.defineProperty 返回一个布尔值,表示属性是否定义成功。Object.defineProperty 在失败时会抛出异常。
  3. Reflect.getOwnPropertyDescriptor(target, propertyKey)

    • 历史方法 Object.getOwnPropertyDescriptor(target, propertyKey)
    • 使用说明
      • 得到指定属性的描述对象。
      • 如果第一个参数不是对象, Object.getOwnPropertyDescriptor 不报错,返回 undefined ,而 Reflect.getOwnPropertyDescriptor 会抛出错误,表示参数非法。
  4. Reflect.isExtensible(target): boolean

    • 历史方法 Object.isExtensible(target)
    • 使用说明
      • 返回一个布尔值, 表示当前对象是否可扩展(即是否可以添加新属性)。
      • 如果参数不是对象,Object.isExtensible 会返回 false ,因为非对象本来就是不可扩展的,而 Reflect.isExtensible 会报错。
  5. Reflect.preventExtensions(target): boolean

    • 历史方法 Object.preventExtensions(target)
    • 使用说明
      • 让一个对象变为不可扩展,返回一个布尔值,表示是否操作成功。
      • 如果参数不是对象, Object.preventExtensions 在 ES5 环境报错,在 ES6 环境返回传入的参数,而 Reflect.preventExtensions 会报错。
  6. Reflect.ownKeys(target)

    • 历史方法 Object.getOwnPropertyNames + Object.getOwnPropertySymbols
    • 使用说明
      • 返回对象的所有属性(包括不可枚举属性)。

3. 应用:观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const queuedObservers = new Set();
const observe = (fn) => queuedObservers.add(fn);
const observable = (obj) => new Proxy(obj, { set });
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach((observer) => observer());
return result;
}

const person = observable({ // 观察目标
name: "张三",
age: 20,
});
function print() { // 观察者
console.log(`${person.name}, ${person.age}`);
}
observe(print); // 一旦观察目标改变,观察者对应的函数会被调用
person.name = "李四";

Proxy

1. 简要概述

  1. 解释:为特定对象设置“拦截”的数据结构,可以对外界对该对象的访问进行过滤或改写。

  2. 实例化

    1
    const proxy = new Proxy(target, handler);

    注:这里的 target 表示要拦截的目标对象,handler 参数用来定制拦截行为。当对 proxy 对象进行操作时,handler 中定义的拦截行为才会生效。

2. 拦截操作

  1. get(target, propKey, receiver)

    • 解释:拦截对象属性的读取,如 proxy.fooproxy['foo']

    • 使用说明:如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错。

    • 应用

      • 访问目标对象不存在的属性时抛错。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        var 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
        14
        function 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
        24
        var 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
        28
        const 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);
  2. set(target, propKey, value, receiver)

    • 解释:拦截对象属性的设置,如 proxy.foo = vproxy['foo'] = v

    • 应用

      • 限制对象属性的取值范围,不满足要求时报错。

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

      • 禁止内部属性(如以下划线开头的字段)被读取或修改。

  3. has(target, propKey): boolean

    • 解释:拦截 propKey in proxy(包括不可枚举和原型链上的属性)的操作,返回一个布尔值。
    • 使用说明
      • 如果原对象不可配置或者禁止扩展,这时 has 拦截会报错。
      • 虽然 for...in 循环也用到了 in 运算符,但是 has 拦截 对 for...in 循环不生效。
  4. deleteProperty(target, propKey): boolean

    • 解释:拦截 delete proxy[propKey] 的操作,返回一个布尔值。
    • 使用说明:如果这个方法抛出错误或者返回 false ,当前属性就无法被 delete 命令删除。
  5. 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 方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
  6. getOwnPropertyDescriptor(target, propKey)

    • 解释:拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  7. defineProperty(target, propKey, propDesc): boolean

    • 解释:拦截 Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
    • 使用说明
      • 如果目标对象不可扩展(extensible),则 defineProperty 不能增加目标 对象上不存在的属性,否则会报错。
      • 如果目标对象的某个属性不可写 (writable)或不可配置(configurable),则 defineProperty 方法不得改变这两个设置。
  8. preventExtensions(target): boolean

    • 解释:拦截 Object.preventExtensions(proxy),返回一个布尔值。
    • 使用说明:只有目标对象不可扩展时(即 Object.isExtensible(proxy)false),proxy.preventExtensions 才能返回 true,否则会报错。为了防止出现这个问题,通常要在 proxy.preventExtensions 方法里面,调用一 次 Object.preventExtensions
  9. getPrototypeOf(target)

    • 解释:拦截 Object.getPrototypeOf(proxy) ,返回一个对象。
    • 使用说明:可拦截的情况共包括以下几种情况,
      • Object.prototype.__proto__
      • Object.prototype.isPrototypeOf()
      • Object.getPrototypeOf()
      • Reflect.getPrototypeOf()
      • instanceof
  10. isExtensible(target): boolean

    • 解释:拦截 Object.isExtensible(proxy) ,返回一个布尔值。
    • 使用说明:该方法的返回值必须与目标对象的 isExtensible 属性保持 一致,否则就会抛出错误。即 Object.isExtensible(proxy) === Object.isExtensible(target)
  11. setPrototypeOf(target, proto): boolean

    • 解释:拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。
    • 使用说明:如果目标对象不 可扩展(extensible),setPrototypeOf 方法不得改变目标对象的原型。
  12. apply(target, object, args)

    • 解释:拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  13. construct(target, args)

    • 解释:拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)
    • 使用说明:construct 方法返回的必须是一个对象,否则会报错。

3. 可取消的 Proxy

  1. 解释:Proxy.revocable() 方法可以用来创建一个可撤销的代理对象。一旦通过调用 revoke 函数撤销了代理,该代理将变得不可用,任何后续对它的操作(如读取、设置属性)都会抛出 TypeError
  2. 作用:精细控制代理对象的生命周期。
  3. 语法:let { proxy, revoke } = Proxy.revocable(target, handler);
    • proxy:Proxy 实例,可以基于拦截规则 handler,拦截对目标对象 target 的访问。
    • revoke:一个不带参数的函数,用于取消该 proxy 实例。

4. this 问题

  1. 解释:当通过 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
    21
    const 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
  2. 解决:为了确保通过代理调用的方法内部的 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
    25
    const 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...inObject.keys() 的结果中)③ 字符串属性 vs. Symbol 属性(即属性的键是字符串还是 ES6 的 Symbol)④ 安全性与健壮性(即对象通过 Object.create(null) 创建时,检测方法是否仍然可靠;对象自身是否可以重写检测方法,如 hasOwnProperty

  1. Object.prototype.hasOwnProperty.call(obj, prop): boolean 关注对象自身独有的属性
    • 自有属性 ✅;继承属性 ❌
    • 可枚举属性 ✅;不可枚举属性 ✅
    • 字符串属性 ✅;Symbol 属性 ✅
    • 安全性与健壮性 ✅
  2. Reflect.ownKeys(obj).includes(prop) 关注对象自身独有的属性
    • 自有属性 ✅;继承属性 ❌
    • 可枚举属性 ✅;不可枚举属性 ✅
    • 字符串属性 ✅;Symbol 属性 ✅
    • 安全性与健壮性 ✅
  3. prop in obj 关注可访问的属性
    • 自有属性 ✅;继承属性 ✅
    • 可枚举属性 ✅;不可枚举属性 ✅
    • 字符串属性 ✅;Symbol 属性 ✅
    • 安全性与健壮性 ✅
  4. Reflect.has(obj, prop) 关注可访问的属性(in 操作符的上位替代)
    • 自有属性 ✅;继承属性 ✅
    • 可枚举属性 ✅;不可枚举属性 ✅
    • 字符串属性 ✅;Symbol 属性 ✅
    • 安全性与健壮性 ✅
  5. Object.keys(obj).includes(prop) 关注对象自身独有、可枚举的字符串属性
    • 自有属性 ✅;继承属性 ❌
    • 可枚举属性 ✅;不可枚举属性 ❌
    • 字符串属性 ✅;Symbol 属性 ❌
    • 安全性与健壮性 ✅
  6. Object.getOwnPropertyNames(obj).includes(prop) 关注对象自身独有的字符串属性
    • 自有属性 ✅;继承属性 ❌
    • 可枚举属性 ✅;不可枚举属性 ✅
    • 字符串属性 ✅;Symbol 属性 ❌
    • 安全性与健壮性 ✅
  7. 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
    11
    function 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
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// --- 1. 模拟数据库 ---
// 在真实场景中,这里会是一个数据库连接池和查询执行器
const db = {
users: [
{ id: 1, username: 'alice', email: 'alice@example.com', age: 30 },
{ id: 2, username: 'bob', email: 'bob@example.com', age: 25 },
],
products: [
{ id: 101, name: 'Laptop', price: 1200 },
{ id: 102, name: 'Mouse', price: 25 },
],

// 模拟执行 SQL 查询的函数
execute(sql) {
console.log(`[DB] executing: ${sql}`);
// 这是一个非常简化的模拟器,并非一个真正的SQL解析器
// 仅用于演示
if (sql.includes("FROM users WHERE username = 'alice'")) {
return this.users.filter(u => u.username === 'alice');
}
if (sql.includes("FROM users")) {
return this.users;
}
return [];
}
};


// --- 2. ORM 模型创建函数 ---
function createModel(tableName) {
// Proxy 的目标对象可以是空的,因为所有操作都将被 get 陷阱拦截
return new Proxy({}, {

// --- 3. Proxy 的 get 拦截器 ---
// 这是所有魔法发生的地方
get(target, propKey, receiver) {
console.log(`[Proxy] 拦截到属性访问: ${String(propKey)}`);

// 匹配 findBy<Field> 模式, e.g., "findByUsername"
const findByMatch = String(propKey).match(/^findBy([A-Z]\w*)$/);

if (findByMatch) {
// 从 "findByUsername" 中提取 "Username"
const fieldName = findByMatch[1];
// 将字段名转换为数据库列名 (e.g., "Username" -> "username")
const columnName = fieldName.toLowerCase();

// 返回一个函数,这个函数将接收查询值
return function(value) {
// 动态构建 SQL 语句
const sql = `SELECT * FROM ${tableName} WHERE ${columnName} = '${value}';`;
// 执行查询并返回结果
return db.execute(sql);
}
}

// 匹配 findAll 模式
if (propKey === 'findAll') {
return function() {
const sql = `SELECT * FROM ${tableName};`;
return db.execute(sql);
}
}

// 如果没有匹配的模式,可以返回 undefined 或抛出错误
console.warn(`[Proxy] ${String(propKey)} 不是一个支持的 ORM 方法。`);
return undefined;
}
});
}

// --- 使用我们简单的 ORM ---

console.log("--- 创建 Users 模型 ---");
const Users = createModel('users');

console.log("\n--- 调用 findByUsername ---");
const foundUsers = Users.findByUsername('alice');
console.log("查询结果:", foundUsers);

console.log("\n--- 调用 findAll ---");
const allUsers = Users.findAll();
console.log("查询结果:", allUsers);

console.log("\n--- 尝试一个不支持的方法 ---");
Users.deleteEverything();

Iterator

核心理解:ITERATOR 为数据结构提供了一个统一的访问机制的接口。

1. 简要概述

  1. 解释:Iterator(遍历器)是一种为各种不同的数据结构提供统一的访问机制的接口。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

  2. Iterator 的作用

    • 为各种数据结构,提供一个统一的、简便的访问接口
    • 使得数据结构的成员能够按某种次序排列
    • for...of 循环提供支持
  3. Iterator 的遍历原理:Iterator 是一个指针对象,最初指向数据结构的起始位置。它具有一个 next 方法。每次调用 next 方法时,指针会移动到下一个成员,并返回一个对象,其中包含两个属性:value 表示当前成员的值,done 是一个布尔值,用于指示遍历是否已结束。

  4. Iterator 接口的类型定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface 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

  1. 解释:只要一个数据结构具有 Symbol.iterator 属性,就可以被视为“可遍历的”(iterable),这意味着该数据结构部署了 Iterator 接口Symbol.iterator 属性是一个函数,它是该数据结构默认的迭代器生成函数。调用这个函数会返回一个迭代器。

  2. 遍历方式:对于一个可遍历的数据结构 ITERABLE,有以下两种方式遍历其中的数据成员,

    • for...of

      1
      2
      3
      for (let x of ITERABLE) {
      console.log(x);
      }
    • while

      1
      2
      3
      4
      5
      6
      7
      var $iterator = ITERABLE[Symbol.iterator]();
      var $result = $iterator.next();
      while (!$result.done) {
      var x = $result.value;
      console.log(x);
      $result = $iterator.next();
      }
  3. 原生部署 Iterator 接口的数据结构:Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象

    对象没有原生部署 Iterator 接口,因为对象的数据是无序的。因此,给对象部署遍历器接口,就相当于部署一种线性变换

    for...of 遍历字符串时,甚至会正确识别 32 位的 UTF-16 字符。

    1
    2
    3
    for (let x of 'a\uD83D\uDC0A\u9CC4\u9C7C') {
    console.log(x); // "a","🐊","鳄","鱼"
    }
    1
    2
    3
    4
    5
    6
    const 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 接口部署

  1. 解释:为了让数据结构是可遍历的,就要为其部署 Iterator 接口,即在数据结构的 Symbol.iterator 属性上添加遍历器生成方法(原型链上添加也奏效)。

  2. Iterator 接口部署示例

  • 对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let 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
    17
    class 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
    7
    let iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };

    如果一个对象存在数值键名和 length 属性,就称之为类数组对象,此时部署 Iterator 接口时可以直接引用数组的 Iterator 接口

  1. 使用说明

    • 普通对象部署数组的 Symbol.iterator 方法,并无效果。
    • 如果 Symbol.iterator 方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

2.3 调用场合

  1. 数组和 Set 的解构赋值

    1
    2
    3
    4
    5
    const 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"
  2. 扩展运算符 ...(只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组)

    1
    2
    3
    4
    5
    const 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]
  3. yield*

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const 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 }
  4. 数组的遍历(包括以数组作为参数的场合)

    • 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 结构

  1. 解释:Iterator 本质上是一个对象,必须包含 next() 方法,可选包含 return()throw() 方法。

    • next():用于遍历数据结构中的所有数据成员,返回值是一个对象,其结构为 {value: any, done: boolean},其中 value 表示当前遍历的数据成员,done 表示数据结构是否遍历完毕。
    • return():当 for...of 循环因为 breakthrow 语句提前退出时,就会调用该方法。
    • throw():主要配合 Generator 函数使用,详见下述关于 Generator 函数的笔记。
  2. 示例

    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
    40
    const 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 循环的优点。

  1. for 循环

    • 写法比较麻烦
  2. 数组的 forEach 方法

    • 无法中途跳出 forEach 循环
  3. for...in 循环(用于遍历键名)

    • 遍历数组时,遍历的键名是索引数字对应的字符串

      1
      2
      3
      4
      const arr = ['boo', 'foo', 'zoo'];
      for(let idx in arr){
      console.log(idx); // '0', '1', '2'
      }
    • 会遍历原型上的键名

      1
      2
      3
      4
      5
      6
      7
      8
      9
      const obj1 = {
      name:'Jack',
      age:19
      }
      const obj2 = Object.create(obj1);
      obj2.gender = 'Male';
      for(let key in obj2){
      console.log(key); // "gender", "name", "age"
      }
    • 会以任意顺序遍历键名

  4. for...of 循环(用于遍历可遍历对象的数据成员)

    • 简洁的语法
    • 有序遍历
    • 可中途跳出循环(与 breakcontinue 配合使用)
    • 遍历所有数据结构的统一接口

Generator 函数

核心理解:GENERATOR 的执行结果是 ITERATOR,同时这个 ITERATOR 可以看作是 GENERATOR 的实例。

1. 简要概述

  1. 解释:Generator 函数是 ES6 提供的一种异步编程解决方案,可以从以下两种角度来理解,

    • 语法上
      • Generator 函数是一个状态机,封装了多个内部状态。
      • Generator 函数是一个遍历器生成函数,执行后返回一个遍历器,可以依次遍历 Generator 函数内部的每一个状态。
    • 形式上
      • Generator 函数使用 function* 来定义,即 function 关键字和函数名之间有一个星号 *
      • Generator 函数内部使用 yield 表达式定义不同的内部状态。
  2. Generator 函数的执行流程

    • 调用 Generator 函数时,该函数不会立即执行,而是返回一个遍历器对象,该对象是指向函数内部的指针。

    • 每次调用遍历器对象的 next() 方法时,Generator 函数会从上次暂停的地方继续执行,直到遇到 yield 表达式或 return 语句。next() 方法返回一个对象,其结构为 {value: any, done: boolean}。其中,value 表示 Generator 函数当前执行位置的值(即 yieldreturn 后的表达式的值),而 done 是一个布尔值,指示遍历是否已完成。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function* 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 }
  3. 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
        3
        function* demo() {
        console.log('Hello' + (yield 123));
        }
      • yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

        1
        2
        3
        4
        function* demo() {
        foo(yield 'a', yield 'b');
        let input = yield;
        }
  4. 使用说明

    • 可以把 Generator 函数赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口,使其成为可遍历对象。

    • Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator 属性,执行后返回自身。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      function* 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
      17
      function* 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
      5
      let obj = {
      * myGeneratorMethod() {
      ···
      }
      };
    • Generator 函数执行返回的遍历器是 Generator 函数的实例,继承了 Generator 函数的 prototype 对象上的方法。

2. next 方法的参数

  1. 解释:yield 表达式本身没有返回值,或者说总是返回 undefinednext 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* 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 }
  2. 使用说明

    • 通过 next() 方法的参数,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
    • 第一次调用 next() 方法时,传递参数是无效的。

3. Generator 函数的原型方法

Generator 函数的执行结果是一个遍历器,可以被视为 Generator 函数的实例对象。因此,下述的 throw()return() 方法都是遍历器对象所调用的方法。

3.1 Generator.prototype.throw()

  1. 解释:在 Generator 函数体外抛出错误,该错误可以在 Generator 函数体内被捕获。

  2. 语法:integrator.throw(e)

    • e:被抛出的错误,建议为 Error 对象的实例。
  3. 使用说明

    • throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法,否则,由于 Generator 函数还没有开始执行,throw 方法抛出的错误只能抛出在函数外部。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var 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
      16
      var 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 属性等于 undefineddone 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

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

  1. 解释:在 Generator 函数体外返回给定的值,并且终结遍历 Generator 函数。

  2. 语法:integrator.return(value)

    • value:返回的值。如果不提供该参数,则默认 value = undefined
  3. 使用说明

    • 如果 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
      20
      var 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* 表达式

  1. 解释:在 Generator 函数中使用 yield* 表达式,相当于委托执行另一个遍历器对象或可遍历对象的所有元素。yield* 会自动遍历该对象的所有元素,并对每个元素单独使用 yield 表达式进行产出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function* 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
  2. 语法

    • yield* 遍历器对象
    • yield* 可遍历对象
  3. 使用说明

    • 如果 yield* 后面跟随的是一个 Generator 函数的遍历器,并且该 Generator 函数包含 return 语句,那么可以使用 var value = yield* iterator 的形式来获取 return 语句返回的值

5. Generator 与协程

  1. 协程(coroutine):一种程序运行的方式,可以理解成 “协作的线程” 或 “协作的函数”。
  2. 协程的实现方式
    • 单线程实现:特殊的子例程(subroutine)
    • 多线程实现:特殊的线程
  3. 协程 Vs 子例程
    • 子例程
      • 一个调用栈。
      • 堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。
    • 协程
      • 多个调用栈。
      • 多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。
  4. 协程 Vs 普通线程
    • 普通线程:同一时间可以有多个线程处于运行状态。
    • 协程:同一时间运行的协程只能有一个,其他协程都处于暂停状态。
  5. Generator 函数 - 半协程:Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。此外,Generator 使用 yield 表达式交换控制权。
  6. Generator 函数 - 上下文:Generator 函数执行产生的上下文环境,一旦遇到 yield 命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行 next 命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

ArrayBuffer

1. 简要概述

  1. 解释:ArrayBuffer 对象代表内存之中的一段二进制数据,开发者可以通过视图TypedArrayDataView)进行操作。由于视图部署了数组接口,因此可以使用数组的方法操作内存ArrayBuffer 对象、TypedArray 视图和 DataView 视图统称为二进制数组

  2. 视图: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

  1. 解释:ArrayBuffer 对象代表内存之中的一段二进制数据,仅支持通过视图进行读写操作。

  2. 基本使用

    • ArrayBuffer 的创建:指定连续的内存区域大小为 byteLength,同时每个字节的默认值为 0。

      1
      new ArrayBuffer(byteLength)
    • ArrayBuffer 的操作(通过视图)

      • DataView 视图:指定 ArrayBuffer 实例为参数创建视图实例。

        1
        2
        const dataView = new DataView(buffer);
        console.log(dataView.getInt8(0));
      • TypedArray 视图:指定 ArrayBuffer 实例为参数创建视图实例。

        1
        2
        const int8Array = new Int8Array(buffer);
        console.log(int8Array.length);

        注:与 DataView 视图不同,TypedArray 视图是一组构造函数,代表不同的数据格式

      • TypedArray 视图:指定 Array 实例为参数创建视图实例,此时会隐含地创建一个 ArrayBuffer 对象。

        1
        2
        const int8Array2 = new Int8Array([1, 2, 3]);
        console.log(int8Array2.length);
  3. ArrayBuffer 的属性和方法

    • ArrayBuffer.prototype.byteLength: number 返回 ArrayBuffer 所分配的内存区域的字节长度。内存区域要求连续,因此大内存区域可能会分配失败,因此可以通过如下方法检测内存是否分配成功

      1
      2
      3
      4
      5
      if (buffer.byteLength === n) {
      // 成功
      } else {
      // 失败
      }
    • ArrayBuffer.prototype.slice(start[, end]): ArrayBuffer 创建一个新的 ArrayBuffer 对象,其内存区域数据拷贝自原 ArrayBuffer 内存区域 [start end) 部分的字节数据。这里的 startend 是字节序号,如果不指定 end,则拷贝原 ArrayBufferstart 字节开始的所有字节数据。

    • ArrayBuffer.isView(obj): boolean 表示参数 obj 是否为 ArrayBuffer 的视图实例,即是否为 TypedArray 实例或 DataView 实例。

  4. Conversion:ArrayBuffer 及对应的 TypedArray <–> string

    • ArrayBuffer 及其对应的 TypedArray --> stringTextDeocder

      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 及其对应的 TypedArrayTextEncoder

      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 概念理解

  1. 视图(view):即对 ArrayBuffer 对象对应内存区域的字节数据的解读方式,分为 TypedArray 视图(将所有数组成员都解读为同一种数据类型)、DataView 视图(将所有数组成员都解读为不同的数据类型)。

  2. 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 个字节。
  3. TypedArray Vs. Array

    差异点 TypedArray Array
    数组成员类型是否相同
    数组成员在内存空间是否连续 不一定(密集数组连续,稀疏数组不连续)
    数组成员的默认值 0 undefined(空位)
    数据存储位置 TypedArray 视图对应的 ArrayBuffer 对象(可通过视图的 buffer 属性获取) Array 对象自身

3.2 构造函数

注:JavaScript 允许基于同一 ArrayBuffer 对象建立多个视图,此时每个视图可以根据自己的理解去修改相应内存区域的数据,同时一个视图对内存的更改会在其他视图上反映出来。

  1. TypedArray(buffer: ArrayBuffer, byteOffset: number=0[, length: number]) 表示从 buffer 的第 byteOffset 个字节开始创建视图,同时确保视图的成员数量为 length。其中 byteOffset0-based 表示,表示视图开始的字节序号,默认值为 0;length 表示视图的成员数量,因此视图对应内存区域的长度 = length * 每个成员字节数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const 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
  2. TypedArray(length) 表示创建一个成员数量为 length 的视图,此时视图实际上创建了一个大小为 length * 视图成员字节数ArrayBuffer。其中 length 表示视图成员数量;视图创建的 ArrayBuffer 可以通过视图的 buffer 属性获取。

    1
    2
    3
    const 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 十六进制表示的每个字节的值)
  3. TypedArray(typedArray) 表示基于给定视图创建一个新的视图,此时新视图对应的 ArrayBuffer 与给定视图的 ArrayBuffer 不是同一个,可以理解为两个 ArrayBuffer 内存区域不同,但是数据相同,即数据发生了拷贝。

    1
    2
    3
    4
    const 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
  4. TypedArray(arrayLikeObject) 表示基于给定类数组对象创建一个视图,此时视图实际上开辟了一个新的 ArrayBuffer,与给定类数组对象无关,即数据发生了拷贝。

    1
    2
    const 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 数组方法

  1. TypedArray 可用的数组方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    TypedArray.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()
  2. 使用说明

    • TypedArray 数组没有 concat 方法,可以通过以下函数实现多个 TypedArray 数组的合并操作。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      function 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
      4
      let ui8 = Uint8Array.of(0, 1, 2);
      for (let byte of ui8) {
      console.log(byte);
      }

3.4 字节序

  1. 大端字节序(Big-Endian):重要的字节排在前边,不重要的字节排在后边。换句话说,高位字节排在内存低位,低位字节排在内存高位。

    • 示例:0x12345678 的大端字节序的存储为 12 34 56 78(内存地址从低到高)。
    • 场景:网络传输(TCP/IP 协议等)。
  2. 小端字节序(Little-Endian,默认):不重要的字节排在前边,重要的字节排在后边。换句话说,低位字节在内存低位,高位字节在内存高位。

    注:TypedArray 视图采取小端字节序读写数据,不支持修改字节序。但是,DataView 视图支持以指定的字节序读写数据。

    • 示例:0x12345678 的小端字节序的存储为 78 56 34 12(内存地址从低到高)。
    • 场景:x86 处理器、Windows 系统。
  3. 字节序判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const 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 属性和方法

  1. 静态属性

    • TypedArray.BYTES_PER_ELEMENT 获取指定视图的数组成员所占据的字节数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Int8Array.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
  2. 实例属性

    • TypedArray.prototype.BYTES_PER_ELEMENTTypedArray.BYTES_PER_ELEMENT 相同。

4. DataView

More in 阮一峰 ES6(第三版)

本贴参考