小程序开发

使用ES6新特性开发微信小程序(6)——元编程


使用ES6新特性开发微信小程序(1)

使用ES6新特性开发微信小程序(2)

使用ES6新特性开发微信小程序(3)

使用ES6新特性开发微信小程序(4)

使用ES6新特性开发微信小程序(5)

使用ES6新特性开发微信小程序(6)



元编程是指的是开发人员对 “语言本身进行编程”。一般是编程语言暴露了一些API,供开发人员来操作语言本身的某些特性。

从ES6开始,新增了Proxy和 Reflect特性,扩展了元编程(Meta Programming)能力,允许拦截并定制基础语言操作行为(比如,属性查找,赋值,枚举,函数调等)。

Proxy(代理)

Proxy是ES6加入的一个新特性,它可以 “代理” 对象的原生行为,替换为执行自定义行为。

Proxy可以理解成在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

Proxy 对象用来为基础操作(例如:属性查找、赋值、枚举、方法调用等)定义用户自定义行为。


创建一个Proxy对象:


var proxy = new Proxy(target, handler);


  • target:目标对象,可以是任意类型的对象,比如数组,函数,甚至是另外一个代理对象。

  • handlert:处理器对象,包含了一组代理方法,分别控制所生成代理对象的各种行为。 


Proxy对象的方法:


  • Proxy.revocable(target, handler):用来创建一个可撤销的代理对象。


处理器对象一共有14种可代理方法:


  • handler.getPrototypeOf():在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。

  • handler.setPrototypeOf():在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。

  • handler.isExtensible():在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。

  • handler.preventExtensions():在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。

  • handler.getOwnPropertyDescriptor():在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。

  • handler.defineProperty():在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。

  • handler.has():在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。

  • handler.get():在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。

  • handler.set():在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。

  • handler.deleteProperty():在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。

  • handler.enumerate():在遍历代理对象的属性时触发该操作,比如在执行 for(i in proxy){} 时。

  • handler.ownKeys():在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。

  • handler.apply():在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。

  • handler.construct():在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。


拦截属性值的读取操作:


let handler1 = {
    get: function(target, name) {
        return name in target ? target[name] : 42;
    }
};
let proxy1 = new Proxy({}, handler1);
proxy1.a = 1;
console.log(proxy1.a, proxy1.b); // 输出: 1  42


上面代码中,Proxy(代理)对象定义一个target和一个handle,handle实现了一个get捕捉方法。通过这个方法,被代理的对象对于未定义的属性,不再返回undefined,而是返回一个42的数字。

拦截属性值的赋值操作:


let employee = { name: 'Zhang san', salary: 50 };
var interceptor = {
    set: function(target, prop, value, receiver) {
        console.log(prop, 'is changed to', value);
        return true;
    }
};
let proxy2 = new Proxy(employee, interceptor);
proxy2.salary = 60; // 输出: salary is changed to 60


上面代码中,设置了set的处理函数,如果我们侦听的对象的属性被更改,那这个处理程序就会被调用,同时通过参数能够得知是哪个属性被更改,更改为了什么值。

同一个拦截器函数,可以设置拦截多个操作:


var handler3 = {
    get: function(target, name) {
        if (name === 'prototype') {
            return Object.prototype;
        }
        return 'Hello, ' + name;
    },
    apply: function(target, thisBinding, args) {
        return args[0];
    },
    construct: function(target, args) {
        return { value: args[1] };
    }
};
var proxy3 = new Proxy(function(x, y) {
    return x + y;
}, handler3);
console.log(proxy3(1, 2)); // 输出: 1
console.log(new proxy3(1, 2)); // 输出: Object {value: 2}
console.log(proxy3.prototype === Object.prototype); // 输出: true
console.log(proxy3.foo); // 输出: Hello, foo


Proxy.revocable方法用来创建一个可撤销的代理对象,一旦某个代理对象被撤销,它将变的几乎完全不可用,在它身上执行任何的可代理操作都会抛出 TypeError 异常。


var revocable = Proxy.revocable({}, {
    get(target, name) {
        return "[[" + name + "]]";
    }
});
var proxy4 = revocable.proxy;
console.log(proxy4.foo); // 输出: [[foo]]
revocable.revoke();
console.log(proxy4.foo); // 输出: TypeError: Cannot perform 'get' on a proxy that has been revoked
proxy4.foo = 1 // 输出: TypeError: Cannot perform 'set' on a proxy that has been revoked
delete proxy4.foo; // 输出: TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked
console.log(typeof proxy4) // 输出: object


Reflect(反射)

ES6 中引入的Reflect是另一个元编程的特性,它使得我们可以直接操纵对象的原生行为。Reflect可操纵的行为与Proxy可代理的行为是一一对应的,这使得可以在Proxy的自定义方法中方便的使用Reflect调用原生行为。

Reflection(反射)促进元编程的一种很有价值的语言特性,它可以在程序运行时动态展现程序本身的特性。

Reflect 对象提供了14个静态方法,它们的名字刚好和那14个代理处理器方法的名字相同,这14个方法中有几个刚好在 Object 对象身上也存在同名方法,虽然它们功能类似,但也存在细微差异。


  • Reflect.apply():对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。

  • Reflect.construct():对构造函数进行 new 操作,相当于执行 new target(...args)。

  • Reflect.defineProperty():和 Object.defineProperty() 类似。

  • Reflect.deleteProperty():删除对象的某个属性,相当于执行 delete target[name]。

  • Reflect.enumerate():该方法会返回一个包含有目标对象身上所有可枚举的自身字符串属性以及继承字符串属性的迭代器,for...in 操作遍历到的正是这些属性。

  • Reflect.get():获取对象身上某个属性的值,类似于 target[name]。
    * Reflect.getOwnPropertyDescriptor():类似于 Object.getOwnPropertyDescriptor()。

  • Reflect.getPrototypeOf(): 类似于 Object.getPrototypeOf()。

  • Reflect.has():判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。

  • Reflect.isExtensible():类似于 Object.isExtensible()。

  • Reflect.ownKeys():返回一个包含所有自身属性(不包含继承属性)的数组。

  • Reflect.preventExtensions():类似于 Object.preventExtensions()。

  • Reflect.set():设置对象身上某个属性的值,类似于 target[name] = val。

  • Reflect.setPrototypeOf():类似于 Object.setPrototypeOf()。 


var shape = { x: 10, y: 20 };
var proxy5 = new Proxy(shape, {
    set: function(target, name, value, receiver) {
        var success = Reflect.set(target, name, value, receiver);
        if (success) {
            console.log('property ' + name + ' on ' + target + ' set to ' + value);
        }
        return success;
    }
});
proxy5.width = 100; // property width on [object Object] set to 100


上面代码中,Proxy方法拦截target对象的属性赋值行为,采用Reflect.set方法将值赋值给对象的属性。


为什么要使用Reflect:


  • 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。

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

  • 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。

  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。


var pet = { id: 1, name: 'cat', weight: 5, color: 'white' };
var loggedObj = new Proxy(pet, {
    get(target, name) {
        console.log('get:', name);
        return Reflect.get(target, name);
    },
    deleteProperty(target, name) {
        console.log('delete:', name);
        return Reflect.deleteProperty(target, name);
    },
    has(target, name) {
        console.log('has:', name);
        return Reflect.has(target, name);
    }
});
console.log(loggedObj.id); // 输出: get: id\n1
console.log("weight" in loggedObj); // 输出: has: weight\ntrue
delete loggedObj.color; // 输出: delete: color
console.log(pet.color); // 输出: undefined


上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。有了Reflect对象以后,很多操作会更易读。

其他

完整代码:https://github.com/guyoung/GyWxappCases/tree/master/ES6