Skip to content

Instantly share code, notes, and snippets.

@ifraixedes
Last active August 13, 2021 14:51
Show Gist options
  • Save ifraixedes/e9311748c961f1dbb93e to your computer and use it in GitHub Desktop.
Save ifraixedes/e9311748c961f1dbb93e to your computer and use it in GitHub Desktop.
Private properties on ES6 Classes using Proxies
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004
(http://www.wtfpl.net/about/)
Copyright (C) 2015 Ivan Fraixedes (https://ivan.fraixed.es)
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
{
"name": "private-prop-es6-class-with-proxy",
"version": "0.0.0",
"description": "Private properties on ES6 Classes using Proxies",
"main": "private-props-es6-class-with-proxy.js",
"dependencies": {
"harmony-reflect": "^1.1.3"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ivan Fraixedes <ivan@fraixed.es> (http://ivan.fraixed.es)",
"license": "WTFPL"
}
#! iojs --harmony --harmony_proxies
'use strict';
require('harmony-reflect');
// Usual class
class Person {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
set name(name) {
this._name = name;
}
greeting(person) {
return `hi ${person.name}`;
}
}
// Create an object from an usual class
let ivanPerson = new Person('Ivan');
// Call the getter of _name
console.log(`name getter from a Person instance: ${ivanPerson.name}`);
// _name property is accessible because JS classes doesn't have access scope
console.log(`_name property from a Person instance: ${ivanPerson._name}`);
// Let's try to protect class object
let ProtectedPerson = new Proxy(Person, {
get(target, name) {
console.log('calling getter in Person wrapped by a proxy');
if (name.startsWith('_')) {
throw new Error('Accessing to a private property is not allowed');
} else {
return target[name];
}
}
});
// Let's create an instance of the class that we wrapped with a proxy
let ivanProtectedPerson = new ProtectedPerson('Ivan');
// Call the getter of _name
console.log(`name getter from a protected person instance: ${ivanProtectedPerson.name}`);
// _name still accessible due proxy wrapped class Person;
// a class is a Function with prototype object, proxy trap calls to the
// class itself hence the target is the class, not its instances
console.log(`_name property from a protected person instance: ${ivanProtectedPerson._name}`);
// let's protect a base class instance
let ivanPersonProtected = new Proxy(ivanPerson, {
get(target, name) {
if (name.startsWith('_')) {
throw new Error('Accessing to a private property is not allowed');
} else {
return target[name];
}
}
})
// Call the getter of _name
console.log(`name getter from a person instance which has been protected: ${ivanPersonProtected.name}`);
try {
// _name property call gets trapped by the proxy
console.log(`_name property from a person instance which has been protected: ${ivanPersonProtected._name}`);
} catch (e) {
if (e.message === 'Accessing to a private property is not allowed') {
console.log('Proxy did its job!!');
} else {
throw e;
}
}
// However it's painful, having to wrap in a proxy every single instance of a class
// is a repeatable tiring task, so let's try to creata a class that protect its
// object instances
class PersonProtected {
constructor(name) {
this._name = name;
// In es6 it also works as in es5: remember es6 class is nothing more than a Function
// and `new` call the function defined by `constructor`;
// in es5 works because when you call a `new` on a function the value retuned is the
// value returned inside the function if it's an object otherwise returns `this`;
// in es6 remains the same for backward compatibility
return new Proxy(this, {
get(target, name) {
if (name.startsWith('_')) {
throw new Error('Accessing to a private property is not allowed');
} else {
return target[name];
}
}
});
}
get name() {
return this._name;
}
set name(name) {
this._name = name;
}
greeting(person) {
return `hi ${person.name}`;
}
}
let ivanPersonProtectedInstance = new PersonProtected('Ivan');
// Call the getter of _name
console.log(`name getter from a PersonProtected instance: ${ivanPersonProtectedInstance.name}`);
try {
// _name property call gets trapped by the proxy
console.log(`_name property from a PersonProtected instance: ${ivanPersonProtectedInstance._name}`);
} catch (e) {
if (e.message === 'Accessing to a private property is not allowed') {
console.log('Class which proxy `this` did its job!!');
} else {
throw e;
}
}
@heisian
Copy link

heisian commented Nov 2, 2017

get name() and set name(name) in the final example are to safely expose the internal _name property via, well, a getter/setter function. So instead of modifying _name directly, you can use the getter/setter to add validation/filtering logic.

@porada
Copy link

porada commented Nov 17, 2017

@ifraixedes, when greeting, a public method, is modified to use a private property, the proxy makes it throw. In theory, it shouldn’t, because the private property isn’t exposed directly. I worry this might disqualify object proxies as a solution to private properties in ES6 classes.

class PersonProtected {
  // …

  greeting(person) {
    return `hi ${person.name}, my name is ${this._name}`; // ← Calling a private property inside a public method
  }
}
console.log(ivanPersonProtectedInstance.greeting('John')); // → Error: Accessing to a private property is not allowed

// Expected: hi John, my name is Ivan

@denik1981
Copy link

I like this implementation but unfortuntely it will still exposed private methods that could change private properties.
If private methods are defined with a "_" prefix then they won't be able to be called by even the class itself, so this will be a huge problem. The only way to make those private methods to work is to use another prefix or no-prefix. But this will make the interface of that class kinf of overbloat. This is 2021, so now, thankfully, we have the "#" prefix to solve this problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment