Skip to content

Commit b991c11

Browse files
committed
explainer § Why: Frequencies of bind/call/apply
See #8 and #6.
1 parent fc74301 commit b991c11

File tree

2 files changed

+241
-183
lines changed

2 files changed

+241
-183
lines changed

README.md

+126-183
Original file line numberDiff line numberDiff line change
@@ -81,219 +81,162 @@ is equivalent to `a->(createFn())`.
8181
There are **no other special rules**.
8282

8383
## Why a bind-`this` operator
84-
[`Function.prototype.bind`][call] and [`Function.prototype.call`][bind]
85-
are very common in **object-oriented JavaScript** code.
86-
They are useful methods that allows us to apply functions to any object,
87-
binding their first arguments to the `this` bindings within those functions,
88-
no matter the current object environment.
89-
`bind` and `call` allow us to **extend** an **object** with a function
90-
as if that function were **its own method**.
91-
They serve as an important link between
92-
the **object-oriented** and **functional** styles in JavaScript.
84+
In short:
85+
86+
1. [`.bind`][bind], [`.call`][call], and [`.apply`][apply]
87+
are very useful and very common in JavaScript codebases.
88+
2. But `.bind`, `.call`, and `.apply` are clunky and unergonomic.
9389

9490
[bind]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
9591
[call]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
96-
97-
Why then would we need an operator that does the same thing?
98-
Because `bind` and `call` are vulnerable to **global mutation**.
99-
100-
For example, when we run our code in an untrusted environment,
101-
an adversary may mutate global prototype objects
102-
such as `Array.prototype`,
103-
reassigning or deleting their methods.
104-
105-
```js
106-
// The adversary’s code.
107-
delete Array.prototype.slice;
108-
109-
// Our own trusted code, running later.
110-
// Due to the adversary, this unexpectedly throws an error.
111-
[0, 1, 2].slice(1, 2);
92+
[apply]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply
93+
94+
### `.bind`, `.call`, and `.apply` are very common
95+
The dynamic `this` binding is a fundamental part of JavaScript design and practice today.
96+
Because of this, developers frequently need to change the `this` binding.
97+
`.bind`, `.call`, and `.apply` are arguably three of the most commonly used functions
98+
in all JavaScript.
99+
100+
We can estimate `.bind`, `.call`, and `.apply`’s prevalences using [Node Gzemnid][].
101+
Although [Gzemnid can be deceptive][], we are only seeking rough estimations.
102+
103+
[Node Gzemnid]: https://github.com/nodejs/Gzemnid
104+
[Gzemnid can be deceptive]: https://github.com/nodejs/Gzemnid/blob/main/README.md#deception
105+
106+
First, we download the 2019-06-04 [pre-built Gzemnid dataset][]
107+
for the top-1000 downloaded NPM packages.
108+
We also need Gzemnid’s `search.topcode.sh` script in the same active directory,
109+
which in turn requires the lz4 command suite.
110+
`search.topcode.sh` will output lines of code from the top-1000 packages
111+
that match the given regular expression.
112+
113+
[pre-built NPM dataset]: https://gzemnid.nodejs.org/datasets/
114+
115+
```bash
116+
./search.topcode.sh '\.call\b' | head
117+
grep -aE "\.call\b"
118+
177726827 debug-4.1.1.tgz/src/common.js:101: match = formatter.call(self, val);
119+
177726827 debug-4.1.1.tgz/src/common.js:111: createDebug.formatArgs.call(self, args);
120+
154772106 kind-of-6.0.2.tgz/index.js:54: type = toString.call(val);
121+
139612972 readable-stream-3.4.0.tgz/errors-browser.js:26: return _Base.call(this, getMessage(arg1, arg2, arg3)) || this;
122+
139612972 readable-stream-3.4.0.tgz/lib/_stream_duplex.js:60: Readable.call(this, options);
123+
139612972 readable-stream-3.4.0.tgz/lib/_stream_duplex.js:61: Writable.call(this, options);
124+
139612972 readable-stream-3.4.0.tgz/lib/_stream_passthrough.js:34: Transform.call(this, options);
125+
139612972 readable-stream-3.4.0.tgz/lib/_stream_readable.js:183: Stream.call(this);
126+
139612972 readable-stream-3.4.0.tgz/lib/_stream_readable.js:786: var res = Stream.prototype.on.call(this, ev, fn);
112127
```
113128

114-
In order to harden JavaScript applications against this attack,
115-
we can extract critical global prototype methods into variables
116-
before any untrusted code may run.
117-
We would then use our critical methods with their `call` methods.
118-
119-
```js
120-
// Our own trusted code, running before any adversary.
121-
const { slice } = Array.prototype;
122-
123-
// The adversary’s code.
124-
delete Array.prototype.slice;
125-
126-
// Our own trusted code, running later.
127-
// In spite of the adversary, this no longer throws an error.
128-
slice.call([0, 1, 2], 1, 2);
129+
We use `awk` to count those matching lines of code
130+
and compare their numbers for `bind`, `call`, `apply`,
131+
and several other frequently used functions.
132+
133+
```bash
134+
> ls
135+
search.topcode.sh
136+
slim.topcode.1000.txt.lz4
137+
> ./search.topcode.sh '\.call\b' | awk 'END { print NR }'
138+
500084
139+
> ./search.topcode.sh '\.apply\b' | awk 'END { print NR }'
140+
225315
141+
> ./search.topcode.sh '\.bind\b' | awk 'END { print NR }'
142+
170248
143+
> ./search.topcode.sh '\b.map\b' | awk 'END { print NR }'
144+
1016503
145+
> ./search.topcode.sh '\bconsole.log\b' | awk 'END { print NR }'
146+
271915
147+
> ./search.topcode.sh '\.set\b' | awk 'END { print NR }'
148+
168872
149+
> ./search.topcode.sh '\.push\b' | awk 'END { print NR }'
150+
70116
129151
```
130152

131-
But this approach is still vulnerable to mutation of `Function.prototype`:
153+
These results suggest that usage of `.call`, `.bind`, and `.apply`
154+
are comparable to usage of other frequently used standard functions.
155+
In this dataset, their combined usage even exceeds that of `console.log`.
132156

133-
```js
134-
// Our own trusted code, running before any adversary.
135-
const { slice } = Array.prototype;
157+
Obviously, [this methodology has many pitfalls][Gzemnid can be deceptive],
158+
but we are only looking for roughly estimated orders of magnitude
159+
relative to other baseline functions.
160+
Gzemnid counts each library’s codebase only once; it does not double-count dependencies.
136161

137-
// The adversary’s code.
138-
delete Array.prototype.slice;
139-
delete Function.prototype.call;
162+
In fact, this method definitely underestimates the prevalences
163+
of `.bind`, `.call`, and `.apply`
164+
by excluding the large JavaScript codebases of Node and Deno.
165+
Node and Deno [copiously use bound functions for security][security-use-case]
166+
hundreds or thousands of times.
140167

141-
// Our own trusted code, running later.
142-
// Due to the adversary, this throws an error again.
143-
slice.call([0, 1, 2], 1, 2);
144-
```
145-
146-
There is currently no way to harden code against mutation of `Function.prototype`.
147-
A new operator, however, would not be vulnerable to mutation:
168+
[security-use-case]: https://github.com/js-choi/proposal-bind-this/blob/main/security-use-case.md
148169

149-
```js
150-
// Our own trusted code, running before any adversary.
151-
const { slice } = Array.prototype;
170+
### `.bind`, `.call`, and `.apply` are clunky
171+
JavaScript developers are used to using methods in a [noun–verb–noun word order][]
172+
that resembles English and other [SVO human languages][]: `obj.fn(arg)`.
152173

153-
// The adversary’s code.
154-
delete Array.prototype.slice;
155-
delete Function.prototype.call;
174+
[SVO human languages]: https://en.wikipedia.org/wiki/Category:Subject–verb–object_languages
156175

157-
// Our own trusted code, running later.
158-
// In spite of the adversary, this no longer throws an error.
159-
// It also is considerably more readable.
160-
[0, 1, 2]->slice(1, 2);
161-
```
176+
However, `.bind`, `.call`, and `.apply` flip this “natural” word order,
177+
They flip the first noun and the verb,
178+
and they interpose the verb’s `Function.prototype` method between them:
179+
`obj.call(arg)`.
162180

163-
As a bonus, the syntax is also considerably more readable
164-
than code that uses `bind` or `call`.
165-
A bound function could be called inline
166-
as if it were **actually a method** in the object
167-
whose **property key** is the **function itself**:
181+
Consider the following real-life code using `.bind` or `.call`,
182+
and compare them to versions that use the `->` operator.
183+
The difference is especially evident when you read them aloud.
168184

169185
```js
170-
function extensionMethod () {
171-
return this;
172-
}
186+
// kind-of-6.0.2.tgz/index.js
187+
type = toString.call(val);
188+
type = val->toString();
173189

174-
obj.actualMethod();
175-
obj->extensionMethod();
176-
// Compare with extensionMethod.call(obj).
177-
```
190+
// debug-4.1.1.tgz/src/common.js
191+
match = formatter.call(self, val);
192+
match = self->formatter(val);
178193

179-
## Real-world examples
180-
Only minor formatting changes have been made to the status-quo examples.
194+
createDebug.formatArgs.call(self, args);
195+
self->(createDebug.formatArgs)(args);
181196

182-
### Node.js
183-
Node.js’s runtime depends on many built-in JavaScript global intrinsic objects
184-
that are vulnerable to mutation or prototype pollution by third-party libraries.
185-
When initializing a JavaScript runtime, Node.js therefore caches
186-
wrapped versions of every global intrinsic object (and its methods)
187-
in a [large `primordials` object][primordials.js].
197+
// readable-stream-3.4.0.tgz/errors-browser.js
198+
return _Base.call(this, getMessage(arg1, arg2, arg3)) || this;
199+
return this->_Base(getMessage(arg1, arg2, arg3)) || this;
188200

189-
Many of the global intrinsic methods inside of the `primordials` object
190-
rely on the `this` binding.
191-
`primordials` therefore contains numerous entries that look like this:
192-
```js
193-
ArrayPrototypeConcat: uncurryThis(Array.prototype.concat),
194-
ArrayPrototypeCopyWithin: uncurryThis(Array.prototype.copyWithin),
195-
ArrayPrototypeFill: uncurryThis(Array.prototype.fill),
196-
ArrayPrototypeFind: uncurryThis(Array.prototype.find),
197-
ArrayPrototypeFindIndex: uncurryThis(Array.prototype.findIndex),
198-
ArrayPrototypeLastIndexOf: uncurryThis(Array.prototype.lastIndexOf),
199-
ArrayPrototypePop: uncurryThis(Array.prototype.pop),
200-
ArrayPrototypePush: uncurryThis(Array.prototype.push),
201-
ArrayPrototypePushApply: applyBind(Array.prototype.push),
202-
ArrayPrototypeReverse: uncurryThis(Array.prototype.reverse),
203-
```
204-
…and so on, where `uncurryThis` is `Function.prototype.call.bind`
205-
(also called [“call-binding”][call-bind]),
206-
and `applyBind` is the similar `Function.prototype.apply.bind`.
201+
// readable-stream-3.4.0.tgz/lib/_stream_readable.js
202+
var res = Stream.prototype.on.call(this, ev, fn);
203+
var res = this->(Stream.prototype.on)(ev, fn);
207204

208-
[call-bind]: https://npmjs.com/call-bind
205+
var res = Stream.prototype.removeAllListeners.apply(this, arguments);
206+
var res = this->(Stream.prototype.removeAllListeners)(...arguments);
209207

210-
In other words, Node.js must **wrap** every `this`-sensitive global intrinsic method
211-
in a `this`-uncurried **wrapper function**,
212-
whose first argument is the method’s `this` value,
213-
using the `uncurryThis` helper function.
208+
// yargs-13.2.4.tgz/lib/middleware.js
209+
Array.prototype.push.apply(globalMiddleware, callback)
210+
globalMiddleware->(Array.prototype.push)(...callback)
214211

215-
The result is that code that uses these global intrinsic methods,
216-
like this code adapted from [node/lib/internal/v8_prof_processor.js][]:
217-
```js
218-
// `specifier` is a string.
219-
const file = specifier.slice(2, -4);
220-
221-
// Later…
222-
if (process.platform === 'darwin') {
223-
tickArguments.push('--mac');
224-
} else if (process.platform === 'win32') {
225-
tickArguments.push('--windows');
226-
}
227-
tickArguments.push(...process.argv.slice(1));
228-
```
229-
…must instead look like this:
230-
```js
231-
// Note: This module assumes that it runs before any third-party code.
232-
const {
233-
ArrayPrototypePush,
234-
ArrayPrototypePushApply,
235-
ArrayPrototypeSlice,
236-
StringPrototypeSlice,
237-
} = primordials;
238-
239-
// Later…
240-
const file = StringPrototypeSlice(specifier, 2, -4);
241-
242-
// Later…
243-
if (process.platform === 'darwin') {
244-
ArrayPrototypePush(tickArguments, '--mac');
245-
} else if (process.platform === 'win32') {
246-
ArrayPrototypePush(tickArguments, '--windows');
247-
}
248-
ArrayPrototypePushApply(tickArguments, ArrayPrototypeSlice(process.argv, 1));
249-
```
212+
// yargs-13.2.4.tgz/lib/command.js
213+
[].push.apply(positionalKeys, parsed.aliases[key])
250214

251-
This code is now protected against prototype pollution by accident and by adversaries
252-
(e.g., `delete Array.prototype.push` or `delete Array.prototype[Symbol.iterator]`).
253-
However, this protection comes at two costs:
215+
// pretty-format-24.8.0.tgz/build-es5/index.js
216+
var code = fn.apply(colorConvert, arguments);
217+
var code = colorConvert->fn(...arguments);
254218

255-
1. These [uncurried wrapper functions sometimes dramatically reduce performance][#38248].
256-
This would not be a problem if Node.js could cache
257-
and use the intrinsic methods directly.
258-
But the only current way to use intrinsic methods
259-
would be with `Function.prototype.call`, which is also vulnerable to mutation.
219+
// q-1.5.1.tgz/q.js
220+
return value.apply(thisp, args);
221+
return thisp->value(...args);
260222

261-
2. The Node.js community has had [much concern about barriers to contribution][#30697]
262-
by ordinary JavaScript developers, due to the unidiomatic code encouraged by these
263-
uncurried wrapper functions.
223+
// rxjs-6.5.2.tgz/src/internal/operators/every.ts
224+
result = this.predicate.call(this.thisArg, value, this.index++, this.source);
225+
result = this.thisArg->(this.predicate)(value, this.index++, this.source);
264226

265-
Both of these problems are much improved by the bind-`this` operator.
266-
Instead of wrapping every global method with `uncurryThis`,
267-
Node.js could cached and used **directly**
268-
without worrying about `Function.prototype.call` mutation:
227+
// bluebird-3.5.5.tgz/js/release/synchronous_inspection.js
228+
return isPending.call(this._target());
229+
return this._target()->isPending();
269230

270-
```js
271-
// Note: This module assumes that it runs before any third-party code.
272-
const $apply = Function.prototype.apply;
273-
const $push = Array.prototype.push;
274-
const $arraySlice = Array.prototype.slice;
275-
const $stringSlice = String.prototype.slice;
276-
277-
// Later…
278-
const file = specifier->$stringSlice(2, -4);
279-
280-
// Later…
281-
if (process.platform === 'darwin') {
282-
tickArguments->$push('--mac');
283-
} else if (process.platform === 'win32') {
284-
tickArguments->$push('--windows');
285-
}
286-
$push->$apply(tickArguments, process.argv->$arraySlice(1));
287-
```
231+
var matchesPredicate = tryCatch(item).call(boundTo, e);
232+
var matchesPredicate = boundTo->(tryCatch(item))(e);
288233

289-
Performance has improved, and readability has improved.
290-
There are no more uncurried wrapper functions;
291-
instead, the code uses the intrinsic methods in a notation
292-
similar to normal method calling with `.`.
234+
// graceful-fs-4.1.15.tgz/polyfills.js
235+
return fs$read.call(fs, fd, buffer, offset, length, position, callback)
236+
return fs->fs$read(fd, buffer, offset, length, position, callback)
237+
```
293238

294-
[node/lib/internal/v8_prof_processor.js]: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/v8_prof_processor.js
295-
[#38248]: https://github.com/nodejs/node/pull/38248
296-
[#30697]: https://github.com/nodejs/node/issues/30697
239+
[noun–verb–noun word order]: https://en.wikipedia.org/wiki/Subject–verb–object
297240

298241
## Non-goals
299242
A goal of this proposal is **simplicity**.

0 commit comments

Comments
 (0)