Khepri Fat Arrows
I overview and criticize the semantics of Javascript’s this
keyword. Then I quickly overview the ECMAScript 6 and Coffee Script solution to the this
problem, before detailing Khepri’s fat arrow syntax. Khepri fat arrows make this
binding explicit and also can unpack the this
object.
Javascript
The Javascript this
keyword is bound to the object that the current function was called on. The dynamic binding of this
differs from the lexical, function block scoping of other bindings.
The this
object is clearest when a this
value is explicitly provided using Function.prototype.call
.
var f = function(x) {
return this.z + x;
};
f.call({'z': 3}, 1); // 4
A member expression provides its base object as this
.
var o = {
'z': 3,
'f': f
};
o.f(1); // 4, same as f.call(o, 1)
Regular, non-member function calls do not provide any this
. In normal mode Javascript, function calls pass the global object as this
, while strict mode passes undefined
.
f(1); // NaN, since: undefined + 1
z = 3; // set global z
f(1); // 4, since: global.z + 1
MDN has a more comprehensive review of this
.
Nested Function Issues
Javascript’s this
semantics complicate the use of function literals for callbacks and events in methods.
var C = function(x) { this.x = x; };
C.prototype.getXGetter = function() {
return function() {
return this.x;
};
};
var o = new C(10);
The programmer intended to have getXGetter
return a function that returns the value of x
stored on the instance of C
. Instead, the result of getXGetter
returns undefined:
o.getXGetter()(); // undefined
// The above code is actually:
C.prototype.getXGetter.call(o).call(undefined);
In the getter returned by getXGetter
, this.x
references the undefined this
of the inner function, not the outer member function getXGetter
that is called with o
as this
.
The solution is to explicitly bind the target this
.
C.prototype.getXGetter = function() {
var self = this;
return function() {
return self.x;
};
};
var o = new C(10);
o.getXGetter()(); // 10
getXGetter
is pretty contrived, but explicitly binding this
is an extremly common Javascript pattern.
This as a Parameter
I think of this
as an additional parameter supplied to functions. Python method definitions for example take an explicit self
parameter.
class C:
def method(self, arg):
return arg
o = C()
o.method(3) # 3, self is automatically provided
I believe that ECMAScript language syntax for this
should also model this
as a parameter. Instead of being an exception in the language, if this
is a parameter, it can follow the same rules as regular parameters.
Other Languages and This
ECMAScript 6
ECMAScript 6 arrow functions are more than a shorter function syntax, they are semantically different from functions defined with the function
keyword. Among the differences, arrow functions use lexical this scoping. They do not introduce a this
binding.
In an arrow function, this
is resolves using lexical scoping rules to the first this
object from a regular function
function.
C.prototype.getXGetter = function() { // normal function
return () => // arrow function
this.x; // `this` resolves to the `this` of `getXGetter`
};
var o = new C(10);
o.getXGetter()(); // 10
ECMAScript 6 is designed around backwards compatibility with ECMAScript 5, and, while arrow functions are a big language improvement, I feel they are in many ways a missed opportunity.
Specially, this
is still a magic keyword instead of a parameter. And in heavily nested functions, you still have to bind this
to a local variable to get correct the behavior.
var Model = function() {
var outer = this;
var SubModel = function() {
// can't reference the Model instance using `this` here
this.onChange = () => outer.x;
};
this.x = 10;
};
Coffee Script
Coffee Script uses the fat arrow =>
to define a function that is bound to the this
in which the function is defined.
C.prototype.getXGetter = () ->
() => this.x;
var o = new C(10);
o.getXGetter()(); // 10
Fat arrow functions behave like ECMAScript 6 arrow function, but are functionally closer to calling Function.prototype.bind
with this
.
C.prototype.getXGetter = function() {
return function() {
return this.x;
}.bind(this);
};
Fat arrows suffer from the same problems as ECMAScript 6 arrow functions. The generated code also uses an extra function call:
// Generated code from example above
C.prototype.getXGetter = function() {
return (function(_this) {
return function() {
return _this.x;
};
})(this);
};
Khepri
A Khepri function may optionally unpack the object it is called on with a this unpack. This unpacks explicitly bind this
to an identifier, and also allow values to be extracted from the this
object using the same unpack patterns as parmeters.
// Khepri fat arrow
// Bind `self` to the `this` object.
C.prototype.getXGetter = \() =self->
\()-> self.x;
// Generated code:
(C.prototype.getXGetter = (function() {
var self = this;
return (function() {
return self.x;
});
}));
Khepri eliminates the this
expression entirely. You always need to use an explicit this
binding.
Fat Arrows
this
unpacks are of the form = PATTERN
, and may appear at the end of a function parameter list before the ->
and function body.
// Bind `self` to the `this` object.
var f = \x = self ->
x + self.z;
f.call({'z': 3}, 1); // 4
This unpacks follow the same rules as parameter unpacks. Values bound in a this unpack are immutable and lexically scoped. This unpacks are run after all other parameters have been unpacked, but before the function body.
C.prototype.getXGetter = \() =self->
\() -> self.x;
var o = new C(10);
o.getXGetter()(); // 10
Like other parameters, a this unpacks may conflict with previous bindings or hide outer bindings.
// Error, self already bound for scope.
\self =self-> ...;
// Inner self binding hides outer binding
\=self-> \=self-> self.x;
Fat arrows work easily with deeply nested functions and make this
explicit:
var Model = \ =model-> {
var SubModel = \ =self-> {
self.onChange = \e -> model.x;
};
model.x = 10;
};
Ballista Arrows
Any identifier pattern, as pattern, array pattern, or object pattern can be used as the this unpack pattern.
var f = \x ={z}-> x + z;
You can go bit crazy:
\args(f z)=self#{children#{length} count}-> ...