loading...

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

somedood profile image Basti Ortiz (Some Dood) ・5 min read

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);
});

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);
});

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));

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'

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);

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);

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]

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);

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));

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.

Posted on Oct 19 '18 by:

somedood profile

Basti Ortiz (Some Dood)

@somedood

Just some dood trying to make code work without bringing the Universe to its demise.

Discussion

markdown guide
 

You do have to be careful with things like

['0', '1', '2'].map(parseInt)
 

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?

 

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.

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?

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.

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

 

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

 

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.

 

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.

 

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.

 

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

 

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.

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

 

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 ]
 

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.

 
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.

 

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)
 

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.

 

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.

 

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.

 

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

 

Now read the title again... slowly...

This is what is wrong with JavaScript and why developers are making fun of it

 

Indeed, JavaScript is a (very) flawed language. Indeed, JavaScript is a laughing stock for "real programmers" who code with more hardcore, lower-level languages. JavaScript was designed in just two weeks after all. I don't blame the community for belittling JavaScript despite its success and omnipresence nowadays.

However, I beg to differ on how you believe JavaScript's treatment of functions as objects is wrong. There is no right or wrong in programming languages. Each language was designed to be how it is for a good reason. It is unfair to say that a language feature is "wrong" because it is rare or unpopular.

Let's take Rust for example. Variables in Rust are immutable by default. Most languages are not designed that way. Would it be fair to say that Rust is "wrong" for having immutable variables by default? I wouldn't say so.

I hope you see my point here even if my analogy might be off. I don't mean to attack you or anything. Sorry if it comes out like that. I just felt the need to defend the language I love, you know?

 

Don't take everything too serious :) You know - there are only two kinds of programming languages: those people always bitch about and those nobody uses.

Modern days JS has evolved a lot, compared to what it used to be back then, when Brendan Eich designed it. And we all why he did this.

As an older generation programmer and someone who used to think how everything represents in memory and what instructions the CPU has to execute, I understand what you mean. In fact what you refer as object is actually a pointer - some data and function reference... and to be more precise it's is even more than actually an object - it also contains the closure with all the surrounding variables of it's parent functions/objects/closures... Heck even trying to explain that can bring me a headache...

Anyway - JS has a lot of issues and a lot of good parts. And for some of the issues TypeScript can actually help and I'm happy to see that one of the best people in our time - Anders Hejlsberg is working on it.

Ah, I see. Thanks for clearing stuff up. Again, sorry if I came out too aggressively.

*This is off topic, but I should really start learning TypeScript. I'll make that my goal by the end of the year.
**Also, you have my +1 Respect for your experience with low level programming. I'm relatively new to all of this (having three years of experience), which is why I find that really cool.

20 years later and you will be tired of all this s*** and everything new will sound like re-inventing the wheel once again but slightly differently, yet it's still a wheel :) Keep up the enthusiasm and learn new things.

Oh yes, and don't apologise! I didn't notice any aggression in your reply anyway, but even if there is - some of us are calling it passion and don't find anything wrong with this.