前言
平時開發過程中經常會用類似
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大部分工具方法或者說靜態方法就是這些了,歡迎大家指正其中的錯誤和問題。
參考資料
文章記錄