4

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/

nils
  • 25,734
  • 5
  • 70
  • 79
  • Even if you hide the properties very well, aren't the values mostly going to be visible from source scripts or ajax responses? – Blake Regalia Oct 12 '15 at 16:36
  • Yes, that's true, I can't hide it completely anyway. I just don't want them to accidentally interact with third-party code (eg. someone loops through my proxied Object). – nils Oct 12 '15 at 16:48
  • `Are the values ... real enough` What do you mean here? – just-boris Oct 13 '15 at 08:58
  • @just-boris are the values I am returning for hidden values those that you would expect to receive if the property does not exist? Eg. the `in` operator should return `false` if a property does not exists (not `undefined`, `null` or anything else). – nils Oct 13 '15 at 12:17
  • You will get this while you have `throwError = false`. Once it will be proxied with throw errors, you will get `Exception` for `in` check, gets and sets on private properties – just-boris Oct 13 '15 at 20:39
  • 2
    I don't think using a proxy to hide all private properties has any use. You're not just hiding them from the outside, you're hiding them from everyone - and if you want to write your methods to simply access the `target` (or `o`) variable directly, you could…er…*should* simply have used closures for privacy in the first place. – Bergi Dec 02 '16 at 02:57
  • If you want to implement private fields using ECMAScript 6 features, don’t use Proxies. Use `WeakMap`. – user3840170 Jun 03 '23 at 16:58

2 Answers2

3

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.

2

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.
Michał Perłakowski
  • 88,409
  • 26
  • 156
  • 177