携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
使用说明
在lodash里,add方法主要是对两个参数进行相加操作。
_.add(6, 4);
// => 10
_.add(1, 'ddd')
// => 1ddd
_.add(1)
// => 1
我们可以通过其功能直接实现如下方法:
function add(num1, num2) {
return num1 + num2
}
不过对于参数我们一般要进行容错处理,毕竟在lodash里只有一个参数的情况并不会报错:
function add(num1, num2) {
if (num1 === undefined && num2 === undefined) return 0
if (num1 !== undefined && num2 === undefined) return num1
if (num2 !== undefined && num1 === undefined) return num2
return num1 + num2
}
通过容错处理,我们实现了简单的add操作方法。
翻看lodash源码实现,我们可以看到其内部的容错判断还添加了对字符串+数字的情况进行处理,即当两个数都存在,并且之间至少出现一个字符串时,将二者转为字符串,其余情况则将两数转为数字:
function add(num1, num2) {
if (num1 === undefined && num2 === undefined) return 0
if (num1 !== undefined && num2 === undefined) return num1
if (num2 !== undefined && num1 === undefined) return num2
if (typeof value === 'string' || typeof other === 'string') {
num1 = value + ''
num2 = other + ''
} else {
num1 = +num1
num2 = +num2
}
return num1 + num2
}
上面的方法就是lodash里add方法的实现,可以发现其内部做了充分的严谨性判断,并且这种对于特例进行判断输出的手法,在《重构》一书中也叫以卫语句取代嵌套条件表达式。
同时以个人的见解,在多数if判断中尽量减少else的产生,让代码更加直观。
考虑到这种对于两个参数的非空判断操作,即很多情况下有的参数需要进行上面的判断操作,可以考虑对其进行封装抽离,实际上,lodash源码里正是这么做的,其内部通过封装成createMathOperation工厂函数,对类似操作进行抽离,在抽离的方法里进行容错处理,该方法从语意上代表了数字操作,即数字操作可以通过该工厂创建。第一个参数为数字操作的处理函数,第二个参数为创建的函数调用出错时返回的默认值。
function createMathOperation(operator, defaultValue) {
return (num1, num2) => {
if (num1 === undefined && num2 === undefined) {
return defaultValue
}
if (num1 !== undefined && num2 === undefined) {
return value
}
if (num2 !== undefined && num1 === undefined) {
return other
}
if (typeof num1 === 'string' || typeof num2 === 'string') {
num1 = num1 + ''
num2 = num2 + ''
}
else {
num1 = +num1
num2 = +num2
}
return operator(num1, num2)
}
}
lodash源码中导出的add方法:
const add = createMathOperation((augend, addend) => augend + addend, 0)
类型转换
在实现数字+字符串的判断逻辑处理,我们用了隐式转换的操作,在lodash里其额外封装了数据类型转换方法,其方法内部更多也是严谨性的判断。
字符串类型转换封装
在日常的类型转换操作中,我们常常通过+运算操作将数据类型转换成字符串,或者调用数据原型链上的toString方法,在lodash的字符串转换操作中是这样写的:
function baseToString(value) {
if (typeof value === 'string') {
return value
}
if (Array.isArray(value)) {
return `${value.map(baseToString)}`
}
if (isSymbol(value)) {
return symbolToString ? symbolToString.call(value) : ''
}
const result = `${value}`
const INFINITY = 1 / 0
return (result === '0' && (1 / value) === -INFINITY) ? '-0' : result
}
其内部主要使用typeof操作符判断是否是字符串类型,是的话直接返回参数,对于数组,则是通过调用Array.isArray方法进行,然后输出其遍历的结果。
这里其实个人倾向于将Array.isArray方法封装成函数方法,在判断直接调用即可,即:
const isArray = value => Array.isArray(value)
对于Symbol类型转字符串,当发现是Symbol类型时则查看其原型链上是否存在toString方法,没有的话则返回空字符串,对于判断Symbol类型以及当前环境是否支持Symbol,lodash内部也封装了相应的方法。
Symbol类型转换封装
判断数据类型是否是Symbol,lodash源码实现如下:
const toString = Object.prototype.toString
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
function isSymbol(value) {
const type = typeof value
return type == 'symbol' || (type === 'object' && value != null && getTag(value) == '[object Symbol]')
}
通过调用typeof操作符获取其类型,在不支持Symbol类型的环境下,typeof判断的Symbol数据为对象类型,所以直接调用Object.prototype.toString方法,不过由于toString方法十分常用,所以lodash内部也进行了相应的封装。
数字类型转换封装
我们先来看看lodash源码里转换为数字类型的方法实现:
const NAN = 0 / 0
function baseToNumber(value) {
if (typeof value === 'number') {
return value
}
if (isSymbol(value)) {
return NAN
}
return +value
}
对于NaN并不是直接声明,而是通过0/0的方式,而该方法重点也是对Symbol类型进行处理返回,其余数据类型则调用隐式类型转换。
源码实现
function isSymbol(value) {
const toString = Object.prototype.toString
function getTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
return toString.call(value)
}
const type = typeof value
return type == 'symbol' || (type === 'object' && value != null && getTag(value) == '[object Symbol]')
}
function baseToString(value) {
if (typeof value === 'string') {
return value
}
if (Array.isArray(value)) {
return `${value.map(baseToString)}`
}
if (isSymbol(value)) {
return symbolToString ? symbolToString.call(value) : ''
}
const INFINITY = 1 / 0
const result = `${value}`
return (result === '0' && (1 / value) === -INFINITY) ? '-0' : result
}
function baseToNumber(value) {
if (typeof value === 'number') {
return value
}
if (isSymbol(value)) {
const NAN = 0 / 0
return NAN
}
return +value
}
function createMathOperation(operator, defaultValue) {
return (value, other) => {
if (value === undefined && other === undefined) {
return defaultValue
}
if (value !== undefined && other === undefined) {
return value
}
if (other !== undefined && value === undefined) {
return other
}
if (typeof value === 'string' || typeof other === 'string') {
value = baseToString(value)
other = baseToString(other)
}
else {
value = baseToNumber(value)
other = baseToNumber(other)
}
return operator(value, other)
}
}
const add = createMathOperation((augend, addend) => augend + addend, 0)
小结
在本篇章中我们从lodash的add方法实现入手,然后学习lodash源码库里的封装方法,对类型错误进行封装处理,并且我们也学习了对于基础数据类型判断的封装方法,如判断Symbol类型,数据转换为数字类型,数据转换为字符串类型等。
可以看出lodash源码对于方法实现更加关注的是严谨性的判断,在方法实现中也经常使用特例去判断。
lodash源码对于基础工具方法的封装放在里一个叫做/.internal的目录下,我们在实际项目开发中也可以学习这种目录结构划分。
对于源码的学习,除了关注实现,我们更应该关注其运用的重构手法以及分层和设计。