Front-end basics: A brief discussion of functions and functional programming

Looking at all the key knowledge that must be mastered in JavaScript, functions are the knowledge points that are most easily overlooked when we first learn . There may be many people and many articles telling you that object-oriented is important and prototypes are important, but few people tell you that almost all the key and difficult points in object-oriented are closely related to functions.

Including the execution context , variable objects, closures, this, etc. introduced in my previous articles , they all revolve around the details of the function.

Many people are eager to start learning object-oriented, learn modules, and learn popular frameworks as soon as possible while learning, and quickly become a master. But I can tell you responsibly that if you don’t understand these basic things about functions to a certain extent, your learning progress will definitely be difficult.

Therefore, everyone must pay attention to functions!

Of course, the key points and difficulties of functions have been discussed in the previous articles. This article mainly summarizes the basic knowledge of functions and initially learns the thinking of functional programming .

1. Function declaration, function expression, anonymous function and self-executing function

Regarding the application of functions in actual development, it can be roughly summarized as function declarations, function expressions, anonymous functions, and self-executing functions.

function declaration

In JavaScript, there are two declaration methods, one is var/let/constthe variable declaration used, and the other is functionthe function declaration used.

In Advanced Front-End Basics (3): Detailed Explanation of Variable Objects [1], I mentioned that during the creation process of variable objects, function declarations have a higher priority in execution order than variable declarations, that is, the function declarations we often mention in advance. Therefore, no matter where we declare the function in the execution context, we can directly use the function in the same execution context.

fn();  // functionfunction fn() {
   
       console.log("function");}

function expression

Different from function declaration, function expression is declared using var/let/const, so when we confirm whether it can be used correctly, we must judge according to the rules of var/let/const, that is, variable declaration. We know that using var to declare variables is actually a two-step operation.

// 变量声明var a = 20;// 实际执行顺序var a = undefined;  // 变量声明,初始值undefined,变量提升,提升顺序次于function声明a = 20;  // 变量赋值,该操作不会提升

In the same way, when we use variable declaration to declare a function, it is what we often call a function expression. Function expressions are hoisted in the same way as variable declarations.

fn(); // 报错var fn = function() {
   
       console.log("function");}

The execution sequence of the above example is:

var fn = undefined;   // 变量声明提升fn();    // 执行报错fn = function() {   // 赋值操作,此时将后边函数的引用赋值给fn    console.log("function");}

Due to the different declaration methods, there are some differences in the use of function declarations and function expressions that we need to pay attention to. Other than that, there is no difference in the use of these two forms of functions.

Regarding the above example, the assignment operation in function expressions is also frequently used in other places. We just need to be clear about the relationship.

// 在构造函数中添加方法function Person(name) {
   
       this.name = name;    this.age = age;    // 在构造函数内部中添加方法    this.getAge = function() {
   
           return this.age;    }    this.}// 给原型添加方法Person.prototype.getName = function() {
   
       return this.name;}// 在对象中添加方法var a = {
   
       m: 20,    getM: function() {
   
           return this.m;    }}

anonymous function

Anonymous functions, as the name suggests, refer to functions that are not explicitly assigned values. Its usage scenario is mostly passed into another function as a parameter.

var a = 10;var fn = function(bar, num) {
   
       return bar() + num;}fn(function() {
   
       return a;}, 20)

In the above example, an anonymous function is passed as the first parameter of fn. Although the anonymous function does not perform an explicit assignment operation and we have no way to reference it in the external execution context, within the fn function, we assign the anonymous function to the variable bar and save it in the arguments object of the fn variable object. .

// 变量对象在fn上下文执行过程中的创建阶段VO(fn) = {
   
       arguments: {
   
           bar: undefined,        num: undefined,        length: 2    }}// 变量对象在fn上下文执行过程中的执行阶段// 变量对象变为活动对象,并完成赋值操作与执行可执行代码VO -> AOAO(fn) = {
   
       arguments: {
   
           bar: function() { return a },        num: 20,        length: 2    }}

Because after the anonymous function is passed into another function, it will eventually be executed in another function, so we often call this anonymous function a callback function . I will explain more about anonymous functions in more detail in my next article that will delve into currying.

This application scenario of anonymous functions takes on almost all the difficult-to-understand knowledge points of functions , so we must understand these details clearly enough. If you still don’t understand the evolution of variable objects, you must go back. Go read this article: Advanced front-end basics (3): Detailed explanation of variable objects [2]

Function self-execution and block-level scope

In ES5, there is no block-level scope, so we often use function self-execution to imitate block-level scope, which provides an independent execution context. Combined with closures, it provides the basis for modularization. Function self-execution is actually an application of anonymous functions.

(function() {
   
      // ...})();

A module can often include: private variables, private methods, public variables, and public methods.

According to the one-way access of the scope chain, it may be easy for outsiders to know that in this independent module, the external execution environment cannot access any internal variables and methods, so we can easily create private variables and private methods belonging to this module. method.

(function() {
   
       // 私有变量    var age = 20;    var name = "Tom";    // 私有方法    function getName() {
   
           return `your name is ` + name;    }})();

But what should we do with shared methods and variables? Do you still remember the characteristics of closures we talked about earlier ? Yes, using closures, we can access the variables and methods inside the execution context. Therefore, you only need to create a closure based on the definition of the closure and open the variables and methods that you think need to be made public.

(function() {
   
       // 私有变量    var age = 20;    var name = "Tom";    // 私有方法    function getName() {
   
           return `your name is ` + name;    }    // 共有方法    function getAge() {
   
           return age;    }    // 将引用保存在外部执行环境的变量中,形成闭包,防止该执行环境被垃圾回收    window.getAge = getAge;})();

Of course, we have emphasized the important role of closures in modules when we explained closures, but this knowledge point is really important and requires us to understand it repeatedly and master it thoroughly.

To help you further understand closures, let's take a look at how modules and closures are used in jQuery.

// 使用函数自执行的方式创建模块(function(window, undefined) {
   
       // 声明jQuery构造函数     var jQuery = function(name) {
   
           // 主动在构造函数中,返回一个jQuery实例         return new jQuery.fn.init(name);     }    // 添加原型方法     jQuery.prototype = jQuery.fn = {
   
            constructor: jQuery,         init:function() { ... },         css: function() { ... }     }     jQuery.fn.init.prototype = jQuery.fn;    // 将jQuery改名为$,并将引用保存在window上,形成闭包,对外开放jQuery构造函数,这样我们就可以访问所有挂载在jQuery原型上的方法了     window.jQuery = window.$ = jQuery; })(window);// 在使用时,直接执行了构造函数,因为在jQuery的构造函数中通过一些手段,返回的是jQuery的实例,所以我们就不用再每次用的时候自己new一个实例$("#div1");

Here, we only need to understand the closure and module parts. As for how the internal prototype chain is wound and why it is written like this, we will slowly analyze it for everyone when we talk about object-oriented. The purpose of giving this example is to hope that everyone can pay attention to functions. In actual development, they are everywhere.

Next I want to share an application of an advanced, very useful module. As our projects get larger and larger, more and more data and states need to be saved. Therefore, we need a special module to maintain this data. At this time, something called a state manager comes into being. As for the state manager, I think the most famous one is redux. Although for everyone who is still learning, redux is a bit unpredictable, but before we learn, we can first use a simple way to let everyone have a general understanding of the implementation principle of the state manager, which will provide us with future learning. Build a solid foundation.

Let’s look directly at the code first.

// 自执行创建模块(function() {
   
       // states 结构预览    // states = {
   
       //     a: 1,    //     b: 2,    //     m: 30,      //     o: {}    // }    var states = {};  // 私有变量,用来存储状态与数据    // 判断数据类型    function type(elem) {
   
           if(elem == null) {
   
               return elem + "";        }        return toString.call(elem).replace(/[\[\]]/g, "").split(" ")[1].toLowerCase();    }    /**     * @Param name 属性名     * @Description 通过属性名获取保存在states中的值    */    function get(name) {
   
           return states[name] ? states[name] : "";    }    function getStates() {
   
           return states;    }    /*    * @param options {object} 键值对    * @param target {object} 属性值为对象的属性,只在函数实现时递归中传入    * @desc 通过传入键值对的方式修改state树,使用方式与小程序的data或者react中的setStates类似    */    function set(options, target) {
   
           var keys = Object.keys(options);        var o = target ? target : states;        keys.map(function(item) {
   
               if(typeof o[item] == "undefined") {
   
                   o[item] = options[item];            }            else {
   
                   type(o[item]) == "object" ? set(options[item], o[item]) : o[item] = options[item];            }            return item;        })    }    // 对外提供接口    window.get = get;    window.set = set;    window.getStates = getStates;})()// 具体使用如下set({ a: 20 });     // 保存 属性aset({ b: 100 });    // 保存属性bset({ c: 10 });     // 保存属性c// 保存属性o, 它的值为一个对象set({
   
       o: {
   
           m: 10,        n: 20    }})// 修改对象o 的m值set({
   
       o: {
   
           m: 1000    }})// 给对象o中增加一个c属性set({
   
       o: {
   
           c: 100    }})console.log(getStates())

Demo instance online address[3]

The reason why I say this is an advanced application is because in a single-page application, we are likely to use this idea. Based on the knowledge we have mentioned, it is actually very simple to understand this example. The difficulty is probably in the processing of the set method. In order to have more applicability, a lot of adaptations have been made, and knowledge such as recursion has been used. If you don't understand it yet, it doesn't matter. Just know how to use it. The above code can be directly applied to practical development. Remember, when you need to save too much state, just think of this piece of code.

There are several other ways to write functions for self-execution, such as !function(){}(),+function(){}()

2. Function parameter passing method: passing by value

Remember the difference in replication between primitive and reference data types? Basic data type copying means that the values ​​are copied directly, so after changes, they do not affect each other. However, when copying a reference data type, the reference stored in the variable object is copied. Therefore, the two references after copying actually access the value in the same heap memory. When one is changed, the other will naturally be changed as well. Take the following example.

var a = 20;var b = a;b = 10;console.log(a);  // 20var m = { a: 1, b: 2 }var n = m;n.a = 5;console.log(m.a) // 5

The same difference occurs when values ​​are passed inside functions as arguments. We know that the parameters of the function are actually stored in the variable object of the function after entering the function. Therefore, a copy occurs at this time. Take the following example.

var a = 20;function fn(a) {
   
       a = a + 10;    return a;}fn(a);console.log(a); // 20
var a = { m: 10, n: 20 }function fn(a) {
   
       a.m = 20;    return a;}fn(a);console.log(a);   // { m: 20, n: 20 }

It is precisely because of this difference that many people have a lot of confusion when understanding how function parameters are passed. Is it passed by value or by reference? In fact, the conclusion is still pass by value, but when we expect to pass a reference type, what is really passed is just the reference of this reference type stored in the variable object. To illustrate this problem, let's look at the following example.

var person = {
   
       name: "Nicholas",    age: 20}function setName(obj) {  // 传入一个引用    obj = {};   // 将传入的引用指向另外的值    obj.name = "Greg";  // 修改引用的name属性}setName(person);console.log(person.name);  // Nicholas 未被改变

In the above example, if person is passed by reference, person will automatically be modified to point to a new object whose name attribute value is Gerg. But we see from the results that the person object has not changed in any way, so only the reference inside the function has been modified.

4. Functional programming

Although JavaScript is not a purely functional programming language, it uses many features of functional programming. Therefore, understanding these characteristics can help us better understand the code we write.

When we want to use a function, usually we actually want to encapsulate some functions, logic, etc. I believe everyone is familiar with the concept of encapsulation.

We usually accomplish one thing through function encapsulation. For example, if we want to calculate the sum of any three numbers, we can encapsulate a simple function using these three numbers as parameters.

function add(a, b, c) {
   
     return a + b + c;}

When we want to calculate the sum of three numbers, we can call this method directly.

add(1, 2, 3); // 6

Of course, when what you want to do is relatively simple, you may not see the convenience brought by encapsulating it into a function. What if what we want to do is a little more complicated. For example I want to calculate the sum of all sub-items in an array.

function mergeArr(arr) {
   
       var result = 0;    for(var i = 0; i < arr.length; i++) { result  += arr[i] }    return result;}

If we don't use function encapsulation, we have to reuse the for loop every time we want to implement this function. The consequence of this is that our code is filled with more and more repeated codes. After encapsulation, when we want to do it again, we only need one sentence.

mergeArr([1, 2, 3, 4, 5]);

Of course, I believe everyone should have a very clear understanding of the meaning of function encapsulation, but the problem we have to face is, when we want to encapsulate a function, what is the best practice?

Functional programming can give us the answer.

When we first learn, we often involuntarily use the imperative programming style to accomplish what we want to do. Because imperative programming is simpler and more straightforward. For example, we now have an array, array = [1, 3, "h", 5, "m", "4"]and now we want to find all the children of type number in this array. When we use imperative programming thinking, we may do this directly.

var array = [1, 3, "h", 5, "m", "4"];var res = [];for(var i = 0; i < array.length; i ++) {
   
       if (typeof array[i] === "number") {
   
           res.push(array[i]);    }}

In this way of implementation, we achieve our goal in a straightforward manner. The problem with this is that when we want to find all the children in another array at another time, we have to write the same logic again. As the number of occurrences increases, our code becomes worse and harder to maintain.

The thinking of functional programming suggests that we encapsulate this function that will appear many times to prepare for calls.

function getNumbers(array) {
   
       var res = [];    array.forEach(function(item) {
   
           if (typeof item === "number") {
   
               res.push(item);        }    })    return res;}// 以上是我们的封装,以下是功能实现var array = [1, 3, "h", 5, "m", "4"];var res = getNumbers(array);

After encapsulating the function, we only need to write one line of code to implement the same function. And if the needs change or are slightly modified in the future, we only need to adjust the getNumbers method. And when we use it, we only need to care about what this method can do, not how it is implemented. This is also one of the differences between functional programming thinking and imperative programming.

Functional programming thinking also has the following characteristics.

Functions are first class citizens

The so-called "first class citizen" means that functions are on an equal footing with other data types. They can be assigned to other variables, passed as parameters to another function, or returned by other functions. value. We should have seen many of these scenes.

var a = function foo() {}  // 赋值function fn(function() {}, num) {}   // 函数作为参数// 函数作为返回值function var() {
   
       return function() {
   
           ... ...    }}

Of course, these are all basic concepts of JavaScript. But I think many people, even yourself reading this, may ignore these concepts. You can verify this with a simple example.

Let's first customize such a function.

function delay() {
   
       console.log("5000ms之后执行该方法.");}

What you need to do now is, if you are asked to combine the setTimeout method to delay the execution of the delay method by 5000ms, what should you do?

It's actually very simple, right? Just do it this way.

var timer = setTimeout(function() {
   
       delay();}, 5000);

So now comes the question. If you have a deep understanding of functions as first-class citizens, I think you will find that there are actually some problems with the above way of writing. So think about it, what is the problem?

Since a function can be passed into another function as a parameter, can we directly use delay as the first parameter of setTimeout without adding an extra layer of anonymous functions?

Therefore, in fact, the most correct solution should be written like this.

var timer = setTimeout(delay, 5000);

Of course, if you have thought of doing this in advance, congratulations, it means that you are more talented in JavaScript than ordinary people. In fact, the first bad method is used by many people, including people with many years of work experience, and they have not completely avoided it. And they don't even know what their problem is yet.

In future practice, you will encounter more similar scenarios. In order to verify readers' understanding, we might as well think about how to optimize the following code.

function getUser(path, callback) {
   
       return $.get(path, function(info) {
   
           return callback(info);    })}getUser("/api/user", function(resp) {
   
       // resp为成功请求之后返回的数据    console.log(resp);})

The principle of optimization is exactly the same as the example of setTimeout. I'm going to keep it a secret here and don't intend to tell you the conclusion. I just remind you that after getUser is optimized, there is only one line of code. It’s time to test everyone’s learning achievements^ ^.

Only use "expression" instead of "statement"

An "expression" is a simple operation process that always returns a value; a "statement" performs a certain operation and has no return value. Functional programming requires the use of expressions only, not statements. In other words, each step is a simple operation and has a return value.

Suppose that in our project, we need to change the background color of an element in many places. So we can encapsulate it like this.

var ele = document.querySelector(".test");function setBackgroundColor(color) {
   
       ele.style.backgroundColor = color;}// 多处使用setBackgroundColor("red");setBackgroundColor("#ccc");

We can clearly feel that setBackgroundColor encapsulates just one statement. This is not an ideal effect. Functional programming expects a function to have inputs and outputs. So good habits should be as follows.

function setBackgroundColor(ele, color) {
   
       ele.style.backgroundColor = color;    return color;}// 多处使用var ele = document.querySelector(".test");setBackgroundColor(ele, "red");setBackgroundColor(ele, "#ccc");

Understanding this can help us develop good habits when encapsulating functions.

pure function

A function that always produces the same output for the same input and has no side effects is a pure function.

The so-called "side effect" refers to the interaction between the inside and outside of a function (the most typical case is to modify the value of a global variable), producing results other than operations.

Functional programming emphasizes that there are no "side effects", which means that the function must remain independent. All functions are to return a new value and have no other actions, especially the values ​​of external variables must not be modified.

That is: as long as the same parameters are passed in, the returned results must be equal.

For example, we hope to encapsulate a function that can get the last item of the passed in array. Then it can be achieved in the following two ways.

function getLast(arr) {
   
       return arr[arr.length];}function getLast_(arr) {
   
       return arr.pop();}var source = [1, 2, 3, 4];var last = getLast(source); // 返回结果4 原数组不变var last_ = getLast_(source); // 返回结果4 原数据最后一项被删除

Although getLast and getLast_ can also obtain the last value of the array, getLast_ changes the original array. And when the original array is changed, when we call the method again, the result will be different. Such an unpredictable packaging method is very bad in our opinion. It will mess up our data very much. Among the data methods natively supported by JavaScript, there are also many impure methods. We need to be very vigilant when using them. We need to clearly know whether changes to the original data will leave hidden dangers.

var source = [1, 2, 3, 4, 5];source.slice(1, 3); // 纯函数 返回[2, 3] source不变source.splice(1, 3); // 不纯的 返回[2, 3, 4] source被改变source.pop(); // 不纯的source.push(6); // 不纯的source.shift(); // 不纯的source.unshift(1); // 不纯的source.reverse(); // 不纯的// 我也不能短时间知道现在source被改变成了什么样子,干脆重新约定一下source = [1, 2, 3, 4, 5];source.concat([6, 7]); // 纯函数 返回[1, 2, 3, 4, 5, 6, 7] source不变source.join("-"); // 纯函数 返回1-2-3-4-5 source不变

Reprinted from: Weidian Reading    https://www.weidianyuedu.com

Guess you like

Origin blog.csdn.net/hdxx2022/article/details/132661428