During this period, I tried to summarize knowledge points through mind mapping, mainly focusing on some relatively important or difficult to understand content. Here are the same articles in the series:

  • Learn about Javascript objects, prototypes, and inheritance
  • “Mind mapping front end” elementary and intermediate front end worth collecting regular expression knowledge literacy

If you need to take a different look at closures, open the read closure directly, this time starting with the ECMAScript lexical environment, execution context.

This article summarizes the javascript function in the common knowledge, closures, contains the basic concept of this point problem, higher-order functions, currie, etc, write code that part is also full of dry goods, whether you are want to practice for the interview, or want to further understand the principle, the article should have you want to see the point, anyhow is still worth a look.

The usual, mind map first.

What is a function

In general, a function is a “subroutine” that can be called from external code (or, in the case of recursion, from an internal function). Like the program itself, a function consists of a series of statements called the function body. Values can be passed to a function that returns a value.

A function is first and foremost an object, and in javascript, a function is a first-class object. Functions can be executed (callable, which has an internal property [[Call]]), which is an essential feature of functions. In addition, a function can be assigned to a variable, can be a function argument, and can be the return value of another function.

Basic concept of function

The function name

The function name identifies the function. If a function is not anonymous, it should be given a function name.

  • Function names must comply with javascript identifier rules. They must start with a letter, underscore _, or dollar sign $, followed by a number, letter, underscore, or dollar sign.
  • Function names cannot use javascript reserved words, which are identifiers in javascript that have special meanings.
  • Functions should be named semantically, using a ver-object structure, a small hump, for examplegetUserName().validateForm().isValidMobilePhone().
  • For constructors, we usually write them in a big hump format (because constructors are strongly associated with the concept of a class).

Here are some unwritten conventions. Unwritten means it doesn’t have to be followed, but we can make development more efficient by following them.

  • __xxx__Stands for nonstandard method.
  • _xxxRepresents a private method.

Function parameters

parameter

Parameters are the convention list of parameters for function definition, wrapped in a pair of parentheses ().

As you can see from MDN, a function can take up to 255 arguments.

However, with too many parameters, the user is always prone to errors in reference. Therefore, for a large number of parameters, it is generally recommended that all parameters be combined into one object as properties or methods, and each parameter be used as properties or methods of the object. For example, the API provided by wechat applet is basically in this form of invocation.

wx.redirectTo(Object object)
Copy the code

The following is an example of an invocation:

wx.redirectTo({
  url: '/article/detail? id=1'.success: function() {},
  fail: function() {}})Copy the code

The number of parameters can be obtained from the function’s length attribute, as shown below.

function test(a, b, c) {}
test.length; / / 3
Copy the code

The arguments

Arguments are passed in when the function is called, and their values are determined before the function is executed.

Javascript does not specify the data types of parameters when defining functions. If you expect a function to be called with the correct data type, you must type the incoming parameters in the body of the function.

function add(a, b) {
  if (typeofa ! = ='number' || typeofb ! = ='number') {
    throw new Error("Arguments must be of numeric type.")}}Copy the code

The good news is that Typescript provides the ability to check data types, which somewhat protects against the unexpected.

The number of arguments can be obtained from the length property of the Arguments object in the function, as shown below.

The number of arguments is not necessarily the same as the number of parameters.

function test(a, b, c) {
  var argLength = arguments.length;
  return argLength;
}
test(1.2); / / 2
Copy the code

The default parameters

The default value of a function argument is undefined. If you don’t pass in an argument, the actual value of the argument will be undefined during the function’s execution.

ES6 also supports setting default values for parameters when a function is declared.

function add(a, b = 2) {
    return a + b;
}
add(1); / / 3
Copy the code

In the add function above, argument b is specified with a default value of 2. So, even if you don’t pass the second parameter b, you get the expected result.

If a function has multiple arguments and we want to pass no value to one of the middle arguments, then the value of the argument must be explicitly specified as undefined, otherwise we expect the value passed to the next argument to be passed to the middle argument.

function printUserInfo(name, age = 18, gender) {
  console.log(` name:${name}Age:${age}Gender:${gender}`);
}
// Use it correctly
printUserInfo('Bob'.undefined.'male');
// Error, 'male' was incorrectly passed to the age argument
printUserInfo('Bob'.'male');
Copy the code

PS: Note that if you want to use the default value of the parameter, be sure to pass undefined, not null.

Of course, we can also determine the parameter data type in the body of the function, to prevent parameter misuse.

function printUserInfo(name, age = 18, gender) {
  if (typeof arguments[1= = ='string') {
    age = 18;
    gender = arguments[1];
  }
  console.log(` name:${name}Age:${age}Gender:${gender}`);
}

printUserInfo('bob'.'male'); // Name: Bob, age: 18, gender: male
Copy the code

That way, the logic of the function doesn’t mess up.

The remaining parameters

The remaining arguments syntax allows us to represent an indefinite number of arguments as an array.

Remaining parameters pass the remaining syntax… Aggregate multiple parameters into an array.

function add(a, ... args) {
  return args.reduce((prev, curr) = > {
    return prev + curr
  }, a)
}
Copy the code

There are three main differences between the remaining arguments and the Arguments object:

  • The remaining arguments contain only arguments that have no corresponding parameters, andargumentsThe object contains all the arguments passed to the function.
  • argumentsThe object is not a true array, and the remaining parameters are trueArrayInstance, which means you can use all array methods directly on it, such assort.map.forEachorpop. whileargumentsNeed to borrowcallTo implement, for example[].slice.call(arguments).
  • argumentsObjects have additional properties (e.gcalleeProperties).

The remaining syntax and the expansion operator look similar, but functionally they are completely opposite.

Rest syntax looks exactly the same as expansion syntax, except that the remaining arguments are used to deconstruct arrays and objects. In a sense, residual syntax is the opposite of expansion syntax: expansion expands an array into its individual elements, while residual syntax collects and “condenses” multiple elements into a single element.

arguments

The actual arguments to the function are stored in an array-like object called Arguments.

ArrayLike objects have a non-negative length property and can be indexed from 0 to look like arrays, such as NodeList, but ArrayLike objects do not have built-in methods for arrays, such as push, pop, forEach, and map.

We can try it out by looking at any website and typing in the console:

var linkList = document.querySelectorAll('a')
Copy the code

We get a NodeList, which we can also access with numeric subscripts, such as linkList[0].

But NodeList is not an array, it’s a class array.

Array.isArray(linkList); // false
Copy the code

Returning to the topic, Arguments is also an array of classes. The length of arguments is determined by the number of arguments, not by the number of parameters.

function add(a, b) {
  console.log(arguments.length);
  return a + b;
}
add(1.2.3.4);
// Print 4 instead of 2
Copy the code

Arguments is also an object associated with strict mode.

  • inNon-strict modeNext,argumentsElements and function arguments are all references to the same value, rightargumentsWill directly affect the function parameters.
function test(obj) {
  arguments[0] = 'The argument passed in was an object, but I changed it to a string.'
  console.log(obj)
}
test({name: 'jack'})
// Print a string, not an object
Copy the code
  • inStrict modeNext,argumentsIs a copy of the function arguments, rightargumentsThe function parameters are not affected by the modification of. butargumentsCan’t be reassigned. About that, I’m inInterpreting closures, this time starting with the ECMAScript lexical environment, execution contextIn this articleImmutable bindingIt’s mentioned from time to time. In strict mode, it cannot be usedarguments.callerandarguments.callee, which limits the ability to detect the call stack.

The body of the function

A FunctionBody is the body of a function, in which the function code is wrapped by a pair of curly braces {}. The body of a function can be empty or consist of any javascript statement.

The calling form of the function

In general, functions can be called in one of the following four ways:

As an ordinary function

The function is called as a normal function, which is the common form of a function call.

function add(a, b) {
  return a + b;
}
add(); // Call the add function
Copy the code

In non-strict mode, when the function is executed, this refers to the global object, or the window object for the browser. In strict mode, the value of this is undefined.

Methods as objects

A function can also be a member of an object, in which case the function is often referred to as an object method. When a function is called as a method of an object, this refers to that object, and other member variables or methods of the object can be accessed through this.

var counter = {
  num: 0.increase: function() {
    this.num++;
  }
}
counter.increase();
Copy the code

As a constructor

A function becomes a constructor when used with the new keyword. Constructors are used to instantiate objects. The execution of constructors is roughly as follows:

  1. First create a new object, the new object’s__proto__Property that points to the constructorprototypeProperties.
  2. At this point the constructorthisPoint to this new object.
  3. Execute the code in the constructor, usually bythisAdds a new member property or method to a new object.
  4. Finally, return the new object.

Instantiating an object can also be made easier by a few tricks, such as returning another object in a constructor, which jQuery does brilliantly. The interviewer really asks: Implementation of new and no new instantiation.

Call, apply

Apply and Call are the prototype methods of Function objects mounted on function.prototype. With these two methods, we can explicitly bind a this as the call context and also set the parameters of the function call.

The difference between apply and Call is that the argument form is different. The apply method accepts an array of arguments, and the call method accepts a list of arguments.

someFunc.call(obj, 1.2.3)
someFunc.apply(obj, [1.2.3])
Copy the code

Note that when using call or apply in non-strict mode, if the first argument is null or undefined, the function executes with this pointing to the global object (window in the browser environment). If the first argument is specified as a primitive value, the primitive value is wrapped. This is covered again in the hand-written code below.

Call is an important method for implementing inheritance. In the subclass constructor, the superclass constructor is called with a call so that the object instance gets properties or methods from the superclass constructor.

function Father() {
  this.nationality = 'Han';
};
Father.prototype.propA = 'I am a property on a superclass stereotype';
function Child() {
  Father.call(this);
};
Child.prototype.propB = 'I'm a property on a subclass stereotype.';
var child = new Child();
child.nationality; // "Han"
Copy the code

call, apply, bind

Call, apply, and bind can all be called to this. The difference is that apply and call call call this function directly after binding this, while bind returns a new function, which is not called directly, and can be called when the programmer decides.

The syntax of bind is as follows:

function.bind(thisArg[, arg1[, arg2[, ...]]])
Copy the code

Bind’s arg1, arg2… Are the preset arguments for the new function (preset arguments are optional). Of course, a new function can continue to append parameters as it executes.

Write a call, apply, bind

When it comes to call, apply, and bind, it’s hard to avoid the topic of handwritten code. Writing code by hand isn’t just for interviews, it’s a great way to clear your head and get into the basics. Handwritten code must not be copied, if there is no thought, you can refer to the code of others to sort out the thought, and then write the code independently in accordance with the thought, and then verify to see if there is any defect, so as to gain, or forget quickly, only a short time to deal with.

So how do you write code smoothly? The first is to understand the role of a piece of code, can start from the official definition and description of it, but also pay attention to some special cases.

Call, for example, is the prototype method of a function object that binds this to the argument and executes the function. The calling form is as follows:

function.call(thisArg, arg1, arg2, ...)
Copy the code

So let’s do it slowly, and we’ll call the function we’re going to implement myCall. First, myCall is a function that takes thisArg as its first argument, and other arguments starting with the second optional argument, arg1, as its arguments.

There are a lot of details to consider, and I’ve outlined them:

  1. Consider whether it is a strict pattern. If not in strict mode, forthisArgSpecial treatment.
  2. How to judge strict mode?
  3. thisArgAfter it is processed, the non-null judgment is made, and then the call is made as a method or as a normal function.
  4. When the target function is called as a method, how can it not override the original properties of the object?

The implementation code is as follows, please look carefully at the comments I wrote, this is the main idea!

// First apply is a method on function.prototype
Function.prototype.myCall = function() {
  // Since the number of arguments to the target function is variable, the parameters are not written here
  // We actually get all the arguments from the arugments object
  // The first argument is the binding of this
  var thisArg = arguments[0];
  // The next step is to determine whether it is a strict mode
  var isStrict = (function(){return this= = =undefined} ())if(! isStrict) {// If thisArg is null or undefined in non-strict mode, you need to make thisArg a global object
    if (thisArg === null || thisArg === undefined) {
      // Obtain global objects in both browser and Node environments
      thisArg = (function(){return this}} ())else {
      // If it is any other primitive value, it needs to be wrapped as an object by the constructor
      var thisArgType = typeof thisArg
      if (thisArgType === 'number') {
       thisArg = new Number(thisArg)
      } else if (thisArgType === 'string') {
        thisArg = new String(thisArg)
      } else if (thisArgType === 'boolean') {
        thisArg = new Boolean(thisArg)
      }
    }
  }
  // Intercept the remaining parameters starting at index 1
  var invokeParams = [...arguments].slice(1);
  // The next step is to call the target function. How to get the target function?
  // In fact, this is the target function, since myCall is called as a method. This of course refers to the calling object, which is the target function
  // This assignment process is done to make the semantics a little clearer
  var invokeFunc = this;
  // If thisArg is still null or undefined, then the object is in strict mode and the first argument is not specified or the value of the first argument is null or undefined, then execute the target function as if it were a normal function and return the result
  if (thisArg === null || thisArg === undefined) {
    returninvokeFunc(... invokeParams) }// Otherwise, make the target function a member method of thisArg object and then call it
  // Intuitively, you can assign the target function directly to an object property, such as the func property, but it is possible that the func property itself exists on thisArg
  // Therefore, in order to prevent overwriting thisArg object's original properties, you must create a unique property name. This can be implemented using Symbol, or if the environment does not support Symbol, a unique value can be constructed using the UUID algorithm.
  var uniquePropName = Symbol(thisArg)
  thisArg[uniquePropName] = invokeFunc
  // Return the result of execution of the target function
  returnthisArg[uniquePropName](... invokeParams) }Copy the code

After writing and thinking for a while, I suddenly found a place to consider a little redundant.

// If thisArg is null or undefined in non-strict mode, you need to make thisArg a global object
if (thisArg === null || thisArg === undefined) {
  // Obtain global objects in both browser and Node environments
  thisArg = (function(){return this}} ())else {
Copy the code

In this case, we don’t need to deal with thisArg, because after the code executes the function, the target function is executed as a normal function, so this naturally points to the global object! So this code can be deleted!

To test myCall’s reliability, I wrote a simple example:

function test(a, b) {
  var args = [].slice.myCall(arguments)
  console.log(arguments, args)
}
test(1.2)

var obj = {
  name: 'jack'
};
var name = 'global';
function getName() {
  return this.name;
}
getName();
getName.myCall(obj);
Copy the code

I can’t guarantee that my myCall method is bug-free, but it’s a lot to consider. Even in the interview process, the interviewer’s main focus is on your thinking and consideration of the comprehensiveness of the problem, if this level does not satisfy the interviewer, it will not be able to do……

After understanding handwritten call, handwritten Apply also naturally by analogy, as long as two points can be noted.

  • myApplyThe second parameter accepted is an array.
  • Consider the case where the actual call does not pass the second argument or the second argument is not an array.

Go directly to the code:

Function.prototype.myApply = function(thisArg, params) {
  var isStrict = (function(){return this= = =undefined} ())if(! isStrict) {var thisArgType = typeof thisArg
    if (thisArgType === 'number') {
     thisArg = new Number(thisArg)
    } else if (thisArgType === 'string') {
      thisArg = new String(thisArg)
    } else if (thisArgType === 'boolean') {
      thisArg = new Boolean(thisArg)
    }
  }
  var invokeFunc = this;
  // Handle the second argument
  var invokeParams = Array.isArray(params) ? params : [];
  if (thisArg === null || thisArg === undefined) {
    returninvokeFunc(... invokeParams) }var uniquePropName = Symbol(thisArg)
  thisArg[uniquePropName] = invokeFunc
  returnthisArg[uniquePropName](... invokeParams) }Copy the code

Test this with the more common math. Max:

Math.max.myApply(null[1.2.4.8]);
// The result is 8
Copy the code

The following is a hand-written bind, first of all, to clarify the differences between bind and call, apply where.

  • bindReturns a new function.
  • This new function has preset parameters.

Okay, so let’s write the code.

Function.prototype.myBind = function() {
  // Save this to bind
  var boundThis = arguments[0];
  // Get the preset parameters
  var boundParams = [].slice.call(arguments.1);
  // Get the target function of the binding
  var boundTargetFunc = this;
  if (typeofboundTargetFunc ! = ='function') {
    throw new Error('The target of the binding must be the function')}// Return a new function
  return function() {
    // Get the parameters passed in at execution time
    var restParams = [].slice.call(arguments);
    // Merge parameters
    var allParams = boundParams.concat(restParams)
    // When the new function is executed, it returns the result by executing the bound target function
    return boundTargetFunc.apply(boundThis, allParams)
  }
}
Copy the code

I thought it was over, but when I read some materials, it was mentioned that handwritten bind needs to support new calls. On second thought, bind returns a new function, and it is possible that this function could be used as a constructor.

The first thing I thought about was, is it possible to tell directly whether a function is executed as a constructor? If you can tell, then the problem is relatively simple.

So I thought about the important thing about constructors, which is that in constructors, this refers to an instance of the object. So, I changed a version of the code using Instanceof.

Function.prototype.myBind = function() {
  var boundThis = arguments[0];
  var boundParams = [].slice.call(arguments.1);
  var boundTargetFunc = this;
  if (typeofboundTargetFunc ! = ='function') {
    throw new Error('The target of the binding must be the function')}function fBound () {
    var restParams = [].slice.call(arguments);
    var allParams = boundParams.concat(restParams)
    // use instanceof to determine if this is an instanceof fBound
    var isConstructor = this instanceof fBound;
    if (isConstructor) {
      // If so, it means that the call was made via new (bug, see below), so just pass the processed parameters to the binding target function and call it via new.
      return newboundTargetFunc(... allParams) }else {
      // If not, it is not called by new
      return boundTargetFunc.apply(boundThis, allParams)
    }
  }
  return fBound
}
Copy the code

Finally, I looked at the polyfill for the BIND function provided by MDN and saw that the idea was a little different, so I compared it with an example.

function test() {}
var fBoundNative = test.bind()
var obj1 = new fBoundNative()
var fBoundMy = test.myBind()
var obj2 = new fBoundMy()
var fBoundMDN = test.mdnBind()
var obj3 = new fBoundMDN()
Copy the code

I find that my writing looks even more like native bind. Instantly doubt yourself, but suddenly did not find the obvious bug……

Finally I realized a big problem. Obj1 is an instance of fBoundNative, obj3 is an instance of fBoundMDN, but obj2 is not an instance of fBoundMy (in fact, obj2 is an instance of Test).

obj1 instanceof fBoundNative; // true
obj2 instanceof fBoundMy; // false
obj3 instanceof fBoundMDN; // true
Copy the code

If I want to extend the prototype properties or methods on fboundmy.prototype, obj2 cannot inherit them. So the most direct and effective way to do this is to use inherited methods, although it does not achieve the effect of native bind, but is sufficient. So I refer to MDN to change a version.

Function.prototype.myBind = function() {
  var boundTargetFunc = this;
  if (typeofboundTargetFunc ! = ='function') {
    throw new Error('The target of the binding must be the function')}var boundThis = arguments[0];
  var boundParams = [].slice.call(arguments.1);
  function fBound () {
    var restParams = [].slice.call(arguments);
    var allParams = boundParams.concat(restParams)
    return boundTargetFunc.apply(this instanceof fBound ? this : boundThis, allParams)
  }
  fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)
  return fBound
}
Copy the code

The two most important points here are to handle the prototype chain and understand the process of constructing instances in bind.

  • Prototype chain processing
fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)
Copy the code

This line of code with a | | operators, | | the ends of the fully considering the myBind function of two possible ways of calling.

  1. Regular function binding
function test(name, age) {
  this.name = name;
  this.age = age;
}
var bound1 = test.myBind('Ming')
var obj1 = new bound1(18)
Copy the code

This kind of situation the fBound. Prototype prototype to boundTargetFunc. Prototype, in full compliance with our thinking.

  1. Direct use of the Function. The prototype. MyBind
var bound2 = Function.prototype.myBind()
var obj2 = new bound2()
Copy the code

This is equivalent to creating a new Function whose target Function is function.prototype. Is function. prototype a Function? Yes, look at it!

typeof Function.prototype; // "function"
Copy the code

I don’t yet know why the second invocation exists, but it exists, and since it exists, we support it.

  • Understand the process of constructing an instance in bind

The first step is to be clear about how new is implemented. If you’re not already aware of this, read my article about what interviewers really ask: Implementation of New and No New instantiation.

As before, you must first determine whether the call is made as a constructor. The core is this:

this instanceof fBound
Copy the code

Let’s use an example to analyze the new process again.

function test(name, age) {
  this.name = name;
  this.age = age;
}
var bound1 = test.myBind('Ming')
var obj1 = new bound1(18)
obj1 instanceof bound1 // true
obj1 instanceof test // true
Copy the code
  1. Execute the constructorbound1, is actually executionmyBindThe new function returned after executionfBound. First, a new object is createdobj1And,obj1Nonstandard properties of__proto__Point to thebound1.prototypeWhich is essentiallymyBindDeclared at execution timefBound.prototypeAnd thefBound.prototypeThe prototype oftest.prototype. So here’s where the prototype chain comes together!
  2. Constructor that executes,thisPoint to theobj1.
  3. Execute the constructor becausefBoundThe execution constructor is essentially the target function to execute the binding, which in this case istest. So if we call it as a constructor, we treat the instance object asthisTo pass totest.apply.
  4. By performingtestThe object instance is mountednameandageProperty, and you have a brand new object!

Finally, I attach the bind implementation written by Raynos. I feel that I have been “critically hit” again! Interested in studying the ultimate mystery of bind friends please click the link to view the source!

This points to the question

Analyzing the reference to this begins with determining the context in which the code is currently executing.

This in the global environment points to

In the global context, this points to the global object (depending on the host environment, browser is window, Node is global).

This in the function refers to

I’ve talked about this pointing in some detail in the previous article when I introduced the call form of the function, but I’ll summarize it briefly here.

The point to this in a function depends on how the function is called, and in some cases is influenced by strict patterns.

  • As a normal function call: In strict mode,thisThe value isundefined, in the non-strict mode,thisPoints to global objects.
  • As a method call:thisPoints to the owning object.
  • Called as a constructor:thisPoints to an instantiated object.
  • Call, apply, bind: if the first argument is specifiedthisArg.thisThe value isthisArg(if it is a raw value, it is wrapped as an object); If you don’t passthisArgTo judge strict mode, strict modethisisundefined, in non-strict modethisPoints to global objects.

Function declarations and function expressions

After tearing up the code for so long, give your brain a rest and watch something lighter.

Function declaration

Function declarations are independent function statements.

function test() {}
Copy the code

In the case of the same name, the function declaration should prevail over the variable declaration (in the claim stage).

Function expression

Function expressions are not independent function statements, often as part of an expression, such as an assignment expression.

Function expressions can be named or anonymous.

// Name the function expression
var a = function test() {}
// Anonymous function expressions
var b = function () {}
Copy the code

An anonymous function is a function that does not have a function name. It cannot be used by itself, but only as part of an expression. Anonymous functions are often used in the form of IIFE (immediate execution of function expressions).

(function(){console.log("I am an IIFE")} ())Copy the code

closure

I’ve already written a very detailed article on closures, which is my own original summary, and I recommend opening them directly, this time starting with the ECMAScript lexical environment and execution context.

PS: Before reading, you should have a simple understanding of some of ECMAScript5’s terms, such as Lexical Environment, Execution Context, etc.

Pure functions

  • Pure functions are idempotent (for the same parameters, the same result will be obtained at any time), which does not cause side effects.

  • The relationship between a pure function and the outside should all come from the function parameters. If a function directly depends on an external variable, it is not a pure function because the external variable is variable and the result of a pure function is not controlled.

/ / pure functions
function pure(a, b) {
  return a + b;
}
// Impure function
function impure(c) {
  return c + d
}
var d = 10;
pure(1.2); / / 3
impure(1); / / 11
d = 20;
impure(1); / / 21
pure(1.2); / / 3
Copy the code

The inertia function

I believe you have written such code when you are compatible with event listening.

function addEvent(element, type, handler) {
  if (window.addEventListener) {
    element.addEventListener(type, handler, false);
  } else if (window.attachEvent){
    element.attachEvent('on' + type, handler);
  } else {
    element['on'+ type] = handler; }}Copy the code

If you take a closer look, you’ll see that every time addEvent is called, an if-else judgment is made, which is clearly repetitive. This is where the lazy function comes in.

A lazy function means that the branch of a function’s execution is executed only the first time the function is called. What we use later is the result of this function execution.

Using lazy function thinking, we can modify the above code.

function addEvent(element, type, handler) {
  if (window.addEventListener) {
    addEvent = function(element, type, handler) {
      element.addEventListener(type, handler, false); }}else if (window.attachEvent){
    addEvent = function(element, type, handler) {
      element.attachEvent('on'+ type, handler); }}else {
    addEvent = function(element, type, handler) {
      element['on' + type] = handler;
    }
  }
  addEvent(element, type, handler);
}
Copy the code

This code may seem a little low, but it does reduce duplication of judgment. In this way, the true value of the function is determined the first time it is executed.

We can also use IIFE to determine the true value of the function in advance.

var addEvent = (function() {
  if (window.addEventListener) {
    return function(element, type, handler) {
      element.addEventListener(type, handler, false); }}else if (window.attachEvent){
    return function(element, type, handler) {
      element.attachEvent('on'+ type, handler); }}else {
    return function(element, type, handler) {
      element['on'+ type] = handler; }}} ())Copy the code

Higher-order functions

Functions are first-class citizens in javascript and can be passed as arguments to other functions, which makes their use full of possibilities.

Let’s take a look at wikipedia’s definition of a high-order Function:

In mathematics and computer science, a higher-order function is one that satisfies at least one of the following conditions:

  1. Accepts one or more functions as input
  2. Output a function

Now, you’re all aware that you’ve used a lot of higher-order functions. Some higher-order functions of arrays are used particularly frequently.

[1.2.3.4].forEach(function(item, index, arr) {
  console.log(item, index, arr)
})
[1.2.3.4].map(item= > ` little bro${item}`)
Copy the code

As you can see, the forEach and map passed in is a function. We can also encapsulate some reused higher-order functions ourselves.

We know that math. Max can find the largest value in the argument list. Many times, however, the data we need to deal with is not just 1, 2, 3, 4, but an array of objects.

Let’s say we have a requirement, we have an array of objects that represent people, and we want to select the oldest person from that array.

At that point, you need a higher-order function to do that.

/** * Determine the largest item in the array according to the evaluation condition@param {Array} Array arr *@param {String|Function} Iteratee returns an evaluation expression that evaluates the maximum item based on the value of an object property, such as item.age. You can also return evaluated expressions through custom functions. * /
function maxBy(arr, iteratee) {
    let values = [];
    if (typeof iteratee === 'string') {
        values = arr.map(item= > item[iteratee]);
    } else if (typeof iteratee === 'function') {
        values = arr.map((item, index) = > {
            return iteratee(item, index, arr);
        });
    }
    const maxOne = Math.max(... values);const maxIndex = values.findIndex(item= > item === maxOne);
    return arr[maxIndex];
}
Copy the code

Using this higher-order function, we can figure out who is the oldest person in the array.

var list = [
  {name: 'Ming'.age: 18},
  {name: 'little red'.age: 19},
  {name: 'xiao li'.age: 20}]// Select * from age; // Select * from age;
var maxItem = maxBy(list, 'age');
Copy the code

We can even define more complex evaluation rules, such as when we need to determine precedence based on an attribute of string type. At this point, you must pass a custom function as an argument.

const list = [
  {name: 'Ming'.priority: 'middle'},
  {name: 'little red'.priority: 'low'},
  {name: 'xiao li'.priority: 'high'}]const maxItem = maxBy(list, function(item) {
  const { priority } = item
  const priorityValue = priority === 'low' ? 1 : priority === 'middle' ? 2 : priority === 'high' ? 3 : 0
  return priorityValue;
});
Copy the code

MaxBy accepts arguments that should eventually be converted to a measurable value for Math.max, otherwise there is no comparability.

To understand higher-order functions like this, we can think of the function passed to the higher-order function as a middleware that preprocesses the data and then passes it to the higher-order function for further operation.

PS: After writing this sentence summary, suddenly feel quite reasonable, backhand to oneself a praise!

Currie,

How to implement an add function so that it can be flexibly called and passed to support the following call example?

add(1.2.3) / / 6
add(1) / / 1
add(1) (2) / / 3
add(1.2) (3) / / 6
add(1) (2) (3) / / 6
add(1) (2) (3) (4) / / 10
Copy the code

To answer such a question, or to understand what is currie.

In computer science, Currying is the technique of transforming a function that takes multiple arguments into one that takes a single argument (the first argument of the original function) and returns a new function that takes the rest of the arguments and returns a result.

This explanation may seem confusing, but here’s an example:

There is a summation function dynamicAdd() that takes any argument.

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) = > {
    return prev + curr
  }, 0)}Copy the code

Now you need to curiefy it into a new function that presets the first argument and can continue passing the rest of the arguments when called.

When I look at this, I feel a bit familiar. The preset parameters feature is very similar to bind. So let’s use the bind idea.

function curry(fn, firstArg) {
  // Return a new function
  return function() {
    // New function calls continue passing arguments
    var restArgs = [].slice.call(arguments)
    // Call the original function via apply
    return fn.apply(this, [firstArg, ...restArgs])
  }
}
Copy the code

Then let’s get a feel for currying through some examples.

// Currie, preset parameter 10
var add10 = curry(dynamicAdd, 10)
add10(5); / / 15
// Curlize, set parameter 20
var add20 = curry(dynamicAdd, 20);
add20(5); / / 25
// You can also add an add10 function that has already been curlized
var anotherAdd20 = curry(add10, 10);
anotherAdd20(5); / / 25
Copy the code

You can see that currying is the transformation of a function to get a new function with preset parameters. At the end of the day, when you call the new function, you’re actually calling the original function.

And the new functions that you get from curryization can continue to be curryized, which looks a little bit like a Russian doll.

In practice, there are also curryization variants that are not limited to presetting only one parameter.

function curry(fn) {
  // Save the preset parameters
  var presetArgs = [].slice.call(arguments.1)
  // Return a new function
  return function() {
    // New function calls continue passing arguments
    var restArgs = [].slice.call(arguments)
    // Call the original function via apply
    return fn.apply(this, [...presetArgs, ...restArgs])
  }
}
Copy the code

Function.protoyp. bind is actually a currie implementation. Not only that, many popular libraries make heavy use of currie ideas.

In practice, the parameters of the curryized function may be of fixed or indefinite length.

Currying with constant parameter length

Assuming that there exists an antiderivative function fn, which takes three arguments a, b, and c, then the function FN can be curifized at most three times (effectively binding the arguments once).

function fn(a, b, c) {
  return a + b + c
}
var c1 = curry(fn, 1);
var c2 = curry(c1, 2);
var c3 = curry(c2, 3);
c3(); / / 6
// There is no point in currying again. The original function takes only three arguments
var c4 = curry(c3, 4);
c4();
Copy the code

In other words, we can determine whether the execution time has reached by the number of parameters in the currie cache. So we have a general pattern of currie.

function curry(fn) {
  // Get the parameter length of the original function
  const argLen = fn.length;
  // Save the preset parameters
  const presetArgs = [].slice.call(arguments.1)
  // Return a new function
  return function() {
    // New function calls continue passing arguments
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    if (allArgs.length >= argLen) {
      // If there are enough arguments, execute the function
      return fn.apply(this, allArgs)
    } else {
      // Otherwise continue currying
      return curry.call(null, fn, ... allArgs) } } }Copy the code

In this way, our writing will support the following form.

function fn(a, b, c) {
  return a + b + c;
}
var curried = curry(fn);
curried(1.2.3); / / 6
curried(1.2) (3); / / 6
curried(1) (2.3); / / 6
curried(1) (2) (3); / / 6
curried(7) (8) (9); / / 24
Copy the code

Currying with variable length parameters

So with that out of the way, we can’t help but ask ourselves, what if the parameters of the function are of variable length? How can we curify that?

First of all, we need to understand that variable arguments mean that a function is declared without specifying specific arguments. Instead, arguments are obtained in the body of the function using arguments and then evaluated. Like this one.

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) => {
    return prev + curr
  }, 0)
}
Copy the code

Going back to the original question, how are all of the following forms of invocation supported?

add(1.2.3) / / 6
add(1) / / 1
add(1) (2) / / 3
add(1.2) (3) / / 6
add(1) (2) (3) / / 6
add(1) (2) (3) (4) / / 10
Copy the code

After thinking about it for a while, I found it difficult to support 1-n calls at the same time with variable parameter lengths. Add (1) can return a value directly after a single call, but it can also be followed by a call to Add (1)(2) as a function, or even continue with Add (1)(2)(3). So when we implement add, do we return a function or a value? This is a bit of a puzzle, and I can’t predict how this function will be called.

And we can take the above results to verify:

curried(1) (2) (3) (4);
Copy the code

Uncaught TypeError: curried(…) (…). (…). Is not a function, because after curried(1)(2)(3), the result is not a function, but a value, and a value, of course, is not allowed to continue as a function.

So if you want to support scenarios with variable parameters, a curlyzed function can return only one function, not one value, at the end of execution. At the same time, let the JS engine in the parsing of the result, can find the value we expect.

You may not understand this, good, say people! Our implementation of Curry should satisfy:

  1. thecurryI get a new function, and I don’t change this point.
// Curry is a function
var curried = curry(add);
Copy the code
  1. The new function still returns a result function.
// Curried10 is also a function
var curried10 = curried(10);
var curried30 = curried10(20);
Copy the code
  1. The resulting function can be parsed by the Javascript engine to get an expected value.
curried10; / / 10
Copy the code

Ok, the key point is 3, how to get the Javascript engine to parse as we expect it to, which brings us back to Javascript basics. ToString is used when parsing the original value of a function.

Console.log (fn) outputs the source of function fn as follows:

console.log ƒ (fn)fn(a, b, c) {
  return a + b + c;
}
Copy the code

So we can just rewrite toString and subtly implement our requirements.

function curry(fn) {
  // Save the preset parameters
  const presetArgs = [].slice.call(arguments.1)
  // Return a new function
  function curried () {
    // New function calls continue passing arguments
    const restArgs = [].slice.call(arguments)
    const allArgs = [...presetArgs, ...restArgs]
    return curry.call(null, fn, ... allArgs) }/ / rewrite the toString
  curried.toString = function() {
    return fn.apply(null, presetArgs)
  }
  return curried;
}
Copy the code

In this way, all the magical uses of ADD are supported.

function dynamicAdd() {
  return [...arguments].reduce((prev, curr) = > {
    return prev + curr
  }, 0)}var add = curry(dynamicAdd);
add(1) (2) (3) (4) / / 10
add(1.2) (3.4) (5.6) / / 21
Copy the code

As for why toString is overwritten instead of valueOf, here is a suspense, you can think about it, but also welcome to communicate with me!

Summary of currie

Currying is a functional programming idea that you may not actually use very much, or very deeply, in your projects, but if you get the hang of it, maybe you’ll use it at some point in the future!

Roughly speaking, curryization has the following characteristics:

  • Concise code: Currie application in more complex scenarios, has the advantages of concise code, high readability.
  • Parameter reuse: Common parameters have been preconfigured through currying.
  • Delayed execution: A new function that returns a preset argument is not executed immediately, but is actually executed when the condition is met.
  • Pipelining programming: it is beneficial to use functions to assemble pipelining processes, without polluting the original function.

summary

This article is a very detailed article summarized by the author when reviewing function knowledge points. In understanding some obscure knowledge modules, I added some personal interpretation, I believe that for friends who want to dig into the details of some of the help. If you found this article helpful, please pay attention and support it.