Underscore. Js is a well known tool library that I use often for processing objects, arrays, and more. In this article, Underscore source code architecture will be taken into account, and then a simple shelf will be written to mimic his example. His source code read down, I think his highlights are mainly as follows:

  • No constructor for new is required
  • Both static and instance method calls are supported
  • Support for chain calls

The example of this article has been uploaded to GitHub, the same repo also has all my blog posts and examples, please get a star:

Github.com/dennis-jian…

The outer layer is a self-executing function

The Underscore outer layer is a self-executing function that mounts _ to the window. This is a familiar pattern for many third-party libraries. If you don’t know how to get started with the source code, don’t know where the entry is, or don’t understand the outer structure of the framework, take a look at the framework source code: jQuery, Zepto, Vue, and Lodash-es for example, this article explains how to get started with the source code. This article focuses on the highlights of the Underscore source architecture without going into detail.

Don’t use the constructor of new

When we use third-party libraries, we often need to get an instance of them first. Some libraries require explicit calls with new, such as the native Promise, and some libraries can get instance objects without new, such as jQuery. Returning an instance without new is definitely not supported by native JS. Libraries with these features are wrapped in their own layers. Different libraries have different approaches to encapsulation, and we’ll look at two of them.

JQuery solution

In another article, I explained how jQuery returns an instance without using new, using jQuery, Zepto, Vue, and Lodash-es as examples. In addition, I also imitates the jQuery scheme to achieve a tool library of my own: learn to use: hand teach you a tool library and package published, incidentally solve the problem of inaccurate JS decimal calculation. Here’s a quick review of this scenario from my toolkit article:

// Create an fc function first. What we will return is an instance of fc
// But we don't want to bother the user new
// So we need to give it new in the constructor and return it directly
function FractionCalculator(numStr, denominator) {
  // Our new is actually fc.fn.init
  return new FractionCalculator.fn.init(numStr, denominator);
}

// fc.fn is the prototype of fc, which is a shorthand, and all instances will have this method
FractionCalculator.fn = FractionCalculator.prototype = {};

// This is the real constructor. This constructor is also very simple, which is to convert the passed argument to a fraction
// Then mount the converted score to this, where this is actually the returned instance
FractionCalculator.fn.init = function(numStr, denominator) {
  this.fraction = FractionCalculator.getFraction(numStr, denominator);
};

// New is init, in fact, return an instance of init
// To give the returned instance access to fc methods, point init's prototype to fc's prototype
FractionCalculator.fn.init.prototype = FractionCalculator.fn;

// Call new instead of new
FractionCalculator();
Copy the code

The Underscore the plan

The jQuery solution is to create a new object in the constructor and point the prototype of that object to the jQuery prototype so that the returned instance has access to the jQuery instance methods. The purpose is achievable, but the solution is rather long and the Underscore solution is much simpler:

function _(){
  if(! (this instanceof _)) {
    return new _();
  }
}

// Call the instance object directly _()
const instance = _();
console.log(instance);
Copy the code

The output of the above code is:

Constructor points to _(), indicating that this is really an instance of _. Let’s examine the code execution flow:

  1. call_ ()The “this” in this refers to the outer scope. In this case, window, since window is not an instance of _, will go inside if.If you’re not sure about this, read this article.
  2. If is callednew _()“, which will take an instance object and place itreturnTo go out.new _()Will be transferred to_ ()Method, but because new is called, this refers to the instance of new, so if can’t enter, end of execution.

Underscore subtly uses the pointer to this to determine if you are called new or generic, and if Underscore is generic, does the new for you and returns.

Both static and instance methods are supported

Those of you who have used Underscore will have noticed that Underscore supports both static and instance method calls for the same method. Here is the official example:

_.map([1.2.3].function(n){ return n * 2; });   // Map is called as a static method_ ([1.2.3]).map(function(n){ return n * 2; });   Map is called as an instance method
Copy the code

When we call a method as a static method, the data we need to process is the first argument; When it is called as an instance method, the data to be processed is passed as an argument to the constructor. Now let’s talk about how this works.

The easiest way to do this is to write two functions, one static and one instance. But if we did, the logic inside the two functions would be very similar, perhaps with slightly different arguments. This is certainly not what an elegant programmer should do. Underscore provides a way to write all methods into static methods and then use a unified function to mount all the static methods onto the prototype, making it an instance method. Let’s try to do it step by step.

Let’s write a static method

Let’s start by writing a simple map method that mounts to _ as a static method:

_.map = function(array, callback) {
    var result = [];
    var length = array.length;
    for(var i = 0; i< length; i++) {
      var res = callback(array[i]);
      result[i] = res;
    }

    return result;
  }
Copy the code

This method can be used directly, with the above example call as follows:

Map to instance methods

The Underscore uses a mixin method to map static methods to the prototype. The mixin method takes an object as a parameter and then copies all the methods on that object to the prototype. The specific process is as follows:

  1. Take the function properties from the parameters and stuff them into an array
  2. Walk through the array and set each item in it to the prototype
  3. When setting up the prototype, take care to handle the parameters of the instance method and the static method

Here’s the code:

_.mixin = function(obj) {
  // iterate over the function properties in obj
  _.each(_.functions(obj), function(item){
    // Fetch each function
    var func = obj[item];
    // Set a function of the same name on the prototype
    _.prototype[item] = function() {
      // Note that the data to be processed by the instance method is the parameter received by the constructor, and the code to modify the constructor comes later
      // Take the data as the first argument to the static method
      var value = this._wrapped;
      var args = [value];
      // Put the data and other parameters in an array as static method parameters
      Array.prototype.push.apply(args, arguments);
      // Call static methods with processed arguments
      var res = func.apply(this, args);
      // Return the result
      returnres; }}); }// Don't forget to call the above mixin after it is written, passing in _ itself as an argument
_.mixin(_);

// The constructor needs to receive the processed data
// and mount it to this, where this is the instance object
function _(value){
  if(! (this instanceof _)) {
    return new _(value);
  }

  this._wrapped = value;
}
Copy the code

The above _. A mixin (_); When called, all static methods on _ are mapped to the prototype, so that instances returned by _() have all static methods, allowing _ to support both calls. If you notice that there are two auxiliary methods (each and functions) in the code above, let’s implement these two methods as well:

// functions returns the names of all functions on an object by stuffing them into an array
_.functions = function(obj){
  var result = [];
  for(var key in obj) {
    if(typeof obj[key] === 'function'){ result.push(key); }}return result;
}

// each iterates through an array, each executing a callback
_.each = function(array, callback){
  var length = array.length;
  for(var i = 0; i < length; i++) { callback(array[i]); }}Copy the code

Mixin incidentally supports plug-ins

The mixin of Underscore not only allows it to support both static and instance methods, but also because it is a static method of _ itself, we can use it too. Official support for custom plugins is used in this way, here is the official example:

_.mixin({
  capitalize: function(string) {
    return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase();
  }
});
_("fabio").capitalize();   // Fabio
Copy the code

The mixin method already supports custom methods as instance methods, but it’s a bit too short for static methods, so we’ll just add a line of code and assign the received argument to _ :

_.mixin = function(obj) {
  _.each(_.functions(obj), function(item){
    var func = obj[item];
    // Note that we also assign this method to _ as a static method, which fully supports custom plugins
    _[item] = func;
    _.prototype[item] = function() {
      var value = this.value;
      var args = [value];
      Array.prototype.push.apply(args, arguments);
      var res = func.apply(this, args);
      returnres; }}); }Copy the code

Support for chain calls

Chain calls are also common, such as jQuery dot dot, which I learned to use in another article: The key is to return the current instance after each instance method has been evaluated. For instance methods, the current instance is this. This approach also applies to Underscore, but Underscore needs to support more scenarios for its chained calls because of its own requirements and API structure:

  1. Instance methods of Underscore also support direct calls to return results rather than simply returning instances
  2. The static methods of Underscore also need to support chained calls

Instance methods support chained calls

Step by step, let’s first solve the problem of instance methods supporting chained calls. We have implemented the mapping of static methods to instance methods, and the return value of the instance method is the return value of the static method. To implement chained calls, we also need the instance method to return the current instance (i.e., this) after it evaluates, so we need a basis to determine whether we should return the result or the current instance. The basis is required from the user in Underscore, which explicitly calls the chain method. According to our analysis, chain should be very simple, give a basis to determine what the instance method should return, that is, set the current instance flag bit:

_.chain = function() {
  this._chain = true;
  return this;
}
Copy the code

Chain is a simple two lines of code, and then we use the _chain method to determine whether to return the result of the calculation or the current instance:

_.mixin = function(obj) {
  _.each(_.functions(obj), function(item){
    var func = obj[item];
    _[item] = func;
    _.prototype[item] = function() {
      var value = this._wrapped;
      var args = [value];
      Array.prototype.push.apply(args, arguments);
      var res = func.apply(this, args);
      // Check the chain call flag if it is a chain call
      // Mount the data to the instance and return the instance
      var isChain = this._chain;
      if(isChain) {
        // If the method is chain itself, do not update _wrapped, otherwise _wrapped will be changed to the return value of chain, which is an instance
        // This is a little ugly
        if(item ! = ='chain') {
          this._wrapped = res;
        }
        return this;
      }
      returnres; }}); }Copy the code

Let’s write another unique method to verify the chain call:

_.unique = function(array){
  var result = [];
  var length = array.length;
  for(var i = 0; i < length; i++) {
    if(result.indexOf(array[i]) === -1){ result.push(array[i]); }}return result;
}
Copy the code

Try a chain call:

We find that the result is correct, but the output is an instance, which is not what we want, so we need a method to print the actual result. This method can only be hung on the prototype, and cannot be written as a static method, otherwise it will go back to our mixin and return an instance:

_.prototype.value = function() {
  return this._wrapped;
}
Copy the code

Let’s try again:

Static methods support chained calls

Static methods need to support chained calls, and we need to make their return values accessible to the instance method. General case the return value of a static method can’t be returned instance, but we now have chain method, we directly make the method constructs a _ instance returns, the above instance methods support chain calls is to use the ready-made instance, return to this, but if the chain, returns a new instance is also compatible with the above, So chain is changed to:

_.chain = function(obj) {
  var instance = _(obj);
  instance._chain = true;
  return instance;
}
Copy the code

So our static method chain can be called as well, and the data is passed to the chain as an argument like any other static method:

Optimize the code

Here we are, but mixin functions still need to be optimized:

  1. var res = func.apply(this, args); This refers to the current instance, but when a method is called as a static method, such as _.map(), this refers to _, so this should be changed to _. We passed this because the chain operated on this. Now we have changed to create a new instance, so we don’t need to pass this, so we changed to the correct _.

  2. This._chain is set to true in apply, so it will go to if. Now we create a new instance, and when we go to apply, This._chain will only be true when calling the next instance method, so if can be removed.

    _.mixin = function(obj) {
      _.each(_.functions(obj), function(item){
        var func = obj[item];
        _[item] = func;
        _.prototype[item] = function() {
          var value = this._wrapped;
          var args = [value];
          Array.prototype.push.apply(args, arguments);
          var res = func.apply(_, args);
    
          var isChain = this._chain;
          if(isChain) {
            // if(item ! == 'chain') {
            this._wrapped = res;
            // }
            return this;
          }
          returnres; }}); }Copy the code
  3. Underscore also uses a single method to capture isChain judgments, which I do not do here.

conclusion

This article focuses on the architecture of the Underscore source code and implements a simple shelf of its own. The specific implementations of some of the variable names and methods may be different, but the principles are the same. By building this simple shelf, we actually learned:

  1. Construct instance objects without using new
  2. mixinHow do I extend static methods to prototypes
  3. By explicit invocationchainTo support chained calls to static and instance methods

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…