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)); ``` - |
-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_.
- @@ -455,4 +455,27 @@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.
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] + +