Overview

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.

The private declaration

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___

Using Private Identifiers

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

Private Identifiers in Object Literals

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;
       }
    };
}

Private Declaration Scoping

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”

Private Declarations Expand to Unique Hidden Variable Names

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);

Accessing Private Identifiers as Soft Field Values

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

Conflict-Free Object Extension Using Soft Fields

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.

Crucial difference

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

Enumeration and Reflection

enumeration and reflection

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.

Soft Fields Support Encapsulation

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.

Interactions with other Harmony Proposals

Enhanced Object Literals

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}
}

Proxies

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.

Modules

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.

References

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.

 
strawman/names_vs_soft_fields.txt · Last modified: 2011/03/03 20:37 by markm
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki