`Proxy` all of the things! Part 1: Counters

lionel-rowe - Aug 26 '21 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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
        },
    }
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Cool, no? In a future installment, we'll look at using Proxy with a setter function as well.

. . . . . . . . . . . . . .