Bind-this operator for JavaScript
ECMAScript Stage-1 Proposal. J. S. Choi, 2021.
- Formal specification
- Babel plugin: Not yet
This proposal is a resurrection of the old Stage-0 bind-operator proposal. It is also an alternative, competing proposal to the Stage-1 extensions proposal. For more information, see § Related proposals.
Syntax
receiver::fn
receiver::ns.fn
receiver::(expr)
receiver
-
A member expression, a call expression, an optional expression, a
new
expression with arguments, another bind-this expression, or a parenthesized expression. The value to which this expression resolves will be bound to the right-hand side’s function object, as that function’sthis
receiver. fn
-
A variable that must resolve to a function object.
ns
-
Instead of a single variable, the right-hand side may be a namespace object’s variable, followed by a chain of property identifiers. This chain must resolve to a function object.
expr
-
An arbitrary expression within parentheses, which must resolve to a function object.
Description
(A formal specification is available.)
The bind-this operator ::
is a left-associative binary operator. It binds
its left-hand side (a receiver value) to its right-hand side (a
function)’s this
value, creating a bound function in the same manner
as Function.prototype.bind
.
For example, receiver::fn
would be equivalent to fn.bind(receiver)
(except
that its behavior does not change if code elsewhere reassigns the global method
Function.prototype.bind
).
Likewise, receiver::(createFn())
would be roughly equivalent to
createFn().bind(receiver)
.
If the operator’s right-hand side does not evaluate to a function during runtime, then the program throws a TypeError.
The bound functions created by the bind-this operator are indistinguishable
from the bound functions that are already created by
Function.prototype.bind
. Both are exotic objects that do not have
a prototype
property, and which may be called like any typical function.
From this definition, receiver::fn(...args)
is also indistinguishable from
fn.call(receiver, ...args)
(except that its behavior does not change if code
elsewhere reassigns the global method Function.prototype.call
).
The operator’s left side has equal precedence with member
expressions, call expressions, new
expressions with arguments, and optional
expressions. Like those operators, the bind-this operator also may be
short-circuited by optional expressions in its left-hand side.
Left-hand side | Example | Grouping |
---|---|---|
Member expressions | obj.prop::fn |
(obj.prop)::fn |
Call expressions | obj()::fn |
(obj())::fn |
Optional expressions | obj?.prop::fn |
(obj?.prop)::fn |
new expressions with arguments |
new C()::fn |
(new C())::fn |
The operator’s right-hand side, as with decorators, may be one of the following:
- A single identifier or private field (like
fn
or#field
). - A chain of identifiers and/or private fields (like
ns.fn
orthis.ns.#field
). - A parenthesized expression (like
(createFn())
).
For example, receiver::ns.ns.ns.fn
groups as receiver::(ns.ns.ns.fn)
.
Similarly to the .
and ?.
operators,
the bind-this operator may be padded by whitespace.
For example, receiver :: fn
is equivalent to receiver::fn
,
and receiver :: (createFn())
is equivalent to receiver::(createFn())
.
The bind-this operator may be optionally chained with ?.
(i.e., ?.::
):receiver::fn
will always result in a bound function,
regardless of whether receiver
is nullish.receiver?.::fn
will result in null
or undefined
if receiver
is null
or undefined
.receiver?.::fn(arg)
also short-circuits as usual, before arg
is evaluated,
if receiver
is nullish.
A new
expression may not contain a bind-this expression without
parentheses. Both new x::fn()
and new x::fn
are SyntaxErrors.
Otherwise, new x::fn()
would be visually ambiguous between(new x)::fn()
, new (x::fn)()
, and new (x::fn())
.
An optional expression may not contain a bind-this expression without
parentheses. For example, receiver::x?.prop
, receiver::x?.[key]
, and
receiver::x?.()
are all SyntaxErrors. Otherwise, receiver::x?.prop
would be
visually ambiguous between (receiver::x)?.prop
and receiver::(x?.prop)
.
(And both of these possibilities would be useless anyway:
A bind-this expression will never evaluate to null
or undefined
,
so (receiver::x)?.prop
is not useful.
And receiver::(null)
and receiver::(undefined)
will always
throw TypeErrors,
so receiver::(x?.prop)
is also not useful.)
Why a bind-this operator
In short:
.bind
and.call
are very useful and very common in JavaScript codebases.- But
.bind
and.call
are clunky and unergonomic.
.bind
and .call
are very common
The dynamic this
binding is a fundamental part of JavaScript design and
practice today. Because of this, developers frequently need to change the this
binding. .bind
and .call
are arguably three of the most commonly used
functions in all JavaScript.
We can estimate .bind
and .call
’s prevalences using Node Gzemnid.
Although Gzemnid can be deceptive, we are only seeking rough estimations.
The following results are from the checked-in-Git source code of the top-1000 downloaded NPM packages.
Occurrences | Method |
---|---|
1,016,503 | .map |
315,922 | .call |
271,915 | console.log |
182,292 | .slice |
170,248 | .bind |
168,872 | .set |
70,116 | .push |
These results suggest that usage of .bind
and .call
are comparable to usage
of other frequently used standard functions. In this dataset, their combined
usage even exceeds that of console.log
.
Obviously, this methodology has many pitfalls, but we are only looking for roughly estimated orders of magnitude relative to other baseline functions. Gzemnid counts each library’s codebase only once; it does not double-count dependencies.
In fact, this method definitely underestimates the prevalences of .bind
and
.call
by excluding the large JavaScript codebases of Node and Deno. Node and
Deno copiously use bound functions for security hundreds or
thousands of times.
What methodology was used to get these results?
First, we download the 2019-06-04 pre-built Gzemnid dataset for the top-1000
downloaded NPM packages. We also need Gzemnid’s search.topcode.sh
script in
the same active directory, which in turn requires the lz4 command suite.
search.topcode.sh
will output lines of code from the top-1000 packages that
match the given regular expression.
./search.topcode.sh '\.call\b' | head
grep -aE "\.call\b"
177726827 debug@4.1.1/src/common.js:101: match = formatter.call(self, val);
177726827 debug@4.1.1/src/common.js:111: createDebug.formatArgs.call(self, args);
154772106 kind-of@6.0.2/index.js:54: type = toString.call(val);
139612972 readable-stream@3.4.0/errors-browser.js:26: return _Base.call(this, getMessage(arg1, arg2, arg3)) || this;
139612972 readable-stream@3.4.0/lib/_stream_duplex.js:60: Readable.call(this, options);
139612972 readable-stream@3.4.0/lib/_stream_duplex.js:61: Writable.call(this, options);
139612972 readable-stream@3.4.0/lib/_stream_passthrough.js:34: Transform.call(this, options);
139612972 readable-stream@3.4.0/lib/_stream_readable.js:183: Stream.call(this);
139612972 readable-stream@3.4.0/lib/_stream_readable.js:786: var res = Stream.prototype.on.call(this, ev, fn);
We use awk
to count those matching lines of code and compare their numbers for
bind
, call
, and several other frequently used functions.
> ls
search.topcode.sh
slim.topcode.1000.txt.lz4
> ./search.topcode.sh '\.call\b' | grep -E --invert-match '//.*\.call|/\*.+\.call|[^a-zA-Z][A-Z][a-zA-Z0-9_$]*\.call\( *this|_super\.call|_super\.prototype\.|_getPrototypeOf|_possibleConstructorReturn|__super__|WEBPACK VAR INJECTION|_objectWithoutProperties|\.hasOwnProperty\.call' | awk 'END { print NR }'
315922
> ./search.topcode.sh '\.bind\b' | awk 'END { print NR }'
170248
> ./search.topcode.sh '\b.map\b' | awk 'END { print NR }'
1016503
> ./search.topcode.sh '\bconsole.log\b' | awk 'END { print NR }'
271915
> ./search.topcode.sh '\.slice\b' | awk 'END { print NR }'
182292
> ./search.topcode.sh '\.set\b' | awk 'END { print NR }'
168872
> ./search.topcode.sh '\.push\b' | awk 'END { print NR }'
70116
Note that, for .call
, we use grep
to exclude several irrelevant occurrences
of .call
either within comments or from transpiled code. We err on the side of
false exclusions.
Excluded pattern | Reason |
---|---|
//.*\.call |
Code comment. |
/\*.+\.call |
Code comment. |
[^a-zA-Z][A-Z][a-zA-Z0-9_$]*\.call\( *this |
Constructor call obsoleted by super . See note. |
_super\.call |
Babel-transpiled super() artifact. |
_super\.prototype\. |
Babel-transpiled super.fn() artifact. |
_objectWithoutProperties |
Babel-transpiled ... artifact. |
_getPrototypeOf |
Babel artifact. |
_possibleConstructorReturn |
Babel artifact. |
__super__ |
CoffeeScript artifact. |
WEBPACK VAR INJECTION |
Webpack artifact. |
\.hasOwnProperty\.call |
Obsoleted by Object.has . |
These excluded patterns were determined by an independent investigator (Scott
Jamison), after manually reviewing the first 10,000 occurrences of .call
in the dataset for why each occurrence occurred.
The excluded [^a-zA-Z][A-Z][a-zA-Z0-9_$]*\.call\( *this
pattern deserves a
special note. This pattern matches any capitalized identifier followed by
.call(this
. We exclude any such occurrences because any capitalized identifier
likely refers to a constructor, and using .call
on a constructor is a pattern
that has largely been obviated by class
and super
syntax. It is likely that
this pattern erroneously excludes many legitimate uses of .call
from our
count, but this bias against .call
is acceptable for the purposes of rough
comparison.
.bind
and .call
are clunky
JavaScript developers are used to using methods in a noun–verb–noun word
order that resembles English and other SVO human languages:
receiver.method(arg)
.
However, .bind
and .call
flip this “natural” word order, They flip the first
noun and the verb, and they interpose the verb’s Function.prototype
method
between them: method.call(receiver, arg)
.
Consider the following real-life code using .bind
or .call
, and compare them
to versions that use the bind-this operator. The difference is especially
evident when you read them aloud.
// kind-of@6.0.2/index.js
type = toString.call(val);
type = val::toString();
// debug@4.1.1/src/common.js
match = formatter.call(self, val);
match = self::formatter(val);
createDebug.formatArgs.call(self, args);
self::createDebug.formatArgs(args);
// rxjs@6.5.2/src/internal/operators/every.ts
result = this.predicate.call(this.thisArg, value, this.index++, this.source);
result = this.thisArg::this.predicate(value, this.index++, this.source);
// bluebird@3.5.5/js/release/synchronous_inspection.js
return isPending.call(this._target());
return this._target()::isPending();
var matchesPredicate = tryCatch(item).call(boundTo, e);
var matchesPredicate = boundTo::(tryCatch(item))(e);
// async@3.0.1/internal/initialParams.js
var callback = args.pop(); return fn.call(this, args, callback);
var callback = args.pop(); return this::fn(args, callback);
// ajv@6.10.0/lib/ajv.js
validate = macro.call(self, schema, parentSchema, it);
validate = self::macro(schema, parentSchema, it);
// graceful-fs@4.1.15/polyfills.js
return fs$read.call(fs, fd, buffer, offset, length, position, callback)
return fs::fs$read(fd, buffer, offset, length, position, callback)
// needle@2.4.0/lib/needle.js
process.nextTick(on_socket_end.bind(socket))
process.nextTick(socket::on_socket_end)
// util.promisify@1.0.0/implementation.js
slice = Function.call.bind(Array.prototype.slice);
slice = Array.prototype.slice::Function.call;
// kleur@3.0.3/index.js
ctx.reset = $.reset.bind(ctx);
ctx.reset = ctx::$.reset;
// bluebird@3.5.5/js/browser/bluebird.core.js
return Promise.resolve(value).bind(thisArg);
return thisArg::(Promise.resolve(value));
// typescript@3.5.1/lib/tsc.js
return fn ? fn.bind(obj) : undefined;
return obj?.::fn;
Non-goals
A goal of this proposal is simplicity. Therefore, this proposal purposefully does not address the following use cases.
Method extraction with implicit binding is not a goal of this proposal. When extracting a method from an object and then calling it on the same object requires that that object be repeated.
const { fn } = receiver;
receiver::fn(...args);
This is not a big problem. In general, methods are extracted from prototype objects and then called on instance objects, so the extraction’s source object and the call’s receiver object are usually different anyway.
const { slice } = Array.prototype;
arr::slice(...args);
We are deferring any special syntax for method extraction with implicit binding to a future proposal.
Extracting property accessors (i.e., getters and setters) is not a goal of
this proposal. Get/set accessors are not like methods. Methods are
properties (which happen to be functions). Accessors themselves are not
properties; 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.
const { get: $getSize } =
Object.getOwnPropertyDescriptor(
Set.prototype, 'size');
// The adversary’s code.
delete Set; delete Function;
// Our own trusted code, running later.
new Set([0, 1, 2])::$getSize();
Function/expression application, in which deeply nested function calls and other expressions are untangled into linear pipelines, is important but not addressed by this proposal. Instead, it is addressed by the pipe operator, with which this proposal’s syntax works well.
Related proposals
Old bind operator
This proposal is a resurrection of the old Stage-0 bind-operator proposal. (A champion of the old proposal has recommended restarting with a new proposal instead of using the old proposal.)
The new proposal is basically the same as the old proposal. The only big difference is that there is no unary form for implicit binding of the receiver during method extraction. (See also non-goals.)
Extensions
The extensions system is an alternative, competing proposal to the Stage-1 extensions proposal.
An in-depth comparison is also available. The concrete differences briefly are:
- Bind-this has no special variable namespace.
- Bind-this has no implicit syntactic handling of property accessors.
- Bind-this has no polymorphic
const ::{ … } from …;
syntax. - Bind-this has no polymorphic
…::…:…
syntax. - Bind-this has no
Symbol.extension
metaprogramming system.
Pipe operator
The pipe operator is a complementary proposal that can be used
to linearize deeply nested expressions like f(0, g([h()], 1), 2)
into
h() |> g(^, 1) |> f(0, ^, 2)
.
This is fundamentally different than the bind-this operator’s purpose, which
would be much closer to property access .
.
It is true that property access .
, bind-this, and the pipe operator all may be
used to linearize code. But this is a mere happy side effect for the first two
operators:
- Property access is tightly coupled to object membership.
- Bind-this is simply changes the
this
binding of a function.
In contrast, the pipe operator is designed to generally linearize all other kinds of expressions.
Just like how the pipe operator coexists with property access:
// Adapted from react@17.0.2/scripts/jest/jest-cli.js
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${^}`
|> chalk.dim(^, 'node', args.join(' '))
|> console.log(^);
…so too can it work together with bind-this:
// Adapted from chalk@2.4.2/index.js
return this._styles
|> (^ ? ^.concat(codes) : [codes])
|> this::build(^, this._empty, key);
PFA syntax
PFA (partial function application) syntax ~()
would tersely create
partially applied functions.
PFA syntax ~()
and bind-this ::
are also complementary and handle different
use cases.
For example, obj.method~()
would handle method extraction with implicit
binding, which bind-this does not address. In other words, when the receiver
object itself contains the function to which we wish to bind, then we need to
repeat the receiver once, with bind-this. PFA syntax would allow us to avoid
repeating the receiver.
n.on("click", v.reset.bind(v))
n.on("click", v::v.reset)
n.on("click", v.reset~())
In contrast, bind-this changes the receiver of an unbound function:
receiver::fn()
. (This unbound function might have already been extracted from
another object.) PFA syntax does not address this use case.
// bluebird@3.5.5/js/release/synchronous_inspection.js
isPending.call(this._target())
this._target()::isPending()
PFA syntax and bind-this can also work together, when creating bound functions with multiple partially applied arguments.
// svgo@1.2.2/plugins/convertTransform.js
floatround = /* … */ smartRound.bind(this, params.floatPrecision) /* … */
floatround = /* … */ this::smartRound~(params.floatPrecision) /* … */