
Meta Programming in JavaScript with Proxies - kiyanwang
https://itnext.io/meta-programming-in-javascript-with-proxies-64fa4898070e
======
benjaminjackman
Having experimented with and used proxies a decent amount, the MDN page is my
goto reference when I need to use them: [https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Refe...](https://developer.mozilla.org/en-
US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)

The biggest pitfalls, in my experience are

1\. Infinite recursion when implementing get and printing proxies with
console.log in node.js (which attempts to access properties from the object
prototype which can lead to infinite recursion problems).

2\. Performance, which I would need to do more digging and benchmarking to
comment on in more depth.

Some libraries that use Proxies to great effect are the
[https://github.com/mobxjs/mobx-state-tree](https://github.com/mobxjs/mobx-
state-tree) and
[https://github.com/mweststrate/immer](https://github.com/mweststrate/immer)
libraries both by mweststrate

~~~
fro0116
Immer is excellent. It makes deeply nested immutable updates a breeze. The API
almost makes it feel like you're programming in a fantasy-land future where
JavaScript's default data structures were persistent data structures like a
proper functional language.

And it accomplishes all that without sacrificing type safety like most string
path based approaches to deep updates often tend to do (think lodash.set or
ramda.assocPath).

The only reason we haven't gone all in on it yet is the existence of browsers
that don't support Proxy (like IE11, which we now unfortunately have to
support), where performance takes a huge hit, and those tend to be the
browsers that can least afford to take that performance hit to begin with.

~~~
pault
I just converted one of our medium sized redux apps to immer and it has been a
resounding success. I highly recommend it and will be using it everywhere in
the future.

~~~
acemarke
Hi, I'm a Redux maintainer. I'd be interested in hearing how your experience
with Immer went, as well as any other feedback you can offer.

Also, as I mentioned just below, please try out our new Redux Starter Kit
package, which uses Immer internally (and offers several other useful
utilities for simplifying common Redux use cases, like store setup, reducer
definitions, and even creating entire "slices" of state at once):

[https://redux-starter-kit.js.org](https://redux-starter-kit.js.org)

~~~
pault
Actually the reason I was willing to take a chance on Immer is because it's
included in redux starter kit. :)

Using immer in production has been about 1000x better than writing immutable
state updates by hand. It literally reduced (hah) the LOC in our reducers by
at least 75%, not to mention the gains from reduced cognitive overhead.

The architecture I ended up using is basically a recreation of redux starter
kit with some customization for convenience (a couple of methods on action
creators that wrap `toString` for creating reducer keys that only handle
actions with certain meta properties, like `updateEntities.of(entity)`).

~~~
acemarke
Neat! I'd be interested in seeing what you came up with if you get a chance.

------
kristiandupont
This rubs me the wrong way somehow.

All of this functionality could be accomplished with simple functions. They
may not be as cool, but they are obvious in the calling code and they are easy
to search for. Magic stuff like this conflicts with community conventions. New
people are going to be perplexed as to what's going on (or worse, they learn
from your code and conversely get perplexed when x[-1] doesn't work on a
normal array). Also, you risk confusing various tools in subtle ways.

~~~
dcherman
I agree. While you can accomplish some really interesting things with Proxy, I
think that this article presents some poor examples that are actively harmful.
I don't expect `delete` to be O(N) (actually worse if you consider the
`ownKeys` implementation. I don't expect setting a property (sort) to invoke
an expensive sorting operation. The list keeps going.

While I appreciate what the author is trying to accomplish, I think this is an
example of what _not_ to do as far as code is concerned.

Most of my practical usage of Proxies to date have been limited to some
debugging tools that I wrote for AngularJS to help detect property changes
that occur outside of a digest cycle. Don't think I've had any use cases that
I've shipped to production code yet, though I believe Vue is considering using
proxies for their reactivity model in the future to avoid the need for
`Vue.get` and `Vue.set` for previous unknown properties or deletions.

~~~
thisacctforreal
I particularly like the ``JSON.stringify({ name: "Albert Einstein" }) in
dbProxy``.

------
invalidusernam3
> get(tgt, prop, rcvr)

Good article, terrible variable naming.

Abbreviated variables are a pet peeve of mine. What is 'tgt' supposed to be?
An abbreviation of 'target'? 'Thank god it's Thursday'? Might as well just use
x,y and z, it's no less clear.

~~~
jcelerier
funny, target, propery, receiver seem evident to me. don't you guys do note-
taking ?

~~~
invalidusernam3
What is the convention used here for deciding the abbreviated names? It's
clearly not removing vowels since prop has an o in it. So it becomes
subjective as to how stuff should be abbreviated. If things are subjective,
there is no standard.

In one function developer A might call it "rcvr", in another function
developer B might call it "rec". Both mean "receiver", just use "receiver".

~~~
daotoad
`rcvr` is clearly Resistor Capacitor Viewer. Duh. And clearly `rec` is
Recovery.

The Story of Bunny Showers:

I worked on a code base where we had, in one class, seemingly endless
variations on the spelling of "business hours". There were variables named
`bsns_hrs`, `business_hours`, ` business_hrs`. Furthermore, in the object's
(horrible, messy, overlarge, mutable as fsck) internal state, there were
elements labeled with `busnessHours`, `bsnsHours`, 'business-hours`, 'bnshrs`.
And so on.

My habbit when reading code like this is to map each spelling to a
phonetically similar word or phrase. So `business_hrs` becomes "business hers"
and `bnshrs` becomes "bunny showers". And so forth. It makes it easier to
rationalize about what is happening, especially if, like me, you move your
lips when you read.

I have ever since called this issue of inconsistent spelling in code the
"bunny showers" problem, owing both to the well-known fecundity of rabbits and
the hellish proliferation of inconsistent names in that fabled codebase.

------
turdnagel
I love proxies. I do worry about their effects on performance, but the benefit
of being able to use native object semantics is sometimes too great.

I’ve developed a proof-of-concept library that uses them to provide optionals
support (no more TypeErrors for writing things like
obj.property.property2.property3 when property doesn’t exist) here:
[https://github.com/jdelman/proxy-lens](https://github.com/jdelman/proxy-lens)

(Btw, I know calling it a lens isn’t right.)

~~~
chrisseaton
> I do worry about their effects on performance

What kind of performance do you mean? Compile time? They should be optimisable
away and shouldn't have any effect on actual run time should they?

~~~
RussianCow
Proxies are entirely a runtime API. Did you read the article?

~~~
chrisseaton
> Proxies are entirely a runtime API.

Yeah I know - but where isn't the performance as good as it should be? Is it
during compile time, runtime, memory, something else? Do they not get compiled
them away so they're free in terms of runtime cost?

> Did you read the article?

Yes of course I read the article. It's against HN guidelines to ask people
this.

~~~
pxndx
They don't get compiled away because Proxies are a runtime thing. The impact
on performance is entirely on the runtime.

~~~
chrisseaton
I have no idea what you are trying to say. I know they're a runtime thing. Why
is it not possible to optimise them away so they don't have a runtime cost,
after having been compiled?

~~~
pxndx
Because they need runtime support from the engine, e.g. you need engine
support in order to intercept calls like:

    
    
      obj[prop] = 'foo'
    

where prop is user input or similar. You could technically compile them away
if you replaced _all_ property set/gets with code like

    
    
      set(obj, prop, "foo")
      get(obj, prop)
    

with a babel step, but the performance impact of this (both in runtime and
compile-time) would be enormous.

~~~
chrisseaton
I don't mean Babel source-to-source compilation. I mean the native compilation
done at runtime. The JIT.

~~~
pxndx
There's some work being done on optimising Proxies, but the current
implementation is just slow (and also varies by engine), but yes, they can
probably be optimised further.

EDIT: there are various blog posts around talking about optimisation:
[https://v8.dev/blog/optimizing-proxies](https://v8.dev/blog/optimizing-
proxies)

------
trh88
An interesting article, but I would be concerned about readability for
newcomers unfamiliar with Proxies.

To be honest, for the particular use cases mentioned here - why not use the
object utilities in Lodash (which is well used, familiar to many, well
documented, and battle hardened)?

> we didn’t get around to implementing slices like array[1:3]

What's wrong with arr.slice(1,3)?

------
alangpierce
One fun use case is that you can use proxies to detect accesses to nonexistent
properties of JS objects, and you can then throw or log a warning rather than
having them just return undefined. Unclear if you'd want to do that everywhere
(due to perf reasons), but might might be a good thing in development/tests.

You can also use proxies to aid in code migrations. For example, let's say you
want to migrate your frontend code from `user.id` to `user.handle` and
eventually stop returning `id` from the backend. If you wrap server responses
in a proxy, then you can warn in production if any code ever accesses `.id`.
That's a lot more comforting than trying to search the code, and even in a
100% TypeScript codebase you still might have some dynamic accesses that the
proxy would catch.

(I haven't actually done either of the two above approaches in a real-world
scenario, but they've always seemed appealing, and I'm curious of others have
real-world experience with them.)

------
kozhevnikov
One of the unexpected behaviours was that if you proxy an object you cannot
call it as a function even if you define `apply` handler. This made proxying
async functions impossible because they always return Promise<T> even if T is
another function, unless you wrap promise in a dummy function before proxying.

[https://github.com/kozhevnikov/proxymise/blob/master/index.j...](https://github.com/kozhevnikov/proxymise/blob/master/index.js#L3)

~~~
diegorbaquero
That is really interesting, I'm building something with Proxies for any Type
and async function proxified worked for me. Mind if I hit you up for a chat?

~~~
kozhevnikov
Sure. I mean if you have an async function that returns another function (so
it actually returns a promise of a function because it's async) proxy of the
original function will call apply handler when invoked, but proxy of the
return value will not because it's actually not a function but an object (a
promise of a function) and as it turned out proxied objects do not call apply
handler when invoked (even if it's defined) but throw an exception.

    
    
        const handler = { apply: () => { console.log('ok') } };
        const foo = new Proxy(() => {}, handler);
        const bar = new Proxy({}, handler);
    
        > foo()
        < ok
        > bar()
        < VM313:1 Uncaught TypeError: bar is not a function
            at <anonymous>:1:1

------
alangpierce
> Unfortunately, to get the last element we still have to do the clunky
> javaScriptArray[ javaScriptArray.length - 1 ] technique. This ... has always
> seemed like a major oversight in the ECMAScript standard.

Sort of an aside, but there's a stage 1 (relatively early) proposal to add an
`array.lastItem` property that should finally improve things:

[https://github.com/keithamus/proposal-array-
last](https://github.com/keithamus/proposal-array-last)

~~~
aidos
I miss python’s list operators so much when I’m in JS (along with lots of
other stuff). Is there a reason why we couldn’t use python’s x[-1] syntax in
JS?

~~~
alangpierce
There's probably existing code out there that relies on `arr[-1]` returning
`undefined`, e.g. looping backward through an array until it gets a falsy
value. You can also assign to `arr[-1]` and it'll work (it assigns to a new -1
key, not to the end of the array), and maybe some people do that in existing
code. They had to pick `lastItem` as the name instead of just `last` since
using `last` is also known to break the web.

It's also not obvious that negative array accesses should behave that way.
It's a tradeoff where it's more convenient in some cases but feels a bit less
elegant and can lead to bugs (particularly dynamic array accesses that are
accidentally negative). But FWIW, `slice` already implements this behavior,
e.g. `arr.slice(-1)[0]` is a way to get the last element of an array, so
there's certainly precedent in JS for it.

------
zubairq
Yep, proxies can be really useful, but use them carefully. We use them at Yazz
so that we can have custom code editors that have variables injected into them

------
daotoad
Yet another way JavaScript has become more like Perl5. It now features `tie`.
[https://perldoc.perl.org/functions/tie.html](https://perldoc.perl.org/functions/tie.html)

------
redka
I use these sometimes in personal projects:

[https://gist.github.com/thisredone/783407f9aacc496c4c13e4076...](https://gist.github.com/thisredone/783407f9aacc496c4c13e4076bca7d63)

------
z3t4
All the examples could be done in ES5 using Object.defineProperty. Proxies do
however have more traps available.

