DEV Community

Basti Ortiz
Basti Ortiz

Posted on

Remembering that "functions are objects" can help in writing more concise code

Introduction

More often than not, we are obsessed with writing concise code. Who wouldn't, right? Concise code is short code that is easier to take in and usually more readable. It is what separates quick-and-dirty code from elegant code. The key word here is elegant. Using shorter and vaguer variable names at the expense of readability just to achieve "concise code" is indeed not concise code. Rather, it is minified gibberish more than anything else.

As developers, we strive to write such code whenever possible. This is why JavaScript has received a huge facelift over the years. To put into perspective how much JavaScript has changed, there was a time not so long ago before ES6 (or ES2015 if you're edgy) when it was mandatory to write the word function to define a function, may it be anonymous or named. For example, the code below attaches a click listener (anonymous function) to an HTML element with an ID of veryNiceExample. For simplicity, the listener then logs the MouseEvent object to the console.

// Using "var" for added immersion
var element = document.getElementById('veryNiceExample');

// Attaches listener
element.addEventListener('click', function(event) {
  console.log(event);
});
Enter fullscreen mode Exit fullscreen mode

With the introduction of ES6, the whole JavaScript community went crazy for arrow functions. We can now do the same thing in a shorter syntax.

// Using "const" for added immersion
const element = document.getElementById('veryNiceExample');

// Attaches listener
element.addEventListener('click', event => {
  console.log(event);
});
Enter fullscreen mode Exit fullscreen mode

If it wasn't short enough already, clever folks began utilizing the implicit return feature of arrow functions to push the limits even more. Implicit returns can then be applied in the code example. Although console.log returns nothing, an implicitly returned arrow function can still be used in this case since it is just a single-purpose function where its return value is not really used for anything.

// Using "const" for added immersion
const element = document.getElementById('veryNiceExample');

// Attaches listener
element.addEventListener('click', event => console.log(event));
Enter fullscreen mode Exit fullscreen mode

Functions are also objects

In JavaScript, everything is an object. Unless an object is created via Object.create(null), everything inherits from Object since it is the last link in the prototype chain. Functions are no exception to this rule. Even primitive data types are objects. To stress this point, all data types (except Symbols) have object wrappers. By that, I mean it is possible to instantiate a primitive as an object by calling its constructor with the new keyword.

DISCLAIMER: For performance reasons, it is not recommended to use object wrappers. This is for demonstration purposes only.

const primitiveString = 'This is a string.';
const wrapperObjectString = new String('This is a string.');

console.log(typeof primitiveString); // 'string'
console.log(typeof wrapperObjectString); // 'object'

Enter fullscreen mode Exit fullscreen mode

Since JavaScript treats functions like objects, it is possible to store functions as values in variables.

// Using "const" for added immersion
const element = document.getElementById('veryNiceExample');

// Stores function through declaration
function handler(event) {
  console.log(event);
}

// Attaches listener
element.addEventListener('click', handler);
Enter fullscreen mode Exit fullscreen mode

It is worth noting that handler is different from handler(). The variable handler returns the value it stores. In this case, the value it stores is the actual definition of the function. On the other hand, handler() executes the function stored in handler and returns the necessary values. In this case, handler (the definition) does not explicitly return a value. Therefore, if handler is executed, handler() returns undefined.

With that said, the code example can now be shortened using the same concept. Since console.log is essentially a function that accepts an argument, its definition can directly be used as the listener for the mouse click event.

// Using "const" for added immersion
const element = document.getElementById('veryNiceExample');

// Attaches listener
element.addEventListener('click', console.log);
Enter fullscreen mode Exit fullscreen mode

EDIT: As raised by @jburgy in his comment, one has to be aware of all the parameters of a function. Some parameter conflicts may arise if one is not careful such as the case with the code below. See the full discussion to see why this does not work as expected.

['0', '1', '2'].map(parseInt); // [0, NaN, NaN]
Enter fullscreen mode Exit fullscreen mode

Catching Promises

With the previous example, it may seem pointless to even bother with considering functions as objects. However, this concept can prove to be useful in the context of promises, where callback functions are ubiquitous.

During the prototyping stage of any JavaScript application, it is understandable to write quick-and-dirty code. For fast debugging, rejected promises are often handled by logging the errors. As an example, the code below fetches data from the main endpoint of the GitHub REST API v3 and logs the received data as JSON. In case of any errors, the catch accepts console.log as its argument. That way, it also logs the Error object.

fetch('https://api.github.com/')
  .then(res => res.json())
  .then(console.log)
  .catch(console.log);
Enter fullscreen mode Exit fullscreen mode

Despite the code above being syntactically legal, it is still common to see one-line arrow functions (or even normal functions) wrapping other functions. In turn, these one-line wrapper functions are unnecessarily passed in as arguments. For instance, consider the following lines of code.

fetch('https://api.github.com/')
  .then(res => {
    return res.json();
  })
  .then(function(data) {
    console.log(data);
  })
  .catch(err => console.log(err));
Enter fullscreen mode Exit fullscreen mode

The two examples do the same operations and yield the same results, but the former is simply more concise and elegant. In contrast, the latter is outright cumbersome and difficult to read. Although it is unlikely that such terribly written code exists (especially in a professional setting), the exaggeration is meant to prove the point.

As an added bonus, negligibly less memory is taken up by the program since the JavaScript interpreter/engine no longer needs to store unnecessary functions into memory.

Conclusion

It never hurts to make code more concise. To write such code, one must always remember that functions, even the built-in ones, are simply values that can be passed into other functions as arguments. That is the basis of callback functions after all. Of course, it is more important to find the balance between elegance and readability. It really just depends on the situation as do most things in life.

In conclusion, thinking more critically about functions can save a few lines of code... and the sanity of a code reviewer.

Top comments (26)

Collapse
 
jburgy profile image
jburgy

You do have to be careful with things like

['0', '1', '2'].map(parseInt)
Collapse
 
somedood profile image
Basti Ortiz

Oh, wow. This is indeed weird. I didn't know this was an outlier.

Why does this happen? Are there any other outliers I should be aware of so I could update the article to mention them?

Collapse
 
ironydelerium profile image
ironydelerium

That one is a side effect of how the combination of Array.prototype.map and parseInt work - the former calls it's argument with (value, index, array) repeatedly, parseInt expects (value, base) where 2 <= base <= 36, or it returns NaN (ecma-262 1e, 15.1.2.2 "parseInt(string, radix)").

Most of the array iteration methods (forEach, map, every, some) pass 3 arguments; I believe reduce passes 4.

Thread Thread
 
somedood profile image
Basti Ortiz

Oh, I see now. There is a conflict between the two parameters (index and base). Since the code you mentioned returns [0, NaN, NaN], why does it return 0 in the "zeroth" element of the array? What even is a base 0 number to JavaScript?

As I experimented on passing in 0 as an argument for the base parameter of parseInt, I found that it works normally. Why would that work? Is it just all in the spec?

Thread Thread
 
robertcoopercode profile image
Robert Cooper

From MDN (radix being the same as base with the previously used verbage):

If radix is undefined or 0 (or absent), JavaScript assumes the following:

  • If the input string begins with "0", radix is eight (octal) or 10 (decimal). Exactly which radix is chosen is implementation-dependent. ECMAScript 5 specifies that 10 (decimal) is used, but not all browsers support this yet. For this reason always specify a radix when using parseInt.
Thread Thread
 
somedood profile image
Basti Ortiz

Thanks for looking into this! We appreciate your efforts. I'll go make a quick edit to the article now to raise this point.

Collapse
 
robertcoopercode profile image
Robert Cooper

Ah, thanks for this example! It was fun digging into this.

Collapse
 
ironydelerium profile image
ironydelerium • Edited

Except that it's not the same thing.

promise.then(value => console.log(value))
// 'this' in the execution of console.log is console
promise.then(console.log)
// 'this' is undefined
promise.then(console.log.bind(console))
// This one is roughly equivalent to the first

// Event listeners on DOM nodes bind 'this' to the element:
node.addEventListener('click', console.log)
// 'this' is 'node'.

Taking a more concise syntax without understanding it's implications is going to lead you to bugs in the long term.

Collapse
 
somedood profile image
Basti Ortiz

I didn't even think about the context of the execution when I wrote this article. That's my mistake on my part. I just wanted to show that one can generally use function definitions* as arguments in the hopes of shortening code just a bit more. Thanks for clarifying and pointing that out, though!

*By functions, I refer to short, simple, single-purpose functions that don't necessarily have significant side effects.

Collapse
 
johnkazer profile image
John Kazer

A useful article thanks but the smug tone is not necessary. "Of course, in a professional setting one would never do such a thing!" Didn't add to the utility or point of what you said, just irritated.

Collapse
 
somedood profile image
Basti Ortiz

Sorry for that. I didn't mean to sound boastful. For my improvement, how would I have rewritten it?

Collapse
 
johnkazer profile image
John Kazer

Don't forget that there are folk of different knowledge levels on dev.to. Maybe focus on the explanation with some descriptive 'colour' from your experience - I think most readers value new insight and want confidence in the person writing about it.

Thread Thread
 
somedood profile image
Basti Ortiz

Thank you for the advice! I will definitely be more sensitive with my words in my future posts.

Collapse
 
gypsydave5 profile image
David Wickes

This is great - but remember there are some gotchas when you start passing console.log in as the function argument. If the function you're passing to will take a variadic function (i.e a function that can take a different numbers of arguments), you may get unexpected results.

For instance

[1, 2, 3].forEach(console.log)

results in

1 0 [ 1, 2, 3 ]
2 1 [ 1, 2, 3 ]
3 2 [ 1, 2, 3 ]
Collapse
 
somedood profile image
Basti Ortiz

It really all comes down to being careful with parameters. As useful as this trick is, it can be dangerous if one has not read enough documentation. "With great power comes great responsibility" after all. Thanks for this! I appreciate all the quirks being discussed here in the comments section because even I wouldn't have thought of these quirks.

Collapse
 
robertcoopercode profile image
Robert Cooper
fetch('https://api.github.com/')
  .then(res => {
    return res.json();
  })
  .then(function(data) {
    console.log(data);
  })
  .catch(err => console.log(err));

^ I did not even know that this could be shortened the way you suggested. Thanks for explaining this idea of a stored function definition. I'm not quite sure I've got my head wrapper around the idea, but I believe to have a better understanding now.

Collapse
 
somedood profile image
Basti Ortiz

Thanks! However, as mentioned by jburgy, we do have to be aware of some weird outliers.

You do have to be careful with things like

['0', '1', '2'].map(parseInt)
Collapse
 
jhderojas_uva profile image
Jesus Hernandez

Great article. This is something I tell to my co-workers but they don't understand at all. They still think that a function is a function not that everything in JS is an object and you must use it as it is... an object.

Collapse
 
somedood profile image
Basti Ortiz • Edited

I struggled with the concept myself when I was a beginner in JavaScript. I couldn't understand why primitives and functions had properties and methods even though they weren't "objects" so to speak. I believe the confusion stems from the constant desire to mold JavaScript into another familiar language such as C++ or Java.

An example would be the implementation of ES6 Classes. There is no such thing as a class in JavaScript, yet it was added as syntactic sugar to accommodate those who came from other object-oriented languages. It also allowed for a straightforward interpretation of inheritance. JavaScript only emulates classical inheritance. Under the hood, it still uses prototypal inheritance.

This is true for functions. In object-oriented languages, a function/method exists as a member of a class. When the class is instantiated as an object, the function is merely a method of the instantiated object, and not a standalone object itself; unlike in JavaScript where a function is an "object". To truly master JavaScript, one must accept that JavaScript is not like the other languages. It is its own beast that needs to be tamed without any preconceived knowledge of other languages.

In conclusion, I think your co-workers are hindered by preconceived knowledge. They just have to treat JavaScript as it is, and not as how they want to mold it. But please do take my advice with a grain of salt. I am only with three years of experience in JavaScript myself. Surely there are others who are more qualified than I am to help you and your co-workers.

Collapse
 
squgeim profile image
Shreya Dahal

I think this is fine for little things, but often maintainability wins over conciseness. It is always easier to maintain explicit code.

Collapse
 
alainvanhout profile image
Alain Van Hout

More often than not, we are obsessed with writing concise code.

More often than not, ‘concise’ is just an alias for ‘dense’. And dense code is almost always unmaintainable code.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.