Recently, I was interested in functional programming, so I reviewed the relevant knowledge points about functions and learned ~~ together

Xiao gang teacher

  • Introduction to functions
  • New ES6 features of the function
  • Higher-order functions are commonly used

Introduction to functions

A function is a “subroutine” that can be called from external code.

In JS, functions are first-class citizens, because in addition to having their own properties and methods, functions can also be called as if they were programs. In JS, functions are actually objects, and each Function is an instance of the Function constructor, so the Function/variable name is actually a pointer to the Function object, and a variable name can only point to one memory address. This is why there are no overloads of functions in JS, because two functions with the same name will be overridden by the following function.

Function properties

  • length

The length attribute represents the number of named arguments that the function expects to receive, excluding undefined arguments.

  • name

The name attribute returns the name of the function. Returns the function name if it has one; If there is no function name, the name of the assigned variable or object property is returned.

  • prototype

Prototype properties are the prototype objects of functions. They are used to add common properties and methods to instances.

function SuperType(name){
  this.name = name
}
SuperType.prototype.sayName = function(){
  alert(this.name);
}
let instance1 = new SuperType('The Phantom Child')
let instance2 = new SuperType('Dreamer')
instance1.sayName === instance2.sayName // true
SuperType.prototype.constructor === SuperType // true
Copy the code

Both instances have the public method sayName. The constructor of the prototype object points to the constructor itself.

  • new.target

Es6 added the meta attribute new.target to determine whether a function is called using the new keyword. When the function is called using the new keyword, the value of new.target is the constructor. New. target is undefined when the function is not called by new.

  • Parameter and real

The difference between a parameter, which corresponds to a variable defined in a function, and an argument, which is passed in during a function call at run time.

Function declarations and function expressions

Functions can be created either by function declarations or by function expressions:

// Function declaration
function bar() {}
console.log(bar) / / ƒ bar () {}
// Function expression
var foo = function bar() {}
console.log(bar) // Uncaught ReferenceError: bar is not defined
// Call function expression immediately (IIFE)
(function bar(){}) ()console.log(bar) // Uncaught ReferenceError: bar is not defined
Copy the code

Simply put, a function declaration is a function in which function is the first word in the declaration, otherwise it is a function expression.

Var foo = function bar() {} foo is the variable name of the function, bar is the function name. There are differences between function and variable names: function names cannot be changed, but variable names can be reassigned; Function names can only be used inside a function, whereas variable names can be used within the scope of a function. The function declaration also creates a variable name that is the same as the function name.

function bar () {}
var foo = bar
bar = 1
console.log(bar) / / 1
console.log(foo) ƒ bar () {}
Copy the code

It can be seen that the bar function is assigned to the variable foo, and the foo variable still ƒ bar () {} even if the variable bar is re – assigned. So, even with function declarations, we usually call functions from outside the function using the variable name, not the function name. Never change the variable name of the function declaration easily, otherwise it will cause semantic difficulties to understand.

The most important difference between function declarations and function expressions is that function declarations have function promotion, while function expressions have only variable promotion (var declarations have variable promotion, let const declarations do not). Function promotion elevates the entire function to the top level of code when the engine parses the code. Variable promotion will only promote the variable name to the top of the code, the value of the variable name is undefined; And function promotion takes precedence over variable promotion, that is, if both variable A and function A exist in the code, then variable A overrides function A:

var a = 1
function a (){}
console.log(a) / / 1
Copy the code

The constructor

All functions in js except the arrow function can be used as constructors. But by convention, constructors should start with a capital letter. Js Object Array Function Boolean String Number are constructors. The constructor with the keyword new creates an instance object, as in: Let instance = new Object() creates an Object, let instance = new Function() creates a Function Object, let instance = new String() creates a String wrapper Object, and so on. In addition to generating an object, constructors can also be used to simulate inheritance.

New ES6 features of the function

Es6 is a major upgrade to JS, making js comfortable to use greatly improved. Es6 extends functions to make the experience of using functions more sour and cool. Its new functions are as follows :(in this paper, es6 refers to the general name of the version after ES2015)

Arrow function

Arrow functions are added in ES6, which greatly improves the comfort of writing functions.

/ / es5 writing
let f = function (v) { return v }
/ / es6 writing
let f = (v) = > { return v }
// If you have only one argument or block of code like this, you can omit the parentheses or curly braces, and the arrow is the return value of the function
let f= v= > v
// If you do not need a return value, use the void keyword
let f = (fn) = > void fn()
// Parentheses are required if there are no parameters
let f = (a)= > { console.log('Where are my parameters?')}// Function arguments are objects that can be destructively assigned to variables
const full = function ({ first, last }){ return first + last }
full({first: 'spectral'.last: ', according to the '}) // I'm not going to be able to do that
// The arrow function is easier to destruct, but the arguments must be in parentheses
const full = ({ first, last }) = > first + last
full({first: 'spectral'.last: ', according to the '}) // I'm not going to be able to do that
Copy the code

In addition to being easy to write, arrow functions have the following characteristics:

  • No one of their ownThis, super, argumentsnew.targetThese values inside the arrow function are taken directly from the peripheral non-arrow function at the time of definition and cannot be changed;
  • Arrow functionthisThe value is not affected by the call(), apply(), bind() methods: the arrow function does not have its ownthis;
  • Cannot be used as a constructor: since arrow functions do not have their ownthisThe constructor needs to have its ownthisPoints to an instance object, so if it passesnewKeyword calls using arrow functions are miscastUncaught TypeError: arrowFunction is not a constructor. And since it can’t be used as a constructor, the arrow function simply doesn’t have its ownprototypeProperties. Even if we add the arrow function manuallyprototypeProperty, which also cannot be used as a constructor;
  • Duplicate named arguments are not supported: Arrow functions do not support duplicate named arguments in strict or non-strict mode; Non-arrow functions cannot have duplicate named arguments only in strict mode.
  • You cannot use the yield command: therefore, arrow functions cannot be usedGeneratorfunction

The biggest thing about arrow functions is that they don’t have their own this. Because of this feature, arrow functions should not be used as methods on objects, since neither the dot call nor the call/bind/ayyly binding can change the arrow function’s this:

let obj = {
  arrow: (a)= > { return this.god },
  foo() { return this.god },
  god: 'Fairy Tale'
}
obj.foo() // select * from the following table.
obj.arrow() // undefined
obj.arrow.call(obj) // undefined
Copy the code

It’s this feature that makes it so much easier to use in vue and other frameworks. Since these frameworks usually bind vue instances to this of hook functions or methods, using arrow functions in these functions makes it easier to use this in nested functions rather than the old and unsyntactic let _this = this:

export default {
  data() {
    return {
      name: 'Fairy Tale'
    }
  },
  created() {
    console.log(this.name) // select * from the following table.
    setTimeout((a)= > {
      this.name = Good guy card
      console.log(this.name) // 'nice card'
      setTimeout((a)= > {
        this.name = 'Your mother sent you home for dinner.'
        console.log(this.name) // 'Your mother asked you to go home for dinner'
      }, 1000)},1000)}}Copy the code

As you can see, as long as it’s an arrow function, no matter how deep the nesting, this is always the same as this in the outer non-arrow function created hook function.

Function parameter default value

Default values for function arguments are handy for functions that require default values for their arguments:

// When parameters are set to default values, even if there is only one parameter, parentheses must be used
let f = (v = 'Fairy Tale') = > v
If the parameter is the last parameter, use the default value
f() // select * from the following table.
// If undefined is passed, the default value is used
f(undefined) // select * from the following table.
// Values passed in other than undefined will not use default values
f(null) // null
Copy the code

Default values can be used with destructively assigned values:

let f = ({ x, y = 1 }) = > { console.log(x, y) }
f({}) // undefined 1
f({ x: 2.y: 2 }) 2 / / 2
f({ x: 1 }) / / 1 1
// An object must be passed in, otherwise it will be thrown incorrectly
f() // Uncaught TypeError: Cannot destructure property `x` of 'undefined' or 'null'.

// You can also give the object a default argument
let f = ({ x = 1 , y = 1} = {}) = > { console.log(x, y) }
// The call can be passed without arguments, just like passing an empty object
f() / / 1 1
Copy the code

After the parameter specifies a default value, the length attribute of the function does not calculate the parameter. If the parameter to which the default value is set is not the last parameter, the length attribute does not count toward subsequent parameters. Rest parameters below also do not count toward the Length attribute. This is because the length attribute means that the function expects the number of arguments to be passed.

Once the default values of parameters are set, the parameters form a separate scope when the function is declared initialized. This is equivalent to using the default values of the parameters and enclosing the block-level scope declared by let:

var x = 1
function foo(x = x) {
  return x
}
foo() // Uncaught ReferenceError: Cannot access 'x' before initialization
// 'let x = x' will fail due to temporary dead zones
var x = 1
{
  let x = x
  function foo() {
    return x
  }
}
foo () // Uncaught ReferenceError: Cannot access 'x' before initialization

// Let's look at another example
var x = 1
function foo(x, y = function() { x = 2; }) {
  x = 3
  y()
  console.log(x)
}
foo() / / 2
x / / 1
// The above code is equivalent to
var x = 1
{
  let x
  let y = function() { x=2 }
  function foo() {
    x = 3
    y()
    console.log(x)
  }
}
foo() / / 2
x / / 1
Copy the code

Just think of the parentheses of the default parameters as the block-level scope of the LET declaration.

The remaining parameters

Residual parameters, as the name implies, are the set of residual parameters, so residual parameters cannot be followed by any parameters. The remaining arguments are the extension operator + variable name:

// The remaining arguments replace the pseudo-array object 'arguments'
let f = function(. args) { return args } // let f = (... arg) => arg
let arr = f(1.2.3.4) // [1, 2, 3, 4]
// You can also use the extension operator to expand an array as a function argumentf(... arr)// [1, 2, 3, 4]
Copy the code

Tail-call optimization

Tail Call optimization is when the last step of a function is to return and Call another function, so the last step of a function’s execution must be to return a function Call:

function f(x){
  return g(x)
}
Copy the code

The function call will create an “execution context” in the execution stack, and another function call will create another “execution context” and push it to the top of the stack. If the function is nested too much, the execution context of the function in the execution stack is stacked too much, and the memory can not be released, the real stack overflow may occur.

However, if a function call is the last step in the current function, there is no need to preserve the execution context of the outer function, because the parameter values of the function to be called have already been determined, and the inner variables of the outer function are no longer needed. Tail-call optimization is to delete the execution context of the external function when this condition is met, and only retain the execution context of the internal calling function.

Tail-call optimization is important for recursive functions (recursion is covered below).

Small indeed fortunate

  • ES2017 specifies that function parameters and arguments can end with a comma. Previously, neither function parameters nor argument can end with a comma.

  • ES2019 regulation Function. The prototype. The toString () to return to the same string of the original code, before the returned string will omit the annotation and Spaces.

  • ES2019 specifies that a catch can omit arguments, so we can now write: try{… }catch{… }

  • Es6 also introduces the Promise constructor and async functions to make asynchronous operations easier. Class inheritance has also been introduced. For a primer on ECMAScript 6, see Ruan Yifong.

Es6 is introduced to this, which is learned from Ruan Yifeng.

Higher-order functions are commonly used

Introduction to higher order functions

A higher-order function is one that has one of the following characteristics:

  1. Functions can be passed as arguments
  2. The function can be output as a return value

Js has many built-in higher-order functions, such as forEach map every some Filter reduce find findIndex, etc., which pass functions as parameters, namely callback functions:

[1.2.3.4].map(v= > v * 2) // [2, 4, 6, 8] returns the diploid group
[1.2.3.4].filter(v= >! (v %2)) // [2, 4] returns an array of even numbers
[1.2.3.4].findIndex(v= > v === 3) // 2 returns the index of the item whose first value is 3
[1.2.3.4].reduce((prev, cur) = > prev + cur) // 10 returns the sum of the items in the array
Copy the code

The common throttling function, for example, takes a function as an argument and returns another function in the function:


/ / image stabilization
function _debounce (fn, wait = 250) {
  let timer
  return function (. agrs) {
    if (timer) clearTimeout(timer)
    timer = setTimeout((a)= > {
      fn.apply(this, args)
    }, wait)
  }
}
/ / throttling
function _throttle (fn, wait = 250) {
  let last = 0
  return function (. args) {
    let now = Date.now()
    if (now - last > wait) {
      last = now
      fn.apply(this, args)
    }
  }
}
/ / application
button.onclick = _debounce (function () {... }) input.keyup = _throttle (function () {... })Copy the code

Throttling and stabilization functions both return another function within a function and use closures to save the required variables, avoiding contaminating the outer scope.

closure

The above throttling function uses closures. For a long time I had a superficial understanding of closures as “functions defined inside a function”. In fact, this is just one of the necessary conditions for closure formation. It wasn’t until I read Kyle’s javascript you Don’t Know the definition of closures that I realized:

Closures occur when a function can remember and access its lexical scope.

let single = (function(){
  let count = 0
  return {
    plus(){
      count++
      return count
    },
    minus(){
      count--
      return count
    }
  }
})()
single.plus() / / 1
single.minus() / / 0
Copy the code

This is a singleton pattern that returns an object and assigns the value to the variable single, which contains two functions plus and minus, both of which use the variable count in their lexical scope. Normally count and its execution context are destroyed at the end of the function execution, but because count is still in use by the external environment, it is not destroyed at the end of the function execution, resulting in closures. Each time single.plus() or single.minus() is called, the count variable in the closure is modified, and both functions retain references to their lexical scope.

A closure is a special function that accesses variables inside a function and keeps the values of those variables in memory and not cleared by garbage collection after a function is called.

Watch a classic Amway:

1 / / method
for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)}2 / / method
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)}Copy the code

In method 1, five timers are set in a loop. After one second, the timer callback function is executed to print the value of variable I. Needless to say, after a second I had increased to 5, so the timer printed 5 five times. (The timer did not find variable I in the current scope, so I was found in the global scope along the scope chain)

In method 2, as the LET of ES6 will create local scopes, five scopes are set in a cycle, and the distribution of variable I in the five scopes is 1-5. A timer is set in each scope to print the value of variable I after one second. One second later, the timers find variables I 1-5 from their respective parent scopes. This is a new way of using closures to solve the problem of variable exceptions in loops.

Closures are a regular feature of some of the most common utility functions, such as the Currization/composition/throttling/stabilization functions.

recursive

Recursion is calling itself in a function:

function factorial(n) {
  if (n === 1) return 1
  return n * factorial(n - 1)}Copy the code

The above is the factorial of a recursive implementation, and since the return value contains n, the execution environment of the outer function cannot theoretically be destroyed. But Chrome is so powerful that factorial(10000) has not exploded. It was a surprise to see thousands of call stacks in the browser:

Last call optimization: the last step of a function is to return and call another function. Using tail-call optimization in recursion becomes tail-recursion. The factorial function above is rewritten to tail recursion as follows:

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
Copy the code

This complies with the rules of tail-call optimization, and in theory there should now be only one call stack. After testing, chrome (version 76.0.3809.100) does not currently support tail-call optimization.

Instead, factorial(10000) will burst the stack because the execution context contains a total variable.

Combination function

See “What intermediate and Advanced Front ends must Know” to thoroughly understand function combinations

The compose function accepts several functions as arguments, with the output of each function as input to the next function, and the output of the last function as the final result. The implementation effect is as follows:

function compose(. fns){... } compose(f,g)(x)// equivalent to f(g(x))
compose(f,g,m)(x) // f(g(m(x))
compose(f,g,m)(x) // f(g(m(x))
compose(f,g,m,n)(x) // equivalent to f(g(m(n(x))...Copy the code

The implementation of combinatorial functions is simple:

function compose (. fns) {
  return function (. args) {
    return fns.reduceRight((arg , fn, index) = > {
      if (index === fns.length - 1) {
        returnfn(... arg) }return fn(arg)
    }, args)
  }
}
Copy the code

Note that the third parameter reduceRight, index, is also in reverse order.

The open closed principle: objects in software (classes, modules, functions, and so on) should be open for extension, but closed for modification.

The open and closed principle is one of the basic principles in our programming, and the componentized modular granulation developed by our front end in recent years also implies the open and closed principle. Based on this principle, the use of composite functions can help us achieve more applicable and extensible code.

Let’s say we have an application that does all sorts of string manipulation. To make it easier to call, we can wrap some of the string methods into pure functions:

function toUpperCase(str) {
    return str.toUpperCase()
}
function split(str){
  return str.split(' ');
}
function reverse(arr){
  return arr.reverse();
}
function join(arr){
  return arr.join(' ');
}
function wrap(. args){
  return args.join('\r\n')}Copy the code

If we wanted to convert a string let STR = ’emosewa si nijeuj’ toUpperCase and then reverse it, we could write join(reverse(split(toUpperCase(STR))))). Let str2 = ‘dlrow olleh’; join(reverse(split(toUpperCase(str2))))); Now that we have the combinatorial function, we can simply write:

let turnStr = compose(join, reverse, split, toUpperCase)
turnStr(str) // JUEJIN IS AWESOME
turnStr(str2) // HELLO WORLD
// You can also pass multiple parameters, see turnStr2
let turnStr2 = compose(join, reverse, split, toUpperCase, wrap)
turnStr2(str, str2) // HELLO WORLD JUEJIN IS AWESOME
Copy the code

There is also a pipe function that processes the data stream from left to right, that is, passing the parameters of the composite function backwards. It feels logical to pass the parameters, but executing it from right to left is more mathematical. Therefore, composite functions are recommended, and pipes are not introduced to avoid your choice of difficulty.

The function is currified

Currization is the technique of converting a multi-parameter function into a series of single-parameter functions. The implementation is that the Currization function will take several arguments, and instead of evaluating them immediately, it will continue to return a new function, save the arguments passed in as a closure, and then evaluate all of the arguments at once when they are actually evaluated.

Here’s a great in-depth article on currization, the kind I couldn’t write: Currization of JavaScript thematic functions.

Here’s one way to do it:

function sub_curry(fn) {
  var args = [].slice.call(arguments.1);
  return function() {
    return fn.apply(this, args.concat([].slice.call(arguments)));
  };
}
function curry(fn, length) {
  length = length || fn.length;
  var slice = Array.prototype.slice;
  return function() {
    if (arguments.length < length) {
      var combined = [fn].concat(slice.call(arguments));
      return curry(sub_curry.apply(this, combined), length - arguments.length);
    } else {
      return fn.apply(this.arguments); }}; }var fn = curry(function(a, b, c) {
  return [a, b, c];
});
fn("a"."b"."c") // ["a", "b", "c"]
fn("a"."b") ("c") // ["a", "b", "c"]
fn("a") ("b") ("c") // ["a", "b", "c"]
fn("a") ("b"."c") // ["a", "b", "c"]
Copy the code

This section looks difficult, suggest that the code copy to the browser to add a breakpoint debugging.

Common tools and functions are introduced here, the next functional programming staple please look forward to ~