源码共读08:学习 underscore 源码整体架构

117 阅读6分钟

underscore Github

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是学习源码整体架构系列第二篇,链接: juejin.cn/post/684490…

从一个官方文档_.chain简单例子看起:

_.chain([1,2,3]).reverse().value()
// ---->[3,2,1] 

从例子看,支持链式调用

链式调用

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

这个函数就是传递obj调用_()。

_函数对象 支持OOP

var _ = function(obj) {
  if (obj instanceof _) return obj;
  if (!(this instanceof _)) return new _(obj);
  this._wrapped = obj;
}

如果参数obj已经是_的实例了,则返回obj。如果this不是_的实例,则手动new _(obj);再次new 调用时,把obj对象赋值_wrapper这个属性。也就是说最后得到的实例对象是这样的结构{wrapper: '参数obj'};它的原型(obj)._proto__是.prototype。

暂时无法在飞书文档外展示此内容

继续分析官方_chain示例,分三步:

var part1 = _.chain([1,2,3]);
var part2 = part1.reverse();
var part3 = part2.value();

// 没有后续part1.reverse()操作的情况下
console.log(part1) // { _wrapper: [1,2,3], _chain: true }
console.log(part2) // {_wrapper: [1,2,3], _chain: true}
console.log(part3) // [3,2,1]

搜索reverse,可以看到如下代码:

var ArrayProto = Array.prototype, ObjProto = Object.prototype;
// Add all mutator `Array` functions to the wrapper.
  // 遍历 数组Array.prototype 的这些方法,赋值到_.prototype
  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
    // 示例里`method`是reverse
    var method = ArrayProto[name];
    _.prototype[name] = function() {
      // 这里的obj 就是数组[1,2,3]
      var obj = this._wrapped;
      if (obj != null) {
        // arguments 是参数集合,指定reverse的this指向obj,参数是arguments,并执行这个函数。执行后obj就是[3,2,1]
        method.apply(obj, arguments);
        if ((name === 'shift' || name === 'splice') && obj.length === 0) {
          delete obj[0];
        }
      }
      // 关键函数 chainResult
      return chainResult(this, obj);
    };
  });
// Helper function to continue chaining intermediate results.
var chainResult = function(instance, obj) {
 // 如果实例中有_chain 为 true 这个属性,则返回实例 支持链式调用的实例对象  { _chain: true, this._wrapped: [3, 2, 1] },否则直接返回这个对象[3, 2, 1]。
 return instance._chain ? _(obj).chain() : obj;
};

if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];

从其他官方库的issue大概的意思是对兼容IE低版本的写法。可以参考issue:

I don't understand the meaning of this sentence.

why delete obj[0]

基于流的编程

至此算是分析完了链式调用_.chain()和_函数对象。这种数据存储在实例对象{_wrapper: '', _chain: true}中,_chain判断是否支持链式调用,来传递给下一个函数处理。这种做法基于流的编程。

最后数据处理完,要返回这个数据怎么办呢?underscore提供了一个value的方法。

_.prototype.value = function() {
  return this._wrapped
}

顺便提供了几个别名。toJSON、valueOf。_.prototype.toJSON = _.prototype.valueOf = _prototype.value;

还提供了toString的方法

_.prototype.toString = function() {
  return String(this._wrapped)
}

这里的String()与new String()效果是一样的,内部实现和_函数对象类似

var String = function() {
  if(!(this instanceof String)) return new String(obj);
}
var chainResult = function(instance, obj) {
  return instance._chain ? _(obj).chain() : obj;
}

chainResult函数中_(obj).chain(),是怎么实现链式调用呢?

而_(obj)返回的实例对象{_wrapped: obj},没有chain()方法。

肯定有地方挂载了这个方法到_.prototype上或其他操作,这就是_mixin()。

.mixin挂载所有的静态方法到.prototype,也可以挂载自定义的方法

_.mixin()混入。但侵入性太强,经常容易出现覆盖之类的问题。记得之前React有mixin功能,Vue也有mixin功能。但版本迭代更新后基本慢慢的都是不推荐或者不支持mixin。

_.mixin = function(obj) {
 // 遍历对象上的所有方法
 _.each(_.functions(obj), function(name) {
  // 比如 chain, obj['chain'] 函数,自定义的,则赋值到_[name] 上,func 就是该函数。也就是说自定义的方法,不仅_函数对象上有,而且`_.prototype`上也有
 var func = _[name] = obj[name];
 _.prototype[name] = function() {
  // 处理的数据对象
  var args = [this._wrapped];
  // 处理的数据对象 和 arguments 结合
  push.apply(args, arguments);
  // 链式调用  chain.apply(_, args) 参数又被加上了 _chain属性,支持链式调用。
  // _.chain = function(obj) {
  // var instance = _(obj);
  // instance._chain = true;
  // return instance;
  };
  return chainResult(this, func.apply(_, args));
 };
 });
 // 最终返回 _ 函数对象。
 return _;
};
_.mixin(_);

.mixin()把静态方法挂载了_.prototype上,也就是_.prototype.chain方法,也就是_.chain方法。

所以_.chain(obj)和_(obj).chain()效果一样,都能实现链式调用。

_.mixin挂载自定义方法

挂载自定义方法,举个例子:

_.mixin({
  log: function() {
    console.log('我被调用了')
  }
})
_.log(); // '我被调用了'
_().log(); // '我被调用了'

_.functions(obj)

_.functions = _.methods = function(obj) {
  var names = []
  for (var key in obj) {
    if(_.isFunction(obj[key])) names.push(key);
  }
  return names.sort();
}

.functions和.methods两个方法,遍历对象上的方法,放入一个数组,并且排序。返回排序后的数组。

underscore.js究竟在_和_.prototype挂载了多少方法和属性

再来看下underscore.js究竟挂载在_函数对象上有多少静态方法和属性,和挂载_.prototype上有多少方法和属性。

使用for in 循环一试便知,看如下代码:

var staticMethods = [];
var staticProperty = [];
for(var name in _){
 if(typeof _[name] === 'function'){
  staticMethods.push(name);
 } else{
  staticProperty.push(name);
 }
}
console.log(staticProperty); // ["VERSION", "templateSettings"] 两个
console.log(staticMethods); // ["after", "all", "allKeys", "any", "assign", ...] 138
var prototypeMethods = [];
var prototypeProperty = [];
for(var name in _.prototype){
 if(typeof _.prototype[name] === 'function'){
  prototypeMethods.push(name);
 } else{
  prototypeProperty.push(name);
 }
}
console.log(prototypeProperty); // []
console.log(prototypeMethods); // ["after", "all", "allKeys", "any", "assign", ...] 152

整体架构概况

匿名函数

(function() {

}())

这样保证不污染外界环境,同时隔离外界环境,不受外界影响内部函数。

外界访问不到里边的变量和函数,里面可以访问到外界的变量,但里面定义了自己的变量,则不会访问外界的变量。匿名函数将代码包裹在里面,防止与其他代码冲突和污染全局环境。自执行函数可以参考文章:[译] JavaScript:立即执行函数表达式(IIFE)

root处理

var root = typeof self == 'object' && self.self === self && self ||
 typeof global == 'object' && global.global === global && global ||
 this ||
 {};

支持 浏览器、node、Web Worker、node vm、微信小程序

导出

if (typeof exports != 'undefined' && !exports.nodeType) {
 if (typeof module != 'undefined' && !module.nodeType && module.exports) {
 exports = module.exports = _;
 }
 exports._ = _;
} else {
 root._ = _;
}

为了支持模块化,我们需要将_在合适的环境中作为模块导出,但是nodejs模块的API曾发生过改变。

// 早期版本
// add.js
exports.addOne = function(num) 
{  return num + 1}

// index.js
var add = require('./add');
add.addOne(2);
// 新版本
// add.js
module.exports = function(1){
    return num + 1
}

// index.js
var addOne = require('./add.js')
addOne(2)

为什么新版本还要使用**exports = module.exports = _**

这是因为在 nodejs 中,exports 是 module.exports 的一个引用,当你使用了 module.exports = function(){},实际上覆盖了 module.exports,但是 exports 并未发生改变,为了避免后面再修改 exports 而导致不能正确输出,就写成这样,将两者保持统一。

写个demo示意:

// demo1
// export 是module.exports 的一个引用
moduel.exports.num = '1'
console.log(exports.num) // '1'
export.num = '2'
console.log(module.exports.num) // '2'

// demo2
// addOne.js
module.exports = function(num) {
 return num + 1
}
export.num = '3'

// result.js 中引入addOne.js
var addOne = require('./addOne.js');
console.log(addOne(1)); // 2
console.log(addOne.num); // defined

// demo3
// addOne.js
exports = modeule.exports = function(num){
  return num + 1
}
exports.num = '3'

// result.js 中引入addOne.js
var addOne = require('./addOne.js');
console.log(addOne(1)); // 2
console.log(addOne.num); // '3'

最后为什么要进行一个 exports.nodeType 判断呢?这是因为如果你在 HTML 页面中加入一个 id 为 exports 的元素,比如:

<div id="exports"></div>

就会生成一个 window.exports 全局变量,你可以直接在浏览器命令行中打印该变量。

此时在浏览器中,typeof exports != 'undefined' 的判断就会生效,然后 exports._ = _,然而在浏览器中,我们需要将 _ 挂载到全局变量上,所以在这里,我们还需要进行一个是否是 DOM 节点的判断。

支持amd模块化规范

if (typeof define == 'function' && define.amd) {
 define('underscore', [], function() {
  return _;
 });
}

_.noConflict防冲突函数

源码:

// 暂存在root上,执行noConflict时再赋值回来
var previousUnderscore = root._
_.noConflict = function() {
  root._ previousUnderscore;
  return this;
}

使用

<script>
var _ = '我就是我,不一样的烟火,其他可不要覆盖我呀';
</script>
<script src="https://unpkg.com/underscore@1.9.1/underscore.js">
</script>
<script>
var underscore = _.noConflict();
console.log(_); // '我就是我,不一样的烟火,其他可不要覆盖我呀'
underscore.isArray([]) // true
</script>

总结

全文根据官方提供的链式调用的例子,.chain([1,2,3]).reverse().value(); 较深入的源码调试和追踪代码,分析链式调用(.chain())和((obj).chain())、OOP、基于流式编程、和.mixin()在.prototype挂载方法。最后架构整体分析,利于自己打造函数式编程类库。

引用文章:

此文章为2024年03月Day1源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。