Home > Article > Web Front-end > A brief analysis of the overall architecture of Underscore
Foreword
Finally, the poster’s “Underscore Source Code Interpretation Series” underscore-analysis is coming to an end. If you pay attention to the timeline, you will find that the poster has recently accelerated the speed of interpretation. November is an eventful month. The author has been exhausted mentally and physically due to a lot of things happening recently. He also wants to finish this series as soon as possible, but he just has one thing on his mind.
This article is expected to be the penultimate article in the interpretation series, and the last article is obviously the summary. The full version of the poster's Underscore series interpretation is at https://github.com/hanzichi/u...
Regular call
The articles written before mostly focus on specific methods, specific knowledge details, and there are also readers' comments and suggestions. The poster talks about the overall architecture. This is something that must be talked about, but the poster arranges it at the end, that is, in this article, because the poster feels that there is no big problem in understanding the specific methods without mastering the overall architecture.
Underscore most of the time is called in the form of _.funcName(xx, xx), which is also the calling method in the document.
_.each([1, 2, 3], alert);
The simplest way to implement it, we can regard _ as a simple object:
var _ = {}; _.each = function() { // ... };
In JavaScript, everything is an object. In fact, the _ variable in the source code is a method:
var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; };
Why is it a method? ?Let’s go see it next.
OOP
Underscore supports OOP form of calling:
_([1, 2, 3]).each(alert);
This is actually a very classic "no new construction", _ is actually a constructor, and the result of _([1, 2, 3]) is an object instance , this instance has a _wrapped attribute, and the attribute value is [1, 2, 3]. The instance needs to call the each method. If it does not have this method, it should come from the prototype chain, that is to say, there should be this method on _.prototype. So, how is the method mounted?
Method mounting
Now we The following two points have been made clear:
_ 是一个函数(支持无 new 调用的构造函数) _ 的属性有很多方法,比如 _.each,_.template 等等
Our goal is to allow the constructed instance of _ to also call these methods. If you think about it carefully, it's actually not difficult. We can traverse the attributes on _. If the attribute value type is a function, then hang the function on the prototype chain of _.
The _.mixin method used to accomplish this in the source code:
// Add your own custom functions to the Underscore object. // 可向 underscore 函数库扩展自己的方法 // obj 参数必须是一个对象(JavaScript 中一切皆对象) // 且自己的方法定义在 obj 的属性上 // 如 obj.myFunc = function() {...} // 形如 {myFunc: function(){}} // 之后便可使用如下: _.myFunc(..) 或者 OOP _(..).myFunc(..) _.mixin = function(obj) { // 遍历 obj 的 key,将方法挂载到 Underscore 上 // 其实是将方法浅拷贝到 _.prototype 上 _.each(_.functions(obj), function(name) { // 直接把方法挂载到 _[name] 上 // 调用类似 _.myFunc([1, 2, 3], ..) var func = _[name] = obj[name]; // 浅拷贝 // 将 name 方法挂载到 _ 对象的原型链上,使之能 OOP 调用 _.prototype[name] = function() { // 第一个参数 var args = [this._wrapped]; // arguments 为 name 方法需要的其他参数 push.apply(args, arguments); // 执行 func 方法 // 支持链式操作 return result(this, func.apply(_, args)); }; }); }; // Add all of the Underscore functions to the wrapper object. // 将前面定义的 underscore 方法添加给包装过的对象 // 即添加到 _.prototype 中 // 使 underscore 支持面向对象形式的调用 _.mixin(_);
The _.mixin method can add your own defined methods to the Underscore library:
_.mixin({ capitalize: function(string) { return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); } }); _("fabio").capitalize(); => "Fabio"
At the same time, Underscore also adds some Array native methods:
// Add all mutator Array functions to the wrapper. // 将 Array 原型链上有的方法都添加到 underscore 中 _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; method.apply(obj, arguments); if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; // 支持链式操作 return result(this, obj); }; }); // Add all accessor Array functions to the wrapper. // 添加 concat、join、slice 等数组原生方法给 Underscore _.each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { return result(this, method.apply(this._wrapped, arguments)); }; });
Chained calls
Underscore also supports chained calls:
// 非 OOP 链式调用 _.chain([1, 2, 3]) .map(function(a) {return a * 2;}) .reverse() .value(); // [6, 4, 2] // OOP 链式调用 _([1, 2, 3]) .chain() .map(function(a){return a * 2;}) .first() .value(); // 2
At first glance, it seems that there are two forms of chained calls, OOP and non-OOP. In fact, it is just one, _.chain([1, 2, 3]) The result of _([1, 2, 3]).chain() is the same. How to implement it? Let’s take a closer look at the chain method.
_.chain = function(obj) {
_.chain = function(obj) { // 无论是否 OOP 调用,都会转为 OOP 形式 // 并且给新的构造对象添加了一个 _chain 属性 var instance = _(obj); // 标记是否使用链式操作 instance._chain = true; // 返回 OOP 对象 // 可以看到该 instance 对象除了多了个 _chain 属性 // 其他的和直接 _(obj) 的结果一样 return instance; };
Let’s look at the result of _.chain([1, 2, 3]) and substitute the parameters into the function. In fact, it means constructing the parameters without new and then returning the instance. , except that the instance has an additional _chain attribute, and the others are exactly the same as direct _([1, 2, 3]). Let's look at _([1, 2, 3]).chain(), _([1, 2, 3]) returns a constructed instance. This instance has a chain method. Call the method, add the _chain attribute to the instance, and return the instance. object. Therefore, the effects of the two are consistent, and the results are converted to OOP.
Having said so much, it seems that we haven’t gotten to the point yet, how is it “chained”? Let’s take the following code as an example:
_([1, 2, 3]) .chain() .map(function(a){return a * 2;}) .first() .value(); // 2
When the map method is called, there may actually be a return value. Let’s take a look at the _.mixin source code:
// 执行 func 方法 // 支持链式操作 return result(this, func.apply(_, args));
result is an important internal helper function (Helper function):
// Helper function to continue chaining intermediate results. // 一个帮助方法(Helper function) var result = function(instance, obj) { // 如果需要链式操作,则对 obj 运行 chain 方法,使得可以继续后续的链式操作 // 如果不需要,直接返回 obj return instance._chain ? _(obj).chain() : obj; };
If a chain operation is required (the instance will have the _chain attribute), call the chain function on the operation result , so that chain calls can continue.