Function โ
Chicken & Egg โ
Yes, functions are of Object. BUT, Object is function as well. Don't get confused with it, it's just a convoluted nominal relationship on the language level. It doesn't mean function and object are self-contradictory, JavaScript runtime can handle it in a real way.
console.log(function() {} instanceof Object) // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(typeof Object); // "function"
console.log(typeof String); // "function"
console.log(typeof Function); // "function"
console.log(typeof Array); // "function"2
3
4
5
6
7
8
The Function Caller โ
JavaScript exposes the function caller as this variable for each time it execute the function. Meaning that a function can attach to arbitrary context during runtime. What makes it drastic comparing to other languages like lua and python is, this is implicitly available in every scope.
Functions in JavaScript are not independent execution template like other scripting languages, they can be bound to any context while you're not aware of.
There are both explicit and implicit approaches to bind a caller to a function.
- Implicitly bind to containing object if the function is assign as member of the object
- Use
Function.prototype.callorFunction.prototype.applyto change caller explicitly. - Create a new function from original one with caller bound using
Function.prototype.bind.
function add(c, d) {
return this.a + this.b + c + d;
}
const o = { a: 1, b: 3 };
console.log(add.call(o, 5, 7)) // 16
console.log(add.apply(o, [10, 20])) // 34
const boundAdd = add.bind(o)
console.log(boundAdd(10, 20)) // 34
o.add = add // implicitly bind `this` to the containing object
o.add(10, 20) // 342
3
4
5
6
7
8
9
10
11
12
13
14
15
NOTE
In non-strict mode, this has arbitrary value on top level(the default binding) depending on the runtime, because JavaScript does not have specification about it. In strict mode JavaScript, this no longer exist on topmost scope.
NOTE
ES2020 added a globalThis variable as a standard interface to access the global context, it's fixed instead of a dynamic context like this. Different JavaScript runtime can have different member set within it, Any field declared in globalThis is globally accessible, pretty much similar to _G in lua.
globalThis.foo = 'I am foo!'
console.log(foo) // 'I am foo!'2
IMPORTANT
You can't re-bind a bound function to change the caller again. Function.prototype.bind returns immutable binding for this. But you can append arguments with re-binding
Function Member Declaration โ
By default JavaScript has implicit binding whenever you assign/declare function member to an object. ES6 introduced special arrow function syntax that does the orthogonal way, with no implicit binding at all, to inherit pure closure.
const obj = {
foo: function () {
console.log(this === obj); // true
},
bar() {
console.log(this === obj); // true
},
baz: () => {
console.log(this === obj); // false
},
};2
3
4
5
6
7
8
9
10
11
Binding in Callbacks โ
The very reason arrow function syntax was added is, to avoid context pollution when invoking callbacks. Callbacks are executed in the context of certain api instead of the scope of containing object, meaning that you can't easily update the status of the object within callback.
const assert = require('assert');
const obj = {
count: 10,
increment() {
setTimeout(function () {
this.count++; // `this` is not obj
assert(this === obj); //
}, 300);
},
};
obj.increment();2
3
4
5
6
7
8
9
10
11
12
13
Before arrow function was a thing in JavaScript, we use Function.prototype.bind to explicitly handle the binding for callbacks.
const assert = require('assert');
const obj = {
count: 10,
increment() {
setTimeout(
function () {
assert(this === obj);
this.count++;
console.log(this.count); // 11
}.bind(obj),
300,
);
},
};
obj.increment();2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Since ES6 we can use arrow function as a simple solution:
const assert = require('assert');
const obj = {
count: 10,
increment() {
setTimeout(() => {
assert(this === obj);
this.count++;
console.log(this.count); // 11
}, 300);
},
};
obj.increment();2
3
4
5
6
7
8
9
10
11
12
13
14
However it's not certain whether the implementation behind would use Function.prototype.bind to implicitly set the caller of callbacks. It's really dynamic and customizable depending on the library/runtime, we should read its documentation first.
const button = document.querySelector('button');
button.addEventListener('click', function(e) {
console.log(this === button); // true
});2
3
4
5
As Extra Context โ
this is an ambient placeholder everywhere, meaning that we can inject it as an extra argument for arbitrary function/callback etc.
const callbackStore = {
foo: function() {
useContext(this);
}
}
// somewhere internal..
function useCallback(callback, ctx) {
callback.bind(ctx)();
}2
3
4
5
6
7
8
9
10
However this approach is implicit and not too readable, because we don't know either the type of this or its semantic. It's not typed in the parameter list, so we are much harder to find potential mistake during compile-time. A modern approach is to avoid this as much as possible, and declare parameters for the callback, because nowadays we have typescript-powered community. It's the same mental model for library developers, and much better readability for user's end.
const callbackStore = {
foo: function(ctx) {
useContext(ctx);
}
}
// somewhere internal..
function useCallback(callback, ctx) {
callback(ctx);
}2
3
4
5
6
7
8
9
10
Callable Object โ
Given the design that a functions is valid object, we add can add properties to it. So the term callable object isn't very correct in JavaScript, since the properties are attached to the function. We may call it objectified function instead.
const callable = function() {
console.log('I am callable!');
};
callable.foo = 'foo';
callable()
console.log(callable.foo) // foo2
3
4
5
6
7
8
Generator Function ES6+ โ
ES6 added generator function that can be consumed by for..of statement and array spread operator.
function* g() {
yield 1;
yield 2;
yield 3;
}
const generator = g();
console.log(generator.next().value); // 1
console.log([...generator]); // [2, 3]
for (const item of generator) {
console.log(item); // unreachable because the generator has finished
}2
3
4
5
6
7
8
9
10
11
12
13
14
15