Nick Ribal
Front-end veteran, consultant and freelancer, who's armed with a decade of experience solving challenges on the Web platform for large Internet companies.
Metaprogramming
Definition
Programming with the ability to treat programs as data. Reading, analysing or transforming itself, while running.
Metaprogramming enables expressing certain solution better, or allows greater flexibility in handling new situations without modification.
function capitalizeProps(obj, capitalizedObj = {}){
for (const key in obj)
capitalizedObj[key.toUpperCase()] = obj[key]
return capitalizedObj
}
capitalizeProps({ foo: 'bar', })
// >> { FOO: 'bar' }
Babel is awesome and gave us ES2015/6 in ES5 via transpilation and polyfills, except for Proxies.
So what Proxies do CANNOT be done in any other way!
Proxies enable you to intercept and customize operations performed on objects (such as getting, setting properties, invoking functions and others). They overload operators such as '.' and 'new'.
Proxies are a metaprogramming feature
Proxies let you do awesome things, which are otherwise impossible
4. Proxies turn you into a 1337 h4x0r!
const target = {}
const handler = {
get(target, key, receiver){
console.info(`getting property ${ key }`)
}
}
const proxy = new Proxy(target, handler)
proxy.foo
// >> getting property foo
target: object to proxy
handler: proxy definition, containing operation traps
trap: method intercepting an operation on the target
const gateway = new Proxy({ isRegularWeb: true }, {
set(target, key, value, receiver){
console.info(`${ key } set to`, value)
return target[key] = value
}
})
// unless a trap is defined, operation is forwarded
// to the target unmodified
delete gateway.isRegularWeb
// >> true
gateway.ip = '127.0.0.1'
// >> ip set to 127.0.0.1
Proxy trap/Reflect method | Operation on Object t = { foo: 'bar' } |
---|---|
getPrototypeOf() | Object.getPrototypeOf(t) |
setPrototypeOf() | Object.setPrototypeOf(t, null) |
isExtensible() | Object.isExtensible(t) |
preventExtensions() | Object.preventExtensions(t) |
getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor(t, 'foo') |
defineProperty() | Object.defineProperty(t, 'bar', {value: 'baz'}) |
ownKeys() | Object.getOwnPropertyNames(t) |
has() | 'foo' in t |
get() | t.foo |
set() | t.bar = 'baz' |
deleteProperty() | delete t.bar |
apply() | t.toString() |
construct() | new t() |
function tracePropAccess(obj, ...propKeys){
const propKeySet = new Set(propKeys)
return new Proxy(obj, {
get(target, propKey, receiver){
if (propKeySet.has(propKey)) {
console.info(`GET ${ propKey }`)
}
return Reflect.get(...arguments)
},
set(target, propKey, value, receiver){
if (propKeySet.has(propKey)) {
console.info(`SET ${ propKey } = ${ value }`)
}
return Reflect.set(...arguments)
},
})
}
class Point {
constructor(x = 0, y = 0, z = 0){
Object.assign(this, { x, y, z, })
}
toString(){
const { x, y, z, } = this
return `Point[${ x }, ${ y }, ${ z }]`
}
}
const origin = new Point()
const tracePoint = tracePropAccess(origin, 'x', 'y')
tracePoint.y = 42
// >> SET y = 42
tracePoint.toString()
// >> GET x
// >> GET y
// >> "Point[0, 42, 0]"
const root = {}
root.tree.branch.leaf = 'awesome'
// >> Uncaught TypeError: Cannot
// >> read property 'branch' of undefined
function Tree(obj = {}){
return new Proxy(obj, {
get(target, key, receiver){
if (!(key in target)) {
target[key] = Tree()
}
return Reflect.get(...arguments)
}
})
}
const root = Tree()
root.tree.branch.leaf = 'awesome'
// >> "awesome"
const render = (text) => `<span>${ text }</span>`
const api = {
foo: 'foo',
getFoo(){ return this.foo },
}
render(api.foo)
// >> "<span>foo</span>"
render(api.getFoo())
// >> "<span>foo</span>"
render(api.bar)
// >> "<span>undefined</span>"
render(api.getBar())
// >> Uncaught TypeError: api.getBar is not a function
function errorMaker(error){
const toString = () => error
const errorMessage = () => ({ error, toString, })
errorMessage.toString = toString
return errorMessage
}
const safeApi = new Proxy(api, {
get(target, key, receiver){
if (!(key in target)) {
return errorMaker(`No '${ key }' found.`)
}
return Reflect.get(...arguments)
}
})
render(safeApi.bar)
// >> "<span>No 'bar' found.</span>"
render(safeApi.getBar())
// >> "<span>No 'getBar' found.</span>"
function throwOnTypeMismatch(target, key, value){
const currentType = typeof target[key]
if (key in target && currentType !== typeof value) {
throw new Error(
`Property '${ key }' must be a ${ currentType }.`
)
}
}
function createTypeSafeObject(object = {}){
return new Proxy(object, {
set(target, key, value){
throwOnTypeMismatch(...arguments)
return Reflect.set(...arguments)
}
})
}
const person = { name: 'Sam', }
const safePerson = createTypeSafeObject(person)
safePerson.name = true
// >> Uncaught Error: Property 'name' must be a string.
safePerson.name = 'Bill'
// >> "Bill"
safePerson.age = 32
// >> 32
safePerson.age = null
// >> Uncaught Error: Property 'age' must be a number.
function getPositiveKey(key, { length, }){
const i = parseInt(key, 10)
return (!isNaN(i) && i < 0) ? (length + i) : key
}
const arr = new Proxy(['a', 'b', 'c'], {
get(target, key, receiver){
const positiveKey = getPositiveKey(key, target)
return Reflect.get(target, positiveKey, receiver)
},
set(target, key, value, rcver){
const posKey = getPositiveKey(key, target)
return Reflect.set(target, posKey, value, rcver)
},
})
arr[-1]
// >> "c"
arr[-1] = 'd'
// >> "d"
function observe(object = {}, observers = new Set()){
const proxy = new Proxy(object, {
set(target, key, value, receiver){
const oldValue = target[key]
const forwardOpResult = Reflect.set(...arguments)
observers.forEach(
observer => observer({ key, oldValue, value, })
)
return forwardOpResult
}
})
proxy.subscribe = (subscriber) => {
observers.add(subscriber)
return proxy
}
return proxy
}
const info = console.info.bind(console)
function logChanges({ key, oldValue, value, }){
info(`${ key } change from ${ oldValue } to ${ value }`)
}
const observable = observe()
observable
.subscribe(logChanges)
.subscribe(info)
observable.foo = 'foo'
// >> foo change from undefined to foo
// >> {key: "foo", oldValue: undefined, value: "foo"}
observable.foo = 'bar'
// >> foo change from foo to bar
// >> {key: "foo", oldValue: "foo", value: "bar"}
const dom = new Proxy({}, {
get: (target, key, receiver) => (attrs = {}, children = '') => {
const isText = typeof children === 'string'
const attrNames = Object.keys(attrs)
if (isText && attrNames.length === 0) {
return children
}
const getAttrs = (acc, attr) =>
`${ acc } ${ attr }='${ attrs[attr] }'`
return [
`<${ key }${ attrNames.reduce(getAttrs, '') }>`,
...(isText ? [children] : children),
`</${ key }>\n`
].join('\n')
}
})
dom.span({ class: 'foo' }, ['text'])
// >> "<span class='foo'>text</span>"
dom.form({ class: 'foo' }, [
dom.label({ for: 'win' }, 'So much'),
dom.button(
{ type: 'submit', id: 'win' },
'WIN!!!'
),
])
<form class='foo'>
<label for='win'>
So much
</label>
<button type='submit' id='win'>
WIN!!!
</button>
</form>
Proxy.revocable
Object property descriptors interop with Proxies via Invariance enforcement
Non-extensibility
Non-configurability
Reflect uses beyond forwarding operations
ES6 In Depth: Proxies by Jason Orendorff
Meta programming with ECMAScript 6 proxies by Axel Rauschmayer
Exploring ES6: Upgrade to the next version of JavaScript book, Metaprogramming with proxies chapter by Axel Rauschmayer
By Nick Ribal
All ES2015/16 features can be polyfilled and transpiled to ES5 - except for one: Proxies! Now that these are finally implemented in all evergreen browsers and node 6, we can explore this exciting new feature. Learn all about Proxies and Reflect APIs, their use cases and their unique superpowers!
Front-end veteran, consultant and freelancer, who's armed with a decade of experience solving challenges on the Web platform for large Internet companies.