- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与
- 这是源码共读的第24期, 点击了解详情
代码下载
仓库连接: github.com/vuejs/vue/b…
工具函数
2.1 emptyObject
Object.freeze 创建一个冻结的空对象,返回的对象,不可修改、新增、删除属性,原型也不可以操作
var emptyObject = Object.freeze({})
2.2 isUndef
判断对象是否未定义,是否 undefined 或者 null
function isUndef(v) {
return v === undefined || v === null
}
2.3 isDef
判断对象是否已定义
function isDef(v) {
return v !== undefined && v !== null
}
2.4 isTrue
判断是否是布尔值 true
function isTrue(v) {
return v === true
}
2.5 isFalse
判断是否是布尔值 false
function isFalse(v) {
return v === false
}
// js 中的假值
// 0
// null
// undefined
// false
// ""
// NaN
2.6 isPrimitive
判断是否原始类型 boolean number string symbol。
function isPrimitive(value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
// mdn 的原始类型 null undefined boolean number string symbol bigint
// 由于 flow 不支持符号,而 vue 需要使用符号,因此只有在使用它们的行上禁用错误才有意义
2.7 isObject
判断是否是引用类型,null 需要单独判断 typeof null === 'object' 是 true
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
2.8 toRawType
通过原型上的判断 toString 方法一个对象的原始类型
var _toString = Object.prototype.toString
function toRawType(value) {
return _toString.call(value).slice(8, -1)
}
// 从 index 8 获取到倒数第二位
// '[object String]' String
// '[object Number]' Number
2.9 isPlainObject
判断是否为纯对象 {xx: xx}
function isPlainObject(obj) {
return _toString.call(obj) === '[object Object]'
}
2.10 isRegExp
判断是否为正则对象
function isRegExp(v) {
return _toString.call(v) === '[object RegExp]'
}
2.11 isValidArrayIndex
判断是否是正确一个的下标
// isFinite 函数用来判断被传入的参数值是否为一个有限数值
// Math.floor 向下取整 5.9 --> 5 5.4 --> 5 -5.1 --> -6
// Math.round 四舍五入
function isValidArrayIndex(val) {
var n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
2.12 isPromise
判断是否是 Promise 对象
// 判断已定义且有 then catch 个属性,函数类型
function isPromise(val) {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
2.13 toString
重写 toString 方法
// 如果是数组或者普通对象且原型上的toString方法没有被覆盖,则直接调用JSON.stringify
function toString(val) {
return val == null ?
'' :
Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ?
JSON.stringify(val, null, 2) :
String(val)
}
// JSON.stringify 接收3个参数,p1 需要转换的对象 p2 replacer 函数或者 [字段过滤数组] p3 指定缩进用的空白字符串
let obj = {
name: 'kwg',
age: 123,
sex: '男'
}
JSON.stringify(obj) // '{"name":"kwg","age":123,"sex":"男"}'
JSON.stringify(obj, null, 2) // '{\n "name": "kwg",\n "age": 123,\n "sex": "男"\n}'
JSON.stringify(
obj,
(key, value) => {
console.log('key:', key, 'value:', value)
if (key == 'name') {
return undefined
}
return value
},
2
)
// key: value: {name: 'kwg', age: 123, sex: '男'}
// VM2281:2 key: name value: kwg
// VM2281:2 key: age value: 123
// VM2281:2 key: sex value: 男
// '{\n "age": 123,\n "sex": "男"\n}'
// 在开始时,replacer 函数会被传入一个空字符串作为 key 值,代表着要被 stringify 的这个对象。随后每个对象或数组上的属性会被依次传入
// 如果返回 undefined,该属性值不会在 JSON 字符串中输出
疑问: val.toString === _toString 为什么需要判断 toString 是否被重写来执行 JSON.stringify ?
2.14 toNumber
转换为数字,转换失败则返回原始数据
function toNumber(val) {
var n = parseFloat(val)
return isNaN(n) ? val : n
}
2.15 makeMap
通过传入的字符串和一个是否忽略大小写的标记,解析,返回一个函数,此用于判断传入的 key 是否存在 map 中
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]
}
}
2.16 isBuiltInTag
isBuiltInTag(val) 判断 val 是否是 slot | component, 且忽略大小写
var isBuiltInTag = makeMap('slot,component', true)
2.17 isReservedAttribute
isReservedAttribute(val) 判断 val 是否是 key, ref, slot, slot-scope, is
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
2.18 remove
删除数组中的一个元素,先判断删除的下标是否存在,存在则删除对应元素
function remove(arr, item) {
if (arr.length) {
var index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
2.19 hasOwn
hasOwn,拦截原型上面的 hasOwnProperty 方法,检测一个对象是否含有特定的自身属性,非原型上的继承属性
var hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(obj, key) {
return hasOwnProperty.call(obj, key)
}
2.20 cached
数据缓存,已经转换过的就不用再次转换了
function cached(fn) {
var cache = Object.create(null)
return function cachedFn(str) {
var hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
2.21 camelize
- 分割的字符串,转换转换驼峰
// 匹配-分割的首字母
var camelizeRE = /-(\w)/g
var camelize = cached(function(str) {
// replace 的第二个参数是函数的话,函数第一个参数是当前匹配的字符,第二个是匹配到的字符将传入的
return str.replace(camelizeRE, function(_, c) {
return c ? c.toUpperCase() : ''
})
})
2.22 capitalize
首字母大写
var capitalize = cached(function(str) {
// 将第一个字母大写,并加上剩余的字母,slice(1)从第二个字母开始截取
return str.charAt(0).toUpperCase() + str.slice(1)
})
2.23 hyphenate
将驼峰命名用'-'连接
/**
* \b 单词边界 '[js] lesson_01.mp4' 可以通过#拆分为 '[#js#] #lesson_01#.#mp4#'
* \B 非单词边界 '[js] lesson_01.mp4' 可以通过#拆分为 '#[j#s]# l#e#s#s#o#n#_#0#1.m#p#4'
* /\B([A-Z])/ 匹配非单词边界和大写字母,formItem 替换成 form-item
*/
var hyphenateRE = /\B([A-Z])/g
var hyphenate = cached(function(str) {
return str.replace(hyphenateRE, '-$1').toLowerCase()
})
2.24 polyfillBind
bind 垫片,通过 call 和 apply 兼容不支持 bind 的环境
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)
}
// 判断是否支持bind,支持就走原生bind,否则就走兼容方案
var bind = Function.prototype.bind ? nativeBind : polyfillBind
2.25 toArray
类数组转换为数组,从第几位开始转换。全部转换的话,可以直接使用 Array.from
// 类数组:都具有 length,都可以进行 for 循环
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
}
2.26 extend
两个对象合并, 后者覆盖前者
function extend(to, _from) {
for (var key in _from) {
to[key] = _from[key]
}
return to
}
2.27 toObject
将一个对象集合的所有元素,合并成单个对象
function toObject(arr) {
var res = {}
for (var i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i])
}
}
return res
}
toObject([{
name: 'kwg'
}, {
age: 8
}]) // {name: 'kwg', age: 8}
2.28 noop
空函数
function noop(a, b, c) {}
2.29 no
永远返回布尔值 false
var no = function(a, b, c) {
return false
}
2.30 identity
永远返回一个相同的值
var identity = function(_) {
return _
}
2.31 genStaticKeys
收集所有 staticKeys,并且逗号分割
/**
* reduce 接受一个 reducer 函数参数和一个 初始值 initialValue
* reducer 函数有四个参数
* previousValue 上次的计算值如果,初次调用默认是 array[0],initialValue 有值就是initialValue
* currentValue 数组中正在处理的元素,初次调用若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]
* currentIndex 数组中正在处理的元素的索引
* array 用于遍历的数组
*/
function genStaticKeys(modules) {
return modules
.reduce(function(keys, m) {
return keys.concat(m.staticKeys || [])
}, [])
.join(',')
}
2.32 looseEqual
比较两个对象是否一致,浅对比 ,只比较值是否相同
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) {
// 对象直接对比key长度是否一致
// 通过用A[key] 的key对比B[key]的数值
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) {
// 其他类型直接转换成 string 进行比较
return String(a) === String(b)
} else {
return false
}
}
2.33 looseIndexOf
模糊查询查找一个值在数组中的index,对比原则使用 looseEqual,找不到则返回 -1
function looseIndexOf(arr, val) {
for (var i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) {
return i
}
}
return -1
}
2.34 once
确保函数只执行一次
function once(fn) {
var called = false
return function() {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
let test = once(() => {
console.log('111')
})
test() // 111
test() // undefined
test() // undefined
总结
部分工具需要结合源码才能更深刻认识到具体的用处,结合 cached 相关部分代码了解了一下正则,功能很强大。顺便推荐一本正则的书《JavaScript 正则表达式迷你书》