nils
nils

Reputation: 27204

Using ES6 Proxies to hide private properties

I'm trying to create a function that hides private properties on Objects as well as possible. I would define private properties here as those that begin with an underscore, eg. _password.

Below is what I've got so far (thanks to Nicolas Bevacqua's great intro to proxies).

Now I was wondering:

  1. Am I covering all bases with the following code? Or am I missing an important proxy trap through which the objects could still be accessed?
  2. Is this the right way to use the Reflect methods in conjunction with a proxy? And do I even need them here?
  3. Are the values I return for private properties real enough to let people think that the property really does not exist?

My function so far:

function privatize(obj, prefix = '_', throwError = false) {
  const proxyHandler = {
    get(target, key) {
        return private(key, 'get') ? undefined : Reflect.get(target, key);
      },
      set(target, key, value) {
        return private(key, 'set') ? undefined : Reflect.set(target, key, value);
      },
      has(target, key) {
        return private(key, 'has') ? false : Reflect.has(target, key);
      },
      deleteProperty(target, key) {
        return private(key, 'delete') ? false : Reflect.deleteProperty(target, key);
      },
      defineProperty(target, key, descriptor) {
        return private(key, 'defineProperty') ? false : Reflect.defineProperty(target, key, descriptor);
      },
      enumerate(target) {
        return Object.keys().filter((key) => {
          return !private(key, null, false);
        })[Symbol.iterator]();
      },
      ownKeys(target) {
        return Reflect.ownKeys(target).filter((key) => {
          return !private(key, null, false);
        });
      },
      getOwnPropertyDescriptor(target, key) {
        return private(key, 'getOwnPropertyDescriptor') ? false : Reflect.getOwnPropertyDescriptor(target, key);
      }
  };

  function private(key, operationName) {
    if (key.indexOf(prefix) === 0) {
      if (throwError) {
        throw new Error(`Operation '${operationName}' is not allowed on private properties.`);
      }
      return true;
    }
  }

  return new Proxy(obj, proxyHandler);
}

var o = {
  first: 'should work',
  _second: 'should fail'
};

var proxied = privatize(o);

console.log(proxied);

PS: For native browser support, you might have to look at it in MS Edge or Firefox Dev Edition.

http://jsfiddle.net/bkd7mde7/1/

Upvotes: 4

Views: 1890

Answers (2)

user663031
user663031

Reputation:

You need to be aware of the concept of "invariants". For instance, if an object is non-extensible, then it is not allowed to hide its properties via the proxy, and a non-configurable property may not be hidden. You cannot defineProperty a property it does not already have. getOwnPropertyDescriptor must return an object or undefined. deleteProperty cannot delete a non-configurable property. set cannot change a non-writable, non-configurable property. Any or all of these could cause your code to fail (by throwing at run-time) in various scenarios.

Other minor issues include the fact that set is supposed to return a boolean (success/failure), although I'm not sure what would happen with the undefined that your are returning.

Upvotes: 3

Michał Perłakowski
Michał Perłakowski

Reputation: 92531

There are a couple of issues with your code:

  • The part throwError = throwError in the private function's parameters is superfluous (and in fact doesn't even work, at least in the latest Chrome), because throwError will be available inside the function anyway.
  • getOwnPropertyDescriptor trap should never return false. It should return undefined for non-existing properties.
  • The enumerate trap is obsolete.
  • Calling Reflect.preventExtensions() on the proxied object breaks it. You should prevent preventing extensions of your object by adding a preventExtensions trap and returning false inside it.

Upvotes: 2

Related Questions