Syntax Sugar for protected state

# 森建 (9 years ago)

Dear ES discuss subscribers,

I'm Kenta Moriuchi,
Department of Art and Information Design
Kyushu University in Japan

I propose Protected State.

In ES2015:

// utility
function createProtectedStorage() {
    const wm = new WeakMap();

    return (self, protectedClass) => {
        const map = wm.get(self);

        if(protectedClass == null) {
            return map || wm.set(self, Object.create(null)).get(self);
        }

        const p = new protectedClass(self);
        if(map) {
            Object.assign(p, map);
        }
        return wm.set(self, p).get(self);
    }
}

const _ = createProtectedStorage();


class Protected_A {

    constructor(publicThis) {
        this.publicThis = publicThis;
    }

    getName() {
        return `${this.publicThis.name} ${this.lastName}`;
    }
}

class A {

    constructor(name, lastName) {
        // protected this
        if(new.target === A)    _(this, Protected_A);

        // public property
        this.name = name;

        // protected property
        _(this).lastName = lastName;
    }

    callGetName() {
        // call protected method
        return _(this).getName();
    }
}

// test
const a = new A("foo", "bar");

// "foo bar"
console.log(a.callGetName());

// "foo"
console.log(a.name);

// undefined
console.log(a.lastName);


// extends
class Protected_B extends Protected_A {

    constructor(publicThis) {
        super(publicThis);
    }

    getAge() {
        return this.age;
    }

}

class B extends A {

    constructor(name, lastName, age) {
        super(name, lastName);

        // protected this
        if(new.target === B)    _(this, Protected_B);

        // protected property
        _(this).age = age;
    }

    callGetAge() {
        return _(this).getAge();
    }

}

// test
const b = new B("foo", "bar", 18);

// "foo bar"
console.log(b.callGetName());

// 18
console.log(b.callGetAge());

As Syntax Suger (ES Next):

class A {

    protected lastName;

    constructor(name, lastName) {
        // public property
        this.name = name;

        // protected property
        protected.lastName = lastName;
    }

    protected getName() {
        return `${this.name} ${protected.lastName}`;
    }

    callGetName() {
        return protected.getName();
    }

}

class B extends A {

    protected age;

    constructor(name, lastName, age) {
        super(name, lastName);
        protected.age = age;
    }

    protected getAge() {
        return protected.age;
    }

    callGetAge() {
        return protected.getAge();
    }

}

Please discuss this proposal, thank you.

# Thomas (9 years ago)

Could this be achieved with decorators?

# kdex (9 years ago)

@Kenta: Is your model likely to break/leak private data into foreign contexts once you start binding this?

On the other hand: Should you ever need to bind this for class functions? Right now, I'm not entirely sure whether the implementation of "protected" can be runtime-safe.

About your nomenclature: Why name it protected rather than private? Am I missing a key difference between the two here?

FWIW, have a look at this.

# Andrea Giammarchi (9 years ago)

It could be achieved with decorators but it won't be the same.

On a side note, that Object.assign operation doesn't do what you think it does, there are better patterns (boilerplates, actually ...) to copy own properties over within their descriptors, if different from enumerable data descriptors.

As summary, beside the implementation, I think the final sugar ain't that bad.

+1 here

P.S. the amount of wm.set(a, b).get(a) operations instead of wm.set(a, b) returning b is too damn high ... I know, I know, old discussion is old (and too late anyway)

# Andrea Giammarchi (9 years ago)

IIRC the difference between protected and private is that a private field works within the class but not subclasses while protected means from a subclass you can use directly protected.whatever even if whatever is not defined but it's inherited from the super class.

Private is private, and not accessible from anywhere else.

At least in PHP (and I think Java too) is like that

# 森建 (9 years ago)

@Thomas

Could this be achieved with decorators?

I want to get Syntax Sugar.

But I didn't have an idea using decorators, thank you!

@kdex

About your nomenclature: Why name it protected rather than private? Am I missing a key difference between the two here?

This is not private proposal, because class B extends A can call getName() in class Protected_A, like below.

class B extends A {

    constructor(name, lastName, age) {
        super(name, lastName);

        // protected this
        if(new.target === B)    _(this, Protected_B);

        // protected property
        _(this).age = age;
    }

    callGetAge() {
        return _(this).getAge();
    }

    // added
    getData() {
        return {
            name: _(this).getName(); // Protected_A
            age:  _(this).getAge();
        }
    }

}

I'm sorry that my JavaScript code in ES2015 is too vain complex to understand.

@Andrea Giammarchi

On a side note, that Object.assign operation doesn't do what you think it does, there are better patterns (boilerplates, actually ...) to copy own properties over within their descriptors, if different from enumerable data descriptors.

I didn't know boilerplates, thank you!

But Object.assign enable to copy accessors by using Object.getOwnPropertyDescriptor according to MDN sample code. Of course, this approach must be bad...

Note: In case, Object.getOwnPropertyDescriptors proposal (Stage 0) in ES2016 isn't same as Object.getOwnPropertyDescriptor. I misunderstood it.

P.S. the amount of wm.set(a, b).get(a) operations instead of wm.set(a, b) returning b is too damn high ... I know, I know, old discussion is old (and too late anyway)

Sorry, it's my corner-cutting. I didn't know the amount, thank you!

# Andrea Giammarchi (9 years ago)

to be clear: no, Object.assign does not copy accessors in a meaningful way, it copies the result of the getter, not its descriptor.

That page at MDN has also many errors, including the function myAssign that copies keys but not Symbols.

Gonna fix that, best

# 森建 (9 years ago)

Sorry, I misunderstood descriptor and accessor. I try it!!

# 森建 (9 years ago)

I make it!

"use strict";

// utility
function createProtectedStorage() {
   const wm = new WeakMap();

   return (self, protectedClass) => {
     const map = wm.get(self);

     if(protectedClass == null) {
       if(map) {
         return map;
       } else {
         const ret = Object.create(null);
         wm.set(self, ret);
         return ret;
       }
     }

     const p = new protectedClass(self);
     if(map) {
       for(let key of Object.getOwnPropertyNames(map)) {
         const descriptor = Object.getOwnPropertyDescriptor(map, key);
         Object.defineProperty(p, key, descriptor);
       }
     }
     wm.set(self, p);
     return p;
   }
}

const _ = createProtectedStorage();


class Protected_A {

   constructor(publicThis) {
     this.publicThis = publicThis;
   }

   getName() {
     return `${this.publicThis.name} ${this.lastName}`;
   }
}

class A {

   constructor(name, lastName) {
     // protected this
     if(new.target === A) {
       _(this, Protected_A);
     }

     // public property
     this.name = name;

     // protected property
     Object.defineProperty(_(this), "lastName", {
       enumerable: false,
       configurable: false,
       writable: false,
       value: lastName
     });
   }

   callGetName() {
     // call protected method
     return _(this).getName();
   }
}

// extends
class Protected_B extends Protected_A {

	constructor(publicThis) {
		super(publicThis);
	}

	getAge() {
		return this.age;
	}
	
}

class B extends A {

     constructor(name, lastName, age) {
         super(name, lastName);

         // protected this
         if(new.target === B)    _(this, Protected_B);

         // protected property
         _(this).age = age;
     }

     callGetAge() {
         return _(this).getAge();
     }

     // added
     getData() {
         return {
             name: _(this).getName(), // Protected_A
             age:  _(this).getAge()
         }
     }

     getLastNameDescriptor() {
       return Object.getOwnPropertyDescriptor(_(this), "lastName");
     }

     getAgeDescriptor() {
       return Object.getOwnPropertyDescriptor(_(this), "age");
     }

}

// test
const b = new B("foo", "bar", 18);

// "foo bar"
console.log(b.callGetName());

// 18
console.log(b.callGetAge());

// { "name": "foo bar", "age": 18 }
console.log(b.getData());

// { "value": "bar", "writable": false, "enumerable": false, "configurable": false }
console.log(b.getLastNameDescriptor());

// { "value": 18, "writable": true, "enumerable": true, "configurable": true }
console.log(b.getAgeDescriptor());
# 森建 (9 years ago)

Sorry, I must use Object.getOwnPropertySymbols.

# 森建 (9 years ago)

maybe okay.

"use strict";

// utility
function createProtectedStorage() {
   const wm = new WeakMap();

   return (self, protectedClass) => {
     const map = wm.get(self);

     if(protectedClass == null) {
       if(map) {
         return map;
       } else {
         const ret = Object.create(null);
         wm.set(self, ret);
         return ret;
       }
     }

     const p = new protectedClass(self);
     if(map) {
       for(let key of [ ...Object.getOwnPropertyNames(map), ...Object.getOwnPropertySymbols(map) ]) {
         const descriptor = Object.getOwnPropertyDescriptor(map, key);
         Object.defineProperty(p, key, descriptor);
       }
     }
     wm.set(self, p);
     return p;
   }
}

const _ = createProtectedStorage();


class Protected_A {

   constructor(publicThis) {
     this.publicThis = publicThis;
   }

   getName() {
     return `${this.publicThis.name} ${this.lastName}`;
   }
}

class A {

   constructor(name, lastName) {
     // protected this
     if(new.target === A) {
       _(this, Protected_A);
     }

     // public property
     this.name = name;

     // protected property
     Object.defineProperty(_(this), "lastName", {
       enumerable: false,
       configurable: false,
       writable: false,
       value: lastName
     });
   }

   callGetName() {
     // call protected method
     return _(this).getName();
   }
}

// extends
class Protected_B extends Protected_A {

	getAge() {
		return this[Symbol.for("$$age")];
	}
	
}

class B extends A {

     constructor(name, lastName, age) {
         super(name, lastName);

         // protected this
         if(new.target === B)    _(this, Protected_B);

         // protected property
         _(this)[Symbol.for("$$age")] = age;
     }

     callGetAge() {
         return _(this).getAge();
     }

     // added
     getData() {
         return {
             name: _(this).getName(), // Protected_A
             [Symbol.for("$$age")]:  _(this).getAge()
         }
     }

     getLastNameDescriptor() {
       return Object.getOwnPropertyDescriptor(_(this), "lastName");
     }

     getAgeDescriptor() {
       return Object.getOwnPropertyDescriptor(_(this), Symbol.for("$$age"));
     }

}

// do nothing class
class Protected_C extends Protected_B {

}

class C extends B {

   constructor(...args) {
     super(...args);
     if(new.target === C)    _(this, Protected_C);
   }

}

// test
const c = new C("foo", "bar", 18);

// "foo bar"
console.log(c.callGetName());

// 18
console.log(c.callGetAge());

// { "name": "foo bar", [Symbol($$age)]: 18 }
console.log(c.getData());

// { "value": "bar", "writable": false, "enumerable": false, "configurable": false }
console.log(c.getLastNameDescriptor());

// { "value": 18, "writable": true, "enumerable": true, "configurable": true }
console.log(c.getAgeDescriptor());
# Andrea Giammarchi (9 years ago)

for(let key of [ ...Object.getOwnPropertyNames(map), ...Object.getOwnPropertySymbols(map) ])

... or ...

for(let key of Reflect.ownKeys(map))

# 森建 (9 years ago)

@Andrea Giammarchi

Thank you for supporting my code, I got JavaScript knowledge a lot!

@Subscribers

If there is not any problems, would you kindly discuss about this syntax sugar?

# Jordan Harband (9 years ago)

It would be helpful to link to gists or github repos rather than pasting code inline.

You may also be interested to read this proposal: wycats/javascript

# 森建 (9 years ago)

Thank you for your advice, I created a Github repository. petamoriken/es-protected-state

# 森建 (9 years ago)

Does anyone help me with being TC39 champion for my personal protected state strawman?

petamoriken/es-protected-state