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

相關文章
相關標籤/搜索