Realization principle and process analysis of JavaScript position parameters

  • 2021-08-16 23:11:27
  • OfStack

1. What are position parameters?

JavaScript: Why Named Parameters Are Better Than Positional Parameters

You must be familiar with position parameters, even if you hear the name for the first time.


function greet(firstName, lastName) { 
 console.log(`Hello ${firstName} ${lastName}`); 
} 

//  Anticipatory usage  

greet('Michael', 'Scott'); 

const fName = 'Harry'; 
const lName = 'Potter'; 
greet(fName, lName); 

//  Wrong usage  

const firstName = 'Erlich'; 
const lastName = 'Bachman'; 
greet(lastName, firstName); 

The greet function takes two arguments: firstName and lastName. The caller must ensure that firstName is the first parameter and lastName is the second. The important point here is that the names of the parameters don't make any sense. The only thing that matters is the order in which the parameters are passed.

This familiar method is called position parameter. Usually, if you pass one or two arguments, this is good because it is difficult to mess up the order of the arguments. But if you have to call a function that requires six arguments, it's hard to remember the order in which the arguments are passed. You do not want to pass a password instead of the username parameter.

2. Position parameter problem

The position parameter is simple, but you will face 1 challenge.

(1) The intermediate parameter/cannot be skipped

Suppose you have changed the greet function so that it now requires three arguments: firstName, middleName, and lastName. Since many people don't have a middle name, you want to make MiddleName an optional parameter. The only way to call the greet function using only firstName and lastName is this method.


greet('Aditya', null, 'Agarwal'); 
// Correct 
greet('Aditya', 'Agarwal'); 
// Incorrect 

You can't just offer firstName and lastName. This problem becomes more obvious when the number of optional parameters increases to 5. Now, you must provide 5 null to provide parameters after these parameters.

(2) Adding types to location parameters is not so clean

Nowadays, it is very common to add types to your utilities. With positional parameters, you have no choice but to inline the type with the function definition. This can make the code a little vague, and it would be better if we could declare the type definitions of all the parameters in one block.

(3) Causing minor errors

The location parameter wraps a lot of hidden behavior, which may be the cause of the subtle bug. Let's look at a common JS technique problem

const numbers = ['1', '4', '8', '10'];
console.log(numbers.map(parseInt));

//You might think that the result will be:
[1, 4, 8, 10]

//This is the actual output:
[ 1, NaN, NaN, 3 ]

Surprised? The reason for this strange output lies behind the recessive position parameter. You will see that the map and parseInt functions hide some of their secrets in obvious cases.

Let's look at the code number. map (parseInt) again.

What the hell happened here?

We run the map function on the numbers array. map gets the first item of the array and passes it to parseInt. Now, for the first item in the array (that is, 1), it will execute parseInt (1). Right...? Mistake! ! !

In fact, map passes three parameters to its callback function. The first is the current item in the array, the second is the index of the item, and the third is the entire array. There is no problem in itself, but the real problem lies in the latter part.

numbers. map (parseInt) and numbers. map (item) = > parseInt (item) is different. You can assume that since the callback only accepts the item parameter and passes it to parseInt, we can skip the additional steps. But the two are different: In the former, we pass all data from map to parseInt, whereas in the latter, we pass only items.

You may not know it, but the second parameter of parseInt is called cardinality. By default, the cardinality value is 10 (base 10, because humans follow the decimal system for counting). The problem with this code is that we pass the index of the current project as the cardinality value to parseInt. These are the actual function calls that occurred:

parseInt('1', 0, [...]);
parseInt('4', 1, [...]);
parseInt('8', 2, [...]);
parseInt('10', 3, [...]);

Now that we know the problem, how can we do better?

3. Substitution of position parameters

What if a function can know what its expected parameters are by its name? In this way, even if you mispass extra data to it, it will only use what it needs.

Let's wrap parseInt. Here is a simple implementation.


//  Realization  
function myCustomParseInt(objArgs) { 
 return parseInt(objArgs.item, objArgs.radix); 
} 

//  Use  
const num = myCustomParseInt({ item: '100', radix: 10 }); 

myCustomParseInt accepts only one parameter, which is one object. This object can have two keys: item and radix. Let's use our custom functions with map. There must be an intermediate step to send the args received by the callback to myCustomParseInt.

const numbers = ['1', '4', '8', '10'];

const result = numbers.map((item, index) = > myCustomParseInt({ item, index }));

console.log(result); // [ 1, 4, 8, 10 ]

Note that even if we pass the index to myCustomParseInt, it will not cause any problems. That's because myCustomParseInt just ignores it. This pattern of passing objects to functions is called named parameters, which is more explicit than positional parameters.

To change the cardinality, we must explicitly pass the cardinality key. This means that if you want to parse a string with a base of 2, you must go to the document and see the exact name of the parameter (cardinality). If we blindly pass any other keys, it will not help. This is great for us because it avoids unexpected behavior.

(1) Named parameters with deconstruction

Not long ago, JavaScript acquired a feature called deconstruction, so let's use it in the myCustomParseInt implementation.


//  Position parameter  
function myCustomParseInt(item, radix) { 
 return parseInt(item, radix); 
} 

//  Old implementation of named parameters  
function myCustomParseInt(objArgs) { 
 return parseInt(objArgs.item, objArgs.radix); 
} 

//  Named parameter deconstruction  
function myCustomParseInt({ item, radix }) { 
 return parseInt(item, radix); 
} 

You'll notice that just by adding two curly braces, we get the benefit of naming args, and you can think of deconstruction as executing const item = objArgs. item; .

If myCustomParseInt is called with undefined, JS raises an error. That's because undefined. item is not allowed. To avoid this, we can add = {} at the end of deconstruction. This way, when we pass an undefined, it executes {}. item which is a valid JS. This is the ultimate realization:


function myCustomParseInt({ item, radix } = {}) { 
 return parseInt(item, radix); 
} 

With the named argument pattern, we can also skip arguments that we don't want to provide, because functions no longer depend on the order in which arguments are passed.

//For the position parameter, we must add 1 null between
function greetPos(firstName, middleName, lastName) {}
greetPos('Aditya', null, 'Agarwal');

//With named parameters, you only need to provide firstName and lastName.
function greetNamed({ firstName, middleName, lastName } = {}) {}
greetNamed({ firstName: 'Aditya', lastName 'Agarwal' });

All in all, I'd like to say that named parameters are a powerful pattern that has become very common these days, but you don't have to use them all the time. Sometimes you can even combine the two in one. The usage of fetch API in the browser is as follows:


//  With url As the location parameter, and the request with the args Make the option of named parameters.  
fetch('https://google.com', { 
 method: 'POST', 
 headers: { 
  'Content-Type': 'application/json', 
 }, 
}); 

// basic GET requests with just positional args 
fetch('https://google.com'); 

The mandatory parameter here (API path) is a positional parameter, and then optional parameters are accepted through named parameters.


Related articles: