摘要:我这里有个不够准确但容易理解的说法,就是检查一个对象是否为另一个构造函数的实例,为了更容易理解,下面将全部以是的实例的方式来说。
underscore源码分析之整体架构
最近打算好好看看underscore源码,一个是因为自己确实水平不够,另一个是underscore源码比较简单,比较易读。
本系列打算对underscore1.8.3中关键函数源码进行分析,希望做到最详细的源码分析。
今天是underscore源码剖析系列第一篇,主要对underscore整体架构和基础函数进行分析。
首先,我们先来简单的看一下整体的代码:
// 这里是一个立即调用函数,使用call绑定了外层的this(全局对象) (function() { var root = this; // 保存当前环境中已经存在的_变量(在noConflict中用到) var previousUnderscore = root._; // 用变量保存原生方法的引用,以防止这些方法被重写,也便于压缩 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; var Ctor = function(){}; // 内部实现省略 var _ = function(obj) {}; // 这里是各种方法的实现(省略) // 导出underscore方法,如果有exports则用exports导出,如果 没有,则将其设为全局变量 if (typeof exports !== "undefined") { if (typeof module !== "undefined" && module.exports) { exports = module.exports = _; } exports._ = _; } else { root._ = _; } // 版本号 _.VERSION = "1.8.3"; // 用amd的形式导出 if (typeof define === "function" && define.amd) { define("underscore", [], function() { return _; }); } }.call(this))全局对象
这段代码整体比较简单,不过我看后来的underscore版本有一些小改动,主要是将var root = this;替换为下面这句:
var root = typeof self == "object" && self.self === self && self || typeof global == "object" && global.global === global && global || this;
这里增加了对self和global的判断,self属性可以返回对窗口自身的引用,等价于window,这里主要是为了兼容web worker,因为web worker中是没有window的,global则是为了兼容node,而且在严格模式下,立即执行函数内部的this是undefined。
void(0) ? undefined扫一眼源码,我们会发现在源码中并没有见到undefined的出现,反而是用void(0)或者void 0来代替的,那么这个void到底是什么?为什么不能直接用undefined呢?
关于void的解释,我们可以看这里:MDN
void 运算符通常只用于获取 undefined的原始值,一般使用void(0),因为undefined不是保留字,在低版本浏览器或者局部作用域中是可以被当做变量赋值的,这样就会导致我们拿不到正确的undefined值,在很多压缩工具中都是将undefined用void 0来代替掉了。
其实这里不仅是void 0可以拿到undefined,还有其他很多方法也可以拿到,比如0["ygy"]、Object.__undefined__、Object.__ygy__,这些原理都是访问一个不存在的属性,所以最后一定会返回undefined
也许有时候我们会碰到这样一种情况,_已经被当做一个变量声明了,我们引入underscore后会覆盖这个变量,但是又不想这个变量被覆盖,还好underscore提供了noConflict这个方法。
_.noConflict = function() { root._ = previousUnderscore; return this; }; var underscore = _.noConflict();
显而易见,这里正常保留原来的_变量,并返回了underscore这个方法(this就是_方法)
_接下来讲到了本文的重点,关于_方法的分析,在看源码之前,我们先熟悉一下_的用法。
这里总结的是我日常的用法,如果有遗漏,希望大家补充。
一种是直接调用_上的方法,比如_.map([1, 2, 3]),另一种是通过实例访问原型上的方法,比如_([1, 2, 3]).map(),这里和jQuery的用法很像,$.extend调用jQuery对象上的方法,而$("body").click()则是调用jQuery原型上的方法。
既然_可以使用原型上面的方法,那么说明执行_函数的时候肯定会返回一个实例。
这里来看源码:
// instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。 // 我这里有个不够准确但容易理解的说法,就是检查一个对象是否为另一个构造函数的实例,为了更容易理解,下面将全部以XXX是XXX的实例的方式来说。 var _ = function(obj) { // 如果obj是_的实例(这种情况我真的没碰到过) if (obj instanceof _) return obj; // 如果this不是_构造函数的实例,那就以obj为参数 new一个实例(相等于修改了_函数) if (!(this instanceof _)) return new _(obj); // 对应_([1,2,3])这种情况 this._wrapped = obj; };
我先从源码上来解释,这里可以看出来_是一个构造函数,我们都知道,我既可以在构造函数上面增加方法,还可以在原型上面增加方法,前者只能通过构造函数本身访问到,后者由于原型链的存在,可以在构造函数的实例上面访问到。
var Person = function() { this.name = "ygy"; this.age = 22; } Person.say = function() { console.log("hello") } Person.prototype.say = function() { console.log("world") } var ygy = new Person(); Person.say(); // hello ygy.say(); // world
所以我们平时用的_.map就是Person.say()这种用法,而_([1, 2, 3]).map则是ygy.say()这种用法。
在继续讲这个之前,我们再来复习一下原型的知识,当我们new一个实例的时候到处发生了什么?
首先,这里会先创建一个空对象,这个空对象继承了构造函数的原型(或者理解为空对象上增加一个指向构造函数原型的指针__proto__),之后会根据实例传入的参数执行一遍构造函数,将构造函数内部的this绑定到这个新对象中,最后返回这个对象,过程和如下类似:
var ygy = {}; ygy.__proto__ = Person.prototype // 或者var ygy = Object.create(Person.prototype) Person.call(ygy);
这样就很好理解了,要是想调用原型上面的方法,必须先new一个实例出来。我们再来分析_方法的源码:
_接收一个对象作为参数,如果这个对象是_的一个实例,那么直接返回这个对象。(这种情况我倒是没见过)
如果this不是_的实例,那么就会返回一个新的实例new _(obj),这个该怎么理解?
我们需要结合例子来看这句话,在_([1, 2, 3])中,obj肯定是指[1, 2, 3]这个数组,那么this是指什么呢?我觉得this是指window,不信你直接执行一下上面例子中的Person()?你会发现在全局作用域中是可以拿到name和age两个属性的。
那么既然this指向window,那么this肯定不是_的实例,所以this instanceof _必然会返回false,这样的话就会return一个new _([1, 2, 3]),所以_([1, 2, 3])就是new _([1, 2, 3]),从我们前面对new的解释来看,这个过程表现如下:
var obj = {} obj.__proto__ = _.prototype // 此时_函数中this的是new _(obj),this instanceof _是true,所以就不会重新return一个new _(obj),这样避免了循环调用 _.call(obj) // 实际上做了这一步: obj._wrapped = [1, 2, 3]
这样我们就理解了为什么_([1, 2, 3]).map中map是原型上的方法,因为_([1, 2, 3])是一个实例。
我这里再提供一个自己实现的_思路,和jQuery的实现类似,这里就不作解释了:
var _ = function(obj) { return new _.prototype.init(obj) } _.prototype = { init: function(obj) { this.__wrapped = obj return this }, name: function(name) { console.log(name) } } _.prototype.init.prototype = _.prototype; var a = _([1, 2, 3]) a.name("ygy"); // ygy
underscore中所有方法都是在_方法上面直接挂载的,并且用mixin方法将这些方法再一次挂载到了原型上面。不过,由于篇幅有限,mixin方法的实现会在后文中给大家讲解。
如果本文有错误和不足之处,希望大家指出。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/93476.html
摘要:在上篇文章整体架构分析中,我们讲过上面的方法有两种挂载方式,一个是挂载到构造函数上以的形式直接调用在后文上统称构造函数调用,另一种则是挂到上以的形式被实例调用在后文上统称原型调用。 underscore源码分析之基础方法 本文是underscore源码剖析系列的第二篇,主要介绍underscore中一些基础方法的实现。 mixin 在上篇文章underscore整体架构分析中,我们讲...
摘要:译立即执行函数表达式处理支持浏览器环境微信小程序。学习整体架构,利于打造属于自己的函数式编程类库。下一篇文章可能是学习的源码整体架构。也可以加微信,注明来源,拉您进前端视野交流群。 前言 上一篇文章写了jQuery整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 虽然看过挺多underscore.js分析类的文章,但总感觉少点什么。这也许就是纸上得来终觉浅,绝知此...
摘要:支持形式的调用这其实是非常经典的无构造,其实就是一个构造函数,的结果就是一个对象实例,该实例有个属性,属性值是。 前言 终于,楼主的「Underscore 源码解读系列」underscore-analysis 即将进入尾声,关注下 timeline 会发现楼主最近加快了解读速度。十一月,多事之秋,最近好多事情搞的楼主心力憔悴,身心俱疲,也想尽快把这个系列完结掉,也好了却一件心事。 本文...
摘要:的不能算作深复制,但它至少比直接赋值来得深一些,它创建了一个新的对象。它们的主要用途是对存在环的对象进行深复制。比如源对象中的子对象在深复制以后,对应于。希望这篇文章对你们有帮助深复制方法所谓拥抱未来的深复制实现参考资料 本文最初发布于我的个人博客:咀嚼之味 一年前我曾写过一篇 Javascript 中的一种深复制实现,当时写这篇文章的时候还比较稚嫩,有很多地方没有考虑仔细。...
阅读 3956·2021-11-11 10:58
阅读 3323·2021-09-26 09:46
阅读 1913·2019-08-30 15:55
阅读 978·2019-08-30 13:52
阅读 1946·2019-08-29 13:11
阅读 3026·2019-08-29 11:27
阅读 1518·2019-08-26 18:18
阅读 2623·2019-08-23 14:17