Functions
We've seen functions a bunch of times in examples, and we'll assume you are familiar with them from other languages. All the same concepts of why we use functions apply in JavaScript, they allow for quality abstraction, reuse, and readability. In this section we will focus on some of the interesting features of functions in JavaScript - as they are more powerful than in some other languages. In fact, many of the features JavaScript provides have been adopted in other languages as well, due to how powerful they actually ar. In many ways, JavaScript is a functional or function oriented programming language - or at least, if you want it to be!
Defining functions
First off, you may have noticed that throughout this chapter so far, we used the following syntax to define functions:
function example() {
console.log("Hello World");
}
example();
While that syntax is still supported (it is the original syntax), it is not the way modern JavaScript programmers tend to write their code.
In JavaScript, functions are objects. It's worth repeating. functions are data. That's a jarring concept to many students. It's likely when you learned to program, you were immediately introduced to the idea that functions were code, and code was different than data. It's one of the biggest hurdles to understanding how to really program with JavaScript. As soon as you wrap your head around the fact that functions work just like data in JavaScript, you will begin to see how so much of the language really fits together - and your skills will improve in leaps and bounds.
Functions as data has many implications - the first is how we typically declare functions.
const example = function () {
console.log("Hello World");
}
example();
In the code above, we are defining the same exact function, and calling it exactly the same way. The difference is that we are intentionally writing the declaration as the declaration of a variable followed by the assignment of a value. The value to the right side of the assignment =
operator just so happens to be the function literal notation - it's a function, without a name. It's an anonymous function. Think of it like const x = 5
, where 5 is a numeric literal. 5
doesn't have a name, it's just a number. const x = 5;
means x
refers to a storage cell that contains the literal number 5
. The above code is saying example
is a variable that points to a storage cell that contains the function that accepts no parameters, prints "Hello World", and returns nothing (undefined
).
It follows that functions can be reassigned, and moved around.
let example = function () {
console.log("Hello World");
}
const x = example;
example = function(y) {
console.log(y);
}
x(); // Prints Hello World
example(10) // Prints 10
The syntax above also suggests that functions can be attached to variable declared with var
, const
, and let
- and indeed they can. They carry with them exactly the same rules about scope too. There is literally nothing about the variables x
and example
above that is different than variables that hold numbers, strings, booleans, objects, or arrays. Functions are objects.
let x = function() {
console.log('Hello');
}
x(); // prints Hello
x = 5;
console.log(5); // prints 5
In modern JavaScript, we nearly never use the original syntax. We will not use it again in this book. You should avoid it.
There is a second way that modern JavaScript developers declare functions:
const x = () => {
console.log('Hello');
}
x(); // prints Hello
The arrow notation at first may seem like simply a syntactic shortcut. We replaced the verbose function
keyword. That's almost true, and in most cases is effectively true, however there are some subtle differences. We will talk about the difference later in this section - for now you can understand that because the difference only matters under very specific circumstance, which you don't necessarily want to use by default, you can default to the =>
syntax and opt for the function
syntax when you explicitly need to.
Therefore, in the absence of a good reason, throughout the remainder of this book, we will see the =>
notation when declaring functions.
Parameters
Functions define parameters, and just like variables, they do not have specific types attached to them. Parameters are always block scoped to the function, and they are mutable, meaning they act list they were declared within the function using let
. They are always pass by copy - however remember that variables (parameters) are references.
This means that when dealing with primitives, parameters behave like pass-by-copy in languages like C++
const example = (a) => {
a++;
console.log(a)
// Prints 6
}
let x = 5;
example(x);
console.log(x);// Prints 5
In the example above, x
could also be declared with const
. The a++
inside example
is operating on a reference called a
, which originally pointed to the same storage cell that x
points to - the storage cell with the number 5
in it. The a++
operator has the effect of changing the a
reference to point to the storage cell that has 6
in it (which might need to be allocated). The x
reference is unchanged.
Now let's look at something similar, but where the parameter is an object:
const example = (a) => {
a.x++;
console.log(a.x)
// Prints 6
}
let o = {x : 5};
example(o);
console.log(o.x);// Prints 6
o
(which certainly can be declared with const
) is a reference that points to an object. That object has a property called x
, which points to a storage cell that has 5
in it. When example is called, a
is a reference that points to the same object that o
refers to. Inside example, that object's x
property is changed to point to a storage cell that has 6
in it. The object is still the same object, it's just that one of it's properties points to a different value now. When example returns, o
is still the same reference. o
points to the very same object whose x
property was changed. 6
is printed.
The above examples are critical to your understanding of parameter passing.
Optional parameters and default values
Functions can have any number of parameters. The can also defined default values for their parameters.
const example1 = (a, b) => {
return a + b;
}
const example2 = (a = 0, b = 0) => {
return a + b;
}
console.log(example1(5, 6)); // 11
console.log(example1(5)); // NaN, since b is undefined
console.log(example1()); // NaN, since a and b are undefined
console.log(example2(5, 6)); // 11
console.log(example2(5)); // 5, since b defaults to 0
console.log(example2()); // 0, since and b default to 0
Note that this example demonstrates not only the value of default values, but also the ability for a caller to invoke functions without the proper parameters in the first place. In fact, there is nothing stopping the caller from calling example
with 0, 1, 2, or 42 parameters. Again - JavaScript is permissive - and it's a double-edged sword.
Arguments
Function calling is so flexible, that when a function is called with too many parameters, the function can still accommodate this - and even capture the parameters.
const example = (a, b) => {
console.log(a);
console.log(b);
}
example(1, 2, 3, 4, 5);
In the code above, no runtime error is generated. The caller has called example
with 5 parameters (or arguments). The example
function receives 1 and 2 in a
and b
is unaware that more parameters had been sent with the call. In this case, it's clear the caller has made an error. JavaScript's philosophy of permissiveness is at work here. It's stance is essentially "no harm no foul". That may or may not feel right to you, and likely the point of view of a professional programmer would be that this is at least deserving of some sort of warning!
There is a hidden way to actually access extra parameters however, through a built-in arguments
array available within a function whenever it is invoked. There is an important restriction however. The arguments
array is NOT supported in functions declared with =>
, only function using the function
syntax.
const example = function (a, b) {
console.log(a);
console.log(b);
if (arguments.length > 2) {
console.log("---- Extra Arguments ---- ");
for (let i = 2; i < arguments.length; i++) {
console.log(arguments[i])
}
}
}
example(1, 2, 3, 4, 5);
1
2
---- Extra Arguments ----
3
4
5
The reason the argument
array is not available to functions with the =>
syntax is that actually =>
syntax functions are different kinds of objects. Functions are objects, and they have objects that define their scope. The scope contains local variable, parameters, etc. It is implicitly references within the function. Traditional function
have slightly different scope principles applied than =>
functions, and the newer =>
function dropped support for arguments
. =>
functions also lack the this
binding (we will discuss this a bit when we talk about prototypes and classes), and cannot be used the same way to define classes.
There is a better, newer, and more broadly supported way of allows functions to truly work with any number of parameters - a concept called variadic functions. The rest parameter syntax allows functions to explicitly define parameters that act as arrays:
const example = (...values) => {
for (const v of values) {
console.log(v);
}
}
example(1, 2, 3, 4, 5);
1
2
3
4
5
This is the preferred approach to working with varied number of parameters in JavaScript functions. It works the same with function
syntax and =>
. A nice example of why this is helpful is when implementing something like a summation function:
const summation = (base, ...values) => {
let sum = base;
for (const v of values) {
sum += values;
}
return sum;
}
// Prints 15
console.log(summation(0, 4, 5, 6));
// Prints 115
console.log(summation(100, 4, 5, 6));
Return Values
The return
keyword works exactly like it does in any other programming languages. Once the execution of the function hits a line with the return
statement, the function is terminated - and the value to the right (if any) of the return
is bubbled up to the caller.
A few implications of JavaScript's typing system (or lack of) are of note however.
- A function can return different types of data, depending on conditions.
For example, you might have something like this:
// This is terrible code, it's an example.
const example = () => {
const v = Math.random();
if (v < 0.5) {
return 1;
} else {
return {a: 1, b:2};
}
}
Imagine calling this function. You have no idea what kind of data it will return, as it returns an integer 50% of the time, and an object 50% of the time. You could check - but you can imagine how dealing with functions that return unpredictable data would lead towards very brittle code.
const r = example();
if (r.a) {
console.log('Object returned');
}
else {
console.log('Integer returned');
}
Generally speaking, you shouldn't be creating functions that return different data depending on it's input (and certainly not a coin flip!). There are exceptions, and when used smartly this "feature" can be used effectively - but you must understand the danger. By returning different kinds of data, you are making the caller responsible for carefully working with the return value. Sometimes callers don't read documentation. As a rule of thumb, if you have a function that returns numbers, strings, or objects based on input, you haven't created a good abstraction around your function, and your code design could be improved. Functions that return different kinds of data are a code smell. A smell isn't an error, but it's usually unwanted.
One caveat is returning undefined
or null
. It's fairly common to have a function return a value under some conditions, and under others, return nothing. This might indicate the presence or absence of an error potentially. This is easier to use for callers, and usually is easier to understand.
const v = send_email(recipient, body);
if (v) {
console.log('There was an error');
console.log(v);
}
else {
console.log('Success!');
}
Functions as properties, parameters, and return values
Now things start to get weird 😉
Functions are data, and we have variables that refer to those functions. Variables are passed into function as parameters. Variables can be assigned to object properties and to elements of an array. Variables are returned from functions. So, it follows that functions can be passed to other functions, put in objects and arrays, and even returned from other functions. Guess what - that's exactly what we do, a lot, in JavaScript!
const add = (a, b) => {
return a+b;
}
const subtract = (a,b) => {
return a -b;
}
const mult = (a, b) => {
return a * b;
}
const div = (a, b) => {
return a / b;
}
const op_obj = {
plus: add,
minus: subtract,
product: mult,
quotient: div
}
const op_arr = [add, subtract, mult, div];
const op_func = (op) => {
switch (op) {
case '+':
return add;
case '-':
return subtract;
case '*':
return mult;
case '/':
return div;
}
}
const a = 10;
const b = 5;
let op = op_func('-');
console.log(op(a, b))
for (const o of op_arr) {
console.log(o(a, b))
}
console.log(op_obj.product(a, b));
5
15
5
50
2
50
Pretty cool huh? There are some probably abuses of cleverness, but study that code. It contains an example of adding functions to objects, and then calling those functions. It shows you that you can have an array of functions, iterate over them, and call each. It also shows you a function that given an input, can decide which function to return, and how you can call that function later.
Now take a look at this:
const math = (operand1, operand2, operation) => {
const result = operation(operand1, operand2);
return result;
}
const answer = math(1, 2, add);
console.log(answer); // prints 3
Here we see the add
function sent as a parameter to math
, and math
calls it just like it would any other function - under the alias operation.
Anonymous Functions
Now take a look at this:
const answer = math(5, 2, (x, y) => {
return (x * x) + (y * y);
});
console.log(answer); // prints 29
That might look really confusing to you at first glance, but it's commonplace. We have the math
function, which expects two operands and a function to call - the third parameter. In the previous example, we called the math
function with a named function, add
. In this example, we call the math
function with a literal function, or an anonymous function.
It's the same concept as this:
const example = (x, y) => {
console.log(x, y);
}
const a = 5;
const b = 10;
example(a, b); // prints 5, 10
example(a, 12) // prints 5, 12
In the code above, you likely aren't confused at all. The first call to example
passes two parameters, they both happened to be named variables. No surprise, 5 and 10 are passed in, become x
and y
within example
, and are printed. In the second call, we pass two parameters again - but this time the second parameter is a literal number - 12
. No matter, x
is 5 and y
is 12 inside `example, and are printed.
const answer = math(5, 2, (x, y) => {
return (x * x) + (y * y);
});
console.log(answer); // prints 29
In the code above, the math
function is receiving 3 parameters. The first to are numbers, and become math's operand1
and operand2
values. The third parameter is a literal function that computes the sum of squares, given two inputs x
and y
.
Creating functions that accept other functions is a very common design pattern in JavaScript. It's encouraged, because it allows you create reusable and flexible code. Many times, we wish to pass simple functions into them, functions that aren't going to be used elsewhere. There is no need to create named functions unless you think you are going to reuse them - especially when they are short. Inlining an anonymous function is a choice, it's not (always) changing behavior (there are some situation where it can, when we need to consider scopes and closures).
Do not resist this new way of coding (if it is new to you). It is effective, and it is commonplace. You will use it judiciously, and you of course will avoid inlining the same function over and over again - for the same reasons you don't write the same literal number in lots of places, or write the same 3 lines of code in a bunch of places. You will, however, find that proper use of this style leads to very readable code.
Scope & Closure
In passing, we noted earlier that functions have a scope object, that contains the variables accessible to it. In JavaScript, functions can be closures that enclose within their scope all variables within in, and the parent function. Before moving forward, let's examine a fairly common design pattern in JavaScript - locally defined functions.
const parent = () => {
let c = 5;
const local = (a, b) => {
console.log(a, b, c++);
}
local(1, 2);
local(3, 4);
}
parent();
In the code above, the function local
is created inside the function called parent
. It is not available outside the parent
function, but it is callable within parent
. The resulting code prints 1, 2, 5
and then 3, 4, 6
. This may seem unusual, but if you understand the concept of local "things" belonging to functions, there's nothing all that unusual going on. Notably, the variable c
defined in parent
is available inside local
because it is defined at the scope that encloses it. This is just like x
being available inside the if
condition below, which shouldn't too surprising at all.
const example = () => {
const x = 5;
if (true) {
console.log(x); // x is available, defined at enclosing scope
}
}
The variable c
is incremented when local
is called - we see 5 print first, and the ++
has the effect of post-incrementing it. When local
is called again, the c
value is once again printed (now it's 6
), and post-incremented again. Now let's extend this example, having parent
return the local
function it had created - so the caller can use it.
const parent = () => {
let c = 5;
const local = (a, b) => {
console.log(a, b, c++);
}
return local;
}
const f = parent();
f(1, 2);
f(3, 4);
It's a bit contrived, but this example is now demonstrating that the locally defined function local
can be returned and used later. The output of the program is exactly the same as before. There is something very interesting happening with c
though. c
is a local variable of parent
. Everything you know about local variables inside functions is probably telling you that after parent returns, it's local variables are destroyed. That's the point of local variables. Yet, after parent returns, we call the local
function (f
) not once, but twice. And each time, c
is valid. In fact, the changes made to it are still tracked - it's 6
when f
is called again!
This is happening because at the time local
is created, c
is in it's scope. Functions are closures, and capture the enclosing scope. They hold on to them, through their lifetime. local
lives on past the lifetime of parent
, and with it, it's reference to c
.
Let's bend this example even further:
const parent = (a, b) => {
let c = 5;
const local = () => {
console.log(a, b, c++);
}
return local;
}
const f = parent(1, 2);
const g = parent(3, 4);
f(); // 1, 2, 5
f(); // 1, 2, 6
g(); // 3, 4, 5
Now a
and b
are moved to the parent
function's parameter list. They are local variables of parent
as before, but now they are being passed into parent
.
The first time we call parent
, we do so with 1
and 2
as parameters. The local
function is created and captures the 1
and 2
, along with the mutable c
variable. local
is returned to the caller. The first time it is called, we get the expected 1, 2, 5
printout. Note, the 1
and 2
are captured just like in the example prior.
We are calling the returned local
function (f
) twice. Notice that the second time, we still get 1
and 2
. The local
function was created once, and it is still alive and well. c
is printed as 6
, since it's the same c
variable as we incremented the first time we called the function. We incremented it the first time we called f
, and now we see that effect.
We also called parent
a second time, with 3
and 4
as parameters. Critically, this second call created a second local
function instance. This second local
function instance was created while a
and b
were bound to 3
and 4
. They are distinct variables, because they belong to the second invocation of parent
, and are enclosed within the closure of the second instance of local
. Also critically, the second invocation of parent
created a second instance of c
- it's own local variable. local
has captured that instance of c. As we can see, when the caller invokes the second instance of local
- by calling g()
, the second instance prints 3, 4
and uses the second instance of c
- which is 5. This is a separate and distinct variable from the c
in the first local
created, which has been incremented (now to 7).
Re-read this section. If you grasp the concepts in the last example, you will be well ahead of the game in terms of being able to read professional level JavaScript code, and being able to write your own. These concepts are powerful. When used correctly, you can create elegant code that actually reduces complexity. When used accidentally, or used incorrectly, this style of programming can lead to lots of confusing errors unfortunately!
Arrays revisited
When we discussed arrays in the last section, we noted that there were a few things that were a lot more powerful if we were able to understand functions first. Let's revisit now that we do.
Sorting
The sort function can only do so much for us, particularly when we are using arrays containing objects, or wish to sort in non-standard ways (i.e. even numbers first, odd numbers after). It's limited only until now however - now that we know how to use functions a bit better. The JavaScript sort
function accepts an optional parameter - a function that it will call whenever it needs to compare two elements in the array it is trying to sort.
// Assumes a and be are numbers
const regular = (a, b) => {
if (a === b) return 0;
else if (a < b ) return -1;
else if (a > b) return 1;
}
// Assumes a and be are objects with
// an x & y property, and sorts by their
// sum
const object_compare = (a, b) => {
const v1 = a.x + a.y;
const v2 = b.x + b.y;
if (v1 === v2) return 0;
else if (v1 < v2 ) return -1;
else if (v1 > v2) return 1;
}
// Assumes a and b are numbers, rounds to integers
// sorts them by even number first, then odd,
// and by value for ties (both even, or both odd)
const even_odd = (a, b) => {
const a_even = Math.round(a) % 2 === 0;
const b_even = Math.round(b) % 2 === 0;
if (a_even && !b_even) return -1
else if (!a_even && b_even) return 1;
else {
if (a === b) return 0;
else if (a < b ) return -1;
else if (a > b) return 1;
}
}
const t1 = [3.6, 9.5, 12.4, 3.1, 6.3];
const t2 = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1}]
t1.sort(regular);
// 3.1, 3.6, 6.3, 9.5, 12.4
console.log(t1.join(", "));
t1.sort(even_odd);
// 3.6, 6.3, 9.5, 12.4, 3.1
console.log(t1.join(", "));
t2.sort(object_compare);
// [ { x: -5, y: 7 }, { x: 9, y: -2 }, { x: 4, y: 11 }, { x: 1 } ]
console.log(t2)
Pro Tip💡 This example is bigger than just sorting. It's critical example for you to really think deeply about. Once this makes intuitive sense to you, you will be able to leverage the concepts of functional programming to your advantage more effectively. Think about any sorting algorithm - bubble sort, quick sort, merge sort. They employ different strategies, but the all need compare elements against each other. The sort
function in JavaScript is simply deferring how that comparison is to be made to the comparison function you give it. It's outsourcing a behavior, and by doing so, it becomes far more flexible. It can work with any data type, and can apply it's sorting algorithm to any method of comparison. It's more than polymorphism from an object-oriented language, this is flexibility taken to the next level!
Searching, and map
and filter
Searching involves comparison too, so it makes sense that the search functions also work with arbitrary functions. Before diving into indexOf
and find
though, we need to take a detour into two foundational methods defined on the array - map
and filter
.
map
and filter
transform arrays. The map
function allows you to easily map each element to another value - creating an array with the same number of elements, but transformed values. The filter
method allows you to defined a function that decides whether a specific element is in the new array - allowing you to remove elements from a source array.
Let's look at a simple example:
// Receives an object with x and y properties,
// returns the sum
const sum_xy = (e) => {
return e.x + e.y;
}
// If the element is even, result is true
const even = (e) => {
return Math.round(e) % 2 === 0;
}
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
const sums = t.map(sum_xy);
// [ 15, 7, 2, 8 ]
console.log(sums);
const even_sums = sums.filter(even);
// [ 2, 8 ]
console.log(even_sums)
map
and filter
are shockingly useful in a variety of circumstances. Once you start writing enough JavaScript, you'll start to notice that you hardly have a program that doesn't use them. They take some practice to get used to, and that practice time will pay huge dividends.
Let's get back to searching now - and revisit indexOf
. The indexOf
function will return the first index where a particular value is found within an array. The indexOf
function does not accept a function to do the comparison however. At first, this looks like a drag - for example, we can't find a matching element within a list of objects very easily, since objects are always compared by memory location.
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Find the object with x,y = 9, -2
const i = t.indexOf({x: 9, y: -2});
console.log(i); // -1, not found
Have no fear though, because map
can transform the array, and we can use indexOf
to search the transformed array.
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Find the object with x,y = 9, -2
const i = t.map((o) => {
return `${o.x}:${o.y}`
}).indexOf(`9:-2`);
console.log(i); // 1, second element
Note, even though map
returned a transformed list, the index returned by indexOf
is the index of the matching object in the original array t
. This is because map
always produces an array of elements that is the same length, and derived from the same inputs, in the same order.
BTW, we can use a simpler function syntax when our inline functions contain just a return statement:
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Find the object with x,y = 9, -2
const i = t.map(o => `${o.x}:${o.y}`).indexOf(`9:-2`);
console.log(i); // 1, second element
The example above works since indexOf
can accurately compare strings. This mechanism is lacking though if we were to be searching for floating point numbers, or something that can't be unambiguously turned into a string. We could alternatively use map
to convert the array into literally a set of true
/false
values depending on search status though:
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Find the object with x,y = 9, -2
const i = t.map(o => o.x === 9 && o.y === -2).indexOf(true);
console.log(i); // 1, second element
Note, the map
function ended up returning an array of booleans: [false, true, false, false]
, and we just used indexOf
to get the first. This strategy, where map is effectively producing a signal for each element is a common and flexible strategy.
The find
method can also help us here, and does accept a comparison function that it will use
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Find the object with x,y = 9, -2
const e = t.find(o => o.x === 9 && o.y === -2)
console.log(e); // {x: 9, y: -2}
Remember, find
returns the element, while indexOf
returns the index of the element found.
We could go further. Let's say we wanted to find only the objects whose sum (of x and y) were even. We could do the following, with indexOf
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Map to sums of x, y, and then map again for evens
// The result of first mapping in [15, 7, 2, 8],
// and after the second mapping we have [false, false, true, true]
const signals = t.map(o => o.x + o.y).map(v => v%2 === 0);
const even_sums = [];
let i = -1;
do {
i = signals.indexOf(true, i+1);
if (i >= 0) {
even_sums.push(t[i]);
}
} while (i >= 0);
// [ { x: -5, y: 7 }, { x: 1, y: 7 } ]
console.log(even_sums);
This is a little awkward though. Instead, We could be a little more clever, and use filter
. Note that in the example earlier, we used map
and filter
to print out even sums, but we lost the object, since map converted each element into it's sum. Let's do a little tweak to that, so we don't actually lose the original source object - and just have an array with objects whose x,y sum is even.
// Just use filter, with a function that computes sum, and returns if it's sum is even.
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
// Filter returns true or false, based on if sum is even
// It does not alter the element
const even_sums = t.filter(o => (o.x + o.y) %2 === 0);
// [{x: -5, y: 7}, {x: 1, y: 7}]
console.log(even_sums);
forEach
What if we wanted to sort the array of objects we had above, using the even/odd sorting strategy we employed in the first example of sorting. One way, is to split the list into even and odds, and then sort them.
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
const sum_xy = (e) => {
return e.x + e.y;
}
const compare_sum = (a, b) => {
const sa = sum_xy(a);
const sb = sum_xy(b);
if (sa === sb) return 0;
else if (sa < sb) return -1;
else return 1;
};
const evens = t.filter(o => (o.x + o.y) % 2 === 0);
const odds = t.filter(o => (o.x + o.y) % 2 !== 0);
evens.sort(compare_sum)
odds.sort(compare_sum);
const result = evens.concat(odds);
//[ { x: -5, y: 7 }, { x: 1, y: 7 }, { x: 9, y: -2 }, { x: 4, y: 11 } ]
console.log(result);
Another way (not necessarily better) is to manipulate each element first, before applying sort. map
and filter
transform arrays, it would be nice if we could just manipulate each element. The simple way of doing that is with a for
loop.
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
for (const o of t) {
o.sum = o.x + o.y;
o.even_sum = (o.sum %2 === 0);
}
const results = t.sort((a, b) => {
if (a.even_sum && !b.even_sum) return -1
else if (!a.even_sum && b.even_sum) return 1;
else {
if (a.sum === b.sum) return 0;
else if (a.sum < b.sum ) return -1;
else return 1;
}
// Use the map again to trim out the
// extra properties we added with the for loop
}).map((o) => {return {x: o.x, y: o.y}});
//[ { x: -5, y: 7 }, { x: 1, y: 7 }, { x: 9, y: -2 }, { x: 4, y: 11 } ]
console.log(results);
Another way of doing that is the forEach
method. The forEach
method is essentially turning a for loop inside out - or, more accurately, allowing you to specify what happens inside the for loop, but allowing the library call to actually implement the loop itself.
const t = [{x: 4, y:11}, {x: 9, y: -2}, {x: -5, y: 7}, {x: 1, y: 7}]
t.forEach((o) => {
o.sum = o.x + o.y;
o.even_sum = (o.sum %2 === 0);
});
const results = t.sort((a, b) => {
if (a.even_sum && !b.even_sum) return -1
else if (!a.even_sum && b.even_sum) return 1;
else {
if (a.sum === b.sum) return 0;
else if (a.sum < b.sum ) return -1;
else return 1;
}
// Use the map again to trim out the
// extra properties we added with the forEach
}).map((o) => {return {x: o.x, y: o.y}});
//[ { x: -5, y: 7 }, { x: 1, y: 7 }, { x: 9, y: -2 }, { x: 4, y: 11 } ]
console.log(results);
The purpose of these examples has been to demonstrated the use of indexOf
, map
, filter
, and forEach
- there are many ways of doing each of the (seemingly useless) examples. Invest some time in trying to make sense out of all the ways arrays can be manipulated though - the investment will pay off!