To better understand the differences between soft fields and private names, this page goes through all the examples from the latter (as of this writing) and explores how they’d look as translated to use soft fields instead. This translation does not imply endorsement of all elements of the names proposal as translated to soft fields, such as the proposed syntactic extensions. However, these translations do establish that these syntactic choices are orthogonal to the semantic controversy and so can be argued about separately.
Identifiers ending with triple underbar below signify unique identifiers generated by expansion that are known not to conflict with any identifiers that appear elsewhere.
Adapted from the private declaration
private secret; //create a new soft field that is bound to the private identifier ''secret''. private _x,_y; //create two soft fields bound to two private identifiers ... foo.secret ... foo.secret = val; const obj = {secret: val, ...}; #.secret
expands to
const secret___ = SoftField(); const _x___ = SoftField(), _y___ = SoftField(); ... secret___.get(foo) ... secret___.set(foo, val); const obj = {...}; secret___.set(obj, val); secret___
Adapted from using private identifiers
function makeObj() { private secret; var obj = {}; obj.secret = 42; //obj has a soft field print(obj.secret);//42 -- accesses the soft field's value print(obj["secret"]); //undefined -- a soft field is not a property return obj; } var obj=makeObj(); print(obj["secret"]); //undefined -- a soft field is still not a property print(obj.secret); //undefined -- this statement is not in the scope of the private declaration so the //string value "secret" is used to look up the property. It is not a soft field.
This technique can be used to define “instance-private” properties:
function Thing() { private key; // each invocation will use a new soft field this.key = "instance private value"; this.hasKey = function(x) { return x.key === this.key; //x.key should be undefined if x!==this }; this.getThingKey = function(x) { return x.key; }; }
Instance-private instance state is better done by lexical capture
function Thing() { const key = "instance private value"; this.hasKey = function(x) { return x === this; }; this.getThingKey = function(x) { if (x === this) { return key; } }; }
Either technique produces the same external effect:
var thing1 = new Thing; var thing2 = new Thing; print("key" in thing1); // false print(thing2.key); //undefined print(thing1.hasKey(thing1)); // true print(thing1.hasKey(thing2)); // false
By changing the scope of the private declaration a similar technique can be used to define “class-private” properties:
private key; //the a soft field shared by all instances of Thing. function Thing() { this.key = "class private value"; this.hasKey = function(x) { return x.key === this.key; }; this.getThingKey = function(x) { return x.key; }; } var thing1 = new Thing; var thing2 = new Thing; print("key" in thing1); // false print(thing1.hasKey(thing1)); // true print(thing1.hasKey(thing2)); // true
Adapted from private identifiers in object literals
function makeObj() { private secret; var obj = {secret: 42}; print(obj.secret);//42 -- access the soft field's value print(obj["secret"]); //undefined -- a soft field is not a property return obj; }
function Thing() { private key; return { key : "instance private value", hasKey : function(x) { return x.key === this.key; //x.key should be undefined if x!==this }, getThingKey : function(x) { return x.key; } }; }
or, preserving the same external behavior:
function Thing() { const key = "instance private value"; return { hasKey : function(x) { return x === this; }, getThingKey : function(x) { if (x === this) { return key; } } }; }
private key; function Thing() { return { key : "class private value", hasKey : function(x) { return x.key === this.key; //x.key should be undefined if x!==this }, getThingKey : function(x) { return x.key; } }; }
Adapted from private declaration scoping
function outer(obj) { private name; function inner(obj) { private name; obj.name = "inner name"; print(obj.name); //"inner name" because outer name declaration is shadowed } obj.name = "outer name"; inner(obj) print(obj.name); //"outer name" } var obj = {}; obj.name = "public name"; outer(obj); print(obj.name); //"public name"
After executing the above code, the object that was created will have one property and two associated soft fields:
| Property or Fields | Value |
|---|---|
| “name” | “public name” |
| private nameouter | “outer name” |
| private nameinner | “inner name” |
Adapted from private declarations exist in a parallel environment
Consider the following very common idiom used in a constructor declaration:
function Point(x,y) { this.x = x; this.y = y; //... methods that use x and y properties } var pt = new Point(1,2);
function Point(x,y) { private x, y; this.x = x; this.y = y; //... methods that use private x and y properties } var pt = new Point(1,2);
function Point(x,y) { const x___ = SoftField(), y___ = SoftField(); x___.set(this, x); y___.set(this, y); //... methods that use private x and y properties } var pt = new Point(1,2);
Adapted from accessing private names as values
The private declaration normally both creates a new soft field and introduces a identifier binding that can be used only in “property name” syntactic contexts to access the new soft field by the lexically bound identifier.
However, in some circumstances it is necessary to access the actual soft field as an expression value, not as an apparent property name on the right of . or the left of : in an object initialiser. This requires a special form than can be used in an expression to access the soft field binding of a private identifier. The syntactic form is #. IdentifierName. This may be used as a PrimaryExpression and yields the soft field of the IdentifierName. This may be either a soft field or a string value, depending upon whether the expression is within the scope of a private declaration for that IdentifierName;
function addPrivateProperty(obj, init) { private pname; //create a new soft field obj.pname = init; //set this soft field return #.pname; //return the soft field }
function addPrivateProperty(obj, init) { const pname___ = SoftField(); pname___.set(obj, init); return pname___; }
var myObj = {}; var answerKey = addPrivateProperty(myObj, 42); print(answerKey.get(myObj)); // AFAICT, this is the *only* claimed advantage of Names over SoftFields. //myObj can now be made globally available but answerKey can be selectively passed to privileged code
Note that simply assigning a soft field to a variable does not make that variable a private identifier. For example, in the above example, the print statement could not validly be replaced with:
print(myObj.answerKey);
This would produce “undefined” because it would access the non-existent property whose string valued property name would be “answerKey”. Only identifiers that have been explicitly declared using private are private identifiers.
“can we subsume private names” explains how soft fields as value proxies could support a property-like usage of [], so this code could indeed be written as
print(myObj[answerKey]);
If #. is not within the scope of a private declaration for its IdentifierName then the value produced is the string value of the IdentifierName.
As an expressive convenience, private declarations can be used to associate a private identifier with an already existing soft field. This is done by using a private declaration of the form:
private Identifier = Initialiser ;
The Names proposal asks: “If Initialiser does not evaluate to a soft field, a TypeError exception is thrown. (
for uniformity, should string values be allowed? In that case, local private name bindings could be string valued.)”
If the answer is true, the one supposed advantage of Names over soft fields goes away. Our contentious bit of code becomes:
private ak = answerKey; // soft field or string print(obj.ak); // works either way
private name1; //value is a new soft field private name2 = #.name1 //name2 can be used to access the same soft field as name1
| Other possible syntactic forms for converting a private identifier to an expression value include: |
private IdentifierName |
(private IdentifierName) |
.IdentifierName |
`IdentifierName |
#`IdentifierName |
#’IdentifierName |
Adapted from conflict-free object extension using private names
function installCloneLibrary() { private clone; // the soft field for clone methods // Install clone methods in key built-in prototypes: Object.prototype.clone = function () { ... }; Array.prototype.clone = function () { ... target[i] = this[i].clone(); // recur on clone method ... } String.prototype.clone = function () {...} ... return #.clone } // Example usage of CloneLibrary: private clone = installCloneLibrary(); installAnotherLibrary(); var twin = [{a:0}, {b:1}].clone();
Similarities: The above client of the CloneLibrary will work even if the other library also defines a method named clone on Object.prototype. The second library would not have visibility of the soft field used for clone so it would either use a string property name or a different soft field for the method. In either case there would be no conflict with the method defined by CloneLibrary.
For defensive programming, best practice in many environments will be to freeze the primordials early, as the dual of the existing best practice that one should not mutate the primordials. Evaluating the dynamic behaviour of Python applications (See also http://gnuu.org/2010/12/13/too-lazy-to-type/) provides evidence that this will be compatible with much existing content. We should expect these best practices to grow during the time when people feel they can target ES5 but not yet ES6.
Consider if Object.prototype or Array.prototype were already frozen, as they should be, before the code above executes. Using soft fields, this extension works. Using private names, it is rejected. Allen argues at Private names use cases that
Allow third-party property extensions to built-in
objects or third-party frameworks that are guaranteed
to not have naming conflicts with unrelated extensions
to the same objects.
is the more important use case. Soft fields provide for this use case. Private names do not.
Who knows whether frozen primordials will catch on? Many JS hackers are vehemently opposed. PrototypeJS still extends built-in prototypes and its maintainers say that won’t change. Allen clearly was talking about extending non-frozen shared objects in his “Private names use cases” message – he did not assume what you assume here. We need to agree on our assumptions before putting forth conclusions that we hope will be shared. I don’t think everyone shares the belief that “We should expect these best practices to grow during [any foreseeable future].”
— Brendan Eich 2010/12/22 01:37
Are we still confusing “any” and “all”? The original quote claims only that these best practices will grow in some environments. Regarding your “any foreseeable future”, this future is already long past. Google JavaScript Style Guide: Modifying prototypes of builtin objects has long stated:
Modifying prototypes of builtin objects [Recommendation:] No Modifying builtins like Object.prototype and Array.prototype are strictly forbidden. Modifying other builtins like Function.prototype is less dangerous but still leads to hard to debug issues in production and should be avoided.
I’m sure other such quotes about JavaScript best practice can be found.
Also, of course, The last initialization step of initSES is to freeze the primordials of its frame. Only code that does not mutate their primordials will be directly compatible with SES without resort to sandboxing.
Mark, the original quote from you is visible above, and it asserts “many”, not “any”. That is a bold claim. Not only Prototype, but SproutCore and Moo (and probably others), extend standard objects. SproutCore adds a w method to String.prototype, along with many other methods inspired by Ruby.
It’s nice that Google has recommendations, which it can indeed enforce as mandates on employees, but the Web at large is under no such authority. The Web is the relevant context for quantifying “many”, not some number of secure subset languages used in far smaller domains. On the Web, it’s hard to rule out maintainers and reusers mixing your code with SproutCore, e.g.
SES is a different language from Harmony, not standardized by Harmony in full. Goal 5 at harmony is about supporting SES, not subsuming it.
I believe we should avoid trying to run social experiments, building up pedagogical regimes, or making predictions about the future, anywhere in the text of future ECMA-262 editions.
— Brendan Eich 2011/01/12 02:12
Even though soft fields are typically implemented as state within the object they extends, because soft fields are semantically not properties of the object but are rather side tables, they do not show up in reflective operations performed on the object itself.
For example:
private b; var obj = {}; obj.a = 1; obj.b = 2; obj.c = 3; var names = []; for (var p in obj) names.push(obj[p]); print(names.toString()); // "1,3" -- soft field "b" was not enumerated
Soft fields created using object literals also not part of the object itself. So obj could have been created to produce the same result by saying:
private b; var obj = { a: 1, b: 2, c: 3 }
Beyond the syntactic expansions explained above, no other change to the definition of object literals is needed.
Creating a soft field that is enumerable makes no sense. Reflective operations that take property names as arguments, such as Object.defineProperty below, if given a non-string argument including a soft field, would coerce it to string and (uselessly) use that as a property name.
private b; var obj = {}; obj.a = 1; obj.b = 2; Object.defineProperty(obj, #.b, {enumerable: true}); obj.c = 3; var names = []; for (var p in obj) names.push(obj[p]); print(names.toString()); // "1,2,3" -- property "[object Object]" is now enumerated
Object.prototype.hasOwnProperty (ES5 15.2.4.5), Object.prototype.propertyIsEnumerable (ES5 15.2.4.7) and the in operator (ES5 11.8.7) do not see soft fields, again, because they are not part of the object.
The JSON.stringify algorithm (ES5 15.12.3) needs no change in order to ignore soft fields, since again they are not part of the object.
All the Object reflection functions defined in ES5 section 15.2.3 remain unchanged, since they need not be aware of soft fields.
An important use case for reflection using soft fields is algorithms that need to perform meta-level processing of all properties of any object. For example, a “universal” object copy function might be coded as:
function copyObject(obj) { // This doesn't deal with other special [[Class]] objects: var copy = Object.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj)); var props = Object.getOwnPropertyNames(obj); var pname; for (var i = 0; i < props.length; i++) { pname = props[i]; Object.defineProperty(copy, pname, Object.getOwnPropertyDescriptor(obj,pname)); } return obj; }
This function will duplicate all properties but not any soft fields, preserving encapsulation, since neither the definer nor the caller of copyObject knows these soft fields. Of course, a more complex copyObject function could be defined that would also copy and re-index those soft fields it was told of.
Adapted from private name properties support only weak encapsulation
No qualifiers needed.
Should so-called “weak encapsulation” actually be desired, “can we subsume private names” explains how to provide weakly encapsulating soft fields (or “wesf”) polymorphically with soft fields.
Adapted from enhanced object literals
private might be supported as either a property modifier keyword that makes the property name a soft field whose private identifier is scoped to the object literal:
var obj={ private _x: 0; get x() {return this._x}, set x(val) {this._x=val} }
This might simplify the declarative creation of objects with instance private soft fields. However, there are internal scoping and hoisting issues that would need to be considered and resolved.
Another alternative is to use meta property syntax to declare object literal local soft field declarations:
var obj={ <prototype: myProto; private _x> _x: 0; get x() {return this._x}, set x(val) {this._x=val} }
While the above proposals are perfectly consistent with soft fields, again, for instance-private instance state, using lexical capture seems strictly superior:
let x = 0; var obj={ get x() {return x}, set x(val) {x=val} }
Adapted from proxies
None of the uses of string valued property names in proxy handlers would need to be extended to accept/produce soft fields in addition to string values.
As covered above, ECMAScript reflection capabilities provides no means to break the encapsulation of an object’s soft fields.
Adapted from modules
It is reasonable to expect that modules will want to define and export soft fields. For example, a module might want to add methods to a built-in prototype object using soft fields and then make those soft fields available to other modules. Within the present definition of the simple module system that might be done as follows:
<script type="harmony"> module ExtendedObject { import Builtins.Object; // however access to Object is obtained. private clone; // the soft field for clone methods export const clone = #.clone; // export a constant with the soft field; Object.prototype.clone = function () { ... }; } </script>
A consumer of this module might look like:
<script type="harmony"> import ExtendedObject.clone; private clone = clone; var anotherObj = someObj.clone(); </script>
The above formulation would work without any additional extensions to the simple module proposal. However, it would be even more convenient if the module system was extended to understand private declarations. In that case this example might be written as:
<script type="harmony"> module ExtendedObject { import Builtins.Object; // however access to Object is obtained. export private clone; // export soft field for clone methods Object.prototype.clone = function () { ... }; } </script>
<script type="harmony"> import private ExtendedObject.clone; var anotherObj = someObj.clone(); </script>
I don’t get the point about “dynamic access to the exported property name environment of first-class module instances”, so at this time I offer no comparison of this last example.
Adapted from references
Any unforgeable reference to a tamper-proof encapsulated object is analogous to a capability in object-capability languages. In this degenerate sense, both Names and Soft Fields are also so analogous. I see no further way in which Names are analogous. In addition, Soft Fields encourage encapsulation friendly patterns, whereas Names encourage unsafe (or “weakly encapsulated”) patterns.