JavaScript 之元编程

本文介绍 JavaScript 元编程中常使用的一些语法和简单应用. 有兴趣的可以参考文末给的链接了解更多的关于 JavaScript 元编程方便的知识.

1. Symbol

class MyClass {
    static [Symbol.hasInstance](lho) {
        return Array.isArray(lho);
    }
}
assert([] instanceof MyClass);

class Collection {
  *[Symbol.iterator]() {
    var i = 0;
    while(this[i] !== undefined) {
      yield this[i];
      ++i;
    }
  }

}
var myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(var value of myCollection) {
    console.log(value); // 1, then 2
}

Symbol 是 ES6 引入的新的数据类型, 应用范围十分广泛.

2. Proxy

一次关于 Proxy 的尝试.

规定要读取环境变量的值用形如 ENV:[A-Z0-9._]+ 的形式表示

let config = {
  "host": "127.0.0.1",
  "name": "admin",
  "pass": "ENV:DB_PASS" // 要求转为环境变量返回
}
config = new Proxy(config, {
  get (target, property) {
    if (property in target) {
      if (/^ENV:[A-Z0-9._]+$/.test(target[property])) {
        return process.env[target[property].slice(4)]
      } else {
        return target[property]
      }
    } else {
      throw new ReferenceError()
    }
  }
})

类似的可以自己约定并使用 Proxy 实现一些特性, 如私有属性规定 _ 开头, 使用 Proxy 拦截请求返回 undefined. 也可以使用 Proxy 实现访问日志.

Proxy 方法有很多, 而且和 Reflect 一一对应.

proxy = new Proxy({}, {
  apply: Reflect.apply,
  construct: Reflect.construct,
  defineProperty: Reflect.defineProperty,
  getOwnPropertyDescriptor: Reflect.getOwnPropertyDescriptor,
  deleteProperty: Reflect.deleteProperty,
  getPrototypeOf: Reflect.getPrototypeOf,
  setPrototypeOf: Reflect.setPrototypeOf,
  isExtensible: Reflect.isExtensible,
  preventExtensions: Reflect.preventExtensions,
  get: Reflect.get,
  set: Reflect.set,
  has: Reflect.has,
  ownKeys: Reflect.ownKeys,
})

另一个例子:

function Foo() {
  return new Proxy(this, {
    get: function (object, property) {
      if (Reflect.has(object, property)) {
        return Reflect.get(object, property);
      } else {
        return function methodMissing() {
          console.log('you called ' + property + ' but it doesn\'t exist!');
        }
      }
    }
  });
}

Foo.prototype.bar = function () {
  console.log('you called bar. Good job!');
}

foo = new Foo();
foo.bar();
//=> you called bar. Good job!
foo.this_method_does_not_exist()
//=> you called this_method_does_not_exist but it doesn't exist

3. Descriptor

const config = require('config-proxy')(require('config'))
console.log(config.Database.pass)
// TypeError: 'get' on proxy: property 'pass' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'ENV:DB_PASS' but got '1231231232131323')

前面的尝试在结合 config 模块使用时会出错,出错原因是 config 的属性的 configurablewritable 都为 false, 在此情况下, 要求必须返回原值.

Object.defineProperty(obj, 'key', {
  __proto__: null,      // 定义原型
  enumerable: false,    // 是否可枚举 (for...in... Object.keys(...))
  configurable: false,  // 是否可重新配置
  writable: false,      // 可否重写(重写无效但不会报错)
  value: undefined,     // 默认值
  get,				   // 定义 getter
  set,                  // 定义 setter
});

4. Reflect

Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target,name,receiver)
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

例子:

Reflect.defineProperty(obj, 'a', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 12345
})
Reflect.getOwnPropertyDescriptor(obj, 'a')
Reflect.deleteProperty(obj, 'a')

结合 Proxy:

function Tree() {
  return new Proxy({}, {
    get: function (target, key, receiver) {
      if (!Reflect.has(target, key)) {
        target[key] = Tree()
      }
      return Reflect.get(target, key, receiver);
    }
  })
}

a = Tree()
a.b.c.d.e.f = 1
console.log(a)    // => Proxy {b: Proxy}
console.log(a.b.c.d.e.f)    // => 1

装逼指南

// 1. call 和 apply 方法
obj.myMethod.apply(obj, args)
Reflect.apply(obj.myMethod, obj, args)
// 2. defineProperty
Object.defineProperty({}, 'foo', {value: 1}) // 无法定义时抛出错误
Reflect.defineProperty({}, 'foo', {value: 1}) // 无法定义时返回 false
// 3. getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor({}, 'foo')
Reflect.getOwnPropertyDescriptor({}, 'foo')
// 4. 删除
delete bar // 命令式操作
Reflect.deleteProperty(foo, 'bar') // 函数式操作
// 5. 存在
"doorbell" in obj // 命令式操作
Reflect.has(obj, "doorbell") // 函数式操作
// 6. 调用函数
Function.prototype.apply.call(Math.floor, undefined, [1.75])
Reflect.apply(Math.floor, undefined, [1.75])
// 7. 如果不怕被打死, 也可以这样写
const obj = new Foo(...args)
const obj = Reflect.construct(Foo, args)

5. reflect-metadata (module)

'reflect-metadata' is a package that is a proposal for ES7. It allow for meta data to be included to a class or function; essentially it is syntax sugar.

往 class 或者 function 添加元数据

class C {
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

Reflect.defineMetadata(metadataKey, metadataValue, C.prototype, "method")

let obj = new C();
let metadataValue = Reflect.getMetadata(metadataKey, obj, "method")

6. decorator (stage 2)

import { Controller, Get, Validate, Joi } from 'leibniz'

@Controller('/post')
class PostController {
  @Get('/:id')
  @Validate({
    params: {
      id: Joi.number().integer()
    }
  })
  async index (ctx) {
    await ctx.service.post.index()
    ctx.status = 200
    ctx.body = {
      success: true
    }
  }
}

三个装饰器 @Controller, @Get, @Validate

  • @Controller

    export function Controller (prefix, ...middleware) {
      return (target, key, descriptor) => {  // [Function: PostController] undefined undefined
        target.prototype.router.router.stack.forEach(layer => {
          layer.stack.unshift(...middleware)
          layer.setPrefix(prefix)
        })
      }
    }
    
  • @Validate

    import 'reflect-metadata'
    
    export function Validate (validation) {
      return function (target, key, descriptor) {
        Reflect.defineMetadata('validate', validation, descriptor.value)
      }
    }
    
    // target      => PostController {}
    // key         => 'index'
    // descriptor  => { value: [AsyncFunction: index], writable: true, enumerable: false, configurable: true }
    
  • @Get

    import Router from 'koa-joi-router'
    import 'reflect-metadata'
    
    export function Get (path, ...middleware) {
      return function (target, key, descriptor) {
        if (!target.router) {
          target.router = new Router()
        }
        target.router.route({
          method: method.toLowerCase(),
          path,
          validate: Reflect.getMetadata('validate', descriptor.value),
          handler: [...middleware, descriptor.value]
        })
      }
    }
    
    // target      => PostController {}
    // key         => 'index'
    // descriptor  => { value: [AsyncFunction: index], writable: true, enumerable: false, configurable: true }
    

参考资料:

  1. ECMAScript 6 入门
  2. 深入浅出ES6(十二):代理 Proxies
  3. reflect-metadata
  4. You-Dont-Know-Js - ch7
  5. Metaprogramming in ES6: Part 1 - Symbol
  6. Metaprogramming in ES6: Part 2 - Reflect
  7. Metaprogramming in ES6: Part 3 - Proxies
  8. 元编程之 JavaScript