几年前 ES6 刚出来的时候接触过 元编程(Metaprogramming)的概念,不过当时尚未深究。今天在应用和学习中不断接触到这概念,好比 mobx 5 中就用到了 Proxy 重写了 Observable 对象,以为有必要梳理总结一下。javascript
本文不生产代码,只当代码、文档的搬运工。因此本文并不是是一篇传统意义上的教程,更相似于 github awesome 这样列表文章。html
Symbol、Reflect 和 Proxy 是属于 ES6 元编程范畴的,能“介入”的对象底层操做进行的过程当中,并加以影响。元编程中的 元 的概念能够理解为 程序 自己。java
”元编程能让你拥有能够扩展程序自身能力“。这句话仍是很抽象,初学者该怎么理解呢?python
我也理解了半天,想到了下面的例子:git
就比如你本来是公司的部门的大主管,虽然你能力很强,但也必须按照规章制度作事,好比早上 8 点必须到公司,不然你就要扣绩效;然后来公司基本规定灵活了,每一个部门能够本身制定打卡制度,此时身为主管的你,依据公司该基本规定,制定本身部门的考勤制度,本部门的职工能够 9 点来公司,还能够不打卡!(固然还能够制定其余规定)es6
在这个例子中:github
这里的例子不必定准确,是我我的的理解,权作参考,也能够去看看知乎上 怎么理解元编程? 的问答。编程
借助这个例子理解元编程,咱们能感知在没有元编程能力的时候,就算你编程能力很厉害,但终究“孙悟空翻不出五指山”;而掌握了元编程能力以后,就差上天了,“给你一个支点,你就能撬动地球”,能力大大扩增。segmentfault
简言之,元编程让你具有必定程度上改变现有的程序规则层面的能力。或者说,元编程可让你以某种形式去影响或更改程序运行所依赖的基础功能,以此得到一些维护性、效率上的好处。设计模式
Javascript 中,eval
、new Function()
即是两个能够用来进行元编程的特性。不过由于性能和可维护的角度上,这两个特性仍是不要用为妙。
在 ES6 以后,标准引入了 Proxy & Reflect & Symbols,从而提供比较完善的元编程能力。
我本来也想仔细讲讲 ES6 中 Symbol
、Proxy
和 Reflect
的基本概念和使用的,但网上这方面的文章不要太多,以为重复码字也没有太必要。这里着重推荐几篇,分为教程类和手册类,通读完以后应该就掌握差很少了。
元编程在 ES6 体现最为突出的是 Proxy
的应用,目前我所找的文章也多偏向 Proxy
。
原理教程类:
Proxy
和 Reflect
相关的知识点,只是阅读起来略微枯燥。应用教程类:
手册类:
在没充分理解元编程以前翻手册仍是挺枯燥的,建议平时使用的时候再从这里补漏
随着时间的推移,上面收集的文章可能会显得陈旧,又有可能出现新的好文章,推荐在搜索引擎中使用 js Metaprogramming
或者 es6 proxy
进行搜索相关文章;
下面摘抄一些代码片断,方便本身后续在应用 JS 元编程的时候快速 "借鉴"。大家若是也有以为不错的代码片断,欢迎在 issue 中回复,我将不按期更新到这儿。
示例来自 ES6 Proxies in Depth
场景:person
是一个普通对象,包含一个 age
属性,当咱们给它赋值的时候确保是大于零的数值,不然赋值失败并抛出异常。
var person = { age: 27 };
思路:经过设置 set
trap,其中包含了对 age
字段的校验逻辑。
代码:
var validator = { set (target, key, value) { if (key === 'age') { if (typeof value !== 'number' || Number.isNaN(value)) { throw new TypeError('Age must be a number') } if (value <= 0) { throw new TypeError('Age must be a positive number') } } return true } } var proxy = new Proxy(person, validator) proxy.age = 'foo' // <- TypeError: Age must be a number proxy.age = NaN // <- TypeError: Age must be a number proxy.age = 0 // <- TypeError: Age must be a positive number proxy.age = 28 console.log(person.age) // <- 28
示例来自 深刻浅出ES6(十二):代理 Proxies
场景:建立一个Tree()函数来实现如下特性,当咱们须要时,全部中间对象 branch1
、branch2
和 branch3
均可以自动建立。
var tree = Tree(); tree // { } tree.branch1.branch2.twig = "green"; // { branch1: { branch2: { twig: "green" } } } tree.branch1.branch3.twig = "yellow"; // { branch1: { branch2: { twig: "green" }, // branch3: { twig: "yellow" }}}
思路:Tree 返回的就是一个 proxy 实例,经过 get
trap ,当不存在属性的时候自动建立一个子树。
代码:
function Tree() { return new Proxy({}, handler); } var handler = { get: function (target, key, receiver) { if (!(key in target)) { target[key] = Tree(); // 自动建立一个子树 } return Reflect.get(target, key, receiver); } };
示例来自 深刻浅出ES6(十二):代理 Proxies
场景:好比将 2 进制转换成 16 进制或者 8 进制,反之也能转换。
思路:因为大部分的功能是相同的,咱们经过函数名字将变量提取出来,而后经过 get
trap 完成进制转换。
代码:
const baseConvertor = new Proxy({}, { get: function baseConvert(object, methodName) { var methodParts = methodName.match(/base(\d+)toBase(\d+)/); var fromBase = methodParts && methodParts[1]; var toBase = methodParts && methodParts[2]; if (!methodParts || fromBase > 36 || toBase > 36 || fromBase < 2 || toBase < 2) { throw new Error('TypeError: baseConvertor' + methodName + ' is not a function'); } return function (fromString) { return parseInt(fromString, fromBase).toString(toBase); } } }); baseConvertor.base16toBase2('deadbeef') === '11011110101011011011111011101111'; baseConvertor.base2toBase16('11011110101011011011111011101111') === 'deadbeef';
示例来自 从ES6从新认识JavaScript设计模式(五): 代理模式和Proxy
场景:以没有通过任何优化的计算斐波那契数列的函数来假设为开销很大的方法,这种递归调用在计算 40 以上的斐波那契项时就能明显的感到延迟感。但愿经过缓存来改善。
const getFib = (number) => { if (number <= 2) { return 1; } else { return getFib(number - 1) + getFib(number - 2); } }
注:这只是演示缓存的写法,递归调用自己就有问题,容易致使内存泄露,在实际应用中须要改写上述的
getFib
函数。
思路:由于是函数调用,因此需使用 apply
trap,利用 Map 或者普通对象存储每次计算的结果,在执行运算前先去 Map 查询计算值是否被缓存。(至关于以空间换时间,得到性能提高)
代码:
const getCacheProxy = (fn, cache = new Map()) => { return new Proxy(fn, { apply(target, context, args) { const argsString = args.join(' '); if (cache.has(argsString)) { // 若是有缓存,直接返回缓存数据 console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`); return cache.get(argsString); } const result = Reflect.apply(target, undefined, args); cache.set(argsString, result); return result; } }) } const getFibProxy = getCacheProxy(getFib); getFibProxy(40); // 102334155 getFibProxy(40); // 输出40的缓存结果: 102334155
在实际应用中数据量越大、计算过程越复杂,优化效果越好,不然有可能会得不偿失。
示例来自 从ES6从新认识JavaScript设计模式(五): 代理模式和Proxy
场景:众所周知,JavaScript是没有私有属性这一个概念的,私有属性通常是以 _
下划线开头,请经过 Proxy 限制以 _
开头的属性的访问。
const myObj = { public: 'hello', _private: 'secret', method: function () { console.log(this._private); } },
思路:看上去比较简单,貌似使用 get
、set
这两个 trap 就能够,但实际上并非。实际上还须要实现 has
, ownKeys
, getOwnPropertyDescriptor
这些 trap,这样就能最大限度的限制私有属性的访问。
代码:
function getPrivateProps(obj, filterFunc) { return new Proxy(obj, { get(obj, prop) { if (!filterFunc(prop)) { let value = Reflect.get(obj, prop); // 若是是方法, 将this指向修改原对象 if (typeof value === 'function') { value = value.bind(obj); } return value; } }, set(obj, prop, value) { if (filterFunc(prop)) { throw new TypeError(`Can't set property "${prop}"`); } return Reflect.set(obj, prop, value); }, has(obj, prop) { return filterFunc(prop) ? false : Reflect.has(obj, prop); }, ownKeys(obj) { return Reflect.ownKeys(obj).filter(prop => !filterFunc(prop)); }, getOwnPropertyDescriptor(obj, prop) { return filterFunc(prop) ? undefined : Reflect.getOwnPropertyDescriptor(obj, prop); } }); } function propFilter(prop) { return prop.indexOf('_') === 0; } myProxy = getPrivateProps(myObj, propFilter); console.log(JSON.stringify(myProxy)); // {"public":"hello"} console.log(myProxy._private); // undefined console.log('_private' in myProxy); // false console.log(Object.keys(myProxy)); // ["public", "method"] for (let prop in myProxy) { console.log(prop); } // public method myProxy._private = 1; // Uncaught TypeError: Can't set property "_private"
注意:其中在 get
方法的内部,咱们有个判断,若是访问的是对象方法使将 this
指向被代理对象,这是在使用 Proxy 须要十分注意的,若是不这么作方法内部的 this 会指向 Proxy 代理。
通常来说,set
trap 都会默认触发getOwnPropertyDescriptor
和defineProperty
示例来自 使用 Javascript 原生的 Proxy 优化应用
场景:控制函数调用的频率.
const handler = () => console.log('Do something...'); document.addEventListener('scroll', handler);
思路:涉及到函数的调用,因此使用 apply
trap 便可。
代码:
const createThrottleProxy = (fn, rate) => { let lastClick = Date.now() - rate; return new Proxy(fn, { apply(target, context, args) { if (Date.now() - lastClick >= rate) { fn.bind(target)(args); lastClick = Date.now(); } } }); }; const handler = () => console.log('Do something...'); const handlerProxy = createThrottleProxy(handler, 1000); document.addEventListener('scroll', handlerProxy);
一样须要注意使用 bind
绑定上下文,不过这里的示例使用了箭头函数,不用 bind
也没啥问题。
示例来自 使用 Javascript 原生的 Proxy 优化应用
场景:为了更好的用户体验,在加载图片的时候,使用 loading
占位图,等真正图片加载完毕以后再显示出来。原始的写法以下:
const img = new Image(); img.src = '/some/big/size/image.jpg'; document.body.appendChild(img);
思路:加载图片的时候,会读取 img.src
属性,咱们使用 constructor
trap 控制在建立的时候默认使用 loading 图,等加载完毕再将真实地址赋给 img
;
代码:
const IMG_LOAD = 'https://img.alicdn.com/tfs/TB11rDdclLoK1RjSZFuXXXn0XXa-300-300.png'; const imageProxy = (loadingImg) => { return new Proxy(Image, { construct(target, args){ const instance = Reflect.construct(target, args); instance.src = loadingImg; return instance; } }); }; const ImageProxy = imageProxy(IMG_LOAD); const createImageProxy = (realImg) =>{ const img = new ImageProxy(); const virtualImg = new Image(); virtualImg.src = realImg; virtualImg.onload = () => { hasLoaded = true; img.src = realImg; }; return img; } var img = createImageProxy('https://cdn.dribbble.com/users/329207/screenshots/5289734/bemocs_db_dribbble_03_gold_leaf.jpg'); document.body.appendChild(img);
示例来自 ES6 Features - 10 Use Cases for Proxy
场景:当普通对象属性更改后,触发所绑定的 onChange
回调;
思路:能更改属性的有 set
和 deleteProperty
这两个 trap,在其中调用 onChange 方法便可
function trackChange(obj, onChange) { const handler = { set (obj, prop, value) { const oldVal = obj[prop]; Reflect.set(obj, prop, value); onChange(obj, prop, oldVal, value); }, deleteProperty (obj, prop) { const oldVal = obj[prop]; Reflect.deleteProperty(obj, prop); onChange(obj, prop, oldVal, undefined); } }; return new Proxy(obj, handler); } // 应用在对象上 let myObj = trackChange({a: 1, b: 2}, function (obj, prop, oldVal, newVal) { console.log(`myObj.${prop} changed from ${oldVal} to ${newVal}`); }); myObj.a = 5; // myObj.a changed from 1 to 5 delete myObj.b; // myObj.b changed from 2 to undefined myObj.c = 6; // myObj.c changed from undefined to 6 // 应用在数组上 let myArr = trackChange([1,2,3], function (obj, prop, oldVal, newVal) { let propFormat = isNaN(parseInt(prop)) ? `.${prop}` : `[${prop}]`, arraySum = myArr.reduce((a,b) => a + b); console.log(`myArr${propFormat} changed from ${oldVal} to ${newVal}`); console.log(` sum [${myArr}] = ${arraySum}`); }); myArr[0] = 4; // myArr[0] changed from 1 to 4 // sum [4,2,3] = 9 delete myArr[2]; // myArr[2] changed from 3 to undefined // sum [4,2,] = 6 myArr.length = 1; // myArr.length changed from 3 to 1 // sum [4] = 4
示例来自 ES6 Features - 10 Use Cases for Proxy
场景:实现单例设计模式;
思路:和建立有关的,是 construct
这个 trap,每次咱们返回相同的实例便可。
代码:
// makes a singleton proxy for a constructor function function makeSingleton(func) { let instance, handler = { construct: function (target, args) { if (!instance) { instance = new func(); } return instance; } }; return new Proxy(func, handler); } // 以这个为 constructor 为例 function Test() { this.value = 0; } // 普通建立实例 const t1 = new Test(), t2 = new Test(); t1.value = 123; console.log('Normal:', t2.value); // 0 - 由于 t一、t2 是不一样的实例 // 使用 Proxy 来 trap 构造函数, 完成单例模式 const TestSingleton = makeSingleton(Test), s1 = new TestSingleton(), s2 = new TestSingleton(); s1.value = 123; console.log('Singleton:', s2.value); // 123 - 如今 s一、s2 是相同的实例。
示例来自 ES6 Features - 10 Use Cases for Proxy
场景:在 python 中,你可使用 list[10:20:3]
来获取 10 到 20 索性中每隔 3 个的元素组成的数组(也支持负数索引)。
思路:因为在 JS 中,数组方括号语法中不支持冒号,只能曲线救国,使用这样 list["10:20:3"]
的形式。只须要实现 get
trap 便可。
// Python-like array slicing function pythonIndex(array) { function parse(value, defaultValue, resolveNegative) { if (value === undefined || isNaN(value)) { value = defaultValue; } else if (resolveNegative && value < 0) { value += array.length; } return value; } function slice(prop) { if (typeof prop === 'string' && prop.match(/^[+-\d:]+$/)) { // no ':', return a single item if (prop.indexOf(':') === -1) { let index = parse(parseInt(prop, 10), 0, true); console.log(prop, '\t\t', array[index]); return array[index]; } // otherwise: parse the slice string let [start, end, step] = prop.split(':').map(part => parseInt(part, 10)); step = parse(step, 1, false); if (step === 0) { throw new RangeError('Step can\'t be zero'); } if (step > 0) { start = parse(start, 0, true); end = parse(end, array.length, true); } else { start = parse(start, array.length - 1, true); end = parse(end, -1, true); } // slicing let result = []; for (let i = start; start <= end ? i < end : i > end; i += step) { result.push(array[i]); } console.log(prop, '\t', JSON.stringify(result)); return result; } } const handler = { get (arr, prop) { return slice(prop) || Reflect.get(array, prop); } }; return new Proxy(array, handler); } // try it out let values = [0,1,2,3,4,5,6,7,8,9], pyValues = pythonIndex(values); console.log(JSON.stringify(values)); pyValues['-1']; // 9 pyValues['0:3']; // [0,1,2] pyValues['8:5:-1']; // [8,7,6] pyValues['-8::-1']; // [2,1,0] pyValues['::-1']; // [9,8,7,6,5,4,3,2,1,0] pyValues['4::2']; // [4,6,8] // 不影响正常的索引 pyValues[3]; // 3
本文总结了本身学习 ES6 元编程相关知识(Symbols & Proxy & Reflect)的理解、教程文档 和 代码片断。
因为教程文档和代码片断将随着学习的进行将增多,因此后续还会不按期更新。若是你也有好的资源,欢迎到 issue 中回复共享。
construct
trap 实现;更新缘由:bugfix,原来的代码所建立的 img
是 proxy 对象,执行 document.body.appendChild(img)
将报错。下面的是个人公众号二维码图片,欢迎关注。