上一篇:
先解决上次留下的疑问,开始看到zepto.z[0]这个东西的时候,我很是不爽,看着它都不顺眼,怎么一个zepto的实例对象var test1=$('#items'); test__proto__ 指向的是zepto.z[0];之前看到过如下代码
zepto.Z.prototype = Z.prototype = $.fn // Export internal API functions in the `$.zepto` namespace zepto.uniq = uniq zepto.deserializeValue = deserializeValue $.zepto = zepto //返回内部的$对象 return $
根据以上代码可以得出出test1__proto__===$.fn 应该是true
既是zepto.z[0]就是$.fn
最终找到如下代码。代码上有英文注释, 让对象有看起来是数组,内部还给其加上了很多数组的行为。
$.fn ={} 但是constructor指向了zepto.Z ,还加了一个length: 0,
我尝试去掉length=0这个属性,得到如下结果
此时不再是zepto.z[0],而是zepto.z
如果再把constructor去掉,此时test__proto__指向Object; 其实最开始的时候$.fn={},本来就是赋值一个Object对象,只是手动改变了内部的constructor加上了length,
我决定来模拟一把,结构和zepto大体相同,声明了相应的c、z、fn。最后创建一个实例对象d,d的__proto__也指向了z[0];
这里折腾了一下,我发现如果fn这个Object对象,缺少forEach: emptyArray.forEach,reduce: emptyArray.reduce,push: emptyArray.push,sort: emptyArray.sort,splice: emptyArray.splice,indexOf: emptyArray.indexOf,其中的一些方法就不会显示z[0],直接会是z,感觉怪怪的。具体没去研究是哪几个方法。z和z[0]明显是不同的,一个是数组,一个就是普通对象。
进入本期主题,工具方法
先看官网上的API
$()这个不是工具方法,$.camelCase
$.camelCase('hello-there') //=> "helloThere"
$.camelCase('helloThere') //=> "helloThere"内部有很多地方均要调用这个方法,实现很简单基本,正则匹配替换 /-+(.)?/g, 前面可以有多个”-“后面(.)匹配一个字符,g全局匹配。
camelize = function(str) { return str.replace(/-+(.)?/g, function(match, chr) { return chr ? chr.toUpperCase() : '' }) }
$.contains
检查父节点是否包含给定的dom节点。
//父元素是否有某个子元素,原生有的支持contains就调用element.contains,不支持就循环判断子节点的父节点 $.contains = document.documentElement.contains ? function(parent, node) { return parent !== node && parent.contains(node) } : function(parent, node) { while (node && (node = node.parentNode)) if (node === parent) return true return false }
$.each
$.each方法用于遍历数组和对象,然后返回原始对象。它接受两个参数,分别是数据集合和回调函数,。回调函数返回 false 时停止遍历。 一般我们在不断的使用for的时候我们就可以考虑是否需要使用each来干掉for。
$.each = function(elements, callback) { var i, key //判断是否是数组 if (likeArray(elements)) { //为什么不 缓存length,这样每次都得去取 elements.length for (i = 0; i < elements.length; i++) if (callback.call(elements[i], i, elements[i]) === false) return elements } else { for (key in elements) if (callback.call(elements[key], key, elements[key]) === false) return elements } return elements }
function likeArray(obj) { return typeof obj.length == 'number' }
$.extend
$.extend(target, [source, [source2, ...]]) ⇒ target
$.extend(true, target, [source, ...]) ⇒ target v1.0+通过源对象扩展目标对象的属性,源对象属性将覆盖目标对象属性。默认情况下为,复制为浅拷贝(浅复制)。如果第一个参数为true表示深度拷贝(深度复制)。有的人没搞清楚深拷贝和浅拷贝的意思,结果掉坑里了。
function extend(target, source, deep) { for (key in source) //深拷贝 并且source[key]是Object或者数组;====source[key] source[key] 可以提前缓存,每次写着不累啊。zepto的代码始终看着不够爽,个人习惯吧 if (deep && (isPlainObject(source[key]) || isArray(source[key]))) { //如果source[key]是纯对象 但是target[key]不是纯对象 if (isPlainObject(source[key]) && !isPlainObject(target[key])) target[key] = {} //source[key]是数组 但是target[key]不是数组 if (isArray(source[key]) && !isArray(target[key])) target[key] = [] //递归 extend(target[key], source[key], deep) //不是深拷贝直接赋值给target } else if (source[key] !== undefined) target[key] = source[key] } // Copy all but undefined properties from one or more // objects to the `target` object. $.extend = function(target) { var deep, //转为数组 args = slice.call(arguments, 1) //第一个参数为boolean类型 if (typeof target == 'boolean') { deep = target //取到args第一个对象 target = args.shift() } args.forEach(function(arg) { extend(target, arg, deep) }) return target }
里面用到了isPlainObject其实$.isPlainObject 就是isPlainObject;改方法测试对象是否是“纯粹”的对象,这个对象是通过 对象常量("{}") 或者 new Object 创建的,如果是,则返回true。
$.isPlainObject({}) // => true$.isPlainObject(new Object) // => true$.isPlainObject(new Date) // => false$.isPlainObject(window) // => false
isPlainObject,先判断是不是Object对象,然后在判断不是window对象,在判断原型是Object.prototype。 Object.getPrototypeOf(obj)用来获取对象的原型 。
function isWindow(obj) { return obj != null && obj == obj.window } function isDocument(obj) { return obj != null && obj.nodeType == obj.DOCUMENT_NODE } function isObject(obj) { return type(obj) == "object" } function isPlainObject(obj) { return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype }
观察发现 isObject 这个函数内部是return type(obj)=='object',我们还得研究type的实现,type函数代码如下
function type(obj) { return obj == null ? String(obj) : class2type[toString.call(obj)] || "object" }
分析type函数里面的逻辑:如果obj===null返回String(obj)===》'null';如果不等于null返回:class2type[toString.call(obj)] || "object"
先调用toString.call(obj)===>toString是什么东西 ===>是这个 class2type.toString
toString.call(obj)就行相当于({}).toString.call(obj)。请看下图
type函数其实就是({}).toString.call(obj) 得到obj的真实类型,然后再到class2type对象中去找对应的值,如果没找到则返回 "object"
那么class2type是个什么东西,有必要看一下,经过寻找,发现以下代码
// Populate the class2type map $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { class2type["[object " + name + "]"] = name.toLowerCase() })
比如({}).toString.call({})==>[object Object]
那么在class2type中应该是这样的 {"[object Object]": "object"},最终我们查看zepto内部class2type的值,如下
如果我们自己去判断一个Object的真实类型会这样写:if(({}).toString.call(obj)==='[object Date]')//判断时间。 这样写明显麻烦,所以zepto内部给我们做了封装,我们只需要这样写 if($.type(obj)==='date') //这样是不是很简洁
我们平时直接试用$.type 感觉是很有好的,主要是zepto内部帮我们做了一些封装,JQuery这里的实现是一样滴。
$.trim
这个基本上没说的,司徒正美那本《Javascript框架设计》讲了十多种字符串trim的实现,神一般的实现,有兴趣可以去看看;
$.grep
获取一个新数组,新数组只包含回调函数中返回 ture 的数组项
$.grep = function(elements, callback) { return filter.call(elements, callback) }
内部其实就是调用了filter,filter又和not扯上了关系,为什么会这样设计,其实都是都为了代码重用。
filter: function(selector) { if (isFunction(selector)) return this.not(this.not(selector)) return $(filter.call(this, function(element) { return zepto.matches(element, selector) })) },not: function(selector) { var nodes = [] if (isFunction(selector) && selector.call !== undefined) this.each(function(idx) { if (!selector.call(this, idx)) nodes.push(this) }) else { var excludes = typeof selector == 'string' ? this.filter(selector) : (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector) this.forEach(function(el) { if (excludes.indexOf(el) < 0) nodes.push(el) }) } return $(nodes) },
最核心的也就这句
this.each(function(idx) {
if (!selector.call(this, idx)) nodes.push(this) })nodes是一个新的数组,把当前的值传个外部指定的回到函数,如果函数返回true就加到nodes中去,最后返回nodes这个新数组
$.inArray $.isArray $.isFunction $.isWindow $.parseJSON
以上几个太简单了基本上没啥说的
最后看一个map
$.map
通过遍历集合中的元素,返回通过迭代函数的全部结果,null 和 undefined 将被过滤掉。代码实现基本都是很常规的没有什么技巧,也是先判断传入的对象是Object还是Array
$.map = function(elements, callback) { var value, values = [], i, key if (likeArray(elements)) for (i = 0; i < elements.length; i++) { value = callback(elements[i], i) if (value != null) values.push(value) } else for (key in elements) { value = callback(elements[key], key) if (value != null) values.push(value) } return flatten(values) }
最后还调用了flatten这个函数。 如果数组长度大于0 则调用$.fn.concat.apply([], array) ,那么在$.fn.concat中的this指向[], 函数参数就是array
concat的实现:遍历arguments,其实就是外面传如的array,挨个判断是不是Zepto对象,如果是value.toArray()就调用它的toArray方法。 最后是concat.apply(zepto.isZ(this) ? this.toArray() : this, args),这里同理,判断当前的this是不是zepto对象,如果是调用toArray方法,不是就传入this。最终把数组链接起来,返回给外部。
function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array }concat: function() { var i, value, args = [] for (i = 0; i < arguments.length; i++) { value = arguments[i] args[i] = zepto.isZ(value) ? value.toArray() : value } return concat.apply(zepto.isZ(this) ? this.toArray() : this, args) }emptyArray = [],concat = emptyArray.concat,
this.toArray()的实现如下:toArray调用get方法,之前说过 this其实就是个维数组,这里转换成数组,里面的元素都是原生的dom对象。当然这里可以传入下标,可以取得对应的原生dom对象。
get: function(idx) { return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length] }, toArray: function() { return this.get() },
看来一个map方法还是挺复杂的,里面各种处理逻辑,牵扯一堆的东西。为什么会这样 Why?? 等以后分析后面的代码就知道了,很多地方要用到map,为了重用,所以这个map设计成这样了!!
看来这个工具方法还是可以扯很多,其实还省略了不少呢!今天到此为止!
本文地址: