相关文章

一本书里面内容较多, 因此分成了多篇 Post, 可以从此处看到相关文章:

Metaprogramming

Metaprogramming 意指使用 JS 原生的一些方法实现一些高级的步骤

Symbol

  1. 使用相同的值重新定义 symbol 将会得到两个完全不同的示例 Symbol('a') !== Symbol('a')

  2. Symbol 创建的示例有自己的域 (Local or Global)

  3. 对于 Local Symbol, 只有得到 Symbol 的引用才能使用这个 Symbol

    1. 可以通过 Object.getOwnPropertySymbols 来查看已经定义的 Symbol 但是依然无法访问和修改
    2. Spread Operator 会 copy 对应的 Symbol, 是完全拷贝
    const clone = {……obj}; 
    obj[symFoo] === clone[symFoo]; // true
    
    1. 可以通过 export 来导出对一个 Symbol 的引用
  4. 对于 Global Symbol 可以在任何位置引用到

Symbol Scope

/* Local */
const symFoo = Symbol('foo');
/* Global */
const symFoo = Symbol.for('foo');  /* 注意使用的方法不同 */
Symbol.keyFor(symFoo);// 'foo'

Symbol for Hidden Property

就因为 Symbol 有这些神奇的功能, 所以可以考虑用来定义一个不可见的方法.

const _count = Symbol("count");
class Counter {
  constructor(count) {
    Object.defineProperty(this, _count, { enumerable: false, writable: true });
    this[_count] = count;
  }
  inc(by = 1) {
    return (this[_count] += by);
  }
  dec(by = 1) {
    return (this[_count] -= by);
  }
}

// Outside this class, there's no way to access the internal count property:
const counter = new Counter(1);
counter._count; // undefined 
counter.count; // undefined
counter[Symbol('count')]; // undefined
counter[Symbol.for("count")]; // undefined

另外, 这边需要注意一下通过一些特定的 api (比如 Reflect.ownKeys and Object.getOwnPropertySymbols ) 还是可以找到这个方法.

利用 Global Symbol 实现方法名的一致性

可以定义一个 Symbol, 使得在使用的时候进行判断其是否存在

export const toJson = (obj) => {
  /* 如果 obj 含有一个特定签名的方法则调用此方法, 否则调用默认方法 */
  return isFunction(obj[Symbol.for("toJson")])
    ? obj[Symbol.for("toJson")]()
    : JSON.stringify(obj);
};

Proxy

  • Proxy 对象用于创建一个对象的代理, 从而实现基本操作的拦截和自定义
  • 可以操作对象的属性查找, 赋值, 枚举, 函数调用等.
const obj = {
  a: 1, 
  b: 2, 
};
const proxy = new Proxy(obj, {
  get: function (target, property) {
    return 3;
  }, 
});

obj.a // 1
proxy.a // 3
proxy.abcdef // 3 全部被代理并改写成 3
proxy // {a:1, b:2} 但是这个地方很神奇的, 依然显示 1 和 2
const traceLogHandler = {
  get(target, key) {
    /* target = credentials */
    console.log( `${(new Date())} [TRACE] Calling: ${key}` );
    return target[key];
  }, 
};

const credentials = {
  username: "@luijar", 
  password: "Som3thingR@ndom", 
  login: () => {
    console.log("Logging in……");
  }, 
};

const credentials$Proxy = new Proxy(credentials, traceLogHandler);

credentials$Proxy.login();

Composing Proxy

const passwordObfuscatorHandler = {
  get(target, key) {
    /* 将密码替换成无意义的字符 */
    if (key === "password" || key === "pwd") {
      return "X".repeat(10);
    }
    return target[key];
  },
  has(target, key) {
    /* 将这个属性从 Proxy 中移除 */
    if (key === "password" || key === "pwd") {
      return false;
    }
    return true;
  },
};

credentials$Proxy.password; // 'XXXXXXXXXX'

/* has 方法移除了这个 key */
'password' in credentials$Proxy;

如果希望有多个 Proxy 嵌套那么就可以考虑使用 compose 方法:

const credentials$Proxy = new Proxy(
  new Proxy(credentials, passwordObfuscatorHandler),
  traceLogHandler
);

转换成

const weave = curry((handler, target) => new Proxy(target, handler));


const tracer = weave(traceLogHandler);  // 使用方法: tracer(target)
const obfuscator = weave(passwordObfuscatorHandler); // 使用方法: obfuscator(target)


const credentials$Proxy = compose(tracer, obfuscator)(credentials);
// 或者可以使用更方便,更简洁的 Pipeline Operator:
const credentials$Proxy = credentials |> obfuscator |> tracer;

Reflect

Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible.

  • Reflect 是一个内置对象
  • Reflect 可以劫持一些 JS 的操作
  • Reflect 不是一个函数对象, 不能进行构造, 因此不能通过 new 关键字来使用它.
const handler = (……props) => ({
  /* 在这个 proxy 里面设定 set 方法 */
  set(target, key) {
    /* 如果我 set 了一个特定的 field */
    if (props.includes(key)) {
      debugger

      /* 调用默认的 set 方法 */
      Reflect.set(……arguments);

      /* 使用被代理的 obj 的内置方法, target.data 当做参数传递进去 */
      const value = Reflect.apply(target.test, target, [target.data]);

      /* 给 target 的 value 设置计算后的值 */
      Reflect.set(target, "value", value);
      return true;
    }
  },
});

const block = {
  test: (v) => v + 1,
};

/* 可以看到这个地方首先创建了一个 proxy */
const smartBlock = new Proxy(block, handler("data"));

smartBlock.data = 3; // 如此一来在每一次设置 data 参数的时候就会自动更新 value
smartBlock.value; // 4
smartBlock.data = 4;
smartBlock.value; // 5

Revocable Proxy

  • 这个和原生的 proxy 也没有什么太大的区别, 就多加了一个可以 switch off 的方法
  • 注意无论这个 prox 是开启还是关闭, obj 都无法感知到其存在
const handler = (……props) => ({
  set(target, key) {
    if (props.includes(key)) {
      Reflect.set(……arguments);
      const value = Reflect.apply(target.test, target, [target.data]);
      Reflect.set(target, "value", value);
      return true;
    }
  }, 
});

const block = {
  test: (v) => v + 1, 
};

/* 使用的时候有一些很小的区别, 这里不使用 new 进行构造 */
const smartBlock =  Proxy.revocable(block, handler("data"));

smartBlock.data = 3; // 如此一来在每一次设置 data 参数的时候就会自动更新 value
smartBlock.value; // 4

smartBlock.revoke()

smartBlock.data = 4;
smartBlock.value; // 因为 Proxy 被 revoked, 这里不等于 5 了


Method Decorators

class Counter {
  constructor(count) {
    this[_count] = count;
  }
  inc(by = 0) {
    return (this[_count] += by);
  }
  dec(by = 0) {
    return (this[_count] -= by);
  }
}

const decorator = {
  actions: {
    before: () => {},
    after: () => {},
  },
  methods: [],
};

/* 这个地方是 decrator 一个使用的例子. */
const validation = {
  actions: { before: checkLimit, after: identity },
  methods: ["inc", "dec"],
};

/* 首先这一设计一个简单的 validate 方法 */
const { isFinite, isInteger } = Number;
const checkLimit = (value = 1) =>
  isFinite(value) && isInteger(value) && value >= 0
    ? value
    : throw new RangeError("Expected a positive number");

/* Proxy 的 get 方法无法直接获得到 proxied method 的实际调用参数, 因此这个地方设计了一个 Higher-order function  */
const decorate = (decorator, obj) =>
  new Proxy(obj, {
    get(target, key) {
      if (!decorator.methods.includes(key)) {
        return Reflect.get(……arguments);
      }
      const methodRef = target[key];
      return (……capturedArgs) => {
        /* 在这里进行 before 调用 */
        const newArgs = decorator.actions?.before.call(target, ……capturedArgs);

        const result = methodRef.call(target, ……[newArgs]);

        /* 在这里进行 after 调用 */
        return decorator.actions?.after.call(target, result);
      };
    },
  });

/* 使用 decorate 这个 higher order function */
const counter$Proxy = decorate(validation, new Counter(3));
counter$Proxy.inc(); // 4 
counter$Proxy.inc(3); // 7 
counter$Proxy.inc(-3); // RangeError, 直接终止 inc 方法的执行

上面这一段 decorator 会产生以下这样的调用逻辑:

Proxy 虽然非常的有用,但是需要注意的是, 因为其调用逻辑很复杂, 可能造成其他开发者在这地方调试很久很久, 所以这类 metaProgramming 使用的时候要慎重.

Summary

  • Metaprogramming is the art of using the programming language itself to influence new behavior or to automate code. It can be implemented statically via symbols or dynamically via code weaving.
  • The Symbol primitive data type is used to create unique, collision-free object properties.
  • Symbols make objects extensible by preventing code from accidentally breaking API contracts or the internal workings of an object.
  • You can use symbols to create static hooks that you can use to alter your code』s behavior with regard to fundamental operations such as looping, primitive conversion, and printing.
  • JavaScript ships with native reflection APIs such as Proxy and Reflect. These APIs allow you to weave code into the runtime representation of objects dynamically without polluting their interface with other concerns.
  • JavaScript』s reflection APIs make it easy to develop method decorators, which allow you to implement cross-cutting behavior and modularize sources of duplication in your code.