前言
平时开发过程中经常会用类似
each
、map
、forEach
之类的方法,Zepto本身也把这些方法挂载到$
函数身上,作为静态方法存在,既可以给Zepto的实例使用,也能给普通的js对象使用。今天我们主要针对其提供的这些api做一些源码实现分析。
具体各个api如何使用可以参照英文文档Zepto.js 中文文档Zepto.js
1. $.camelCase
该方法主要是将连字符转化成驼峰命名法。例如可以将
a-b-c
这种形式转换成aBC
,当然连字符的数量可以是多个,a---b-----c
=>aBC
,具体实现已经在这些Zepto中实用的方法集说过了,可以点击查看。而其代码也只是将camelize
函数赋值给了$.camelCase
$.camelCase = camelize
2. $.contains
$.contains(parent, node) ⇒ boolean
该方法主要用来检测parent是否包含给定的node节点。如果parent和node为同一节点,则返回false。
举例
<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 兄弟节点
源码
$.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
返回false
。
如果原生不支持就需要我们自己写一个方法了。主要逻辑还是通过一个while
循环,判断传入的node
节点的父节点是否为parent
,如果一个循环下来,还不是最后才返回false
其实这里应该是可以做一个优化的,一进来的时候就先判断两个节点是否为同一节点,不是再进行后续的判断
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)
的转化规则,这里截取一张图。
看起来转化规则非常复杂,但是有几点我们可以确定,
- 如果输入的是数字例如
1
,1.3
那转化后的还是数字, - 如果输入的是字符串数字类型例如
'123'
,'12.3'
那转化后的也是数字 - 如果输入的是空字符串
''
那转化后得到的是0 - 如果输入是类似字符串
'123aaa'
,那转化后得到的是NaN
所以再结合下面的判断
- 通过
val != null
排除掉null
和undefined
- 通过
type != 'boolean'
排除掉,true
和false
- 通过
isFinite(num)
限定必须是一个有限数值 - 通过
!isNaN(num)
排除掉被Number(val)
转化为NaN
的值 (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]]
})
运行结果如下
可以看出
resultArr1
和resultArr3
的区别是$.map
把undefined
和null
给过滤掉了。resultArr2
与resultArr4
的区别是$.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
删除字符串首尾的空白符,如果传入
null
或undefined
返回空字符串
源码实现
$.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大部分工具方法或者说静态方法就是这些了,欢迎大家指正其中的错误和问题。
参考资料
文章记录