Function and this
JavaScript Chats with ACM Hack Session 1
October 7, 2019
- Function Arguments
- What is
this
? - ES6 arrow function
- Functional Programming in JavaScript
- Final Challenge
Function Arguments
Let's do a little exercise.
function sayHello(name) {
console.log(`Welcome to JavaScript Chat with ACM Hack, ${name}!`);
}
/* What does this output? */
sayHello('Galen');
/* Is this legal? If yes, what is the output? */
sayHello();
/* Is this legal? If yes, what is the output? */
sayHello('Tim', 0xdeadbeef);
Answer to the exercise
All of them are in fact legal. The output is
Welcome to JavaScript Chat with ACM Hack, Galen!
Welcome to JavaScript Chat with ACM Hack, undefined!
Welcome to JavaScript Chat with ACM Hack, Tim!
In some other programming languages, like Python, C/C++, this is usually forbidden. But we are free to pass in any arbitrary number of arguments to a JavaScript function. In fact, instead of looking at the number of named parameters, it might be better to view the arguments to a function as an array of arbitrary length!
The Magic arguments
"array"
JavaScript is very lenient about the parameters that it passes in.
In fact, the arguments are directly accessible in the function callee
as an Array-like object bound to the name
arguments
.
Using this, you can in fact access all the parameters even if you do
not specify them in the parameter list.
function printAllArguments() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
printAllArguments('a', null, {}, [], 123);
/* output:
a
null
{}
[]
123
*/
Question: what would happen if we do this?
function printAllArgumentsFunctionally() {
arguments.forEach(arg => console.log(arg));
}
This is gonna give you an error saying that forEach
is not
defined on the arguments
objects. Why? Isn't it like an array?
Note how I use the word "array-like". arguments
is not actually an
array, but rather it is just a plain object. It does not support all
the Array functions like slice
, push
, map
, etc.
To put this in some concrete code,
const myObj = {};
myObj[0] = 1;
myObj[1] = 2;
myObj[2] = 3;
myObj.length = 3;
Now you can iterate through myObj
using a for
loop just like an array.
arguments
is just like myObj
: they are both "array-like".
Yikes~
Yup, welcome to the world of JavaScript.
Because of these quirks, the arguments
object is no longer the
preferred way tp access the list of arguments. ES2015 introduces
the rest parameters syntax, illustrated as follows.
function printAllArguments(...args) {
for (let i = 0; i < args.length; i++) {
console.log(args[i]);
}
}
function fnWithSomeNamedArguments(a, b, ...theRest) {
return Array.isArray(theRest);
}
Notice that args
and theRest
here are actual arrays, which
makes way more sense.
Why inventing another way to do the same thing?
We will see in just a second.
When calling a function, we use the companion spread parameters syntax to provide function call arguments as using an array or multiple arrays.
function sum(a, b) {
return a + b;
}
function product(a, b, c) {
return a * b * c;
}
const args = [1, 2];
sum(...args); // equivalent to sum(1, 2)
product(...args, 3); // equivalent to product(1, 2, 3)
product(0, ...args); // equivalent to product(0, 1, 2)
product(0, ...args, 3, ...args.reverse());
// equivalent to product(0, 1, 2, 3, 2, 1)
Destructuring in Function Parameters
Some of you might have seen code like
const [a, b] = foo();
const { c } = bar();
where foo()
returns an array of 2 elements,
and bar()
returns an object with key c
.
If you are familiar with functional programming languages such as OCaml (perhaps through CS131), this pattern should be quite familiar to you. This is called pattern matching.
(* OCaml *)
let (a, b, c) = 3, 1, 4
// JavaScript
let [a, b, c] = [3, 1, 4];
This is called object destructuring in JavaScript. It can be applied to function parameters as well.
// destructuring array
function sumOfPair([a, b]) {
return a + b;
}
sumOfPair([1, 2]);
// 3
// destructuring object (with rest operator)
function apiCallWithConfig(url, { method, credentials, ...theRestOfStuff }) {
// Do some API call...
// theRestOfStuff is an object containing other fields
}
apiCallWithConfig('http://www.example.com', {
method: 'GET',
credentials: 'my-secret-password-or-cookie',
userAgent: 'curl/7.54.0' // stored in theRestOfStuff['userAgent']
});
Let's take a step back from the syntactical side of things, and think about what are the benefits you get from these language features of JavaScript. More specifically, let's compare these features with respect to helping us write code with high extendibility.
🍏Application: Building Extensible APIs
When I say "extensible", I mean backward- and forward-compatability. We want our API evolve and possess new capability, without having to change old code using the API. But we also want it to be future-proof to some extent.
Let's say you are going to build an API called makeHttpRequest
.
At the beginning, the requirement is simple.
It takes in a URL, makes a GET request, and returns some data.
function makeHttpRequest(url) {
// do something
}
But your boss now find out that HTTP is not safe enough.
He asks you also write another API called makeHttpSecureRequest
using HTTPS.
function makeHttpRequest(url) {/* things */}
function makeHttpSecureRequest(url) {/* things */}
// usage
makeHttpRequest('foo.com');
makeHttpSecureRequest('bar.com');
But now, your boss realized a GET is not enough. We want to also use other HTTP verbs like POST or DELETE. How would you go about doing that?
Knowing the fact that we can give any arbitrary number of arguments to a function, we can add arguments without breaking old code!
function makeHttpRequest(url, method) {/* things */}
function makeHttpSecureRequest(url, method) {/* things */}
// ✅still works!
makeHttpRequest('foo.com');
makeHttpSecureRequest('bar.com');
Let's assume there is some function that call
either makeHttpRequest
or makeHttpSecureRequest
by some condition.
function makeHttpRequest(url, method) {/* things */}
function makeHttpSecureRequest(url, method) {/* things */}
function makeApiCall(isSecure, url, method) {
if (isSecure) {
makeHttpSecureRequest(url, method);
} else {
makeHttpRequest(url, method);
}
// fine, we know you are fancier than this:
// (isSecure ? makeHttpSecureRequest : makeHttpRequest)(url, method);
}
You might start to see a problem here. We are repeating the variables
url
and method
a lot.
Sure, we can simplify it a bit using the arguments
array or the rest
and spread syntax.
function makeApiCall(isSecure, ...callParams) {
if (isSecure) {
makeHttpSecureRequest(...callParams);
} else {
makeHttpRequest(...callParams);
}
}
But your annoying boss also want a custom handler callback function in case the TLS negotiation failed. That's fine. We just add another parameters, right?
For context, TLS negotiation is a process in which HTTPS establishes a secure connection. TLS only exists in HTTPS, not in HTTP.
function makeHttpRequest(url, method) {/* things */}
// changes here
function makeHttpSecureRequest(url, method, callback) {/* things */}
// ✅still works!
function makeApiCall(isSecure, ...callParams) {
if (isSecure) {
makeHttpSecureRequest(...callParams);
} else {
makeHttpRequest(...callParams);
}
}
// usage
makeApiCall(true, 'foo.com', 'POST', callback);
makeApiCall(false, 'foo.com', 'GET');
Now you want to add a timeout to these function so they don't take too long. What do you do?
We add it to the back of the parameters list?
function makeHttpRequest(url, method, timeout) {}
function makeHttpSecureRequest(url, method, callback, timeout) {}
// ❌breaks now!
makeApiCall(true, 'foo.com', 'POST', callback, 5000);
makeApiCall(false, 'foo.com', 'GET', 5000);
Then, the ...callParams
tricks no longer works.
The nature of parameter list relying on some sort of ordering is really
limiting us in evolving our API. What if we take a step back,
and consider object destructuring instead?
We rewrite our API to takes in a configuration object, instead of having a thousand different named parameters.
function makeHttpRequest(url, config) {}
function makeHttpSecureRequest(url, config) {}
function makeApiCall(isSecure, url, config) {
if (isSecure) {
makeHttpSecureRequest(url, config);
} else {
makeHttpRequest(url, config);
}
}
// usage
makeApiCall(false, 'foo.com', { timeout: 5000, method: 'POST' });
makeApiCall(true, 'bar.com', { timeout: 5000, callback: someFunc, method: 'GET' });
And we can pass all the options, like timeout, callback, method, within the config object. This pattern is called the options bag pattern. Doing so makes your API better in multiple ways.
- Your API is now more developer friendly. Developer will not need to remember the order of the parameters and which position they are in.
config
object can now be reused by developer, which leads to cleaner code.- The
makeApiCall
function is now forward-compatible. Any addition or deletion toconfig
does not require code changes inmakeApiCall
.
We can do one better. We can merge the isSecure
into the config
object as well.
function makeApiCall(url, { isSecure, ...config }) {
if (isSecure) {
makeHttpSecureRequest(url, config);
} else {
makeHttpRequest(url, config);
}
}
This pattern of merging different levels of configuration object is actually
very common in API designs.
For example, the props
object within React component. You only need to know
that it is used by some child, but you do not need to care which child uses
which options.
By merging the different levels of configuration into one at the top level
API, we hide the internals of our API to the caller. The API caller do not
need to know that there are different configs. For example,
// bad
function makeApiCall(url, tlsConfig, requestConfig, responseConfig) {}
The user don't want to know what each config options are used for, they just need to know there are configs available.
Hopefully, at this point, I have convinced you that the options bag pattern gives a higher degree of extensibility in building API. Let's see some examples of real world API that uses this pattern.
/* the `fetch` method */
fetch(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json'
}
});
/* express */
app.get('/api', function (res, req) {
// `res` and `req` are objects
// ...
});
/* React */
function MyComponent({ name, ...props }) {
return <ChildComponent displayName={name} {...props} />;
}
Next time when you see a new language feature that you have not seen before, we hope you can see past the syntax and think about how these impacts things such as API designs and developer-friendliness.
Default Parameters
We saw from the above example that function assigns undefined
to the named parameters if they are not provided.
sayHello();
// Welcome to JavaScript Chat with ACM Hack, undefined!
But we can indeed specify default parameters.
function sayHelloWithDefault(name = 'friends') {
console.log(`Welcome to JavaScript Chat with ACM Hack, ${name}!`);
}
sayHello();
// Welcome to JavaScript Chat with ACM Hack, friends!
/* Notice this special case */
sayHello(undefined);
// Welcome to JavaScript Chat with ACM Hack, friends!
function.length
?
The length
property tells us how many named arguments there are.
For example,
function sumOfABC(a, b, c) {
return a + b + c;
}
console.log(sumOfABC.length);
// 3
Take note that the behavior is different for default arguments, or the rest operator.
function fn1(a, b, ...rest) {
//...
}
function fn2(a, b = 1, c) {
// ...
}
console.log(fn1.length);
// 2
console.log(fn2.length);
// 1
🍏Application: Polyfilling
Just like any other languages, JavaScript has different versions and different features came at different times. Different browsers and different versions of browsers have support of different subsets of all JavaScript features. As the backing language of the web, the correctness of code within a website really is dependent on the browser.
Side note: there is a great website called caniuse.com. You can see what features, including HTML, CSS and JavaScript, are available in what browsers.
For instance, the rest syntax (...args
) we saw above is only compatible with
92.84% of all users' browsers in the world.
(src: https://caniuse.com/#feat=rest-parameters).
The reason is that this is a ES6 feature, and not all browsers have full support
for that version of JavaScript (ahem IE 11).
Therefore, we need some code changes to make sure that our code works with existing browsers, but we also want to write code in the nicer way of the rest syntax. You decide to write a simple automatic code replacer that replace the spread syntax to something that is backwards-compatible.
Given this code
function fn1(a, b, ...rest) {
// function body
}
With features that we have seen so far,
can you remove and the rest arguments (...rest
) from the function
parameter list, and add some code at the beginning of the function fn1
such that the rest
arguments are available later to the code in the
function body? This way, we don't need to rewrite the code in our function
body.
To get you started,
function fn1(a, b) {
// insert your code here
// `rest` should be available by now
// function body
}
Click to see a simple solution
function fn1(a, b) {
var rest = [];
for (let i = fn1.length; i < arguments.length; i++) {
rest.push(arguments[i]);
}
// function body
}
Does the simple version works for all cases? Can you think of a case where our solution will not work?
Click to see a case that does not work
function fn2(a = 0, b) {
var rest = [];
// fn2.length here returns 0;
for (let i = fn2.length ; i < arguments.length; i++) {
rest.push(arguments[i]);
}
}
For these more complex cases, we will need to do better than a naive replacement. We will need to do some syntactic analysis, probably involving generating an abstract syntax tree (AST). But don't worry, these are the jobs of a transpiler like Babel. We never have to do it ourselves.
By the way, this process of making JavaScript code compatible with older browsers is called polyfilling.
What is this
?
I am not going to get into object oriented programming
here. We are only care about using the this
pointer to
access the object itself in some methods.
Again, let's start with a little exercise.
const a = {
username: 'Galen',
getNameInsideA: function () {
console.log(this.username);
},
getAgeInsideA: function () {
console.log(this.age);
}
};
const getNameOutsideA = a.getNameInsideA;
const getAgeOutsideA = a.getAgeInsideA;
// what is the output?
a.getNameInsideA();
getNameOutsideA();
var age = 19;
a.getAgeInsideA();
getAgeOutsideA();
Click to see answer
Galen
undefined
undefined
19
The first time I encounter the binding of this
was a horrible
experience. I could not figure out how this
would work.
Let's understand how the this
binding works together.
The this
value could be bound either implicitly or explicitly.
- implicit binding: at call time as the object on which the function is called
- explicit binding: using one of the
apply
,call
, andbind
methods
Implicit this
binding
The this
value is bound when it is called – not when the function
is defined . The value of this
is determined by the "context"
of the function call, where "context" refers to the obejct it
is called on.
a.getNameInsideA();
a
is the "context object" here. Therefore, this
is bound to a
in getNameInsideA
.
But what if there is no such "context"?
getNameOutsideA();
Usually, this
will get bound to the global object when the "context"
is missing. They refer to different values in different runtime,
namely browser (where it is window
) vs Node.js.
But it does not really matter to us what it binds to. We just have
to know that it is bound to some value.
One weird property of JavaScript is that when you declare a variable
with the var
at the top level, that variable is put into the global object.
var magic = 123456;
console.log(globalThis.magic);
// globalThis refers to the global object in all runtimes
That is why the call to getAgeOutsideA()
prints 19.
Yikes~
Again, welcome to the world of JavaScript.
Explicit this
binding
There are three built-in methods, call
, apply
and bind
, to
help us explicitly set the value of this
.
The call
method let us set the this
object and call the function.
const tim = { username: 'Tim' };
function sayHi(withEmoji) {
if (withEmoji) {
console.log('Hi,', this.username + '. 😀');
} else {
console.log('Hi,', this.username + '.');
}
}
sayHi.call(tim, false);
// Hi, Tim.
The apply
function in fact is just the same as call
but
we pass in the list of arguments as an array instead.
sayHi.apply(tim, [false]);
// Hi, Tim.
A trick to remembering apply and call:
- A[pply] for array
- C[all] for comma (separated)
(found from: https://medium.com/@ginalee1114/javascript-technical-questions-series-what-is-bind-call-apply-what-is-this-7bd29fe06ded)
Exercise: Can you implement
apply
withcall
?
The bind
function creates a new function that has this
bound to the given value. Then, this
will never change
again. No matter if you use apply
, call
, or implicit
binding.
It does not alter the original function.
const galen = { username: 'Galen' };
const sayHiToTim = sayHi.bind(tim);
sayHiToTim(true);
// Hi, Tim. 😀
sayHiToTim.call(galen, true);
// Hi, Tim. 😀
galen.sayHi = sayHiToTim;
galen.sayHi();
// Hi, Tim. 😀
Harder Exercise: Can you implement
bind
withapply
withcall
?
🍏Application: React callbacks
If you have used React before, you might have used bind
:
class MyComponent extends React.Component {
constructor() {
this.state = { count: 0 };
// focus on the following line
this.incrementCounter = this.incrementCounter.bind(this);
}
incrementCounter() {
const currentCount = this.state.count;
this.setState({ count: currentCount + 1 });
}
render() {
const { count } = this.state;
return (
<div>
<h1>count: {count}</h1>
<button onClick={this.incrementCounter}> plus 1 </button>
</div>
);
}
}
Have you ever wondered why we do that? To see why the bind
is needed,
we imagine how we would write the button
component.
/* an imaginary button component */
class button extends React.Component {
// imaginary function that gets executed when button is clicked
whenClicked() {
const { onClick } = this.props;
onClick();
}
// ...
}
Without an object to call on, the this
of onClick
might get bound
to the global object or some value like undefined
or even this.props
of
the button
component. We do not really know since it depends on the
internal implementation of button
.
But in all cases, the this
is not bound to an instance of MyComponent
,
therefore our state is not mutated, since this.state
does not point to
the state of MyComponent
and this.setState
does not point to the
setState
of MyComponent
.
To solve this problem, we use bind
to make sure our onClick
callback
executes with this
pointing to MyComponent
.
Yeah, this is kinda complicated. Welcome to JavaScript.
ES6 arrow function
If you haven't seen the syntax, here it is.
const sayHi = name => {
console.log('Hi, ' + name + '.');
};
// shorthand
const add = (a, b) => a + b;
Let's repeat the same exercise above, just to reinforce your understanding. But instead of the function keyword, we use the arrow function syntax.
const a = {
username: 'Galen',
getNameInsideA: () => {
console.log(this.username);
},
getAgeInsideA: () => {
console.log(this.age);
}
};
const getNameOutsideA = a.getNameInsideA;
const getAgeOutsideA = a.getAgeInsideA;
// what is the output?
a.getNameInsideA();
getNameOutsideA();
var age = 19;
a.getAgeInsideA();
getAgeOutsideA();
Click to see output
undefined
undefined
19
19
Weird, huh?
wElcOmE tO jAvAsCriPt
this
binding in arrow function
A crucial difference between the function
keyword and
the arrow function is the this
binding. In an arrow function,
the this
value is identical inside the arrow function
is identical to the this
value outside it.
In other words, the this
is determined at declaration time
rather than call time in an arrow function.
Let's look at the exercise example.
const a = {
username: 'Galen',
getNameInsideA: () => {
console.log(this.username);
},
getAgeInsideA: () => {
console.log(this.age);
}
};
When getNameInsideA
and getAgeInsideA
is defined, the
this
is referring to the global object (not to a
).
Therefore, getNameInsideA
gives undefined
no matter
how you call it.
And yes, bind
, call
, apply
do not work on arrow functions.
In fact, the arrow function can be equivalently written as
const a = () => {};
// is equivalent to
const a = (function () {}).bind(this);
However, the this
binding in arrow functions can still
be subjected to implicit/explicit runtime this
binding.
This will be the case when the scope also has a runtime
bound this
. A concrete example will be an arrow function
defined in a function with the function
keyword.
No arguments
in arrow function
The arguments
object does not work in arrow function.
const a = () => { console.log(arguments); }
a();
// ReferenceError: arguments is not defined
Remember I asked why JavaScript decided to have the rest
syntax and the arguments
features at the same time when
they do the same thing?
The JavaScript language committee decided that arguments
is such a bad idea that it would do the world a great good
to disable that syntax in newer language features. As a result,
you are forced to use the better rest parameters syntax to use
arrow functions at all.
🍏Application: React callback revisited
Now we know that the this
binds statically with arrow function.
We can replace bind
with arrow function in MyComponent
:
class MyComponent extends React.Component {
constructor() {
this.state = { count: 0 };
// we removed the bind
}
incrementCounter() {
const currentCount = this.state.count;
this.setState({ count: currentCount + 1 });
}
render() {
const { count } = this.state;
return (
<div>
<h1>count: {count}</h1>
{/* We instead use an arrow function here */}
<button onClick={() => { this.incrementCounter(); }}> plus 1 </button>
</div>
);
}
}
Our onClick
callback is now an arrow function, which means that this
is
bound to the current scope. Within the render
function, this
always
binds to the instance of the object. Therefore, this callback works
as well.
Syntactic Pitfall
Syntactic shorthand is nice, but...
const fn1 = a => a;
const fn2 = a => { a };
const fn3 = a => ({ a });
console.log(fn1(1));
console.log(fn2(1));
console.log(fn3(1));
Click to see output
1
undefined
{ a: 1 }
Why does fn2
return undefined
?
It might be easier to see if we rewrite it with some newlines and semicolons.
const fn2 = a => {
a;
};
The difference between the function
keyword and the arrow function
can be summarized in a table.
function() {} |
() => {} |
|
---|---|---|
this binding |
call time | declaration time |
arguments |
✅ | ❌ |
Functional Programming in JavaScript
The most frequently used example of functional programming
in JavaScript is the map
, filter
, reduce
functions on
arrays.
const arr = [1, 2, 3];
const squared = arr.map(x => x * x);
const even = arr.filter(x => x % 2 == 0);
const sum = arr.reduce((runningSum, curr) => runningSum + curr, 0);
Functions as first-class citizens
With arrow functions, you can see that function are just like
other values: they can be sotred in and passed around as variables.
We oftentimes pass functions to other functions.
For example, to get an array whose values are those of an
existing array, squared, we pass x => x * x
to array.map
.
This means that we can also return functions from functions. Let's say we want to create some an increment function that increments a number by a certain value. We can hand-write such increment functions for all numbers, or we can write a function to return different incrementers instead.
function getIncrementer(incrementBy) {
const incrementer = x => x + incrementBy;
return incrementer;
}
// usage
const plus5 = getIncrementer(5);
console.log(plus5(5)); // 10
const plus42 = getIncrementer(42);
console.log(plus42(0)); // 42
Functional programming is a fascinating and well-developed programming paradigm. We are only scratching the surface here by introducing some pervasive patterns. To become more proficient in functional programming, you will have to get used to thinking about program "functionally". Consider taking CS 131 which covers OCaml and Scheme, both languages designed with functional programming in mind.
Final Challenge
To apply functional programming and some syntax we learned today, let's do a challenge.
We want a function called add3
. It can be invoked like the following.
add3(1, 2, 3) === 6
add3(1)(2)(3) === 6
add3(1, 0)(5) === 6
add3(2)(2 ,2) === 6
You can decide its behavior for add3(1, 2, 3, 4)
.
Try it out, apply some of the features we learned today,
and good luck with JavaScript.
Click to see solution (plz try before you click)
function add3(...args) => {
if (args.length >= 3) return args.reduce((accu, curr) => accu + curr, 0);
return (...newargs) => add3(...args, ...newargs);
}