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.