盘点Vue源码中用到的工具函数

以下摘取的函数,在 shared 目录下公用的工具方法。文件在 util.js 中,githu地址
提取了一些常用通用的函数进行剖析,主要包含以下内容:

1.创建一个被冻结的空对象
2.判断是否是 undefined 或 null
3.判断是否不是 undefined 和 null
4.判断是否是原始类型
5.判断是否是对象类型
6.判断有效的数组下标
7.判断是否是一个 Promise 对象
8.删除数组中指定元素
9.用做缓存的高阶函数
10.递归判断一个对象是否和另个一个对象完全相同
11.函数只执行一次
12.自定义 bind 函数

1. 创建一个被冻结的空对象

export const emptyObject = Object.freeze({})

一旦创建不能给这个对象添加任何属性。

2.判断是否是 undefined 或 null

function isUndef (v) {
  return v === undefined || v === null
}

在源码中很多地方会判断一个值是否被定义,所以这里直接抽象成一个公共函数。
传入任意值,返回是一个布尔值。

3.判断是否不是 undefined 和 null

function isDef (v) {
  return v !== undefined && v !== null
}

当传入的值,既不是 undefined 也不是 null 返回true。

4. 判断是否是原始类型

function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

在js中提供了两大类数据类型:

  1. 原始类型(基础类型):String、Number、Boolean、Null、Undefined、Symbol
  2. 对象类型:Object、Array、Function

5. 判断是否是对象类型

function isObject (obj: mixed) {
  return obj !== null && typeof obj === 'object'
}

传入的值排除掉 null,因为在js中 null 使用运算符 typeof 得到的值是 object,这是一个 bug。因为历史原因放弃修复了。具体可以参考这里查看

6. 判断有效的数组下标

function isValidArrayIndex (val) {
  const n = parseFloat(String(val)); // 转成数字
  // 下标大于等于0,并且不是小数,并且是有限的数
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}
  • 可以传入任意值,先调用 String 转成字符串,目的是防止传入的值为 Symbol 类型,那样直接调用 parseFloat 会报错,例如:
let test = Symbol('test');
console.log(parseFloat(test))
控制台捕获错误:Uncaught TypeError: Cannot convert a Symbol value to a string

原因是在调用 parseFloat 时,内部会调用内置的 ToString 方法,可以参考这里。而内置的 ToString 方法在遇到 Symbol类型的值时,会抛出 TypeError 错误,可以参考这里

跟使用一些隐式转换遇到的问题一样,例如使用 + 号:

let test = '' + Symbol('text');
控制台捕获错误:Uncaught TypeError: Cannot convert a Symbol value to a string

都是因为内部会调用内置的 ToString 方法造成的。

而如果手动调用 toString 方法或者调用 String,转换为字符串,则不会报错:

let test = Symbol('test');
console.log(test.toString()); // "Symbol(test)"
console.log(String(test)) // "Symbol(test)"
  • 接下来判断 n >= 0 ,数组的下标不能小于0,这样就会排除掉小于0的数,以及 NaN
  • 并且 Math.floor(n) === n 一个数向下取整并且还等于自己,那只能是正整数,排除掉小数,因为数组的下标不能是小数。
  • 并且用 isFinite 来判定一个数字是否是有限数
console.log(isFinite(Infinity)); // false
console.log(isFinite(-Infinity)); // false
console.log(isFinite(123)); // true

7. 判断是否是一个 Promise 对象

function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

当一个对象存在then方法,并且也存在catch方法,可以判定为Promise 对象。

8. 删除数组中指定元素

这个方法有效的避免了进行删除数组某一项时,都要进行查找位置再删除的重复工作。

function remove (arr, item){
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
  • 先判断数组长度,如果数组是空的,则没必要进行删除操作
  • indexOf 方法查找到元素在数组中的位置,如果找到返回元素所在的位置下标,如果不存在,则返回-1
  • index>-1代表存在数组中,则调用 splice 进行删除,并返回删除的元素组成的数组,也就是 splice 的返回值。

9. 用做缓存的高阶函数

用高阶函数的好处是无需暴露不同要求的缓存对象在外面,形成一个闭包。下面这个函数的技巧,应用在工作中,可以提高代码运行的效率

function cached(fn) {
  // 创建一个缓存对象
  const cache = Object.create(null)
  return (function cachedFn (str) {
    // 先从缓存对象中找,要操作的值,是否已经有了操作结果
    const hit = cache[str]
    // 如果有,则直接返回;没有,则调用函数对值进行操作,并把操作结果存在缓存对象中
    return hit || (cache[str] = fn(str))
  })
}
  • 调用 cached 时会传入一个 fn 函数,这个函数对某些值进行操作,操作之后会产生返回值
  • cached 函数先定义一个没有原型的对象,会比用{} 高效,因为不需要继承一大堆Object.prototype 上的属性。
  • 执行完 cached 会返回一个函数cachedFn,将来接收需要操作的值。函数cachedFn 内部调用fn 函数得到操作后的值,并缓存在对象 cache 中,如果再对同一个值进行操作时,则直接从缓存中取,无需再调用函数计算。

例如以下运用,函数的作用是把字符串的首字母大写。

const capitalize = cached((str) => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})
  • 先调用 cached 传入一个函数,这个函数是对字符串进行首字母大写的操作,并返回首字母大写的字符串结果,可以说创建了一个计算函数。
  • cached 的返回值是函数,也就是上面的cachedFn 函数。

这时我们就可以调用 capitalize 对字符串进行首字母大写了。

capitalize('test');  // "Test"
capitalize('test');  // "Test"
capitalize('test');  // "Test"

第一次调用 capitalize 函数,先从缓存对象中取值,没有,则调用计算函数进行计算结果返回,同时存入缓存对象中。这时的缓存对象为:

{test: 'Test'}

再多次调用 capitalize 时,从缓存对象中取值,命中,直接返回,无需再进行计算操作。

10. 递归判断一个对象是否和另个一个对象完全相同

判断两个对象是否相同,主要是判断两个对象包含的值都是一样的,如果包含的值依然是个对象,则继续递归调用判断是否相同。

function isObject (obj){
  return obj !== null && typeof obj === 'object'
}

function looseEqual (a, b) {
  // 如果是同一个对象,则相同
  if (a === b) return true
  // 判断是否是对象
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)
  // 两者都是对象
  if (isObjectA && isObjectB) {
    try {
      // 判断是否是数组
      const isArrayA = Array.isArray(a)
      const isArrayB = Array.isArray(b)
      // 两者都是数组
      if (isArrayA && isArrayB) {
        // 长度要一样,同时每一项都要相同,递归调用
        return a.length === b.length && a.every((e, i) => {
          return looseEqual(e, b[i])
        })
      } else if (a instanceof Date && b instanceof Date) {  // 如果都是时间对象,则需要保证时间戳相同
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) { // 两者都不是数组,则为对象
        // 拿到两者的key值,存入数组
        const keysA = Object.keys(a)
        const keysB = Object.keys(b)
        // 属性的个数要一样,递归的判断每一个值是否相同
        return keysA.length === keysB.length && keysA.every(key => {
          return looseEqual(a[key], b[key])
        })
      } else {  
        return false
      }
    } catch (e) {
      return false
    }
  } else if (!isObjectA && !isObjectB) {  // 两者都不是对象
    // 转成字符串后,值是否一致
    return String(a) === String(b)
  } else {
    return false
  }
}
  • 判断两个值是否相同,无论是原始类型还是对象类型,如果相同,则直接返回true。
  • 如果两个都会对象,则分为两种情况,数组和对象。
    • 都是数组,则保证长度一致,同时调用 every 函数递归调用函数,保证每一项都一样
    • 是时间对象,则保证时间戳相同
    • 是对象,则先取出 key 组成的数组,两者 key 的个数要相同;再递归调用比较 value 值是否相同
    • 以上都不满足,直接返回false
  • 如果两者都不是对象,转成字符串后进行比较。
  • 以上都不满足,直接返回false

例子:

let a1 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
let b1 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
console.log(looseEqual(a1,b1)); // true

let a2 = [1,2,3,{a:1,b:2,c:[1,2,3,4]}];
let b2 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
console.log(looseEqual(a2,b2)); // false

11. 函数只执行一次

同样利用高阶函数,在闭包内操作标识的真假,来控制执行一次。

function once (fn) {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
  • 传入要执行一次的函数 fn
  • 设置标识为 false
  • 返回一个函数

实际运用:

function test(){
  console.log('我只被执行一次');
}
let test2 = once(test);
test2(); // 我只被执行一次
test2();
test2();
test2();
  • 调用 once 函数后,会返回一个函数,赋值给 test2
  • 第一次调用 test2 后,在函数的尼内部,called 初次为 false, 所以可以执行函数 test,然后把标识 called 设置为true,就类似关闭了大门,下次不再执行。
  • 之后在调用 test2 , test 将不再执行。

12. 自定义 bind 函数

function polyfillBind (fn, ctx) {
  function boundFn (a) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

自定义的 bind 函数的场景,都是用来兼容不支持原生 bind 方法的环境。 在自己模拟的 bind 函数中,实际上调用的是 callapply

这个方法写的相对简单,如有偏差欢迎指正学习,谢谢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容