diff --git a/README.md b/README.md index 3b1dc56..a711022 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Bind operator for JavaScript +# Bind-`this` operator for JavaScript ECMAScript Stage-0 Proposal. J. S. Choi, 2021. * **[Formal specification][]** @@ -10,8 +10,8 @@ ECMAScript Stage-0 Proposal. J. S. Choi, 2021. (A [formal specification][] is available.) **Method binding** `->` is a **left-associative infix operator**. -Its right-hand side is an **identifier** -or an **expression** in `(` `)`, +Its right-hand side is an **identifier** (like `f`) +or a parenthesized **expression** (like `(hof())`), either of which must evaluate to a **function**. Its left-hand side is some expression that evaluates to an **object**. The `->` operator **binds** its left-hand side @@ -30,28 +30,40 @@ equivalent to `createMethod().bind(obj)`. If the operator’s right-hand side does not evaluate to a function during runtime, then the program throws a `TypeError`. -Function binding has equal **[precedence][]** with -**member expressions**, call expressions, `new` expressions with arguments, -and optional chains. - -[precedence]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence - -| Left-hand side | Example | -| ------------------------------- | ------------- | -| Primary expressions | `a->fn` | -| Member expressions | `a.b->fn` | -| Call expressions | `a()->fn` | -|`new` expressions with arguments | `new C()->fn` | -| Optional chains | `a?.b->fn` | - -The bound functions created by the bind operator +The bound functions created by the bind-`this` operator are **indistinguishable** from the bound functions that are already created by [`Function.prototype.bind`][bind]. Both are **exotic objects** that do not have a `prototype` property, and which may be called like any typical function. -Similarly to the `?.` optional-chaining token, -the `->` token may be **padded by whitespace**.\ +From this definition, `o->f(...args)` +is **indistinguishable** from `f.call(o, ...args)`, +except that its behavior does **not change** +if code elsewhere **reassigns** the global method `Function.prototype.call`. + +The `this`-bind operator has equal **[precedence][]** with +**member expressions**, call expressions, `new` expressions with arguments, +and optional chains. +Like those operators, the `this`-bind operator also may be short-circuited +by optional chains in its left-hand side. + +[precedence]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence + +| Left-hand side | Example | Grouping +| ---------------------------------- | ------------ | -------------- +| Member expressions |`a.b->fn.c` |`((a.b)->fn).c` +| Call expressions |`a()->fn()` |`((a())->fn)()` +| Optional chains |`a?.b->fn` |`(a?.b)->fn` +|`new` expressions with arguments |`new C(a)->fn`|`(new C(a))->fn` +|`new` expressions without arguments*|`new a->fn` |`new (a->fn)` + +\* Like `.` and `?.`, the `this`-bind operator also have tighter precedence +than `new` expressions without arguments. +Of course, `new a->fn` is not a very useful expression, +just like how `new (fn.bind(a))` is not a very useful expression. + +Similarly to the `.` and `?.` operators, +the `->` operator may be **padded by whitespace**.\ For example, `a -> m`\ is equivalent to `a->fn`,\ and `a -> (createFn())`\ @@ -59,7 +71,7 @@ is equivalent to `a->(createFn())`. There are **no other special rules**. -## Why a bind operator +## Why a bind-`this` operator [`Function.prototype.bind`][call] and [`Function.prototype.call`][bind] are very common in **object-oriented JavaScript** code. They are useful methods that allows us to apply functions to any object, @@ -107,7 +119,7 @@ delete Array.prototype.slice; slice.call([0, 1, 2], 1, 2); ``` -But this is still vulnerable to mutation of `Function.prototype`: +But this approach is still vulnerable to mutation of `Function.prototype`: ```js // Our own trusted code, running before any adversary. @@ -155,38 +167,124 @@ obj->extensionMethod(); // Compare with extensionMethod.call(obj). ``` -The bind operator can also **extract** a **method** from a **class** -into a function whose first parameter becomes its `this` binding:\ -for example, `const { slice } = Array.prototype; arr->slice(1, 3);`.\ -It can also similarly extract a method from an **instance** -into a function that always uses that instance as its `this` binding:\ -for example, `const arr = arr->(arr.slice); slice(1, 3);`. - ## Real-world examples Only minor formatting changes have been made to the status-quo examples. - - - - - -
Status quo -With binding +### Node.js +Node.js’s runtime depends on many built-in JavaScript global intrinsic objects +that are vulnerable to mutation or prototype pollution by third-party libraries. +When initializing a JavaScript runtime, Node.js therefore caches +wrapped versions of every global intrinsic object (and its methods) +in a [large `primordials` object][primordials.js]. -
+Many of the global intrinsic methods inside of the `primordials` object +rely on the `this` binding. +`primordials` therefore contains numerous entries that look like this: +```js +ArrayPrototypeConcat: uncurryThis(Array.prototype.concat), +ArrayPrototypeCopyWithin: uncurryThis(Array.prototype.copyWithin), +ArrayPrototypeFill: uncurryThis(Array.prototype.fill), +ArrayPrototypeFind: uncurryThis(Array.prototype.find), +ArrayPrototypeFindIndex: uncurryThis(Array.prototype.findIndex), +ArrayPrototypeLastIndexOf: uncurryThis(Array.prototype.lastIndexOf), +ArrayPrototypePop: uncurryThis(Array.prototype.pop), +ArrayPrototypePush: uncurryThis(Array.prototype.push), +ArrayPrototypePushApply: applyBind(Array.prototype.push), +ArrayPrototypeReverse: uncurryThis(Array.prototype.reverse), +``` +…and so on, where `uncurryThis` is `Function.prototype.call.bind` +(also called [“call-binding”][call-bind]), +and `applyBind` is the similar `Function.prototype.apply.bind`. + +[call-bind]: https://npmjs.com/call-bind +In other words, Node.js must **wrap** every `this`-sensitive global intrinsic method +in a `this`-uncurried **wrapper function**, +whose first argument is the method’s `this` value, +using the `uncurryThis` helper function. + +The result is that code that uses these global intrinsic methods, +like this code adapted from [node/lib/internal/v8_prof_processor.js][]: ```js -??? + // `specifier` is a string. + const file = specifier.slice(2, -4); + + // Later… + if (process.platform === 'darwin') { + tickArguments.push('--mac'); + } else if (process.platform === 'win32') { + tickArguments.push('--windows'); + } + tickArguments.push(...process.argv.slice(1)); ``` -From ???. +…must instead look like this: +```js +// Note: This module assumes that it runs before any third-party code. +const { + ArrayPrototypePush, + ArrayPrototypePushApply, + ArrayPrototypeSlice, + StringPrototypeSlice, +} = primordials; + + // Later… + const file = StringPrototypeSlice(specifier, 2, -4); + + // Later… + if (process.platform === 'darwin') { + ArrayPrototypePush(tickArguments, '--mac'); + } else if (process.platform === 'win32') { + ArrayPrototypePush(tickArguments, '--windows'); + } + ArrayPrototypePushApply(tickArguments, ArrayPrototypeSlice(process.argv, 1)); +``` + +This code is now protected against prototype pollution by accident and by adversaries +(e.g., `delete Array.prototype.push` or `delete Array.prototype[Symbol.iterator]`). +However, this protection comes at two costs: + +1. These [uncurried wrapper functions sometimes dramatically reduce performance][#38248]. + This would not be a problem if Node.js could cache + and use the intrinsic methods directly. + But the only current way to use intrinsic methods + would be with `Function.prototype.call`, which is also vulnerable to mutation. - +2. The Node.js community has had [much concern about barriers to contribution][#30697] + by ordinary JavaScript developers, due to the unidiomatic code encouraged by these + uncurried wrapper functions. + +Both of these problems are much improved by the bind-`this` operator. +Instead of wrapping every global method with `uncurryThis`, +Node.js could cached and used **directly** +without worrying about `Function.prototype.call` mutation: ```js -??? +// Note: This module assumes that it runs before any third-party code. +const $apply = Function.prototype.apply; +const $push = Array.prototype.push; +const $arraySlice = Array.prototype.slice; +const $stringSlice = String.prototype.slice; + + // Later… + const file = specifier->$stringSlice(2, -4); + + // Later… + if (process.platform === 'darwin') { + tickArguments->$push('--mac'); + } else if (process.platform === 'win32') { + tickArguments->$push('--windows'); + } + $push->$apply(tickArguments, process.argv->$arraySlice(1)); ``` -
+Performance has improved, and readability has improved. +There are no more uncurried wrapper functions; +instead, the code uses the intrinsic methods in a notation +similar to normal method calling with `.`. + +[node/lib/internal/v8_prof_processor.js]: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/v8_prof_processor.js +[#38248]: https://github.com/nodejs/node/pull/38248 +[#30697]: https://github.com/nodejs/node/issues/30697 ## Non-goals A goal of this proposal is **simplicity**. @@ -201,23 +299,27 @@ but method extraction is **already possible** with this proposal.\ is not much wordier than\ `const slice = arr&.slice; slice(1, 3);` -**Extension getters and setters** -(i.e., extending objects with new property getters or setters -**without mutating** the object) -may **also** be useful, -and this proposal would be **forward compatible** with such a feature -using the **same operator** `->` for **property-object binding**, -in addition to this proposal’s **method binding**. -Getter/setter binding could be added in a separate proposal -using `{ get () {}, set () {} }` objects. -For example, we could add an extension getter `allDivs` -to a `document` object like so: +**Extracting property accessors** (i.e., getters and setters) +is not a goal of this proposal. +Get/set accessors are **not like** methods. Methods are **values**. +Accessors themselves are **not values**; +they are functions that activate when getting or setting properties. +Getters/setters have to be extracted using `Object.getOwnPropertyDescriptor`; +they are not handled in a special way. +This verbosity may be considered to be desirable [syntactic salt][]: +it makes the developer’s intention (to extract getters/setters – and not methods) +more explicit. + ```js -const allDivs = { - get () { return this.querySelectorAll('div'); } -}; +const { get: $getSize } = + Object.getOwnPropertyDescriptor( + Set.prototype, 'size'); -document->allDivs; +// The adversary’s code. +delete Set; delete Function; + +// Our own trusted code, running later. +new Set([0, 1, 2])->$getSize(); ``` **Function/expression application**, @@ -228,3 +330,6 @@ Instead, it is addressed by the **pipe operator**, with which this proposal’s syntax **works well**.\ For example, we could untangle `h(await g(o->f(0, v)), 1)`\ into `v |> o->f(0, %) |> await g(%) |> h(%, 1)`. + +[syntactic salt]: https://en.wikipedia.org/wiki/Syntactic_sugar#Syntactic_salt +[primordials.js]: https://github.com/nodejs/node/blob/master/lib/internal/per_context/primordials.js diff --git a/spec.html b/spec.html index f3cd9d6..e5de817 100644 --- a/spec.html +++ b/spec.html @@ -1,5 +1,5 @@
-title: ES bind operator (2021)
+title: ES bind-this operator (2021)
 status: proposal
 stage: 0
 location: https://github.com/js-choi/proposal-bind-operator
@@ -12,7 +12,7 @@
 
   

Introduction

This is the formal specification - for a proposed bind operator `->` in JavaScript. + for a proposed bind-`this` operator `->` in JavaScript. It modifies the original ECMAScript specification with several new or revised clauses. See Static Semantics: IsFunctionDefinition MemberExpression `.` PrivateIdentifier MemberExpression `->` `(` Expression `)` - MemberExpression `->` IdentifierReference + MemberExpression `->` IdentifierReference @@ -277,12 +277,12 @@

Static Semantics: Early Errors

-

This production exists in order to prevent automatic semicolon insertion rules () from being applied to the following code with the bind operator:

+

This production exists in order to prevent automatic semicolon insertion rules () from being applied to the following code with the bind-`this` operator:


             a->b
             `c`
           
-

so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without the bind operator:

+

so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without the bind-`this` operator:


             a.b
             `c`
@@ -300,12 +300,12 @@ 

Static Semantics: Early Errors

-

This production exists in order to prevent automatic semicolon insertion rules () from being applied to the following code with the bind operator:

+

This production exists in order to prevent automatic semicolon insertion rules () from being applied to the following code with the bind-`this` operator:


             a()->b
             `c`
           
-

so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without the bind operator:

+

so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without the bind-`this` operator:


             a().b
             `c`
@@ -323,12 +323,12 @@ 

Static Semantics: Early Errors

-

This production exists in order to prevent automatic semicolon insertion rules () from being applied to the following code with the bind operator:

+

This production exists in order to prevent automatic semicolon insertion rules () from being applied to the following code with the bind-`this` operator:


             a?.()->b
             `c`
           
-

so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without the bind operator:

+

so that it would be interpreted as two valid statements. The purpose is to maintain consistency with similar code without the bind-`this` operator:


             a?.().b
             `c`
@@ -340,7 +340,7 @@ 

Static Semantics: Early Errors

-

Bind Operator

+

Bind-`this` operator

This section is a wholly new subclause to be inserted after the 1. Return _F_. -

Function objects created using the bind operator are exotic objects. They also do not have a *"prototype"* property.

+

Function objects created using the bind-`this` operator are exotic objects. They also do not have a *"prototype"* property.

@@ -455,4 +455,27 @@

Runtime Semantics: ChainEvaluation

+ + +

Decorators

+

Syntax

+ + DecoratorList[Yield, Await] : + DecoratorList[?Yield, ?Await]? Decorator[?Yield, ?Await] + + Decorator[Yield, Await] : + `@init:` DecoratorMemberExpression[?Yield, ?Await] + `@init:` DecoratorCallExpression[?Yield, ?Await] + `@` DecoratorMemberExpression[?Yield, ?Await] + `@` DecoratorCallExpression[?Yield, ?Await] + + DecoratorMemberExpression[Yield, Await] : + IdentifierReference[?Yield, ?Await] + DecoratorMemberExpression[?Yield, ?Await] `.` IdentifierName + `(` Expression[+In, ?Yield, ?Await] `)` + + DecoratorCallExpression[Yield, Await] : + DecoratorMemberExpression[?Yield, ?Await] Arguments[?Yield, ?Await] + +