Function.prototype.bind

David-Sarah Hopwood david.hopwood at industrial-designers.co.uk
Thu Sep 11 00:39:42 PDT 2008


Brendan Eich wrote:
> On Sep 10, 2008, at 5:37 PM, David-Sarah Hopwood wrote:
>> Brendan Eich wrote:
>>> On Sep 10, 2008, at 10:15 AM, David-Sarah Hopwood wrote:
>>>
>>>> However, unlike what I proposed, 'strawman:lexical_scope' doesn't make
>>>> the bindings of the primordials that are visible when it is active
>>>> deeply immutable. I suppose that's difficult if you want the
>>>> prototypes to appear to be the same between code that is in such a
>>>> block and code that is not -- for instance, '"" instanceof String'
>>>> should work whether or not 'String' refers to a deeply immutable or
>>>> mutable constructor.
>>>
>>> Right. Also, people mutate standard objects, whether it's a good idea in
>>> the large or not. The "use lexical scope" idea is not meant to break
>>> compatibility on that point.
>>>
>>> It's rarer (although still done -- see
>> [https://bugzilla.mozilla.org/show_bug.cgi?id=409252#c4])
>>> for web JS to mutate the global bindings for Object, Date, etc.
>>
>> ECMA-262 is entirely unclear about what effects that is supposed to have,
> 
> Or where the language is clear, it is not consistent:
> 
> http://wiki.ecmascript.org/doku.php?id=clarification:which_prototype
> 
>> and it did different things between implementations (for example FF2 used
>> shallow binding of at least Array,
> 
> What do you mean by "shallow binding"?

Shallow binding, as I'm using it here, is where a reference -- in this
case the implicit reference to Array when an array literal is evaluated --
is resolved as if it were textually substituted. For example:

  (function() {
    function Array() { alert('hi'); }
    [];
  })();

would alert on FF2, because the '[]' is interpreted as if it said
'new Array()', where Array is shallowly bound to the local function
definition.

In FF3, Array is no longer shallowly bound, and so this code does not
alert.

Having googled "shallow binding", I see that it also has other meanings
(which probably are more canonical; for example Henry Baker's definition
should take precedence), so I should have been more explicit.

>> while IE7 mostly ignored changes to
>> these bindings). So if there is any code relying on doing this, at best
>> it worked by accident.
> 
> But it does work, and that's why we couldn't change it for Firefox 3.

No, it does not work cross-browser. The above code doesn't alert on IE7,
for instance. Also,

  Array = function() { alert('hi'); };
  [];

alerts on FF2 but not IE7.

> Joel Spolsky would have our heads.
> 
>> I see only references to rebinding of 'Date' in that bug report (and
>> 'Namespace' which is not relevant to ES3.1).
> 
> Namespace is from ECMA-357, E4X. It's relevant because we do not want to
> special-case the constructor binding rules per standard object.

Well, that is exactly what my proposal does (and I explain why below).
I'd appreciate it if you would address your criticism to what I actually
proposed, rather than something I didn't.

There are exactly two categories of name in my proposal:

a) Array, Function, Object, String and RegExp
b) Everything else.

There are several strong arguments for treating a) differently from b).
To expand on them:

 - If you allow rebinding the names in a), you must specify precisely
   what it means to do so (otherwise allowing it is useless). There's a
   very significant specification complexity cost in doing that.

   Note that if creating an object or a function runs the user-specified
   Object or Function constructor, then that constructor will not be
   able to do anything useful, because creating any object or function
   in it will cause an infinite regress.

   If creating an object or function does not run the user-specified
   constructor, OTOH, then what was the point of rebinding it?

 - Existing JS implementations do not handle rebinding of the names
   in a) consistently (as demonstrated by the examples above). There
   is inconsistency between browsers, between browser versions, and
   between particular names in each version. Any code that rebinds
   these names works only by coincidence, and is liable to break at
   any time, regardless of whether it is nominally allowed by the spec.

 - Allowing these names to be rebound inhibits useful optimizations.
   For example, it inhibits the optimization that is assumed to be
   possible in the rationale document for the Object static methods.
   (Maybe this optimization can be recovered if it is done only within
   an 'if' block that tests 'Object === original_Object', and does not
   itself rebind 'Object'. But I think it is an unreasonable amount
   of implementation complexity to detect this case.)

 - The names in a) (and static methods on them) are those that we need
   to use to write self-hosting specifications, and that might be used
   in the expansions of syntactic sugar added in ES-Harmony. It is, I
   believe, completely infeasible to write *any* ECMAScript code that
   has predictable behaviour if these names cannot be relied on. (It's
   difficult enough if these are the *only* names that can be relied
   on, but it is possible, with care.)

 - Existing web content relies on rebinding the names in b), but not
   (or to a much lesser extent) the names in a).

> We want one rule to bind them all,

That would be nice, but I don't believe that this consideration
should override the arguments given above. ES3 is already rather
haphazard in its assignment of attributes, particularly ReadOnly.
(I'm not defending that, just pointing out that what we are starting
from in how ES3 specifies attributes with is very far from "one rule
to bind them all".)

> partly for simplicity of implementation,

The implementation is trivial, using Object.defineProperty. So is the
specification. We can add a footnote pointing out the irregularity,
so that implementors can't miss it.

> mostly for cognitive simplicity for programmers using JS,

Cognitive simplicity is thrown out of a 100th-storey window for any
code that rebinds the names in a). Even if it were allowed, it would be
necessary to avoid doing this if cognitive simplicity for programmers
is a goal.

I agree that allowing names of built-in global constructors other than
those in a) to be rebound, is slightly irregular. There's a simple
solution to that: don't write code that rebinds other built-in global
constructors either. Just because it is allowed (for compatibility),
doesn't mean that it should be done. In fact we should probably
explicitly deprecate it (which would potentially allow making *all*
built-in global constructor names non-Writable and non-Configurable
in some future version).

> and anyway to avoid borrowing compatibility trouble (more below,
> where you propose borrowing more than your net worth :-/).

> You have not looked at all the regressions from the original bug:

At the risk of repeating myself, let's be clear about which names I
proposed to be non-Writable and non-Configurable:

  Array, Function, Object, String and RegExp

Only those names. Not Date, XML, Namespace, Iterator, Error, Boolean,
Number, Math, or anything else. So most of the compatibility issues
described in these regressions are simply not relevant to what I
proposed.

> https://bugzilla.mozilla.org/show_bug.cgi?id=376957
> 
> listed in the "Depends on:" row of links.
> 
> https://bugzilla.mozilla.org/show_bug.cgi?id=407323 (XML, ECMA-357).

Not relevant because 'XML' is not in category a). As Jeff Walden says
in comment #4 of that bug:

# Rebinding XML doesn't do anything interesting -- unlike []/{} nothing
# uses the XML binding.  To me this makes it much less interesting as
# something to lock down to make the language more predictable.

> https://bugzilla.mozilla.org/show_bug.cgi?id=407727 (let Object, same as
> var Object)

I am perfectly happy to let this code break. Note that this report does
not cite any evidence of web content that is actually using Object as a
local; it is purely a theoretical incompatibility.

> https://bugzilla.mozilla.org/show_bug.cgi?id=407957 (Iterator, similar
> to Namespace)

Iterator is not in category a).

> and the big one, bigger even than fogbuz (sorry, Joel):
> 
> https://bugzilla.mozilla.org/show_bug.cgi?id=412324 (Error)
>
> An old MSN compat.js file also rebinds Error unconditionally. I'm not
> sure it is used any longer, but it's still up:
> 
> http://stj.msn.com/br/gbl/js/3/mozcompat.js

Error is not in category a).

(But suppose for the sake of argument that it were. This code says
"root.Error=function(){};", where 'root' refers to the global object.
Since it is run in non-strict mode, the assignment to a non-Writable
property will be silently ignored -- which is completely harmless in
this case. In fact it is desirable, since the actual constructor for
Error might do something important in a given implementation.

The same argument, of course, applies to assignments to Array, Function,
Object, String and RegExp, where the code either happens to work, or was
deliberately written to work if the assignment is silently ignored.)

>> The semantics of rebinding
>> Date are much clearer than they are for constructors that are implicitly
>> accessed as a result of evaluating literals (Array, Function, Object,
>> String and RegExp). In particular, there are intractable bootstrapping
>> issues in specifying what it should mean to rebind 'Object' or
>> 'Function', at least. Furthermore, allowing rebinding of these
>> implicitly accessed constructors could inhibit useful optimizations,
>> while I assume no-one really cares about optimizing Date.
> 
> Without literal syntax, you are right as far as optimization goes.
> Integrity is another issue, I don't need to tell you!
> 
> Error and its subtypes have no literal syntax, but are constructed by
> the runtime and thrown. ES3 says (e.g. 15.1.2.1 step 2) "throw a
> *SyntaxError* exception", and this is taken to mean construct an
> instance without reflecting on the current binding of SyntaxError,
> rather use a memoized prototype and internal [[Class]], etc.

Yes, but that's easy to handle without making SyntaxError itself
non-Writable/Configurable. Just define non-Writable/Configurable
properties:

  Object.Error
  Object.EvalError
  Object.RangeError
  Object.ReferenceError
  Object.SyntaxError
  Object.TypeError
  Object.URIError
  Object.Date
  Object.Number
  Object.Boolean
  Object.Math
  Object.XML
  Object.Iterator
  Object.Namespace

Now it is possible for any code that needs to refer to the original
constructors, including the specification itself, to do so easily.
For example 15.1.2.1 step 2 would just say
"throw an Object.SyntaxError exception".

This is reasonable because there are only a relatively small number
of places in the spec where Error objects are created (78, by my count).
If we were to do the same thing for Array, Function, String and RegExp,
then we'd have to make huge changes to the specification; it would be
completely impractical.

Besides, making all of those changes would imply that it would not be
*useful* to rebind Array, Function, String and RegExp, even if it were
possible, because the rebinding would effectively be ignored (or worse,
it would be used inconsistently, with some objects using the rebound
prototypes and some not).

>> So here is my strawman proposal:
>>  - leave 'Date' Writable in the global scope;
>>  - make Array, Function, Object, String and RegExp non-Writable and
>>    non-Configurable, unconditionally;
> 
> No; https://bugzilla.mozilla.org/show_bug.cgi?id=412324 is just a taste
> of what you'll get in the way of bug reports, I predict.

That bug involves code that rebinds Error, which is not in category a).

> I'll bet real money on this.
> 
>>  - make Array, Function, Object, String and RegExp not declarable as
>>    variable names;
>>  - make all of the new "static" methods on Array, Function, Object
>>    and String non-Writable and non-Configurable, unconditionally.
> 
> I think you are missing a few classes (e.g. Error).

The omission of the Error constructors was quite deliberate. They're
not created by literals, they're not important to optimize, and it is
easy to work around their rebinding, as described above.

> Having such complicated rules also seems bad. There will be more
> built-in classes. And what about "host constructors" like Image, Option,
> HTMLDivElement, etc.? Why should they have different binding rules?

They don't; they're like everything else in category b).

Having two categories of global name is not especially complicated.
I also think you're underestimating the extent to which this approach
can be used to simplify the specification.

>> This will not break Ajax library code that tries to install compatible
>> implementations of these new methods, because:
>>
>>  - if that code is running in non-strict mode, the attempted updates
>>    will be silently ignored.
> 
> See https://bugzilla.mozilla.org/show_bug.cgi?id=412324 -- silent
> failure to update a ReadOnly property still breaks things.

Error is not in category a).

>>  - if it is running in strict mode, then it must be new code that
>>    knows to catch the resulting exception.
>>
>> Note that the Array, Function, Object, String and RegExp constructors
>> would not be sealed, and the prototypes would still be mutable. So this
>> proposal is not attempting to address the secure sublanguage issues
>> (at least not on its own), only the issue with self-hosting.
> 
> Self-hosting is a specification and potentially an implementation
> detail,

My proposal addresses three issues:

1. making it practical to use self-hosting in the specification;
2. simplifying the expansions of syntactic sugar;
3. allowing the run-times of secure sublanguages to refer to the
   unmodified constructors.

2 and 3 are just as important as 1.

> not a reason for incompatible change that we could not ship in
> Firefox 3.

I think that your assertions of incompatibility are frankly overblown.
Most of them do not even apply to what I proposed. Besides, we are
talking about code that does not work in IE7 (user-provided constructors
for Array, Function, Object and RegExp are not run [*]), and that in the
case of Array, has behaviour that visibly changed between FF2 and FF3.

[*] a user-provided constructor for String *is* run, for some reason.
    See how inconsistent this is?

> Good luck getting any other browser, including IE9 (in two
> years?) to do likewise.
> 
>> With this design, it's no longer necessary to worry about rebinding
>> or shadowing in the self-hosting specifications, *or* in the expansions
>> of new syntactic sugar, provided that they only use the static methods
>> and do not use Date.
> 
> Sorry, you're mistaken. The self-hosted Date needs to refer to its own
> original binding and properties.

That's trivial -- see the code below.

> Etc.

What "etc."? If you want to imply that there are other problems, please
state what they are.

>> I do not believe that anything important on the web
> 
> I don't know what to make of this. You're wrong, and I cited evidence to
> the contrary.

You cited *no* evidence that what I actually proposed would break. All
of your examples in which concrete evidence of real code was provided
were for names other than Array, Function, Object, String and RegExp.

> But feel free to start a new browser project, break
> compatibility, grow your market share, and prove me wrong.
> 
>> will break as a result of these changes -- and if it does, then I think
>> it desperately needed to be broken as soon as possible.
> 
> This is wrong in at least six dimensions. You are not god-emperor of the
> web. The web is more like the boss of you. You must woo it, win it over
> with better ways to do things currently done via bad old APIs today,
> before you can remove those APIs and force your will on it.

This is why we have an insecure web. This attitude does a disservice to
users. We are language designers; we should design, not roll over to
accomodate the frightful code of every web developer who exploited
obscure corner cases in the spec. Unless and until security is given a
greater priority relative to compatibility, the web will stay insecure.

[...]
>> The self-hosting specification of Function.prototype.bind would be:
>>
>>   Object.defineProperty(Function.prototype, 'bind', {
>>     writable: false, enumerable: false, configurable: false, value:
>>     function(self, var_args) {
>>       const thisFunc = this;
>>       const leftArgs = Array.slice(arguments, 1);
>>       return function(var_args) {
>>         const args = Array.concat(leftArgs, Array.slice(arguments, 0));
>>         return Function.staticApply(thisFunc, self, args);
>>       };
>>     }
>>   });
>>
>> (no 'use lexical scope' needed).
> 
> Let's see a Date method or three.

(function() {
  Object.defineProperty(global, 'NaN', {
    writable: false, configurable: false, enumerable: true, value: 0/0
  });
  Object.defineProperty(global, 'undefined', {
    writable: false, configurable: false, enumerable: true, value: void 0
  });
  Object.defineProperty(global, 'Infinity', {
    writable: false, configurable: false, enumerable: true, value: 1/0
  });

  // These are all straightforward arithmetic, no binding issues.
  function LocalTime(t) { ... }
  function UTC(t) { ... }
  function HourFromTime(t) { ... }
  function MinFromTime(t) { ... }
  function SecFromTime(t) { ... }
  function msFromTime(t) { ... }
  function MakeTime(hour, min, sec, ms) { ... }
  function MakeDay(year, month, date) { ... }
  function MakeDate(day, time) { ... }
  function TimeClip(time) { ... }

  function CurrentTime() { ... }
  function TimeToString(time) { ... }

  function Date_parse(string) {
    ...
  };

  function Date_UTC(year, month, date, hours, minutes, seconds, ms) {
    return TimeClip(ComputeDate(
      year, month, date, hours, minutes, seconds, ms));
    }
  };

  const ToNumber = Object.Number;

  // Used by Date constructor and Date.UTC.
  function ComputeDate(year, month, date, hours, minutes, seconds, ms) {
    year    = ToNumber(year);
    month   = ToNumber(month);
    date    = date    !== undefined ? ToNumber(date)    : 1;
    hours   = hours   !== undefined ? ToNumber(hours)   : 0;
    minutes = minutes !== undefined ? ToNumber(minutes) : 0;
    seconds = seconds !== undefined ? ToNumber(seconds) : 0;
    ms      = ms      !== undefined ? ToNumber(ms)      : 0;
    if (year === year) {
      const iYear = Object.toInteger(year);
      if (0 <= iYear && iYear <= 99) year = 1900 + iYear;
    }
    const day = MakeDay(year, month, date);
    const time = MakeTime(hours, minutes, seconds, ms);
    return MakeDate(day, time);
  }

  function Date(yearOrValue, month, date, hours, minutes, seconds, ms) {
    // assume proposed ES3.1 semantics for 'this' binding
    if (this === undefined) {
      return TimeToString(CurrentTime());
    }

    this.__Class__ = 'Date';
    if (month !== undefined) {
      this.__Value__ = TimeClip(UTC(ComputeDate(
        yearOrValue, month, date, hours, minutes, seconds, ms)));

    } else if (yearOrValue !== undefined) {
      const value = Object.toPrimitive(yearOrValue);
      const V = typeof value === 'string' ? Date_parse(value)
                                          : ToNumber(value);
      this.__Value__ = TimeClip(V);

    } else {
      this.__Value__ = CurrentTime();
    }
  }

  Date.parse = Date_parse;
  Date.UTC = Date_UTC;

  Object.defineProperty(Date, 'prototype', {
    writable: false, configurable: false, enumerable: true, value:
    Date.prototype
  });

  Object.defineProperty(Date.prototype, 'constructor', {
    writable: false, configurable: false, enumerable: false, value: Date
  });

  Object.defineProperty(Date.prototype, 'toString', {
    writable: true, configurable: true, enumerable: false, value:
    function() {
      if (this.__Class__ !== 'Date') throw new TypeError();
      return TimeToString(this.__Value__);
    }
  });

  Object.defineProperty(Date.prototype, '__Class__', {
    writable: false, configurable: false, enumerable: false, value: 'Date'
  });

  Object.defineProperty(Date.prototype, '__Value__', {
    writable: false, configurable: false, enumerable: false, value: NaN
  });

  // We don't actually need Object.Date directly, but it might be useful
  // for secure subset run-times.
  Object.defineProperty(Object, 'Date', {
    writable: false, configurable: false, enumerable: true, value: Date
  });

  global.Date = Date;
})();

As you can see, it's trivial for the implementation of each "class" to use
a local binding for the class name within a module, so that there is no
issue with rebinding of the corresponding global. Some care is needed
to only use local bindings of Writable properties -- for example, since
Date.parse is defined by ES3 to be Writable (not ReadOnly), we need to
use the local Date_parse instead in the Date constructor.

Note that this *only works* because Array, Function, Object, and String
cannot be rebound, because we are using literals of those classes in the
implementation.  RegExp happened not to be used in the code above, but
a more complete implementation might use it.

-- 
David-Sarah Hopwood


More information about the Es-discuss mailing list