📜 ⬆️ ⬇️

Variable number of arguments: problems and solutions

It will be about writing JavaScript functions with a variable number of arguments, problems arising in the process and ways to solve these problems.

Problem


From time to time there is a need to write a function with a variable number of arguments. This is especially true for libraries: there is an easy way to call a function that is appropriate in ninety percent of cases, and there is a complex one, needed in the remaining ten percent, when you need to transfer some additional settings or data.

An example is jQuery.get, which can be called as $ .get (url, callback), or as $ .get (url, data, callback).

JavaScript is not particularly rich in argument handling tools (unlike, for example, Python ), so in order to implement functions with an interface like jQuery.get, you have to write something like this:
')
function openTheDoor(door, options, callback) { if (typeof options === 'function') { callback = options options = {} } //      var handlePosition = door.getHandlePosition() // ... } 

What's wrong with this code?

First, the beginning of the function does not carry a semantic load, it is just a superstructure above the method of passing arguments. To see what the function does, you need to “squander” four lines of code in your head.

Secondly, there is a temptation to combine juggling with parameters and assigning default values, and this can be sideways - the initialization will simply be “squandered” like a standard boilerplate next time.

A similar approach can be found in the code of many libraries that work asynchronously. This problem must have a solution.

Bad decisions


There are quite a few modules in the npm registry that solve the problem by processing the arguments object. The code looks like this:

 var somehowParse = require('some-fancy-args') function openTheDoor() { var args = somehowParse(arguments) //      //       var handlePosition = args.first.getHandlePosition() // ... } 

Some libraries provide a small “parameter definition language”:

 var parseArgs = require('another-fancy-args') function openTheDoor() { var args = parseArgs(['door|obj', 'options||obj', 'callback||func'], arguments) //      //   ,     var handlePosition = args.door.getHandlePosition() // ... } 

There are also magic libraries that call the original function twice in order to set the arguments correctly, and for this reason they force you to write code inside the anonymous function and pass it to the library call this:

 var magicArgs = require('oh-so-magic-args') function openTheDoor(door, options, callback) { return magicArgs(this, ['obj', '|obj', '|func'], function () { //      //     var handlePosition = door.getHandlePosition() // ... }) } 

What is bad about these decisions? Yes, almost everything.

The beginning of the function in any case remains a template that you need to “squander” to get to the code. This template introduces an additional level of abstraction, and sometimes magic, in which you need to understand separately.

"Nonmagic" libraries also spoil the code of the function itself a little - the parameters do not come in the form of separate variables, as is usually the case, but as an object, and the parameters do not always have names.

Towards a good decision


Let's talk about the functions.

First, a well-written function should not have too many parameters. If the function has more than three parameters, most likely it needs refactoring. A complex function can be divided into several separate ones, it is possible to group part of the parameters into one parameter object, or to simplify and rewrite it in some way (a good book on the topic is Refactoring. Improving the existing Martin Fowler code ).

Secondly, JavaScript uses a simple and logical scheme for working with missing parameters. If the value for the parameter is not passed when the function is called, it becomes undefined. To specify the default parameter values, it is convenient to use constructions of the form options = options || {}. [one]

Thirdly, there is the last callback agreement, which simplifies asynchronous programming. In most cases, it is this agreement that makes it necessary to juggle with parameters — since callback must always go last, the optional parameters are forced to be in the middle of the list.

Taking into account all three points, we get a fairly simple solution: the only thing that should be done is to add the list of arguments with undefined values ​​so that the callback takes its last place . This is what the vargs-callback module does , which I wrote to implement the solution I found.

Vargs-callback module


The module exports a single function that should be used as a decorator.

Common (Named) Functions: [2]

 var vargs = require('vargs-callback') function openTheDoor(door, options, callback) { //     // options    undefined,      door  callback var handlePosition = door.getHandlePosition() // ... } openTheDoor = vargs(openTheDoor) //    

Expression Functions:

 var vargs = require('vargs-callback') var openTheDoor = vargs(function (door, options, callback) { //  - //     // options    undefined,      door  callback var handlePosition = door.getHandlePosition() // ... }) 

The vargs decorator is triggered when the decorated function is called and performs the following:

  1. If the number of passed arguments is less than the declared number of parameters and the last argument passed is of type “function”, put the values ​​undefined before the last argument until the number of arguments and the number of parameters match.
  2. Call the decorated function with modified arguments.
  3. If enough arguments are passed or the last argument is not a function, do nothing.


Conclusion


In the solution found, I can note the following advantages:

No additional level of abstraction is used to define the parameters of the function. There is no need for magic or “parameter definition language”, only what is in JavaScript itself is used.

The code becomes cleaner - the declared parameters are used, there is no need to “flip through” the beginning of the function, and to set the default values ​​of the parameters, you can use the appropriate method for each case.

How do you like the idea, colleagues?

Github source code

Notes


  1. This method is not safe to use for parameters that can take falsy values. You can use type checking for them: options = typeof options! == 'undefined'? options: {}
  2. Meaning a function declaration .

Source: https://habr.com/ru/post/205446/


All Articles