Skip to content

Instantly share code, notes, and snippets.

@hemanth
Created June 17, 2012 05:33
Show Gist options
  • Save hemanth/2943524 to your computer and use it in GitHub Desktop.
Save hemanth/2943524 to your computer and use it in GitHub Desktop.
Protected properties using Name objects

Object.protectProperty

  • Object.protectProperty(object : Object, key : String) -> Name
    protects object[key] and returns the new Name which protects it
  • Object.protectProperty(object : Object, key : String, protector : Name) -> Name protects object[key] with existing protector
  • Object.protectProperty(object : Object, currentProtector : Name) -> String
    returns the key that the protector is protecting on object
  • Object.protectProperty(object : Object, currentProtector : Name, newProtector : Name) -> String
    changes the protector for the key on object that currentProtector protects to newProtector and returns the key

Semantics

  • Objects keep an internal map of protector Name objects to keys.
  • A protector can only protect one key per object.
  • Each key on an object can only have one protector (this isn't necessary but seems preferable).
  • The key a protector protects can vary between objects.
  • The protector for an object's key can be changed.
  • A non-writable property's value can always be changed via the protector (but not direct assignment).
  • Setting a property to non-configurable makes it non-protected (they are mutually exclusive and configurability trumps).
  • By extension, a non-configurable property cannot become protected.
  • For Accessors, the public property of the protector is passed to the setter (matches how private Names work with Proxies).

Usage

let x = { a: 10 };

Protecting a property

let writeA = Object.protectProperty(x, 'a');
// -- or --
let writeA = new Name();
Object.protectProperty(x, 'a', writeA);

Describing a protected property

Object.isProtected(x, 'a');
// -->
  true

Object.getOwnPropertyDescriptor(x, 'a')
// -->
  { value: 10,
    writable: false,    // default protectProperty to set non-writable?
    protected: true,
    enumerable: true,
    configurable: true }

Writing to protected property

x.a = 50
// silent fail normally, throws in strict "Cannot assign to protected read-only property 'a'"

x[writeA] = 50;
// x is now ->
  { a: 50 }

Modifying a protected property description

Object.defineProperty(x, 'a', { writable: true });
// throws "Cannot modify protected property 'a'"

Object.defineProperty(x, writeA, { writable: true });
// -->
  { value: 50,
    writable: true,
    protected: true,
    enumerable: true,
    configurable: true }

Unprotecting a property

Object.defineProperty(x, writeA, { protected: false });
// -- or --
Object.protectProperty(x, writeA);

Changing a protector

Object.defineProperty(x, writeA, { protected: false });
let newWriteA = Object.protectProperty(x, 'a');
// -- or --
let newWriteA = new Name();
Object.protectProperty(x, writeA, newWriteA);

x[writeA] = 100;
// throws 'Invalid protected property assignment'

Example

Base on http://wiki.ecmascript.org/doku.php?id=strawman:maximally_minimal_classes

const pHealth = new Name();
const wAlive = new Name();

class Monster {
  constructor(name, health) {
    this.name = name;
    this.alive = true;
    this[pHealth] = health;
    Object.protectProperty(this, 'alive', wAlive);
  }

  attack(target) {
    log('The monster attacks ' + target);
  }

  defend(roll) {
    let damage = Math.random() * roll;
    this[pHealth] -= damage;
    if (this[pHealth] <= 0) {
      this[wAlive] = false;
    }
  }

  set health(value) {
    if (value < 0) {
      throw new Error('Health must be non-negative.');
    }
    this[pHealth] = value;
  }
}
// The following depends on a quirk in V8 to work.
// Depends on Map and WeakMap, which can be:
// * turned on in Chrome's about:flags
// * enabled in node using --harmony_collections or --harmony
// * shimmed via https://github.com/Benvie/ES6-Harmony-Collections-Shim
// Provides:
// * Name
// * Object.protectProperty
var Name = function(global){
var getDescriptor = Object.getOwnPropertyDescriptor;
var getProperties = Object.getOwnPropertyNames;
var define = Object.defineProperty;
var defines = Object.defineProperties;
var call = Function.call.bind(Function.call);
var catcher = Object.prototype.__proto__ = Object.create(null);
var protectors = createStorage(WeakMap);
var properties = createStorage(Map);
function createStorage(creator){
var storage = new WeakMap;
return function(o){
var data = storage.get(o);
if (!data)
storage.set(o, data = creator());
return data;
}
}
function isObject(o){
return o != null && typeof o === 'object' || typeof o === 'function';
}
var Name = function(){
// crypto.getRandomValues / require('crypto').psuedoRandomBytes,
// ideally, but this will do for now
function randomName(len){
var chars = [];
while (len--)
chars[len] = String.fromCharCode(Math.random() * 255 | 0);
return btoa(chars.join(''));
}
// base64 converter so this works in Node.js unmodified
var btoa = global.btoa || function btoa(str){
return new Buffer(str).toString('base64');
}
var ToString; ({}[{ toString: function x(){ ToString = x.caller; } }]);
//var DELETE; delete ({}[{ toString: function x(){ DELETE = x.caller; } }]);
//var IN; ({ toString: function x(){ IN = x.caller; } } in {});
var names = createStorage(Object.create.bind(null, null));
var readonly = { enumerable: true, configurable: true, writable: false };
// #############
// ### Name ####
// #############
function Name(str){
if (!(this instanceof Name))
return new Name(str);
var values = new WeakMap;
var store = names(this);
var self = this;
store.propertyName = randomName(40);
store.name = str;
this.public = {};
define(catcher, store.propertyName, {
configurable: true,
get: function(){
var keymap = protectors(self);
if (keymap.has(this))
return this[keymap.get(this)];
else
return values.get(this);
},
set: function(v){
var keymap = protectors(self);
if (keymap.has(this)) {
var k = keymap.get(this);
var desc = getDescriptor(this, k);
if (!desc) {
readonly.value = v;
define(this, k, readonly);
readonly.value = null;
} else if ('set' in desc) {
if (desc.set)
call(desc.set, this, v, self.public);
} else {
desc.value = v;
define(this, k, desc);
}
} else {
values.set(this, v);
}
}
});
}
Name.prototype = Object.create(null, {
toString: { value: function toString(){
var caller = toString.caller;
if (caller === ToString)
return names(this).propertyName;
else
return names(this).name;
} }
});
return Name;
}();
function protectProperty(object, key, protector){
var map = properties(object);
var keymap;
if (typeof key === 'string') {
// protecting a new property
// Object.protectProperty(object : Object, key : String, [protector : Name]) -> Name
var desc = getDescriptor(object, key);
if (desc && !desc.configurable)
throw new Error('Can only protect configurable properties');
if (map.has(key)) {
// key is already protected
if (map.get(key) !== protector)
throw new Error('Property is already protected');
else
return protector;
}
if (!(protector instanceof Name))
protector = new Name;
keymap = protectors(protector);
if (keymap.has(object)) {
if (keymap.get(object) !== key)
throw new Error('Protector can only protect one key per object');
else
return protector;
}
// write protect property
if (desc && desc.writable)
define(object, key, { writable: false });
// map protector to key and key to protector
keymap.set(object, key);
map.set(key, protector);
return protector;
} else if (key instanceof Name) {
if (protector instanceof Name) {
// changing protector
// Object.protectProperty(object : Object, [protector : Name], [newProtector : Name]) -> String
var newProtector = protector;
protector = key;
keymap = protectors(protector);
if (!keymap.has(object))
throw new TypeError('Invalid protector for object');
var newKeymap = protectors(newProtector);
if (newKeymap.has(object))
throw new Error('Protector can only protect one key per object');
// retrieve the key the protector protects
key = keymap.get(object);
// remap key to new protector and remove old route
map.set(key, newProtector);
newKeymap.set(object, key);
keymap.delete(object);
return key;
} else {
return protectors(protector).get(object);
}
}
}
function defineProperty(o, k, desc){
if (!isObject(o))
throw new TypeError('Object.defineProperty called on non-object');
if (!isObject(desc))
throw new TypeError('Property description must be an object');
var props = properties(o);
if (k instanceof Name) {
var keymap = protectors(k);
var key = keymap.get(o);
if (key === undefined)
throw new Error('Object.defineProperty called with invalid protector');
// unprotect
if (desc.protected === false || desc.configurable === false) {
keymap.delete(o);
props.delete(key);
}
return define(o, key, desc);
} else if (typeof k === 'string') {
if (props.has(k))
throw new Error('Object.defineProperty called on protected property');
return define(o, k, desc);
}
}
function defineProperties(o, descs){
if (!isObject(o))
throw new TypeError('Object.defineProperty called on non-object');
if (!isObject(descs))
throw new TypeError('Property descriptions must be an object');
Object.keys(descs).forEach(function(key){
defineProperty(o, key, descs[key]);
});
return o;
}
function getOwnPropertyDescriptor(o, k){
if (!isObject(o))
throw new TypeError('Object.getOwnPropertyDescriptor called on non-object');
var desc = getDescriptor(o, k);
if (desc) {
desc.protected = desc.configurable && properties(o).has(k);
}
return desc;
}
function getOwnPropertyNames(o){
return o === catcher ? [] : getProperties(o);
}
defines(Object, {
getOwnPropertyDescriptor: { value: getOwnPropertyDescriptor },
getOwnPropertyNames: { value: getOwnPropertyNames },
defineProperties: { value: defineProperties },
defineProperty: { value: defineProperty },
protectProperty: { value: protectProperty, configurable: true, writable: true }
});
return Name;
}(new Function('return this')());
void function(){
var hasOwn = Object.hasOwnProperty;
function assert(message, expression){
var success = expression !== false ? 'Passed' : 'Failed';
console.log(success+': '+message);
}
function _throws(callback){
if (typeof callback === 'string')
callback = new Function(callback);
try { callback(); return true; }
catch (e) { return false; }
}
function throws(message, callback){
var success = !_throws(callback) ? 'Passed' : 'Failed';
console.log(success+': '+message);
}
function doesntThrow(message, callback){
var success = _throws(callback) ? 'Passed' : 'Failed';
console.log(success+': '+message);
}
function similar(message, o1, o2){
var keys1 = Object.getOwnPropertyNames(o1);
var keys2 = Object.getOwnPropertyNames(o2);
var out = {};
[keys1, keys2].forEach(function(keys, i){
keys.forEach(function(key){
if (!hasOwn.call(o1, key))
out[key] = 'extra property';
else if (!hasOwn.call(o2, key))
out[key] = 'missing property';
else if (o1[key] !== o2[key])
out[key] = { o1: o1[key], o2: o2[key] };
});
});
if (Object.keys(out).length)
console.log('Failed: '+message, out);
else
console.log('Passed: '+message);
}
console.log('==== Basics ====');
void function(){
var x = { y: 10, z: 'abc' };
var y = Object.protectProperty(x, 'y');
assert('protectProperty returns a Name object', y instanceof Name);
var z = new Name;
assert('propertyProperty uses passed in Name', Object.protectProperty(x, 'z', z) === z);
x.y = 20;
assert('writing to property directly fails', x.y === 10);
x[y] = 20;
assert('writing with the protector works', x.y === 20);
var newY = new Name;
assert('changing protector returns key', Object.protectProperty(x, y, newY) === 'y');
x[newY] = 50;
assert('new protector works', x.y === 50);
x[y] = 100;
assert('old protector is invalidated', x.y === 50);
Object.defineProperty(x, newY, { protected: false });
x[newY] = 1000;
assert('setting protected to false invalidates the protector', x.y === 50);
}();
console.log('==== Object.defineProperty ====');
void function(){
var x = { y: 10, z: 'abc' };
var y = Object.protectProperty(x, 'y');
throws('throws without the protector', function(){
Object.defineProperty(x, 'y', { writable: true });
});
doesntThrow('does not throw with valid protector', function(){
Object.defineProperty(x, y, { writable: true });
});
throws('defineProperty throws on invalid protector', function(){
Object.defineProperty(x, new Name, { protected: true });
});
}();
console.log('==== Object.getOwnPropertyDescriptor ====')
void function(){
var desc = {
value: 10,
writable: true,
enumerable: true,
configurable: true,
protected: false
};
var x = { y: 10 };
similar('shows protected attribute', Object.getOwnPropertyDescriptor(x, 'y'), desc);
desc.protected = true;
desc.writable = false;
var y = Object.protectProperty(x, 'y');
similar('reflects protection of property', Object.getOwnPropertyDescriptor(x, 'y'), desc);
Object.defineProperty(x, y, { protected: false });
desc.protected = false;
similar('reflects removal of protected', Object.getOwnPropertyDescriptor(x, 'y'), desc);
}();
console.log('==== Accessor ====');
void function(){
var toSetter = {};
var x = {
set y(val, name){
assert('correct receiver in setter', this === x);
assert('public name is passed', name === y.public);
assert('value is correctly passed', val === toSetter);
},
set z(val, name){
assert('correct receiver in setter', this === x);
assert('public name is NOT passed', name !== z.public);
assert('value is correctly passed', val === toSetter);
},
};
var y = Object.protectProperty(x, 'y');
var z = Object.protectProperty(x, 'z');
x[y] = toSetter;
x.z = toSetter;
}();
console.log('==== Strict mode ====');
void function(){
"use strict";
var x = { y: 10 };
var y = Object.protectProperty(x, 'y');
throws('strict mode direct write throws', function(){
x.y = 20;
});
doesntThrow('strict mode protector write does not throw', function(){
x[y] = 20;
});
assert('strict mode write sets values to non-writable property', x.y === 20);
}();
}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment