Skip to content

Commit b3a1233

Browse files
js-choiljharbbmeck
authored
Node examples and other improvements (#7)
* explainer/spec: Rename to bind-this operator * explainer: Clarify property-accessor non-goal * explainer § Description: Reword, note Function.call equivalence * explainer § Description: Improve precedence table * explainer: Node.js real-world examples * explainer § Description: Fix inconsistent RHS See #7 (comment). Closes #5. Co-authored-by: Jordan Harband <[email protected]> Co-authored-by: Bradley Farias <[email protected]>
1 parent 4d34810 commit b3a1233

File tree

2 files changed

+196
-68
lines changed

2 files changed

+196
-68
lines changed

README.md

Lines changed: 162 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Bind operator for JavaScript
1+
# Bind-`this` operator for JavaScript
22
ECMAScript Stage-0 Proposal. J. S. Choi, 2021.
33

44
* **[Formal specification][]**
@@ -10,8 +10,8 @@ ECMAScript Stage-0 Proposal. J. S. Choi, 2021.
1010
(A [formal specification][] is available.)
1111

1212
**Method binding** `->` is a **left-associative infix operator**.
13-
Its right-hand side is an **identifier**
14-
or an **expression** in `(` `)`,
13+
Its right-hand side is an **identifier** (like `f`)
14+
or a parenthesized **expression** (like `(hof())`),
1515
either of which must evaluate to a **function**.
1616
Its left-hand side is some expression that evaluates to an **object**.
1717
The `->` operator **binds** its left-hand side
@@ -30,36 +30,48 @@ equivalent to `createMethod().bind(obj)`.
3030
If the operator’s right-hand side does not evaluate to a function during runtime,
3131
then the program throws a `TypeError`.
3232

33-
Function binding has equal **[precedence][]** with
34-
**member expressions**, call expressions, `new` expressions with arguments,
35-
and optional chains.
36-
37-
[precedence]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
38-
39-
| Left-hand side | Example |
40-
| ------------------------------- | ------------- |
41-
| Primary expressions | `a->fn` |
42-
| Member expressions | `a.b->fn` |
43-
| Call expressions | `a()->fn` |
44-
|`new` expressions with arguments | `new C()->fn` |
45-
| Optional chains | `a?.b->fn` |
46-
47-
The bound functions created by the bind operator
33+
The bound functions created by the bind-`this` operator
4834
are **indistinguishable** from the bound functions
4935
that are already created by [`Function.prototype.bind`][bind].
5036
Both are **exotic objects** that do not have a `prototype` property,
5137
and which may be called like any typical function.
5238

53-
Similarly to the `?.` optional-chaining token,
54-
the `->` token may be **padded by whitespace**.\
39+
From this definition, `o->f(...args)`
40+
is **indistinguishable** from `f.call(o, ...args)`,
41+
except that its behavior does **not change**
42+
if code elsewhere **reassigns** the global method `Function.prototype.call`.
43+
44+
The `this`-bind operator has equal **[precedence][]** with
45+
**member expressions**, call expressions, `new` expressions with arguments,
46+
and optional chains.
47+
Like those operators, the `this`-bind operator also may be short-circuited
48+
by optional chains in its left-hand side.
49+
50+
[precedence]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
51+
52+
| Left-hand side | Example | Grouping
53+
| ---------------------------------- | ------------ | --------------
54+
| Member expressions |`a.b->fn.c` |`((a.b)->fn).c`
55+
| Call expressions |`a()->fn()` |`((a())->fn)()`
56+
| Optional chains |`a?.b->fn` |`(a?.b)->fn`
57+
|`new` expressions with arguments |`new C(a)->fn`|`(new C(a))->fn`
58+
|`new` expressions without arguments*|`new a->fn` |`new (a->fn)`
59+
60+
\* Like `.` and `?.`, the `this`-bind operator also have tighter precedence
61+
than `new` expressions without arguments.
62+
Of course, `new a->fn` is not a very useful expression,
63+
just like how `new (fn.bind(a))` is not a very useful expression.
64+
65+
Similarly to the `.` and `?.` operators,
66+
the `->` operator may be **padded by whitespace**.\
5567
For example, `a -> m`\
5668
is equivalent to `a->fn`,\
5769
and `a -> (createFn())`\
5870
is equivalent to `a->(createFn())`.
5971

6072
There are **no other special rules**.
6173

62-
## Why a bind operator
74+
## Why a bind-`this` operator
6375
[`Function.prototype.bind`][call] and [`Function.prototype.call`][bind]
6476
are very common in **object-oriented JavaScript** code.
6577
They are useful methods that allows us to apply functions to any object,
@@ -107,7 +119,7 @@ delete Array.prototype.slice;
107119
slice.call([0, 1, 2], 1, 2);
108120
```
109121

110-
But this is still vulnerable to mutation of `Function.prototype`:
122+
But this approach is still vulnerable to mutation of `Function.prototype`:
111123

112124
```js
113125
// Our own trusted code, running before any adversary.
@@ -155,38 +167,124 @@ obj->extensionMethod();
155167
// Compare with extensionMethod.call(obj).
156168
```
157169

158-
The bind operator can also **extract** a **method** from a **class**
159-
into a function whose first parameter becomes its `this` binding:\
160-
for example, `const { slice } = Array.prototype; arr->slice(1, 3);`.\
161-
It can also similarly extract a method from an **instance**
162-
into a function that always uses that instance as its `this` binding:\
163-
for example, `const arr = arr->(arr.slice); slice(1, 3);`.
164-
165170
## Real-world examples
166171
Only minor formatting changes have been made to the status-quo examples.
167172

168-
<table>
169-
<thead>
170-
<tr>
171-
<th>Status quo
172-
<th>With binding
173+
### Node.js
174+
Node.js’s runtime depends on many built-in JavaScript global intrinsic objects
175+
that are vulnerable to mutation or prototype pollution by third-party libraries.
176+
When initializing a JavaScript runtime, Node.js therefore caches
177+
wrapped versions of every global intrinsic object (and its methods)
178+
in a [large `primordials` object][primordials.js].
173179

174-
<tbody>
175-
<tr>
176-
<td>
180+
Many of the global intrinsic methods inside of the `primordials` object
181+
rely on the `this` binding.
182+
`primordials` therefore contains numerous entries that look like this:
183+
```js
184+
ArrayPrototypeConcat: uncurryThis(Array.prototype.concat),
185+
ArrayPrototypeCopyWithin: uncurryThis(Array.prototype.copyWithin),
186+
ArrayPrototypeFill: uncurryThis(Array.prototype.fill),
187+
ArrayPrototypeFind: uncurryThis(Array.prototype.find),
188+
ArrayPrototypeFindIndex: uncurryThis(Array.prototype.findIndex),
189+
ArrayPrototypeLastIndexOf: uncurryThis(Array.prototype.lastIndexOf),
190+
ArrayPrototypePop: uncurryThis(Array.prototype.pop),
191+
ArrayPrototypePush: uncurryThis(Array.prototype.push),
192+
ArrayPrototypePushApply: applyBind(Array.prototype.push),
193+
ArrayPrototypeReverse: uncurryThis(Array.prototype.reverse),
194+
```
195+
…and so on, where `uncurryThis` is `Function.prototype.call.bind`
196+
(also called [“call-binding”][call-bind]),
197+
and `applyBind` is the similar `Function.prototype.apply.bind`.
198+
199+
[call-bind]: https://npmjs.com/call-bind
177200

201+
In other words, Node.js must **wrap** every `this`-sensitive global intrinsic method
202+
in a `this`-uncurried **wrapper function**,
203+
whose first argument is the method’s `this` value,
204+
using the `uncurryThis` helper function.
205+
206+
The result is that code that uses these global intrinsic methods,
207+
like this code adapted from [node/lib/internal/v8_prof_processor.js][]:
178208
```js
179-
???
209+
// `specifier` is a string.
210+
const file = specifier.slice(2, -4);
211+
212+
// Later…
213+
if (process.platform === 'darwin') {
214+
tickArguments.push('--mac');
215+
} else if (process.platform === 'win32') {
216+
tickArguments.push('--windows');
217+
}
218+
tickArguments.push(...process.argv.slice(1));
180219
```
181-
From ???.
220+
…must instead look like this:
221+
```js
222+
// Note: This module assumes that it runs before any third-party code.
223+
const {
224+
ArrayPrototypePush,
225+
ArrayPrototypePushApply,
226+
ArrayPrototypeSlice,
227+
StringPrototypeSlice,
228+
} = primordials;
229+
230+
// Later…
231+
const file = StringPrototypeSlice(specifier, 2, -4);
232+
233+
// Later…
234+
if (process.platform === 'darwin') {
235+
ArrayPrototypePush(tickArguments, '--mac');
236+
} else if (process.platform === 'win32') {
237+
ArrayPrototypePush(tickArguments, '--windows');
238+
}
239+
ArrayPrototypePushApply(tickArguments, ArrayPrototypeSlice(process.argv, 1));
240+
```
241+
242+
This code is now protected against prototype pollution by accident and by adversaries
243+
(e.g., `delete Array.prototype.push` or `delete Array.prototype[Symbol.iterator]`).
244+
However, this protection comes at two costs:
245+
246+
1. These [uncurried wrapper functions sometimes dramatically reduce performance][#38248].
247+
This would not be a problem if Node.js could cache
248+
and use the intrinsic methods directly.
249+
But the only current way to use intrinsic methods
250+
would be with `Function.prototype.call`, which is also vulnerable to mutation.
182251

183-
<td>
252+
2. The Node.js community has had [much concern about barriers to contribution][#30697]
253+
by ordinary JavaScript developers, due to the unidiomatic code encouraged by these
254+
uncurried wrapper functions.
255+
256+
Both of these problems are much improved by the bind-`this` operator.
257+
Instead of wrapping every global method with `uncurryThis`,
258+
Node.js could cached and used **directly**
259+
without worrying about `Function.prototype.call` mutation:
184260

185261
```js
186-
???
262+
// Note: This module assumes that it runs before any third-party code.
263+
const $apply = Function.prototype.apply;
264+
const $push = Array.prototype.push;
265+
const $arraySlice = Array.prototype.slice;
266+
const $stringSlice = String.prototype.slice;
267+
268+
// Later…
269+
const file = specifier->$stringSlice(2, -4);
270+
271+
// Later…
272+
if (process.platform === 'darwin') {
273+
tickArguments->$push('--mac');
274+
} else if (process.platform === 'win32') {
275+
tickArguments->$push('--windows');
276+
}
277+
$push->$apply(tickArguments, process.argv->$arraySlice(1));
187278
```
188279

189-
</table>
280+
Performance has improved, and readability has improved.
281+
There are no more uncurried wrapper functions;
282+
instead, the code uses the intrinsic methods in a notation
283+
similar to normal method calling with `.`.
284+
285+
[node/lib/internal/v8_prof_processor.js]: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/v8_prof_processor.js
286+
[#38248]: https://github.com/nodejs/node/pull/38248
287+
[#30697]: https://github.com/nodejs/node/issues/30697
190288

191289
## Non-goals
192290
A goal of this proposal is **simplicity**.
@@ -201,23 +299,27 @@ but method extraction is **already possible** with this proposal.\
201299
is not much wordier than\
202300
`const slice = arr&.slice; slice(1, 3);`
203301

204-
**Extension getters and setters**
205-
(i.e., extending objects with new property getters or setters
206-
**without mutating** the object)
207-
may **also** be useful,
208-
and this proposal would be **forward compatible** with such a feature
209-
using the **same operator** `->` for **property-object binding**,
210-
in addition to this proposal’s **method binding**.
211-
Getter/setter binding could be added in a separate proposal
212-
using `{ get () {}, set () {} }` objects.
213-
For example, we could add an extension getter `allDivs`
214-
to a `document` object like so:
302+
**Extracting property accessors** (i.e., getters and setters)
303+
is not a goal of this proposal.
304+
Get/set accessors are **not like** methods. Methods are **values**.
305+
Accessors themselves are **not values**;
306+
they are functions that activate when getting or setting properties.
307+
Getters/setters have to be extracted using `Object.getOwnPropertyDescriptor`;
308+
they are not handled in a special way.
309+
This verbosity may be considered to be desirable [syntactic salt][]:
310+
it makes the developer’s intention (to extract getters/setters – and not methods)
311+
more explicit.
312+
215313
```js
216-
const allDivs = {
217-
get () { return this.querySelectorAll('div'); }
218-
};
314+
const { get: $getSize } =
315+
Object.getOwnPropertyDescriptor(
316+
Set.prototype, 'size');
219317

220-
document->allDivs;
318+
// The adversary’s code.
319+
delete Set; delete Function;
320+
321+
// Our own trusted code, running later.
322+
new Set([0, 1, 2])->$getSize();
221323
```
222324

223325
**Function/expression application**,
@@ -228,3 +330,6 @@ Instead, it is addressed by the **pipe operator**,
228330
with which this proposal’s syntax **works well**.\
229331
For example, we could untangle `h(await g(o->f(0, v)), 1)`\
230332
into `v |> o->f(0, %) |> await g(%) |> h(%, 1)`.
333+
334+
[syntactic salt]: https://en.wikipedia.org/wiki/Syntactic_sugar#Syntactic_salt
335+
[primordials.js]: https://github.com/nodejs/node/blob/master/lib/internal/per_context/primordials.js

0 commit comments

Comments
 (0)