摘要:所以,刚开始,我从源码比较短的包含注释只有行开始学习起。一般,在客户端浏览器环境中,即为,暴露在全局中。学习以后判断直接使用看起来也优雅一点滑稽脸。在的函数视线中,的作用执行一个传入函数次,并返回由每次执行结果组成的数组。
前言
最近在社区浏览文章的时候,看到了一位大四学长在寻求前端工作中的面经,看完不得不佩服,掌握知识点真是全面,无论是前端后台还是其他,都有涉猎。
在他写的文章中,有这么一句话,大概意思是,没有看过一个库或者框架的源码还敢出来混。然后自己心虚了一下,一直以来,都只是学习使用框架或库,或者在过程中有学习框架的思想,但并不深入。例如,在学习Vue.js中,我曾经去探索过Vue中的双向绑定是如何实现的,通过什么模式,什么API,作者的思想是什么,也曾经实现过简单版的双向绑定。
但是感觉自己在这方面并没有什么提高,尤其在原生JavaScript的学习中,一些不常用的API经常忘,思维也不够好。所以有了学习优秀的库的源码的想法,一方面能够学习作者的思想,提高自己的分析能力,另一方面我觉得如果能好好分析一个库的源码,对自己的提升也是有的。
所以,刚开始,我从源码比较短的underscore.js(包含注释只有1.5k行)开始学习起。
什么是underscoreUnderscore一个JavaScript实用库,提供了一整套函数式编程的实用功能,但是没有扩展任何JavaScript内置对象。它是这个问题的答案:“如果我在一个空白的HTML页面前坐下, 并希望立即开始工作, 我需要什么?“...它弥补了部分jQuery没有实现的功能,同时又是Backbone.js必不可少的部分。——摘自Underscore中文文档
我的学习之路是基于Underscore1.8.3版本开始的。
// Current version. _.VERSION = "1.8.3";作用域包裹
与其他第三方库一样,underscore最外层是一个立即执行函数(IIFE),来包裹自己的业务逻辑。一般使用IIFE有如下好处,可以创建一个独立的沙箱似的作用域,避免全局污染,还可以防止其他代码对该函数内部造成影响。(但凡在立即执行函数中声明的函数、变量等,除非是自己想暴露,否则绝无可能在外部获得)
(function(){ // ...执行逻辑 }.call(this))
_对象学习的点,当我们要写自己的库或者封装某个功能函数时,可以给自己的库或函数在最外层包裹一个立即执行函数,这样既不会受外部影响,也不会给外部添麻烦。
underscore有下划线的意思,所以underscore通过一个下划线变量_来标识自身,值得注意的是,_是一个函数对象或者说是一个构造函数,并且支持无new调用的构造的函数,所有API都会挂载在这个对象上,如_.each,_.map等
var _ = function(obj) { if(obj instanceof _) return obj; if(!(this instanceof _)) return new _(obj) //实例化 this._wrapped = obj }全局命名空间
underscore使用root变量保存了全局的this。
var root = this;
为了防止其他库对_的冲突或影响,underscore做了如下处理,
var previousUnderscore = root._ _.noConflict = function() { root._ = perviousUnderscore; return this; }执行环境判断
underscore 既能够服务于浏览器,又能够服务于诸如 nodejs 所搭建的服务端。
一般,在客户端(浏览器)环境中,_即为window._=_,暴露在全局中。若在node环境中,_将被作为模块导出,并且向后兼容老的API,即require。
if (typeof exports !== "undefined") { if (typeof module !== "undefined" && module.exports) { exports = module.exports = _; } exports._ = _ ; } esle { root._ = _; }缓存局部变量及快速引用
underscore本身用到了不少ES5的原生方法,在浏览器支持的条件下,underscore率先使用原生的ES5方法。如下代码所示,underscore通过局部变量来保存一些常用到的方法或者属性。
这样做有几个好处:
便于压缩代码
提高代码性能,减少在原型链中的查找次数
同时也可减少代码量,避免在使用时冗长的书写
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; var push = ArrayProto.push, slice = ArrayProto.slice, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; var nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind, nativeCreate = Object.create;undefined处理
在underscore中,有很多函数都会有一个context函数,也就是当前函数的执行上下文,underscore对其进行了处理,如果没有传入context即context为undefined,则返回原函数。
这里判断值为undefined用的是void 0,如下:
if (context === void 0) return func
作为一只涉猎尚浅的小白,查阅资料之后终于知道这里作者为什么要用void 0来做判断了。
详情可点链接了解,这样做更加安全可靠。
在还没看到这个代码时, 如果我要判断一个值是不是undefined,我会这样写
if (context === undefined) {}
但是,在发现作者的void 0之后,才发现这样写并不可靠,在JavaScript中,我们可以这样写:
args => { let undefined = 1 console.log(undefined) // => 1 if (args === undefined) { //... } }
如果这样写,undefined就被轻易地修改为了1,所以对于我们之后定义的undefined的理解有歧义。所以,在JavaScript中,把undefined直接解释为“未定义”是有风险的,因为它可能被修改。
处理类数组学习:以后判断undefined直接使用void 0, 看起来也优雅一点(滑稽脸)。
// getLength 函数 // 该函数传入一个参数,返回参数的 length 属性值 // 用来获取 array 以及 arrayLike 元素的 length 属性值 var getLength = property("length"); // 判断是否是 ArrayLike Object // 类数组,即拥有 length 属性并且 length 属性值为 Number 类型的元素 // 包括数组、arguments、HTML Collection 以及 NodeList 等等 // 包括类似 {length: 10} 这样的对象 // 包括字符串、函数等 var isArrayLike = function(collection) { var length = getLength(collection); return typeof length == "number" && length >= 0 && length <= MAX_ARRAY_INDEX; };对象创建的特殊处理
为了处理Object.create的跨浏览器的兼容性,underscore进行了特殊的处理。我们知道,原型是无法直接实例化的,因此我们先创建一个空对象,然后将其原型指向这个我们想要实例化的原型,最后返回该对象其一个实例。其代码如下:
var Ctor = function() {}; // 用于代理原型转换的空函数 var baseCreate = function(prototype) { if (!(_.isObject(prototype))) return {}; // 如果参数不是对象,直接返回空对象 if (nativeCreate) return nativeCreate(prototype); // 如果原生的对象创建可以使用,返回该方法根据原型创建的对象 // 处理没有原生对象创建的情况 Ctor.prototype = prototype; // 将空函数的原型指向要使用的原型 var result = new Ctor(); // 创建一个实例 Ctor.prototype = null; // 恢复Ctor的原型供下次使用 return result; // 返回该实例 };underscore中的迭代(iteratee)
在函数式编程中,使用更多的是迭代,而不是循环。
迭代:
var res = _.map([1,2], function(item){ return item * 2 })
循环:
var arr = [1,2] var res = [] for(var i = 0; i < arr.length; i++) { res.push(arr[i] * 2) }
在underscore中迭代使用非常巧妙,源码也写的非常好,通过传入的数据类型不同而选择不同的迭代函数。
首先,在underscore中_.map的实现如下:
_.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj) //(value, index, obj) } return results; }
可以看到,在_.map函数中的第二个参数iteratee,这个参数的格式可以是函数,对象,字符串。underscore会将其处理成一个函数,这将由回调函数cb来完成,我们来看一下cb的实现:
var cb = function(value, context, argCount) { // 是否用默认的迭代器 如果没有传入value 则返回当前迭代元素自身 if (value == null) return _.identity; // 如果value是一个回调函数, 则需要优化回调 优化函数为optimizeCb if (_.isFunction(value)) return optimizeCb(value, context, argCount); // 如果value是个对象, 则返回一个matcher进行对象匹配 if (_.isObject(value)) return _.matcher(value) // 否则, 如果value只是一个字面量, 则把value看做是属性名称, 返回一个对应的属性获得函数 return _.property(value); }
前面两个比较容易理解,看看当传入的数据格式为对象的情况,如果 value 传入的是一个对象,那么返回iteratee(_.matcher)的目的是想要知道当前被迭代元素是否匹配给定的这个对象:
var results = _.map([{name:"water"},{name: "lzb",age:13}], {name: "lzb"}); // => results: [false,true]
如果传入的是字面量,如数字,字符串等, 他会返回对应的key值,如下:
var results = _.map([{name:"water"},{name:"lzb"}],"name"); // => results: ["water", "lzb"];回调处理
在上面的cb函数中,我们可以看到,当传入的数据格式是函数,则需要通过optimizeCb函数进行统一处理,返回对应的回调函数,下面是underscore中optimizeCb函数的实现:
// 回调处理 // underscore 内部方法 // 根据 this 指向(context 参数) // 以及 argCount 参数 // 二次操作返回一些回调、迭代方法 var optimizeCb = function(func, context, argCount) { // // void 0 会返回纯正的undefined,这样做避免undefined已经被污染带来的判定失效 if (context === void 0) return func; switch (argCount == null ? 3 : argCount) { // 回调参数为1时, 即迭代过程中,我们只需要值 // _.times case 1: return function(value) { return func.call(context, value); }; case 2: return function(value, other) { return func.call(context, value, other); }; // 3个参数(值,索引,被迭代集合对象) // _.each、_.map (value, key, obj) case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; // 4个参数(累加器(比如reducer需要的), 值, 索引, 被迭代集合对象) // _.reduce、_.reduceRight case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } // 如果都不符合上述的任一条件,直接使用apply调用相关函数 return function() { return func.apply(context, arguments); }; }
optimizeCb 的总体思路就是:传入待优化的回调函数 func,以及迭代回调需要的参数个数argCount,根据参数个数分情况进行优化。
在underscore的_.times函数视线中,_times的作用执行一个传入iteratee函数n次,并返回由每次执行结果组成的数组。它的迭代过程iteratee只需要1个参数(当前迭代的索引)
_.times函数在underscore中的实现:
_.times = function(n, iteratee, context) { vat accum = Array(Math.max(0, n)); iteratee = optimizeCb(iteratee, context, 1); for (var i = 0; i < n; i++) accum[i] = iteratee(i); return accum; }
_.times的使用
function getIndex(index) { return index; } var results = _.times(3, getIndex); // => [0,1,2]
optimizeCb函数中当argCount的个数为2的情况并不常见,在_.each,_.map等函数中,argCount的值为3(value, key, obj),当argCount需要四个参数时,这四个参数的格式为:
accumulator:累加器
value:迭代元素
index:迭代索引
collection:当前迭代集合
underscore中reduce的实现如下:
/** * reduce函数的工厂函数, 用于生成一个reducer, 通过参数决定reduce的方向 * @param dir 方向 left or right * @returns {function} */ function createReduce(dir) { function iterator(obj, iteratee, memo, keys, index, length) { for(; index >= 0 && index < length; index += dir) { var currentKey = keys ? keys[index] : index; // memo 用来记录最新的 reduce 结果 // 执行 reduce 回调, 刷新当前值 memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; } return function(obj, iteratee, memo, context) { // 优化回调 iteratee = optimizeCb(iteratee, context, 4); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, index = dir > 0 ? 0 : length - 1; if (arguments.length < 3) { // 如果没有传入memo初始值 则从左第一个为初始值 从右则最后一个为初始值 memo = obj[keys ? keys[index] : index]; index += dir; } // return func return iterator(obj, iteratee, memo, keys, index, length); } }
例如在_.reduce、_.reduceRight中,argCount的值为4。看看underscore中_.reduce的使用例子
var sum = _.reduce([1,2,3,4], function(accumulator, value, index, collection){ return accumulator + value; }, 0) // => 10
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/91906.html
摘要:所以它与其他系列的文章并不冲突,完全可以在阅读完这个系列后,再跟着其他系列的文章接着学习。如何阅读我在写系列的时候,被问的最多的问题就是该怎么阅读源码我想简单聊一下自己的思路。感谢大家的阅读和支持,我是冴羽,下个系列再见啦 前言 别名:《underscore 系列 8 篇正式完结!》 介绍 underscore 系列是我写的第三个系列,前两个系列分别是 JavaScript 深入系列、...
摘要:本文同步自我得博客最近准备折腾一下,在事先了解了之后,我知道了对这个库有着强依赖,正好之前也没使用过,于是我就想先把彻底了解一下,这样之后折腾的时候也少一点阻碍。 本文同步自我得博客:http://www.joeray61.com 最近准备折腾一下backbone.js,在事先了解了backbone之后,我知道了backbone对underscore这个库有着强依赖,正好undersc...
摘要:译立即执行函数表达式处理支持浏览器环境微信小程序。学习整体架构,利于打造属于自己的函数式编程类库。下一篇文章可能是学习的源码整体架构。也可以加微信,注明来源,拉您进前端视野交流群。 前言 上一篇文章写了jQuery整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 虽然看过挺多underscore.js分析类的文章,但总感觉少点什么。这也许就是纸上得来终觉浅,绝知此...
摘要:所以经常会在一个源码中看到写法吧立即执行函数创建变量,保存全局根变量。 // ================立即执行函数================ // 使用(function(){}())立即执行函数,减少全局变量 // ----????----函数声明 function (){} 与函数表达式 var funName = function(){}----????---- /...
阅读 2842·2023-04-26 01:02
阅读 1862·2021-11-17 09:38
阅读 790·2021-09-22 15:54
阅读 2899·2021-09-22 15:29
阅读 888·2021-09-22 10:02
阅读 3432·2019-08-30 15:54
阅读 2007·2019-08-30 15:44
阅读 1585·2019-08-26 13:46