Source: logrocket
Author: Maciej Cie lar
Translator: Front-end wisdom
In order to ensure readability, free translation is adopted instead of literal translation.
For more quality articles, please Poke GitHub Blog More than a hundred high-quality articles a year are waiting for you!
In order to give back to readers, Daqian World is held from time to time (one to three times a month), cash lottery, bottom 200, plus user appreciation. I hope you can become a small Koi in Daqian World. Come and try it.
In JS, arrow functions can be used in many ways, just like ordinary functions. However, they are generally used for expressions requiring anonymous functions, such as callbacks.
The following example shows the example arrow function as a callback function, especially for array methods such as map(), filter(), reduce(), sort().
const scores = [ 1, 28, 66, 666]; const maxScore = Math.max(...scores); scores.map(score => +(score / maxScore).toFixed(2));
At first glance, it seems that arrow functions can be defined and used as normal functions, but this is not the case. Because of the simplicity of the arrow function, it is different from the conventional function. In other words, the arrow function may be regarded as an abnormal JS function.
Although the grammar of arrow functions is very simple, this is not the focus of this article. This article mainly talks about the difference between the behavior of arrow function and normal function, and if we use these differences to make better use of arrow function.
- In both strict and non-strict modes, arrow functions cannot have duplicate naming parameters.
- The arrow function has no arguments binding. However, they can access the arguments objects closest to non-arrow parent functions.
- Arrow functions can never be used as constructors, and naturally they cannot be invoked using the new keyword, so there is no prototype attribute for arrow functions.
- Throughout the life cycle of a function, the internal values of the arrow function remain unchanged and are always bound to the values in the nearest non-arrow parent function.
Named function parameters
Functions in JS are usually defined with named parameters. Named parameters are used to map parameters to local variables in the scope of a function according to their location.
Let's look at the following functions:
function logParams (first, second, third) { console.log(first, second, third); } // first => 'Hello' // second => 'World' // third => '!!!' logParams('Hello', 'World', '!!!'); // "Hello" "World" "!!!" // first => { o: 3 } // second => [ 1, 2, 3 ] // third => undefined logParams({ o: 3 }, [ 1, 2, 3 ]); // {o: 3} [1, 2, 3]
The logParams() function is defined by three named parameters: first, second, and third. If the named parameter is more than the parameter passed to the function, the remaining parameter undefined.
For named parameters, JS functions behave strangely in a non-strict mode. In non-strict mode, JS functions allow duplicate named parameters. Let's see an example:
function logParams (first, second, first) { console.log(first, second); } // first => 'Hello' // second => 'World' // first => '!!!' logParams('Hello', 'World', '!!!'); // "!!!" "World" // first => { o: 3 } // second => [ 1, 2, 3 ] // first => undefined logParams({ o: 3 }, [ 1, 2, 3 ]); // undefined [1, 2, 3]
As you can see, the first parameter repeats, so it is mapped to the value of the third parameter passed to the function call, covering the first parameter, which is not a pleasant behavior.
// Due to parameter duplication, strict mode will report errors function logParams (first, second, first) { "use strict"; console.log(first, second); }
How do arrow functions handle duplicate parameters
About the arrow function:
Unlike conventional functions, arrow functions are not allowed to duplicate parameters in either strict or non-strict modes, and duplicate parameters will cause grammatical errors.
// As long as you dare to write repetitive parameters, I dare to die to show you. const logParams = (first, second, first) => { console.log(first, second); }
function overloading
Function overloading is the ability to define functions, so that the corresponding functions can be called according to the number of parameters. JS can use binding to achieve this function.
Let's look at a simple overload function and calculate the average of the incoming parameters:
function average() { const length = arguments.length; if (length == 0) return 0; // Converting parameters to arrays const numbers = Array.prototype.slice.call(arguments); const sumReduceFn = function (a, b) { return a + Number(b) }; // Returns the sum of array elements divided by the length of the array return numbers.reduce(sumReduceFn, 0) / length; }
In this way, the function can be called with any number of parameters, and the maximum number of parameters acceptable to the function should be 255.
average(); // 0 average('3o', 4, 5); // NaN average('1', 2, '3', 4, '5', 6, 7, 8, 9, 10); // 5.5 average(1.75, 2.25, 3.5, 4.125, 5.875); // 3.5
Now try to copy the average() function using the clipping function grammar. Generally, we will think that it's not difficult. We can't do that.
const average = () => { const length = arguments.length; if (length == 0) return 0; const numbers = Array.prototype.slice.call(arguments); const sumReduceFn = function (a, b) { return a + Number(b) }; return numbers.reduce(sumReduceFn, 0) / length; }
Now when we test this function, we will find that it throws a reference error, arguments are undefined.
What have we done wrong?
For arrow functions:
Unlike conventional functions, arguments do not exist in arrow functions. However, you can access arguments objects that are not arrow parent functions.
Based on this understanding, the average() function can be modified to a regular function, which returns the result of execution of the nested arrow function called immediately, and the nested arrow function can access arguments of the parent function.
function average() { return (() => { const length = arguments.length; if (length == 0) return 0; const numbers = Array.prototype.slice.call(arguments); const sumReduceFn = function (a, b) { return a + Number(b) }; return numbers.reduce(sumReduceFn, 0) / length; })(); }
This solves the problem of undefined arguments objects, but obviously there's a lot more to this shit.
Do something different
Whether there is an alternative to the above problem, the rest parameter of es6 can be used.
Using the ES6 rest parameter, we can get an array that holds all the parameters passed to the function. The rest syntax applies to all types of functions, whether regular or arrow functions.
const average = (...args) => { if (args.length == 0) return 0; const sumReduceFn = function (a, b) { return a + Number(b) }; return args.reduce(sumReduceFn, 0) / args.length; }
There are a few things to note when using rest parameters:
- The rest parameter is different from the arguments object inside the function. The rest parameter is an actual function parameter, while the arguments object is an internal object bound to the scope of the function.
- A function can only have one rest parameter, and it must be in the last parameter. This means that a function can contain a combination of named parameters and rest parameters.
- When the rest parameter is used with the named parameter, it does not contain all the incoming parameters. However, when it is the only function parameter, it represents the function parameter. On the other hand, the arguments object of a function always captures the parameters of all functions.
- The rest parameter points to the array object that contains all the capture function parameters, while the arguments object points to the class array object that contains all the function parameters.
Next, consider another simple overloaded function that converts a number from an incoming binary to a binary number of another class. The function can be invoked with one or three parameters. However, when it is called with two or fewer parameters, it swaps the second and third function parameters. As follows:
function baseConvert (num, fromRadix = 10, toRadix = 10) { if (arguments.length < 3) { // swap variables using array destructuring [toRadix, fromRadix] = [fromRadix, toRadix]; } return parseInt(num, fromRadix).toString(toRadix); }
Call the baseConvert method:
// num => 123, fromRadix => 10, toRadix => 10 console.log(baseConvert(123)); // "123" // num => 255, fromRadix => 10, toRadix => 2 console.log(baseConvert(255, 2)); // "11111111" // num => 'ff', fromRadix => 16, toRadix => 8 console.log(baseConvert('ff', 16, 8)); // "377"
Use the arrow function to rewrite the above method:
const baseConvert = (num, ...args) => { // Deconstruction of `args'arrays and arrays // Set `fromRadix'and `toRadix' local variables let [fromRadix = 10, toRadix = 10] = args; if (args.length < 2) { // Deconstructing Exchange Variables with Arrays [toRadix, fromRadix] = [fromRadix, toRadix]; } return parseInt(num, fromRadix).toString(toRadix); }
Constructor
You can use the new keyword to call a regular JS function, which is used as a class constructor to create new instance objects.
function Square (length = 10) { this.length = parseInt(length) || 10; this.getArea = function() { return Math.pow(this.length, 2); } this.getPerimeter = function() { return 4 * this.length; } } const square = new Square(); console.log(square.length); // 10 console.log(square.getArea()); // 100 console.log(square.getPerimeter()); // 40 console.log(typeof square); // "object" console.log(square instanceof Square); // true
When a regular JS function is called with the new keyword, the [[Construct]] method inside the function is called to create a new instance object and allocate memory. After that, the function body will execute normally and map this to the newly created instance object. Finally, the function implicitly returns this (newly created instance object), only specifying a different return value in the function definition.
In addition, all regular JS functions have a prototype attribute. The prototype attribute of a function is an object that contains the attributes and methods shared by all instance objects created by the function when used as constructors.
The following is a minor modification to the previous Square function, this time from the prototype of the function, rather than the constructor itself.
function Square (length = 10) { this.length = parseInt(length) || 10; } Square.prototype.getArea = function() { return Math.pow(this.length, 2); } Square.prototype.getPerimeter = function() { return 4 * this.length; } const square = new Square(); console.log(square.length); // 10 console.log(square.getArea()); // 100 console.log(square.getPerimeter()); // 40 console.log(typeof square); // "object" console.log(square instanceof Square); // true
As you know, everything is still working as expected. In fact, here's a little secret: class ES6 performs an operation in the background similar to the code snippet above - class is just a grammatical sugar.
What about the arrow function?
Do they share this behavior with regular JS functions? The answer is No. About the arrow function:
Unlike regular functions, arrow functions can never be called using the new keyword because they have no [[Construct]] method. Therefore, the arrow function does not have a prototype attribute.
Arrow functions cannot be used as constructors, and they cannot be called using the new keyword. If they do, an error will be thrown indicating that the function is not a constructor.
Therefore, for arrow functions, there are no new.target bindings within functions that can be called as constructors. Instead, they use the closest new.target value of a non-arrow parent function.
In addition, because arrow functions cannot be invoked using the new keyword, they are not actually required to have prototypes. Therefore, the arrow function does not have a prototype attribute.
Because the prototype of the arrow function is undefined, attempting to extend it with attributes and methods, or accessing attributes above it, can cause errors.
const Square = (length = 10) => { this.length = parseInt(length) || 10; } // throws an error const square = new Square(5); // throws an error Square.prototype.getArea = function() { return Math.pow(this.length, 2); } console.log(Square.prototype); // undefined
What is this?
Each invocation of a JS function is associated with the invocation context, depending on how or where the function is invoked.
The value of this inside a function depends on the calling context of the function when it is called, which usually forces developers to ask themselves a question: what is the value of this?
The following is a summary of the different types of function calls that this points to:
- Call with the new keyword: this points to a new instance object created by the function's internal [[Construct]] method. This (newly created instance object) is usually returned by default, except that different return values are explicitly specified in the function definition.
- Call directly without using the new keyword: In a non-strict mode, this points to the window object (in the browser). However, in strict mode, this value is undefined; therefore, attempting to access or set this property will cause an error.
- Indirectly using bound object calls: Function.prototype objects provide three ways to bind functions to arbitrary objects when calling functions: call(), apply() and bind(). When using these methods to call functions, this points to the specified binding object.
- As an object method call: this points to the object of the calling function (method), whether the method is defined as the object's own property or parsed from the object's prototype chain.
- Called as an event handler: For a regular function used as a DOM event listener, this points to the target object, DOM element, document, or window that triggers the event.
Let's look at a function that will be used as a click event listener, for example, a form submission button:
function processFormData (evt) { evt.preventDefault(); const form = this.closest('form'); const data = new FormData(form); const { action: url, method } = form; } button.addEventListener('click', processFormData, false);
As you saw earlier, this value in the event listener function is the DOM element that triggers the click event, in this case button.
Therefore, you can use the following command to point to the parent form of the submit button
this.closest('form');
What happens if you change the function to the arrow function grammar?
const processFormData = (evt) => { evt.preventDefault(); const form = this.closest('form'); const data = new FormData(form); const { action: url, method } = form; } button.addEventListener('click', processFormData, false);
If we try this now, we will get a mistake. On the surface, the value of this is not what you want. For some reason, it no longer points to the button element, but to the window object.
How to repair this pointing
Force binding this value to the button element using Function.prototype.bind() mentioned above:
button.addEventListener('click', processFormData.bind(button), false);
But that doesn't seem to be the solution you want. This still points to the window object. Is this a problem specific to arrow functions? Does this mean that arrow functions cannot be used for event handling that depends on this?
Why did you make a mistake?
One last thing about arrow functions:
Unlike regular functions, arrow functions do not have this binding. The value of this will be resolved to the value of the closest non-arrow parent function or global object.
This explains why this value in the event listener arrow function points to the window object (global object). Since it is not nested in the parent function, it uses this value from the nearest parent scope, which is global.
However, this does not explain why event listener arrow functions cannot be bound to button elements using bind(). There is an explanation for this:
Unlike regular functions, this value of the internal arrow function remains unchanged and cannot be changed throughout its life cycle regardless of the calling context.
This behavior of arrow functions allows the JS engine to optimize them because function bindings can be determined beforehand.
Consider a slightly different scenario in which event handlers are defined using a regular function in the object method and depending on another method of the same object:
({ _sortByFileSize: function (filelist) { const files = Array.from(filelist).sort(function (a, b) { return a.size - b.size; }); return files.map(function (file) { return file.name; }); }, init: function (input) { input.addEventListener('change', function (evt) { const files = evt.target.files; console.log(this._sortByFileSize(files)); }, false); } }).init(document.getElementById('file-input'));
The above is a one-time object with _sortByFileSize() method and init() method, and immediately adjust the init method. The init() method accepts an input element and sets up a change event handler for the input element, which sorts the uploaded files by file size and prints them to the browser's console.
If you test this code, you will find that when you select the file to upload, the list of files will not be sorted and printed to the console; instead, an error will be thrown on the console and the problem will occur on this line:
console.log(this._sortByFileSize(files));
Inside the event listener function, this points to the input element, so this._sortByFileSize is undefined.
To solve this problem, you need to bind this in the event listener to the external object containing the method so that you can call this._sortByFileSize(). Here, you can use bind(), as follows:
init: function (input) { input.addEventListener('change', (function (evt) { const files = evt.target.files; console.log(this._sortByFileSize(files)); }).bind(this), false); }
Everything is normal now. Instead of using bind(), you can simply replace the event listener function with an arrow function. The arrow function uses the value of this in the parent init() method:
init: function (input) { input.addEventListener('change', (function (evt) { const files = evt.target.files; console.log(this._sortByFileSize(files)); }).bind(this), false); }
Consider a scenario where a simple timer function can be called as a constructor to create a countdown timer in seconds. Use setInterval() to count down until the duration expires or the interval is cleared, as follows:
function Timer (seconds = 60) { this.seconds = parseInt(seconds) || 60; console.log(this.seconds); this.interval = setInterval(function () { console.log(--this.seconds); if (this.seconds == 0) { this.interval && clearInterval(this.interval); } }, 1000); } const timer = new Timer(30);
If you run this code, you'll see that the countdown timer seems to have been broken, printing NaN on the console all the time.
The problem here is that in the callback function passed to setInterval(), this points to the global window object, not the newly created instance object within the scope of the Timer() function. Therefore, this.seconds and this.interval are undefined.
As before, to fix this problem, you can use bind() to bind this value in the setInterval() callback function to the newly created instance object, as shown below
function Timer (seconds = 60) { this.seconds = parseInt(seconds) || 60; console.log(this.seconds); this.interval = setInterval((function () { console.log(--this.seconds); if (this.seconds == 0) { this.interval && clearInterval(this.interval); } }).bind(this), 1000); }
Or, a better way is to replace the setInterval() callback function with an arrow function so that it can use the latest value of this non-arrow parent function:
function Timer (seconds = 60) { this.seconds = parseInt(seconds) || 60; console.log(this.seconds); this.interval = setInterval(() => { console.log(--this.seconds); if (this.seconds == 0) { this.interval && clearInterval(this.interval); } }, 1000); }
Now that you understand how arrow functions handle this keyword, you also need to note that arrow functions are not ideal for situations where you need to retain this value - for example, when defining object methods that need to be referenced, you use methods that need to refer to the target object to extend the object or the prototype of the extension function.
Non-existent binding
In this article, you've seen some bindings that can be used in regular JS functions, but there are no bindings for arrow functions. Instead, the arrow function derives such binding values from the nearest non-arrow parent function.
In summary, here is a list of bindings that do not exist in arrow functions:
- arguments: List of parameters passed to the function when invoked
- new.target: A reference to a function called by a constructor using the new keyword
- super: A reference to the prototype of the object to which the function belongs, provided that the object is defined as a concise object method
- this: Reference to the calling context object of a function
Original text: https://s0dev0to.icopy.site/b...
The BUG that may exist after code deployment can not be known in real time. In order to solve these BUGs afterwards, it takes a lot of time to debug the log. By the way, I recommend a useful BUG monitoring tool. Fundebug.
Communication
The dry goods series articles are summarized as follows. I think it's good to order Star. Welcome to learn from each other.
https://github.com/qq44924588...
I am Xiao Zhi, author of the public name "Great Move to the World" and a fan of front-end technology. I will often share what I have learned and seen. On the way to advancement, I will encourage you to do it together! ___________
Pay attention to the public number and reply to the welfare in the background, you can see the welfare, you understand.