Zepto核心模块之工具方法拾遗

前言

平时开发过程当中常常会用相似eachmapforEach之类的方法,Zepto自己也把这些方法挂载到$函数身上,做为静态方法存在,既能够给Zepto的实例使用,也能给普通的js对象使用。今天咱们主要针对其提供的这些api作一些源码实现分析。javascript

源码仓库
原文连接css

具体各个api如何使用能够参照英文文档Zepto.js 中文文档Zepto.jshtml

1. $.camelCase

该方法主要是将连字符转化成驼峰命名法。例如能够将a-b-c这种形式转换成aBC,固然连字符的数量能够是多个,a---b-----c => aBC,具体实现已经在这些Zepto中实用的方法集说过了,能够点击查看。而其代码也只是将camelize函数赋值给了$.camelCasejava

$.camelCase = camelize复制代码

2. $.contains

$.contains(parent, node) ⇒ boolean该方法主要用来检测parent是否包含给定的node节点。若是parent和node为同一节点,则返回false。node

举例git

<ul class="list">
  <li class="item">1</li>
  <li>2</li>
</ul>
<div class="test"></div>复制代码
let oList = document.querySelector('.list')
let oItem = document.querySelector('.item')
let oTest = document.querySelector('.test')

console.log($.contains(oList, oItem)) // true 父子节点
console.log($.contains(oList, oList)) // false 同一节点
console.log($.contains(oList, oTest)) // false 兄弟节点复制代码

源码github

$.contains = document.documentElement.contains ?
  function (parent, node) {
    // 防止parent和node传相同的节点,故先parent !== node
    // 接着就是调用原生的contains方法判断了
    return parent !== node && parent.contains(node)
  } :
  function (parent, node) {
    // 当node节点存在,就把node的父节点赋值给node
    while (node && (node = node.parentNode))
      // 若是node的父节点和parent相等就返回true,不然继续向上查找
      // 其实有一个疑问,为何开头不先排查node === parent的状况呢
      // 否则通过循环最后却获得false,很是的浪费
      if (node === parent) return true
    return false
  }复制代码

用了document.documentElement.contains作判断,若是浏览器支持该方法,就用node.contains从新包了一层获得一个函数,差异就在于若是传入的两个节点相同,那么原生的node.contains返回true,具体用法能够查看MDN Node.contains可是$.contains返回falsejson

若是原生不支持就须要咱们本身写一个方法了。主要逻辑仍是经过一个while循环,判断传入的node节点的父节点是否为parent,若是一个循环下来,还不是最后才返回falseapi

其实这里应该是能够作一个优化的,一进来的时候就先判断两个节点是否为同一节点,不是再进行后续的判断数组

3. $.each

用来遍历数组或者对象,相似原生的forEach可是不一样的是,能够中断循环的执行,而且服务对象不局限于数组。

举例

let testArr = ['qianlongo', 'fe', 'juejin']
let testObj = {
  name: 'qianlongo',
  sex: 'boy'
}

$.each(testArr, function (i, val) {
  console.log(i, val)
})

// 0 "qianlongo"
// 1 "fe"
// 2 "juejin"

$.each(testObj, function (key, val) {
  console.log(key, val)
})

// name qianlongo
// sex boy复制代码

须要注意的是,此时回调函数中的this指向的就是数组或者对象的某一项。这样主要是方便内部的一些其余方法在遍历dom节点的时候,this很方便地就指向了对应的dom

源码实现

$.each = function (elements, callback) {
  var i, key
  // 若是是类数组就走这个if
  if (likeArray(elements)) {
    for (i = 0; i < elements.length; i++)
      // 能够看到用.call去执行了callback,而且第一个参数是数组中的item
      // 若是用来遍历dom,那么内部的this,指的就是当前这个元素自己
      // 判断callback执行的结果,若是是false,就中断遍历
      // 中断遍历这就是和原生forEach不一样的地方
      // 2017-8-16添加,原生的forEach内部的this指向的是数组自己,可是这里指向的是数组的项
      // 2017-8-16添加,原生的forEach回调函数的参数是val, i...,这里反过来
      if (callback.call(elements[i], i, elements[i]) === false) return elements
  } else {
    // 不然回去走for in循环,逻辑与上面差很少
    for (key in elements)
      if (callback.call(elements[key], key, elements[key]) === false) return elements
  }

  return elements
}复制代码

likeArray已经在这些Zepto中实用的方法集说过了,能够点击查看。

4. $.extend

Zepto中提供的拷贝方法,默认为浅拷贝,若是第一个参数为布尔值则表示深拷贝。

源码实现

$.extend = function (target) {
  // 将第一个参数以外的参数变成一个数组
  var deep, args = slice.call(arguments, 1)
  // 处理第一个参数是boolean值的状况,默认是浅复制,深复制第一个参数传true
  if (typeof target == 'boolean') {
    deep = target
    target = args.shift()
  }
  // $.extend(true, {}, source1, source2, source3)
  // 有可能有多个source,遍历调用内部extend方法,实现复制
  args.forEach(function (arg) { extend(target, arg, deep) })
  return target
}复制代码

能够看到首先对第一个参数是否为布尔值进行判断,有意思的是,只要是布尔值都表示深拷贝,你传true或者false都是一个意思。接着就是对多个source参数进行遍历调用内部方法extend

接下来咱们主要来看内部方法extend

function extend(target, source, deep) {
  // 对源对象source进行for in遍历
  for (key in source)
    // 若是source[key]是纯对象或者数组,而且指定为深复制
    if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
      // 若是source[key]为纯对象,可是target[key]不是纯对象,则将目标对象的key设置为空对象
      if (isPlainObject(source[key]) && !isPlainObject(target[key]))
        target[key] = {}
      // 若是 若是source[key]为数组,可是target[key]不是数组,则将目标对象的key设置为数组
      if (isArray(source[key]) && !isArray(target[key]))
        target[key] = []
      // 递归调用extend函数 
      extend(target[key], source[key], deep)
    }
    // 浅复制或者source[key]不为undefined,便进行赋值
    else if (source[key] !== undefined) target[key] = source[key]
}复制代码

总体实现其实还挺简单的,主要是遇到对象或者数组的时候,而且指定为深赋值,则递归调用extend自己,从而完成复制过程。

5. $.grep

其实就是数组的原生方法filter,最终结果获得的是一个数组,而且只包含回调函数中返回 true 的数组项

直接看源码实现

$.grep = function (elements, callback) {
  return filter.call(elements, callback)
}复制代码

经过call形式去调用原生的数组方法 filter,过滤出符合条件的数据项。

6. $.inArray

返回数组中指定元素的索引值,没有找到该元素则返回-1,fromIndex是一个可选的参数,表示从哪一个地方开始日后进行查找。

$.inArray(element, array, [fromIndex]) ⇒ number

举例

let testArr = [1, 2, 3, 4]

console.log($.inArray(1, testArr)) // 0
console.log($.inArray(4, testArr)) // 3
console.log($.inArray(-10, testArr)) // -1
console.log($.inArray(1, testArr, 2)) // -1复制代码

源码实现

$.inArray = function (elem, array, i) {
  return emptyArray.indexOf.call(array, elem, i)
}复制代码

可见其内部也是调用的原生indexOf方法。

7. $.isArray

判断obj是否为数组。

咱们知道判断一个值是否为对象,方式其实挺多的,好比下面的这几种方式

// 1. es5中的isArray

console.log(Array.isArray([])) // true

// 2. 利用instanceof判断

console.log([] instanceof Array) // true

// 3. 最好的方式 toString
console.log(Object.prototype.toString.call([]) === '[object Array]') // true复制代码

而Zepto中就是采用的第二种方式

var isArray = Array.isArray || function (object) {     return object instanceof Array
}

$.isArray = isArray复制代码

若是支持isArray方法就用原生支持的,不然经过instanceof判断,其实不太清楚为何第二种方式,咱们都知道这是有缺陷的,在有iframe场景下,就会出现判断不准确的状况.

8. $.isFunction

判断一个值是否为函数类型

源码实现

function isFunction(value) { 
  return type(value) == "function" 
}

$.isFunction = isFunction复制代码

主要仍是经过内部方法type来实现的,详情能够点击这些Zepto中实用的方法集查看。

9. $.isNumeric

若是传入的值为有限数值或一个字符串表示的数字,则返回ture。

举例

$.isNumeric(null) // false
$.isNumeric(undefined) // false
$.isNumeric(true) // false
$.isNumeric(false) // false
$.isNumeric(0) // true
$.isNumeric('0') // true
$.isNumeric('') // false
$.isNumeric(NaN) // false
$.isNumeric(Infinity) // false
$.isNumeric(-Infinity) // false复制代码

源码

$.isNumeric = function (val) {
  var num = Number(val), type = typeof val
  return val != null && type != 'boolean' &&
    (type != 'string' || val.length) &&
    !isNaN(num) && isFinite(num) || false
}复制代码

首先val通过Number函数转化,获得num,而后获取val的类型获得type

咱们来回顾一下Number(val)的转化规则,这里截取一张图。

Number转化规则
Number转化规则

看起来转化规则很是复杂,可是有几点咱们能够肯定,

  1. 若是输入的是数字例如1,1.3那转化后的仍是数字,
  2. 若是输入的是字符串数字类型例如'123', '12.3'那转化后的也是数字
  3. 若是输入的是空字符串''那转化后获得的是0
  4. 若是输入是相似字符串'123aaa',那转化后获得的是NaN

因此再结合下面的判断

  1. 经过val != null排除掉nullundefined

  2. 经过type != 'boolean'排除掉,truefalse

  3. 经过isFinite(num)限定必须是一个有限数值

  4. 经过!isNaN(num)排除掉被Number(val)转化为NaN的值

  5. (type != 'string' || val.length), val为字符串,而且字符串的长度大于0,排除''空字符串的场景。

以上各类判断下来基本就知足了这个函数原来的初衷要求。

9. $.isPlainObject

测试对象是不是“纯粹”的对象,这个对象是经过 对象常量("{}") 或者 new Object 建立的,若是是,则返回true

10. $.isWindow

若是object参数为一个window对象,那么返回true

该两个方法在这些Zepto中实用的方法集也聊过了,能够点击查看一下。

11. $.map

和原生的map比较类似,可是又有不一样的地方,好比这里的map获得的记过有可能不是一一映射的,也就是可能获得比原来数组项数更多的数组,以及这里的map是能够用来遍历对象的。

咱们先看几个例子

let testArr = [1, 2, null, undefined]
let resultArr1 = $.map(testArr, (val, i) => {
  return val
})
let resultArr2 = $.map(testArr, (val, i) => {
  return [val, [val]]
})

// 再来看看原生的map的表现
let resultArr3 = testArr.map((val, i) => {
  return val
})
let resultArr4 = testArr.map((val, i) => {
  return [val, [val]]
})复制代码

运行结果以下

能够看出

  1. resultArr1resultArr3的区别是$.mapundefinednull给过滤掉了。
  2. resultArr2resultArr4的区别是$.map把回调函数的返回值给铺平了。

接下来看看源码是怎么实现的。

$.map = function (elements, callback) {
  var value, values = [], i, key
  // 若是是类数组,则用for循环
  if (likeArray(elements))
    for (i = 0; i < elements.length; i++) {
      value = callback(elements[i], i)
      // 若是callback的返回值不为null或者undefined,就push进values
      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)
}复制代码

从源码实现上能够看出由于value != null以及flatten(values)形成了上述差别。

12. $.noop

其实就是引用一个空的函数,什么都不处理,那它到底有啥用呢?

好比。咱们定义了几个变量,他将来是做为函数使用的。

let doSomeThing = () => {}
let doSomeThingElse = () => {}复制代码

若是直接这样

let doSomeThing = $.noop
let doSomeThingElse = $.noop复制代码

宿主环境就没必要为咱们建立多个匿名函数了。

其实还有一种可能用的很少的场景,在判断一个变量是不是undefined的时候,能够用到。由于函数没有返回值,默认返回undefined,也就是排除了那些老式浏览器undefined能够被修改的状况

if (xxx === $.noop()) {
  // xxx
}复制代码

13. $.parseJSON

原生JSON.parse方法的别名,接收的是一个字符串对象,返回一个对象。

源码实现

$.parseJSON = JSON.parse复制代码

14. $.trim

删除字符串首尾的空白符,若是传入nullundefined返回空字符串

源码实现

$.trim = function (str) {
  return str == null ? "" : String.prototype.trim.call(str)
}复制代码

15. $.type

获取JavaScript 对象的类型。可能的类型有: null undefined boolean number string function array date regexp object error.

该方法内部实现其实就是内部的type函数,而且已经在这些Zepto中实用的方法集聊过了,能够点击查看。

$.type = type复制代码

结尾

Zepto大部分工具方法或者说静态方法就是这些了,欢迎你们指正其中的错误和问题。

参考资料

读zepto源码之工具函数

MDN trim

MDN typeof

MDN isNaN

MDN Number

MDN Node.contains

文章记录

  1. 原来你是这样的jsonp(原理与具体实现细节)

  2. 谁说你只是"会用"jQuery?

  3. 向zepto.js学习如何手动触发DOM事件

  4. mouseenter与mouseover为什么这般纠缠不清?

  5. 这些Zepto中实用的方法集

相关文章
相关标签/搜索