本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是
学习源码整体架构系列第二篇,链接: 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.
基于流的编程
至此算是分析完了链式调用_.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源码共读,每一次脑海里闪过努力的念头,都是未来的你在向你求救。