JavaScript is evolving rapidly with a bunch of yearly additions. Some of them never reach the spotlight, others get forgotten or deprecated. Let's put these into increasingly hacky editable code sandboxes. The goal is to make you play with occasional horror on your face.
The examples are taken from real libraries or apps that I have written over the years. The last few are controversial though, and I do not encourage you to use them in your code.
Finally
I usually use finally when I have to wrap other devs' functions with some extra logic. finally
does this without messing with the original behavior - like return values or thrown Errors.
This example instruments the passed function with performance measurements.
function measure (fn) {
const start = Date.now()
try {
return fn()
} finally {
console.log(`${fn.name} took ${Date.now() - start} ms`)
}
}
WeakSet and WeakMap
These two provide truly private properties (unlike Symbols). I use them when I have to append some metadata to others' objects. Directly mutating these would be bad manners, plus you get screwed if the object is frozen.
This example stringifies the passed object and caches the result. The memoization speeds up the operation and the WeakMap usage avoids object pollution and memory leaks.
const cache = new WeakMap()
export default function stringify(obj) {
let result = cache.get(obj)
if (!result) {
result = JSON.stringify(obj, null, 2)
cache.set(obj, result)
}
return result
}
Proxy
Proxies can tweak language behavior. Most of the time I use them to extend the language and occasionally I use them completely change some default behavior.
This reactive example lets you create observables and reactions. A reaction automatically re-runs when an observable - used inside it - is mutated. The snippet is extremely oversimplified but some additional coding can take it pretty far.
let runningFn
const reactions = new WeakMap()
export function observe(fn) {
runningFn = fn
try {
return fn()
} finally {
runningFn = undefined
}
}
export function observable(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
runningFn && reactions.set(target, runningFn)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
const reaction = reactions.get(target)
reaction && reaction()
}
})
}
New Function
new Function() is similar to eval
but it creates functions in the global scope, which is outside of any 'strict mode'
declarations. We will abuse this fact soon.
This example gets the global
object - regardless of the platform - which is currently trickier than it should be. It is also used in the ES2019 globalThis proposal polyfill.
const globalThis = new Function('return this')()
This won't work if you have CSP set up for your page.
With
with extends the scope chain for a statement. Sadly, it is deprecated and it won't work in strict mode - which includes ES6 modules. A few tricks can save the day though.
This example lets you write Vue or Angular like template files and renders them in the context of the passed state.
export default function compile(template) {
return new Function("state", `with (wrap(state)) { return \`${template}\` }`)
}
window.wrap = state => new Proxy(state, {
has: () => true
})
It tweaks with
behavior with the following tricks:
It uses
new Function()
- instead ofeval
- to create functions in the global scope. The global scope is outside strict mode sowith
works there.It uses Proxies to tweak
with
behavior a bit. Normallywith
extends the scope chain and does not replace it completely. When a property is not found on the passed object it will search for it in the next scope - in our case the global one. We do not want the global scope to leak into the template so we have to trickwith
into thinking that every possible property is present on the passed object. This is pretty simple with thehas
Proxy trap.
Everything Together
We accidentally created a blazing fast lightweight MVC framework along the way.
Please check the repo here and don't forget to leave a star!
I hope you found some time to play with the sandboxes. Leave a comment if you created something crazy.
Thanks for reading!
Top comments (5)
Looking for advice, do you feel like using a WeakMap would be better here than using the symbol?
Yep, I think WeakMap suits this more but it's just a semantical change in this case, not a critical one. Theoretically, this implementation could cause issues with some Proxy based libraries (which do intercept Symbol get/set).
Generally, I like to use symbols like well-known symbols. When I create an internal feature that I want to expose to experienced devs (by exporting the symbol) but don't want to be accidentally overwritten (like a normal prop).
I hope this helps a bit.
PS: I use both Symbols and WeakMaps in a lib which I perf profile often and I found no real difference in performance (in Chrome and Node at least.)
Cool. Maybe I'll switch then.
It was fun to type this, with the "wrapped" functions having a literal number attached to them, but it's not like either the
Symbol
or theWeakMap
will ever be exported.On the other hand, I won't be able to tell if a function is "wrapped" by type tooltip any more or see its bytelength :/
That's true. I only use TypeScript when I must so I tend to not think about typings.
Great article! A lot of these I didn't know. I especially like
finally
, from the naming it already says a lot about what it does. Your measuring example is nice too, I might use that myself :)