# jQuery 中的工具函数和方法
文中涉及到 jQuery 暴露的接口:
- jQuery.fn.toArray
- jQuery.fn.get
- jQuery.fn.pushStack
- jQuery.fn.each
- jQuery.fn.map
- jQuery.fn.slice
- jQuery.fn.first
- jQuery.fn.last
- jQuery.fn.even
- jQuery.fn.odd
- jQuery.fn.eq
- jQuery.fn.end
- jQuery.fn.push
- jQuery.fn.sort
- jQuery.fn.splice
- jQuery.error
- jQuery.noop
- jQuery.isEmptyObject
- jQuery.globalEval
- jQuery.find(未作详细说明)
- jQuery.expr(未作详细说明)
- jQuery.camelCase
- jQuery.isArray
- jQuery.parseJSON
- jQuery.nodeName
- jQuery.isFunction
- jQuery.isWindow
- jQuery.type
- jQuery.now
- jQuery.trim
- jQuery.isNumeric
- jQuery.parseXML
- jQuery.proxy
相关属性:
- jQuery.fn.jquery
- jQuery.fn.constructor
- jQuery.fn.length
- jQuery.expando
- jQuery.isReady
- jQuery.support
- jQuery.guid
其它私有函数:
- access
- swap
jQuery 在修改其原型时同时还制定了一系列的属性和方法(L160):
;(function(window, noGlobal) {
var version = '3.5.1'
var arr = []
push = arr.push
jQuery.fn = jQuery.prototype = {
// The current version of jQuery being used
jquery: version,
constructor: jQuery,
// The default length of a jQuery object is 0
length: 0,
toArray: function() {
return slice.call(this)
},
// 获取匹配的元素,可以指定索引,默认以数组返回所有的元素
// 如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推
get: function(num) {
// Return all the elements in a clean array
if (num == null) {
return slice.call(this)
}
// Return just the one element from the set
return num < 0 ? this[num + this.length] : this[num]
},
// 将指定的元素集合推入栈顶,并返回栈顶 jQuery 对象
pushStack: function(elems) {
// Build a new jQuery matched element set
var ret = jQuery.merge(this.constructor(), elems)
// 创建一个属性指向栈中下一层的 jQuery 对象
// 有点链表的味道,不过这里结合下面的 end() 方法模拟的是栈的结构
ret.prevObject = this
// Return the newly-formed element set
return ret
},
// 类似于数组的 forEach(),内部实际上使用的是 jQuery 的静态方法,它同时支持对类数组和对象进行遍历
each: function(callback) {
return jQuery.each(this, callback)
},
map: function(callback) {
return this.pushStack(
jQuery.map(this, function(elem, i) {
return callback.call(elem, i, elem)
}),
)
},
slice: function() {
return this.pushStack(slice.apply(this, arguments))
},
first: function() {
return this.eq(0)
},
last: function() {
return this.eq(-1)
},
even: function() {
return this.pushStack(
jQuery.grep(this, function(_elem, i) {
return (i + 1) % 2
}),
)
},
odd: function() {
return this.pushStack(
jQuery.grep(this, function(_elem, i) {
return i % 2
}),
)
},
eq: function(i) {
var len = this.length,
j = +i + (i < 0 ? len : 0)
return this.pushStack(j >= 0 && j < len ? [this[j]] : [])
},
end: function() {
return this.prevObject || this.constructor()
},
// For internal use only.
// Behaves like an Array's method, not like a jQuery method.
push: push,
sort: arr.sort,
splice: arr.splice,
}
})
其中的 map
方法调用了同名的静态方法,这个静态方法和数组的 map
依然很像,不过它同时支持对类数组和对象进行处理,并且对于最后的结果还会进行深度为一层的拍平处理(L454):
jQuery({
map: function(elems, callback, arg) {
var length,
value,
i = 0,
ret = []
// 通过 for 循环取出其中的每一项交给回调函数进行处理,并把处理结果存储到新的数组中
if (isArrayLike(elems)) {
length = elems.length
for (; i < length; i++) {
value = callback(elems[i], i, arg)
if (value != null) {
ret.push(value)
}
}
// 如果是非类数组对象则通过 for-in 进行遍历
} else {
for (i in elems) {
value = callback(elems[i], i, arg)
if (value != null) {
ret.push(value)
}
}
}
// Flatten any nested arrays
return flat(ret)
},
})
另外,还有一个类似数组 filter
方法的 grep
方法,它将根据指定的回调检测数值元素,并返回符合条件所有元素的数组(L434):
jQuery({
grep: function(elems, callback, invert /* 是否对指定的条件取反 */) {
var callbackInverse,
matches = [],
i = 0,
length = elems.length,
callbackExpect = !invert // 通过对 invert 取反得到一个布尔值
// 仅返回通过函数校验的元素
for (; i < length; i++) {
callbackInverse = !callback(elems[i], i) // 对回调的结果进行取反得到一个布尔值
if (callbackInverse !== callbackExpect) {
matches.push(elems[i])
}
}
return matches
},
})
这里同时对参数 invert
和回调函数取反,不仅得到了两者的布尔值,同时保证了结果的一致性,可谓一举两得。
其它方法看起来就比较明确了,紧接着 jQuery 又向自身扩展了一些静态方法,除了前面已经见过的外,还包括(L325):
;(function(window, noGlobal) {
var support = {}
jQuery.extend({
// Unique for each copy of jQuery on the page
// 后续用作 HTMLElement 或 JS 对象的属性名
expando: 'jQuery' + (version + Math.random()).replace(/\D/g, ''),
// 当前版本 jQuery 已经被分成了多个模块,当没有引入 ready 模块时则假设已经就绪
isReady: true,
error: function(msg) {
throw new Error(msg)
},
noop: function() {},
isEmptyObject: function(obj) {
var name
for (name in obj) {
return false
}
return true
},
// 在提供的上下文中执行脚本;如果未指定,则在全局执行
globalEval: function(code, options, doc) {
DOMEval(code, { nonce: options && options.nonce }, doc)
},
// 对象的全局 GUID 计数器
guid: 1,
// jQuery.support 中记录了一些兼容性的情况,在核心模块中它是空的
// 在需要的模块中将会对其进行扩展,所以在此先行声明
support: support,
})
})
对于支持 Symbol 的环境,jQuery 还会在原型添加一个生成器,该生成器可以被 for...of
循环使用:
;(function(window, noGlobal) {
if (typeof Symbol === 'function') {
jQuery.fn[Symbol.iterator] = arr[Symbol.iterator]
}
})
再然后就是 Sizzle 部分了,主要对 jQuery 扩展了几个静态方法(L2978):
;(function(window, noGlobal) {
jQuery.find = Sizzle
jQuery.expr = Sizzle.selectors
// Deprecated
jQuery.expr[':'] = jQuery.expr.pseudos
jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort
jQuery.text = Sizzle.getText
jQuery.isXMLDoc = Sizzle.isXML
jQuery.contains = Sizzle.contains
jQuery.escapeSelector = Sizzle.escape
})
由于 Sizzle 模块不小,且随着 JavaScript 对选择器的支持,我们对它的依赖也越来越小,所以在这里先不展开了,在需要时则先对相应的方法进行模拟,以完成基本的工作。
在 Sizzle 之后又陆续地添加了一些静态方法,其中包括一些直接指向元素的方法,以及一些前面了解过的函数:
;(function(window, noGlobal) {
jQuery.camelCase = camelCase
jQuery.isArray = Array.isArray
jQuery.parseJSON = JSON.parse
jQuery.nodeName = nodeName
jQuery.isFunction = isFunction
jQuery.isWindow = isWindow
jQuery.type = toType
jQuery.now = Date.now
})
其中一个转驼峰的函数 camelCase
,不一样的是它对 IE 的前缀样式做了特殊的处理(#9572):
var rmsPrefix = /^-ms-/,
rdashAlpha = /-([a-z])/g
// Used by camelCase as callback to replace()
function fcamelCase(_all, letter) {
return letter.toUpperCase()
}
function camelCase(string) {
return string.replace(rmsPrefix, 'ms-').replace(rdashAlpha, fcamelCase)
}
接下来一个从一个字符串的两端删除空白字符的 trim()
方法:
// Make sure we trim BOM(\uFEFF) and NBSP(\xA0:HTML 中经常用到的 )
// Unicode3.2 之前,\uFEFF 表示零宽不换行空格(Zero Width No-Break Space);
// Unicode3.2 新增了 \u2060 用来表示零宽不换行空格,\uFEFF 就只用来表示字节次序标记,也就是 BOM
var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g
jQuery.trim = function(text) {
return text == null ? '' : (text + '').replace(rtrim, '')
}
其中主要分为三步:1、将空值取为空字符串,2、将非字符串转为字符串,3、通过正则清除空白(包括 BOM 和 NBSP)。
另外,jQuery 提供了 isNumeric()
方法检查它的参数是否代表一个数值(L10798):
jQuery.isNumeric = function(obj) {
var type = jQuery.type(obj)
return (type === 'number' || type === 'string') && !isNaN(obj - parseFloat(obj))
}
通过 parseFloat
方法对给定的参数进行转换,如果返回的结果是 NaN 基本上就可以说是非数字了。不过对于 Infinity 它还是返回其本身,所以我们还需要让参数和自身相减,这样如果是 Infinity 也会返回 NaN。
接着要介绍了的是一个解析 XML 文档的方法(L8858):
jQuery.parseXML = function(data) {
var xml
if (!data || typeof data !== 'string') {
return null
}
// Support: IE 9 - 11 only
// IE throws on parseFromString with invalid input.
try {
xml = new window.DOMParser().parseFromString(data, 'text/xml')
} catch (e) {
xml = undefined
}
// 如果解析失败, DOMParser 不会抛出任何异常,而是会返回一个给定的错误文档,其中包含 parsererror 标签
if (!xml || xml.getElementsByTagName('parsererror').length) {
jQuery.error('Invalid XML: ' + data)
}
return xml
}
可见内部采用了原生解析函数 DOMParser 来将存储在字符串中的 XML 源代码解析为一个 DOM Document。你也可以使用 XMLSerializer 接口提供 serializeToString()
方法来构建一个代表 DOM 树的 XML 字符串。
在 jQuery 中还有一个名为 proxy
的方法,它的机制几乎和 bind
函数一样,根据它的注释也可以知晓这点。虽然它已经不被推荐使用了,但暂时还不会被移除:
jQuery.proxy = function(fn, context) {
var tmp, args, proxy
// 这里主要是处理一种简写形式:如果我们要将对象的一个方法的 this 绑定为所在的对象,这可以指定第一个参数为对象,第二个为字符串的方法名
// Handle: var obj = { test() {} }; jQuery.proxy(obj, 'test')
if (typeof context === 'string') {
tmp = fn[context]
context = fn
fn = tmp
}
// Quick check to determine if target is callable, in the spec
// this throws a TypeError, but we will just return undefined.
if (!isFunction(fn)) {
return undefined
}
args = slice.call(arguments, 2)
proxy = function() {
// 通过 apply 改变原函数的 this 指向,同时将预存参数和新接受的参数一起传递给原函数
return fn.apply(context || this, args.concat(slice.call(arguments)))
}
// 通过设置为与原始处理程序相同且唯一的 guid,以便后期可以删除它
proxy.guid = fn.guid = fn.guid || jQuery.guid++
return proxy
}
接下来是一个略微复杂的 access
函数,它主要在内部使用,用于处理像 attr
,prop
,css
这些方法的取值和设置值的问题,比如通过它 attr
方法就可以有以下几种用法:
$('div').attr('title')
$('div').attr('title', '标题')
$('div').attr({ title: '标题' })
$('div').attr('title', function() {
return '标题'
})
可见其最大的特点就是模拟实现了函数的重载(L4143):
var access = function(
/* 操作的元素集合 */
elems,
/* 具体工作的函数,如 attr, css 等 */
fn,
/* 设置属性的 key,也可能是个对象 */
key,
/* 要设置的值,可以不指定 */
value,
/* 是否可以继续链式调用,除了可以通过参数指定外,内部在设置值的情况下都会将其置为 true */
chainable,
/* 如果 jQuery 没有选中到元素的返回值 */
emptyGet,
/* 是否为原始数据 */
raw,
) {
var i = 0,
len = elems.length,
bulk = key == null /* 是否是批量操作,此时没有表面要设置或获取特定的属性 */
// 如果参数 key 是对象,表示要设置多个属性,则遍历参数 key 调用 access 方法
// Handle: $("div").attr({ title: "标题" });
if (toType(key) === 'object') {
chainable = true
for (i in key) {
// 此处递归调用时需要注意把参数 chainable 直接传递 true,因为这里已经确定了是在存储值,要避免在递归里面去执行获取值的逻辑
access(elems, fn, i, key[i], true, emptyGet, raw)
}
// 到这里已经确定了 key 不是对象(但不一定有值)
// 如果 key 不是对象且 value 有值则说明是在设置单个值
} else if (value !== undefined) {
chainable = true
if (!isFunction(value)) {
raw = true
}
// 批量操作针对整个集合运行
if (bulk) {
// 对于原始值我们将把工作函数的 this 指向选中的集合,并把设置的值传递给它
// 调用完毕之后,同时将 fn 设置为空
if (raw) {
fn.call(elems, value)
fn = null
// 如果 value 此时是个函数,则对其进行一层包装,并将工具函数传递给它
} else {
bulk = fn
fn = function(elem, _key, value) {
return bulk.call(jQuery(elem), value)
}
}
}
// 基本上,批量操作 + value 是一个函数、非批量操作两种情况都会进入这个分支
if (fn) {
for (; i < len; i++) {
fn(
elems[i],
key,
/* 如果时原始值则直接将其作为值进行设置,如果是函数则将返回值作为设置的值 */
raw
? value
: value.call(
elems[i],
i,
/* 将当前项和 key 传递给工具函数,通常是在获取值 */
fn(elems[i], key),
),
)
}
}
}
// 如果 chainable 为 true,说明是在设置值,此时返回 elems 实现链式调用
if (chainable) {
return elems
}
// 能走到这里说明执行的是取值操作
// 同样对于批量操作会将集合作为 this 指向来调用工具函数,并将结果作为返回值
if (bulk) {
return fn.call(elems)
}
// 如果选中的元素是多个,那么获取值时只会通过工具函数获取第一个元素的值
// 如果没有取到值则返回默认的空值
return len ? fn(elems[0], key) : emptyGet
}
根据上面对代码的只是应该就能大概理解其工作原理,比较令人疑惑的就是在批量操作且 value
为一个函数时的处理:它将原来的工具函数包装为一个和非批量操作时一样的处理函数。
后面在遍历选中项时会先通过新的工具函数获取参数(此时它接受两个参数,而第二个参数在内部没有用,所以内部调用之前的函数时只是改变了 this 指向没有传递任何参数)。
通过得到的参数在此调用新的工具函数时,传递了三个参数,对应到内部原始工具函数,除了改变了 this
指向外,还得到了刚刚得到的参数。为什么要这么需要得到使用到的时候在做细说了。
最后再来说说我们的 swap
函数吧,这个函数在内部并不是用来调换位置什么的,而是通过暂存样式来做一些操作,之后再还原回去(L6424):
var swap = function(elem, options, callback) {
var ret,
name,
old = {}
// Remember the old values, and insert the new ones
for (name in options) {
old[name] = elem.style[name]
elem.style[name] = options[name]
}
ret = callback.call(elem)
// Revert the old values
for (name in options) {
elem.style[name] = old[name]
}
return ret
}
比较好理解的一个用法就是用来获取一个隐藏元素的高度(display: none),由于通过这种方式隐藏的元素是不占据位置的,所以我们无法获取它的高度,但是我们可以把它临时换成 visible
的方式来进行隐藏(当然还有许多细节要处理),在执行完获取高度的回调函数后再将之前的样式还原回去。