`Proxy` all of the things! Part 1: Counters
lionel-rowe
Posted on August 26, 2021
To celebrate the long-overdue death of Internet Explorer, I'm writing a series of articles on a massively useful and underused API that's available in every other mainstream JavaScript environment: Proxy
.
With a Proxy
, you can "intercept and redefine fundamental operations" for an object, such as getters and setters.
Let's start with a simple example: counters with a default value.
Let's say you're implementing a simple algorithm to count the number of occurrences of each word in a text. In a language like Ruby, you could do that easily like this:
def word_counts(text)
counters = Hash.new(0)
text.split(/\W+/).each do |word|
counters[word] += 1
end
counters
end
wc = word_counts 'a a a b b c' # {"a" => 3, "b" => 2, "c" => 1}
wc['a'] # 3
wc['d'] # 0
That Hash.new(0)
is really neat: it gives us key-value pairs with a default value of 0
that we can increment from.
JavaScript objects, on the other hand, can't be given a default value. Passing a parameter to an Object
constructor instead converts that value itself into an object: new Object(0)
returns Number {0}
, which isn't what we want at all.
However, we can easily mimic Ruby's Hash.new
behavior with a proxy:
/**
* @template T
* @param {T} defaultVal
* @returns {Record<string, T>}
*/
const hashWithDefault = (defaultVal) => new Proxy(
Object.create(null),
{
get(target, key) {
return target[key] ?? defaultVal
},
}
)
The target
parameter passed to the getter is the proxied object itself — the first argument passed to the Proxy
constructor. In this case, we use an empty object with no properties (not even those from Object.prototype
), which we create using Object.create(null)
.
As we didn't override set
, setting simply works as normal — the property is set on that same target
.
Our JavaScript hashWithDefault(0)
now works very similarly to Ruby's Hash.new(0)
. We can now easily and ergonomically write our word count function like this:
/** @param {string} text */
const wordCounts = (text) => {
const counters = hashWithDefault(0)
for (const word of text.split(/\W+/)) {
counters[word]++
}
return counters
}
const wc = wordCounts('a a a b b c') // Proxy {a: 3, b: 2, c: 1}
wc.a // 3
wc.d // 0
Cool, no? In a future installment, we'll look at using Proxy
with a setter function as well.
Posted on August 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024