
本文是 重温基础 系列文章的第十四篇。
这是第一个基础系列的最后一篇,后面会开始复习一些中级的知识了,欢迎持续关注呀
接下来会统一整理到我的【Cute-JavaScript】的JavaScript基础系列中。
今日感受:独乐乐不如众乐乐。
本章节复习的是JS中的元编程,涉及的更多的是ES6的新特性。
1. 概述
元编程,其实我是这么理解的:让代码自动写代码,可以更改源码底层的功能。
元,是指程序本身。
有理解不到位,还请指点,具体详细的介绍,可以查看维基百科 元编程 。
从ES6开始,JavaScrip添加了对 Proxy和 Reflect对象的支持,允许我们连接并定义基本语言操作的自定义行为(如属性查找,赋值,枚举和函数调用等),从而实现JavaScrip的元级别编程。
-
Reflect: 用于替换直接调用 Object的方法,并不是一个函数对象,也没有 constructor方法,所以不能用 new操作符。 -
Proxy: 用于自定义对象的行为,如修改 set和 get方法,可以说是ES5中 Object.defineProperty()方法的ES6升级版。 - 两者联系: API完全一致,但
Reflect一般在 Proxy需要处理默认行为的时候使用。
参考资料:
名称 | 地址 |
| MDN Reflect |
| MDN Proxy |
元编程 | MDN 元编程 |
本文主要从Proxy介绍,还会有几个案例,实际看下怎么使用。
2. Proxy介绍
proxy 用于修改某些操作的默认行为,可以理解为一种拦截外界对目标对象访问的一种机制,从而对外界的访问进行过滤和修改,即代理某些操作,也称“代理器”。
2.1 基础使用
基本语法:
-
let p = new Proxy(target, handler);
proxy实例化需要传入两个参数, target参数表示所要拦截的目标对象, handler参数也是一个对象,用来定制拦截行为。
-
let p = new Proxy({}, { -
get: function (target, handler){ -
return 'leo'; -
} -
}) -
p.name; // leo -
p.age; // leo -
p.abcd; // leo
上述 a实例中,在第二个参数中定义了 get方法,来拦截外界访问,并且 get方法接收两个参数,分别是目标对象和所要访问的属性,所以不管外部访问对象中任何属性都会执行 get方法返回 leo。
注意:
- 只能使用
Proxy实例的对象才能使用这些操作。 - 如果
handler没有设置拦截,则直接返回原对象。
-
let target = {}; -
let handler = {}; -
let p = new Proxy(target, handler); -
p.a = 'leo'; -
target.a; // 'leo'
同个拦截器函数,设置多个拦截操作:
-
let p = new Proxy(function(a, b){ -
return a + b; -
},{ -
get:function(){ -
return 'get方法'; -
}, -
apply:function(){ -
return 'apply方法'; -
} -
})
这里还有一个简单的案例:
-
let handler = { -
get : function (target, name){ -
return name in target ? target[name] : 16; -
} -
} -
let p = new Proxy ({}, handler); -
p.a = 1; -
console.log(p.a , p.b); -
// 1 16
这里因为 p.a=1 定义了 p中的 a属性,值为 1,而没有定义 b属性,所以 p.a会得到 1,而 p.b会得到 undefined从而使用 nameintarget?target[name]:16;返回的默认值 16;
Proxy支持的13种拦截操作:
13种拦截操作的详细介绍:打开阮一峰老师的链接。
-
get(target,propKey,receiver): 拦截对象属性的读取,比如proxy.foo和proxy['foo']。 -
set(target,propKey,value,receiver): 拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 -
has(target,propKey): 拦截propKey in proxy的操作,返回一个布尔值。 -
deleteProperty(target,propKey): 拦截delete proxy[propKey]的操作,返回一个布尔值。 -
ownKeys(target): 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 -
getOwnPropertyDescriptor(target,propKey): 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 -
defineProperty(target,propKey,propDesc): 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 -
preventExtensions(target): 拦截Object.preventExtensions(proxy),返回一个布尔值。 -
getPrototypeOf(target): 拦截Object.getPrototypeOf(proxy),返回一个对象。 -
isExtensible(target): 拦截Object.isExtensible(proxy),返回一个布尔值。 -
setPrototypeOf(target,proto): 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 -
apply(target,object,args): 拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 -
construct(target,args): 拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
2.2 取消Proxy实例
使用 Proxy.revocable方法取消 Proxy实例。
-
let a = {}; -
let b = { -
get: function(target, name) { -
return "[[" + name + "]]"; -
} -
}; -
let revoke = Proxy.revocable(a, b); -
let proxy = revoke.proxy; -
proxy.age; // "[[age]]" -
revoke.revoke(); -
proxy.age; // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked -
proxy.age = 10; // Uncaught TypeError: Cannot perform 'set' on a proxy that has been revoked -
delete proxy.age; // Uncaught TypeError: Cannot perform 'deleteProperty' on a proxy that has been revoked -
typeof proxy; // "object"
2.3 实现 Web服务的客户端
-
const service = createWebService('http://le.com/data'); -
service.employees().than(json =>{ -
const employees = JSON.parse(json); -
}) -
function createWebService(url){ -
return new Proxy({}, { -
get(target, propKey, receiver{ -
return () => httpGet(url+'/'+propKey); -
}) -
}) -
}
3. Proxy实践
3.1 数据拦截验证
通过 Proxy代理对象的 set和 get方法来进行拦截数据,像 Vue就是用数据拦截来实现数据绑定。
-
let handler = { -
// 拦截并处理get方法 -
// 可以理解为设置get方法返回的默认值 -
get : function (target, key){ -
return key in target ? target[key] : 30; -
}, -
// 拦截并处理set方法 -
// 可以理解为设置set方法的默认行为 -
set : function (target, key, value){ -
if(key === "age"){ -
if (!Number.isInteger(value)){ -
throw new TypeError("age不是一个整数!"); -
} -
if (value > 200){ -
throw new TypeError("age不能大于200!"); -
} -
} -
// 保存默认行为 -
target[key] = value; -
} -
} -
let p = new Proxy({}, handler); -
p.a = 10; // p.a => 10 -
p.b = undefined; // p.b => undefined -
p.c; // 默认值 30 -
p.age = 100; // p.age => 100 -
p.age = 300; // Uncaught TypeError: age不能大于200! -
p.age = "leo"; // Uncaught TypeError: age不是一个整数!
3.2 函数节流
通过拦截 handler.apply()方法的调用,实现函数只能在1秒之后才能再次被调用,经常可以用在防止重复事件的触发。
-
let p = (fun, time) => { -
// 获取最后点击时间 -
let last = Date.now() - time; -
return new Proxy (fun, { -
apply(target, context, args){ -
if(Date.now() - last >= time){ -
fun.bind(target)(args); -
// 重复设置当前时间 -
last = Date.now(); -
} -
} -
}) -
} -
let p1 = () => { -
console.log("点击触发"); -
} -
let time = 1000; // 设置时间 -
let proxyObj = p(p1, time); -
// 监听滚动事件 -
document.addEventListener('scroll', proxyObj);
3.3 实现单例模式
通过拦截 construct方法,让不同实例指向相同的 constructer,实现单例模式。
-
let p = function(fun){ -
let instance; -
let handler = { -
// 拦截construct方法 -
construct: function(targer, args){ -
if(!instance){ -
instance = new fun(); -
} -
return instance; -
} -
} -
return new Proxy(fun, handler); -
} -
// 创建一个construct案例 -
function Cons (){ -
this.value = 0; -
} -
// 创建实例 -
let p1 = new Cons(); -
let p2 = new Cons(); -
// 操作 -
p1.value = 100; -
// p1.value => 100 , p2.value => 0 -
// 因为不是相同实例 -
// 通过Proxy实现单例 -
let singleton = p(Cons); -
let p3 = new singleton(); -
let p4 = new singleton(); -
p3.value = 130; -
// p1.value => 130 , p2.value => 130 -
// 现在是相同实例
参考资料
1.MDN 元编程
2. ES6中的元编程-Proxy & Reflect










