ES6 classes with private members
Tags: javascript, es6, closures, proxies
Published: 2020-03-03 10:06:00

If you researched a bit into private ES6 class members, you might have found that implementing them with "clean code" is hardly possible. They are even harder to apply on an existing codebase without breaking things. Just read this best of messy code and hacky workarounds from a StackOverflow question:

https://stackoverflow.com/questions/22156326/private-properties-in-javascript-es6-classes

However, we can still implement some privacy while not violating [SRP](https://en.wikipedia.org/wiki/Single_responsibility_principle) and keep the class code readable, because we wont even touch existing class code.

In this short article, we will make use a mix of a [closure](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures), a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and a pattern, close to an abstract factory to create custom private instances of a given example class.

## A simple example `class`

Information hiding in "classic" ES5 code is well-known and elegantly solved, for example using the [module pattern](https://dev.to/search?q=javascript%20module%20pattern) or using [closures and a factory function](https://dev.to/somedood/emulating-private-variables-in-javascript-with-closures-and-factory-functions-2314) This tutorial, however strictly focuses on ES6+ classes. 

Our example class contains two primitive members, that are internally accessed within a function:

```javascript
class Person {
  constructor ({ name, age }) {
    this.name = name
    this.age = age
  }
  
  greet () {
    return `Hello, my name is ${this.name}. I am ${this.age} years old.`
  }
}
```


The class is clearly readable and testable but we want to preserve state within the instances after construction - meaning `name` and `age` should not be accessible, while the `greet` function should be.


## Log the property access with a `Proxy`

This section should be of interest for those who are not familiar with using a `Proxy`.

The [proxie's `get` function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get) is a "trap", which it's invoked every time some code tries to access a property of the `target`. It can be used to decide, whether the property is available for access or not. Let's check for a moment how this works by logging the property access:

```javascript
const person = new Person({ name: 'John Doe', age: 42 })
const proxy = new Proxy(person, {
  get: function (target, property, receiver) {
    const value = target[property]
    console.log(`${property} => ${value}`)
    return value
  }
})
```

Let's see the logged values:

```javascript
proxy.name // "name => John Doe"
proxy.age // "age => 42"
proxy.greet // "greet => function() greet {}" 
```

The trap works even when the `target` aims to access its own members. In our case `proxy.greet()` will cause three access calls:

```javascript
proxy.greet() 
// "greet => function() greet {}" 
// "name => John Doe"
// "age => 42"
```

## Restricting access to `name` and `age`

The goal is to enable to the access of `greet` while at the same time preventing the access of `name` and `age`. A simple approach is to only return true, in case the property is of type `'function'`:

```javascript
const person = new Person({ name: 'John Doe', age: 42 })
const proxy = new Proxy(person, {
  get: function (target, property, receiver) {
    return typeof target[property] === 'function'
  }
})

proxy.name // undefined
proxy.age // undefined
proxy.greet() // Hello, my name is undefined. I am undefined years old.
```

Huh!? This is not the expected output, since `name` and `age` have been resolved as `undefined`. Fortunately the explanation is as simple as the solution:

The internal access to `this.name` and `this.age` in `greet` is also running through the `get` trap. Since we allow only `function` types to be accessible, these properties will resolve to `undefined`.

The solution is to `bind` the function properties to the original `person` in order to circumvent the `get` trap:

```javascript
const person = new Person({ name: 'John Doe', age: 42 })
const proxy = new Proxy(person, {
  get: function (target, property, receiver) {
    const member = target[property] 
    if (typeof member === 'function') {
      return member.bind(person)
    }
  }
})

proxy.name // undefined
proxy.age // undefined
proxy.greet() // Hello, my name is John Doe. I am 42 years old.
```

If you are really concerned about hiding `name` and `age` you may also log the proxy itself: `console.log(proxy)` which will reveal to users in the browser console the original `person` instance under `[[target]]`. To get rid of it, you need to pass a plain Object as `target` to the proxy constructor and internally use the `person` reference within the `get` trap:

```javascript
const person = new Person({ name: 'John Doe', age: 42 })
const proxy = new Proxy({}, {
  get: function (target, property, receiver) {
    const member = person[property] 
    if (typeof member === 'function') {
      return member.bind(person)
    }
  }
})
```

In our next step we will use some closure code (via factory function) to complete the hiding of any access to the original `person` instance.

## Wrap this code into a factory function

We are able now to create proxies to instances with well-defined property access but we need get rid of `person` being in scope for external access, otherwise this is all useless. Factory functions to the rescue:

```javascript
const createPerson = ({ name, age}) => {
  const person = new Person({ name, age })
  return new Proxy({}, {
    get: function (target, property, receiver) {
      const member = person[property] 
      if (typeof member === 'function') {
        return member.bind(person)
      }
    }
  })
}

const anon = createPerson({ name: 'John Doe', age: 42 })
anon.name // undefined
anon.age // undefined
anon.greet() // Hello, my name is John Doe. I am 42 years old.
```

Let's push it further as we have much more space for abstraction: 

1. If this works for our `Person` class it should work for any other defined `class`, too.
2. If rules are based on a function that resolves to a Boolean, we can pass a function to decide for the privacy level of a member
3. For creating rules, we should only need the name of the property and it's type, preventing the value from leaking to the outside world 

To do this, we surround our factory function by another factory function, creating somewhat of an abstract factory pattern (not 100% the original pattern but it gets close):

```javascript
const createFactory = (ClassDefiniton, callback) => {
  const factory = (...args) => {
    // create a new instance, let user (dev) manage
    // args validation within the class or before this call
    const instance = new ClassDefiniton(...args)
    
    // our final product
    return new Proxy({}, {
      get: function (target, property, receiver) {
        const member = target[property] 
        const type = typeof member

        // decide to allow access via callback
        const allowed = callback(property, type)
        if (!allowed) return

        // in case the prop is a function -> bind
        return type === 'function'
          ? member.bind(instance)
          : member
      }
    })
  }
}

// create the new factory, including on our custom rule
const createPrivatePerson = createFactory(Person, (key, type) => type === 'function')

const anon = createPrivatePerson({ name: 'John Doe', age: 42 })
anon.name // undefined
anon.age // undefined
anon.greet() // Hello, my name is John Doe. I am 42 years old.
```

From here you could write rules to whitelist/blacklist members by name and type. You could for example hide all properties that include and `_` in the beginning of their name, a convention that is often used to indicate private properties. Also, beware to not leak the value of the property to external code. The property name and the type should be sufficient for most of the cases.

## A final note

I do not want to propose this method as definitely secure. There may be still ways to get access to the original instance or its properties. If you found a way through, please let me know :-)

Furthermore, I'd like to underline, that I have also written a lightweight module and released it to the npm registry that implements the abstract factory nearly the same as the one used in this article:

https://github.com/jankapunkt/js-class-privacy

https://www.npmjs.com/package/class-privacy