【若川视野 x 源码共读】第24期 | vue2工具函数

253 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

1.学习资料

2.学习内容

2.1 emptyObject

var emptyObject = Object.freeze({a: {b:1}});
console.log(emptyObject)
emptyObject.b = 2
console.log(emptyObject)
emptyObject.a.b = 2
console.log(emptyObject)

注意点:第一层不可以修改所以给加上b属性也无济于事,而修改emptyObject.a.b为2则可以修改成功

Object.isFrozen可以用于判断对象是否是冻结状态

const isFrozen = Object.isFrozen(emptyObject);
console.log(isFrozen)
//true

2.2 isUndef

function isUndef (v) {
  return v === undefined || v === null
}
console.log(isUndef(false))
// false
console.log(isUndef(undefined))
// true
console.log(isUndef(null))
// true

2.3 isDef

function isDef (v) {
  return v !== undefined && v !== null
}
console.log(isDef(false))
// true
console.log(isDef(undefined))
// false
console.log(isDef(null))
// false

2.4 isTrue

function isTrue (v) {
  return v === true
}

2.5 isFalse

function isFalse (v) {
  return v === false
}

2.6 isPrimitive

/**
 * Check if value is primitive.
 */
function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

console.log(isPrimitive('str'))
// true
console.log(isPrimitive(2))
// true
console.log(isPrimitive(true))
// true
console.log(isPrimitive(Symbol('1')))
// true
console.log(isPrimitive({a:1}))
// false
console.log(isPrimitive([1,2,3]))
// false

2.7 isObject

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

console.log(isObject(null))
// false
console.log(isObject([]))
// true
function func () {
}
console.log(isObject(func))
// false
console.log(isObject({a: 1}))
// true

函数不属于对象;数组属于对象;typeof null 会是 'object'所以进行排除

2.8 toRawType

var _toString = Object.prototype.toString;

function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

toRawType转成数据的原始类型

ecma规范网址: 262.ecma-international.org/6.0/#sec-ob…

var _toString = Object.prototype.toString;

function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

console.log(toRawType('str'))
// String
console.log(toRawType(1))
// Number
console.log(toRawType(true))
// Boolean
console.log(toRawType(null))
// Null
console.log(toRawType(undefined))
// Undefined
console.log(toRawType([1,2]))
// Array
console.log(toRawType({a:1}))
// Object
function func(){}
console.log(toRawType(func))
// Function
console.log(toRawType(Symbol(1)))
// Symbol

2.9 isPlainObject

var _toString = Object.prototype.toString;

function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}
console.log(isPlainObject({a:1}))
// true
console.log(isPlainObject([1,2]))
// false

判断是否为纯对象

2.10 isRegExp

var _toString = Object.prototype.toString;

function isRegExp (v) {
  return _toString.call(v) === '[object RegExp]'
}

console.log(isRegExp(/[123]/))
// true

判断是否为正则

2.11 isValidArrayIndex

function isValidArrayIndex (val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

console.log(isValidArrayIndex(1))
// true
console.log(isValidArrayIndex('1'))
// true
console.log(isValidArrayIndex('-1'))
// false
console.log(isValidArrayIndex(-1))
// false
console.log(isValidArrayIndex(0))
// true

isFinite全局函数用于判断是否是有限数值:

developer.mozilla.org/zh-CN/docs/…

Infinity,-Infinity 是无限的

2.12 isPromise

function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}
function isDef (v) {
  return v !== undefined && v !== null
}
console.log(isPromise(new Promise((resolve, reject) => {
	
})))
// true

判断是否为Promise

2.13 toString

function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}
var _toString = Object.prototype.toString;

function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}

console.log(toString({a:1,b:2}))
// {
//   "a": 1,
//   "b": 2
// }

2.14 toNumber

function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}
console.log(toNumber('a'))
 // 'a'
console.log(toNumber('1') )
// 1
console.log(toNumber('1a') )
// 1
console.log(toNumber('a1'))
// 'a1'

2.15 makeMap,isBuiltInTag,isReservedAttribute

function makeMap (
  str,
  expectsLowerCase
) {
  var map = Object.create(null);
  var list = str.split(',');
  for (var i = 0; i < list.length; i++) {
    map[list[i]] = true;
  }
  return expectsLowerCase
    ? function (val) { return map[val.toLowerCase()]; }
    : function (val) { return map[val]; }
}

const map = makeMap('a,b')
console.log(map('a'))
// true
console.log(map('b'))
// true
console.log(map('c'))
// undefined

咋一看我不是很喜欢这个方法,但是还是被吸引到了。为啥?人家用到闭包了,map变量。

另外,源码中对makeMap的应用:isBuiltInTag,isReservedAttribute

var isBuiltInTag = makeMap('slot,component', true);
console.log(isBuiltInTag('slot'))  // true
console.log(isBuiltInTag('component')) // true
console.log(isBuiltInTag('Slot'))  // true
console.log(isBuiltInTag('Component'))  // true

var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
 
 console.log(isReservedAttribute('key'))// true
 console.log(isReservedAttribute('ref')) // true
 console.log(isReservedAttribute('slot')) // true
 console.log(isReservedAttribute('slot-scope')) // true
 console.log(isReservedAttribute('is') )// true
 console.log(isReservedAttribute('IS') )// undefined

2.16 remove

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}
const test = [1,2,3]
remove(test, 3)
console.log(test)

const arr = [{a:1}, 2, {b:2}]

remove(arr, arr[0])

console.log(arr)

2.17 hasOwn

var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}
console.log(hasOwn({__proto__: { a: 1 }}, 'a'))
 // false
console.log(hasOwn({ a: undefined }, 'a'))
 // true
console.log(hasOwn({}, 'a'))
 // false
console.log(hasOwn({}, 'hasOwnProperty'))
 // false
console.log(hasOwn({}, 'toString') )
// false

2.18 缓存以及其应用

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}


var multi = cached((a) => {
	console.log('被调用了')
	return a*2
})

multi(1)
multi(2)
multi(2)
multi(3)
// 被调用了
// 被调用了
// 被调用了

利用闭包缓存数据

应用是额外的几个工具函数:

(1)转成驼峰命名

var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});

const res = camelize('on-click')
console.log(res)
// onClick

replace的第二个参数是回调函数的情况,说明一下参数的含义:

所以_代表on, c代表click。

(2)首字母转成大写

var capitalize = cached(function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
});

const res = capitalize('onClick')
console.log(res)
// OnClick

(3)小驼峰转连字符

var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
});

const res = hyphenate('onClick')
console.log(res)
// on-click

根据查看API可以知道 \B和\b都是边界匹配符,区别如下图:

正则表达式就是把不是单词边界的大写字母找到,然后replace去替换这个大写字母,转换成小写的,前面加上-。

2.19 polyfillBind

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

  boundFn._length = fn.length;
  return boundFn
}

function nativeBind (fn, ctx) {
  return fn.bind(ctx)
}

var bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind;

// 用例
var a = 1
function logA() {
	console.log(this.a)
}
var obj = {a:2}
bind(logA,obj)()
// 2

如果函数的构造函数原型上有bind则使用此函数从原型继承的bind,否则使用polyfillBind。

polyfillBind在实现上要判断被bind函数修改this指向函数的参数个数(说人话:就是被调用函数的参数个数)。如果参数个数为0则使用call实现bind;如果不为0则判断长度是否大于1,大于1则使用apply,因为apply的参数是数组,而arguments正好是类数组对象;如果长度为1说明参数就1个就是a,使用call实现bind。

2.20 toArray

function toArray (list, start) {
  start = start || 0;
  var i = list.length - start;
  var ret = new Array(i);
  while (i--) {
    ret[i] = list[i + start];
  }
  return ret
}

const list = {
	0:1,
	1: 'a',
	2: Symbol(3),
	3: 'str',
	length:4
}
const res = toArray(list)
console.log(res)
//  [1, 'a', Symbol(3), 'str']

const res2 = toArray(list,2)
console.log(res2)
// [Symbol(3), 'str']

类数组转成数组,start是开始的位置。

2.21 extend

function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}

var obj = {a:1,b:2}
var obj2 = {a: 'hello', c: 3}
const res =  extend(obj, obj2)
console.log(obj)
console.log(res)
console.log(obj === res)
// {a: 'hello', b: 2, c: 3}
// {a: 'hello', b: 2, c: 3}
// true

对象的合并,工具方法toObject的定义是在extend之上进行的

/**
 * Merge an Array of Objects into a single Object.
 */
function toObject (arr) {
  var res = {};
  for (var i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i]);
    }
  }
  return res
}
var obj = {a:1,b:2}
var obj2 = {a: 'hello', c: 3}
const res =  toObject([obj, obj2])
console.log(res)
// {a: 'hello', b: 2, c: 3}

根据其注释了解到其作用是把由对象组成的数组合并到一个对象中去。

2.22 noop

function noop (a, b, c) {}
noop()

啥也不执行的函数

2.23 no

var no = function (a, b, c) { return false; };

console.log(no())// false

永远返回false

2.24 identity

返回函数的第一个参数

var identity = function (_) { return _; };

const res = identity(1,23)
console.log(res)// 1

2.25 宽松相等

/**
 * Check if two values are loosely equal - that is,
 * if they are plain objects, do they have the same shape?
 */
function looseEqual (a, b) {
  if (a === b) { return true }
  var isObjectA = isObject(a);
  var isObjectB = isObject(b);
  if (isObjectA && isObjectB) {
    try {
      var isArrayA = Array.isArray(a);
      var isArrayB = Array.isArray(b);
      if (isArrayA && isArrayB) {
        return a.length === b.length && a.every(function (e, i) {
          return looseEqual(e, b[i])
        })
      } else if (a instanceof Date && b instanceof Date) {
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) {
        var keysA = Object.keys(a);
        var keysB = Object.keys(b);
        return keysA.length === keysB.length && keysA.every(function (key) {
          return looseEqual(a[key], b[key])
        })
      } else {
        /* istanbul ignore next */
        return false
      }
    } catch (e) {
      /* istanbul ignore next */
      return false
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  } else {
    return false
  }
}
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}


var obj1 = {a:1}
var obj2 = {a:1}
console.log(obj1 == obj2)
// false
console.log(obj1 === obj2)
// false
console.log(looseEqual(obj1,obj2))
// true

是一个递归方法。如果是基本类型值则直接判断;如果是对象则遍历对象的属性,属性也有可能是引用类型,所以递归判断;如果是数组则遍历数组元素,数组元素可能是引用类型,所以递归判断;如果是日期类型则转成时间戳比较是否相等。

基于宽松相等有定义了looseIndexOf工具方法:

/**
 * Return the first index at which a loosely equal value can be
 * found in the array (if value is a plain object, the array must
 * contain an object of the same shape), or -1 if it is not present.
 */
function looseIndexOf (arr, val) {
  for (var i = 0; i < arr.length; i++) {
    if (looseEqual(arr[i], val)) { return i }
  }
  return -1
}

// 测试代码
var obj1 = {a:1}
var obj2 = {b:2}
const res = looseIndexOf([obj1, obj2], {a:1})
console.log(res)
// 0

2.26 once

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}


const func = once(function(){
  console.log('调用');
})
func()
// 调用
func()
// 不输出
func()
// 不输出

3.总结和收获

总结一下这些工具函数:

收获:

1.更深刻的理解闭包的具体用法(缓存和once)

2.了解到怎么使用call和apply实现一个bind

3.如何写一个宽松相等的比较函数(递归)