Zepto核心模組之工具方法拾遺

謙龍發表於2019-01-20

前言

平時開發過程中經常會用類似eachmapforEach之類的方法,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)的轉化規則,這裡擷取一張圖。

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中實用的方法集

相關文章