1
- # Bind operator for JavaScript
1
+ # Bind- ` this ` operator for JavaScript
2
2
ECMAScript Stage-0 Proposal. J. S. Choi, 2021.
3
3
4
4
* ** [ Formal specification] [ ] **
@@ -10,8 +10,8 @@ ECMAScript Stage-0 Proposal. J. S. Choi, 2021.
10
10
(A [ formal specification] [ ] is available.)
11
11
12
12
** 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()) ` ) ,
15
15
either of which must evaluate to a ** function** .
16
16
Its left-hand side is some expression that evaluates to an ** object** .
17
17
The ` -> ` operator ** binds** its left-hand side
@@ -30,36 +30,48 @@ equivalent to `createMethod().bind(obj)`.
30
30
If the operator’s right-hand side does not evaluate to a function during runtime,
31
31
then the program throws a ` TypeError ` .
32
32
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
48
34
are ** indistinguishable** from the bound functions
49
35
that are already created by [ ` Function.prototype.bind ` ] [ bind ] .
50
36
Both are ** exotic objects** that do not have a ` prototype ` property,
51
37
and which may be called like any typical function.
52
38
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** .\
55
67
For example, ` a -> m ` \
56
68
is equivalent to ` a->fn ` ,\
57
69
and ` a -> (createFn()) ` \
58
70
is equivalent to ` a->(createFn()) ` .
59
71
60
72
There are ** no other special rules** .
61
73
62
- ## Why a bind operator
74
+ ## Why a bind- ` this ` operator
63
75
[ ` Function.prototype.bind ` ] [ call ] and [ ` Function.prototype.call ` ] [ bind ]
64
76
are very common in ** object-oriented JavaScript** code.
65
77
They are useful methods that allows us to apply functions to any object,
@@ -107,7 +119,7 @@ delete Array.prototype.slice;
107
119
slice .call ([0 , 1 , 2 ], 1 , 2 );
108
120
```
109
121
110
- But this is still vulnerable to mutation of ` Function.prototype ` :
122
+ But this approach is still vulnerable to mutation of ` Function.prototype ` :
111
123
112
124
``` js
113
125
// Our own trusted code, running before any adversary.
@@ -155,38 +167,124 @@ obj->extensionMethod();
155
167
// Compare with extensionMethod.call(obj).
156
168
```
157
169
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
-
165
170
## Real-world examples
166
171
Only minor formatting changes have been made to the status-quo examples.
167
172
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 ] .
173
179
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
177
200
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] [ ] :
178
208
``` 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 ));
180
219
```
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.
182
251
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:
184
260
185
261
``` 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 ));
187
278
```
188
279
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
190
288
191
289
## Non-goals
192
290
A goal of this proposal is ** simplicity** .
@@ -201,23 +299,27 @@ but method extraction is **already possible** with this proposal.\
201
299
is not much wordier than\
202
300
` const slice = arr&.slice; slice(1, 3); `
203
301
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
+
215
313
``` js
216
- const allDivs = {
217
- get () { return this . querySelectorAll ( ' div ' ); }
218
- } ;
314
+ const { get : $getSize } =
315
+ Object . getOwnPropertyDescriptor (
316
+ Set . prototype , ' size ' ) ;
219
317
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 ();
221
323
```
222
324
223
325
** Function/expression application** ,
@@ -228,3 +330,6 @@ Instead, it is addressed by the **pipe operator**,
228
330
with which this proposal’s syntax ** works well** .\
229
331
For example, we could untangle ` h(await g(o->f(0, v)), 1) ` \
230
332
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