前言
大家好,我是沐浴在曙光下的贰货道士。在日常开发中,我们经常会面对各种挑战和繁琐的任务。不管是处理复杂的数据结构,还是解决棘手的编程问题,都可能让我们感到沮丧和无从下手。但是,幸运的是,有一些神奇的方法可以让我们的开发变得更加轻松、高效,甚至让我们在编码的过程中快乐地摸🐟。
我们将介绍一些在日常开发中非常实用的方法,它们能够极大地提升我们的开发效率和幸福感。这些方法不仅仅是技术层面的技巧,更是关于如何优雅地解决问题、简化复杂性的智慧。通过掌握这些方法,我们能够更加从容地面对开发中的困难,并以更高的效率和质量完成我们的工作。有喜欢本文的朋友,欢迎一键三连哦。让我们一起快乐地摸🐟吧~
1. ⭐ toFixed
方法的二次封装,解决toFixed
精度问题
在金钱计算中,精度问题是一个非常重要且常见的挑战。例如,当我们进行货币计算时,需要确保结果的精度准确,并且不出现舍入错误或精度损失。使用IEEE 754
标准 的JavaScript
语言内置的toFixed
方法,是处理小数精度的一种常见方式,它用于将数字保留指定的小数位数。然而,toFixed
方法在某些情况下可能会导致精度问题,特别是在处理金钱计算时。例如,当进行复杂的浮点数运算时,toFixed
可能会产生不准确的结果或舍入错误,这可能会对最终的计算结果产生意想不到的影响。
具体会有哪些影响呢?js
中存在一个最大安全整数,可以使用Number.MAX_SAFE_INTEGER
获取 。值为9007199254740991
,即2
的53
次方减1
。当js
的数值超过这个最大安全整数,就会出现精度丢失问题(对于采用
IEEE 754标准的数值,最好使用字符串去表示,才不会丢失精度
):
js
console.log(Number.MAX_SAFE_INTEGER) // 输出:9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1) // 输出:9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2) // 输出:9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 3) // 输出:9007199254740994
9007199254740999 // 输出:9007199254741000
'9007199254740999' // 输出:'9007199254740999',未丢失精度
在讲解Number.toFixed(digits)
方法产生的精度问题之前,先提及两个方法:
value.toString(radix):
radix
可选,默认为10进制。取值范围是2到36
js
`1. 数值类型(调用Number原型链上的toString方法):`
const num = 42
num.toString(2) // 输出:'101010'
num.toString(16) // 输出:'2a'
num.toString('16') // radix转换为数字, 输出:'2a'
num.toString(2.7) // radix向下取整, 输出:'101010'
num.toString(37) // 提示RangeError
`验证:修改数值原型链上的toString方法:`
Number.prototype.toString = () => { console.log('cxk你太美') }
num.toString(2) // 输出:'cxk你太美'
`2. 布尔类型(调用Boolean原型链上的toString方法):`
const bool = true
console.log(bool.toString()) // 输出:'true'
`验证:修改布尔原型链上的toString方法:`
Boolean.prototype.toString = () => { console.log('cxk你太美') }
bool.toString() // 输出:'cxk你太美'
`3. 数组类型(调用数组原型链上的toString方法):`
const arr = [1, 2, 3]
[1, 2, 3].toString() // 输出: '1,2,3'
[1, 2, 3].toString(5) // 输出: '1,2,3'
`验证:修改数组原型链上的toString方法:`
Array.prototype.toString = () => { console.log('cxk你太美') }
arr.toString() // 输出:'cxk你太美'
`4. 对象类型(调用对象原型链上的toString方法):`
const obj = { name: 'cxk', age: 18 }
console.log(obj.toString()) // 向上找原型链,调用Object.prototype.toString方法,输出:'[object Object]'
`验证:修改对象原型链上的toString方法:`
const obj = {
name: 'cxk',
age: 18,
toString() {
return `name: ${this.name}, age: ${this.age}`
}
}
console.log(obj.toString()) // 输出:name: cxk, age: 18
Number.toPrecision(precision):
precision
可选。四舍五入,将value
转换为precision
指定的显示数字位数,取值范围为1
到100
。如果省略该参数,则调用Number.prototype.toString()
方法,返回原始数字的字符串形式。如果参数不在1
和100
(包括)之间,将会抛出一个RangeError
。
js
`Number.toPrecision同样会存在精度问题,在下文会提及`
const num = 142.55
num.toPrecision(1) // 输出:'1e+2',num有3位数字,无法保留1位有效数字,所以结果为1e+2
num.toPrecision(2) // 输出:'1.4e+2'
num.toPrecision(3) // 输出:'143'
num.toPrecision(4) // 输出:'142.6'
num.toPrecision(5) // 输出:'142.55'
num.toPrecision(6) // 输出:'142.550'
OK!咱们开始看toFixed
的精度问题,以及分析为什么会出现这一情况:
js
`Number.toFixed(x): 将Number四舍五入为指定小数位数的字符串,x和toString方法的传参类似,只不过限制在0到20`
(10.23).toFixed() // 输出:'10',不指定传参,默认为0
(1.55).toFixed(1) // 输出:'1.6'
(1.45).toFixed(1) // 输出:'1.4'
`不是四舍五入吗?为什么会得到这种结果呢?`
`让我们先看下,1.45转换为二进制会得到什么结果?`
(1.45).toString(2) // 输出: '1.0111001100110011001100110011001100110011001100110011'
`可以发现,1.45转换为二进制的结果,是一直循环的,无法用有限位数字来表示。`
`那么结论来了:所有转换为二进制出现这种情况的,都会出现精度问题,或多或少一些`
`然后,让我们看下1.45在计算机里存储的真实样貌:`
(1.45).toPrecision(60) // 输出:'1.44999999999999995559107901499373838305473327636718750000000'
`所以,1.45保留一位小数,结果是'1.4'`
(1.45).toPrecision(2) // 输出:'1.4', 所以Number.toPrecision方法同样会存在精度问题
`同理,让我们看下1.55在计算机里存储的真实样貌:`
(1.55).toPrecision(60) // 输出:'1.55000000000000004440892098500626161694526672363281250000000'
`所以,1.55保留一位小数,结果是'1.6'`
(1.55).toPrecision(2) // 输出:'1.6'
(核心) 明白了这些原理之后,我们该如何处理使用IEEE 754
标准的toFixed
方法呢?
js
`利用网上封装的方法:`
`num是要进行四舍五入的数字,precision是需要保留的小数位数:`
`先将num放大,并保留原始的一位小数。利用Math.round四舍五入,再缩放到指定小数位`
function toFixed(num, precision) {
const adjustment = Math.pow(10, precision)
return (Math.round(num * adjustment) / adjustment)
}
`验证:`
toFixed(1.45, 1) // 输出:1.5, 可以的
toFixed(1158.725, 2) // 输出:1158.72, 芭比Q了
`为什么会出现这一现象?`
`因为拥有IEEE 754标准的js语言,二进制无法转换为有限位表示的小数,本就存在精度问题`
`此时,利用js内置的所有计算方法来处理小数的计算,也会伴随出现精度问题`
`对于超过最大安全整数的数值,也没办法规避精度问题, 就算使用某些库也一样。`
`因为这个数值,本身就无法在拥有IEEE 754标准的js语言中正确表示`
toFixed(181818181818181818.23, 1) // 输出:'181818181818181820'
那么,有什么方法可以用来解决toFixed
的精度问题呢?答案是有的,那就是借助第三方库,比如big.js
。因为字符串不会存在精度丢失的问题,所以这些库的底层原理,就是将这些数字转换为字符串,然后按位进行计算的。
js
`方法封装:`
import Big from 'big.js'
function toFixed(num, precision) {
return new Big(num).toFixed(precision)
}
`验证:`
toFixed(1.45, 1) // 输出:'1.5',
toFixed(1158.725, 2) // 输出:1158.73,完美!!!
`注意:利用这个方法,对于超过js最大安全整数的数,必须传入字符串。`
`因为它都无法正确存储在内存中,所以必然会损失精度。`
`如果要正确表示'181818181818181818.23',除非已知这个字符串,或者由后端返回该字符串才行。`
toFixed('181818181818181818.23', 1) // 输出: '181818181818181818.2'
2. 🌙 展开方法展万物------获取后端多层嵌套数据的绝对利器
在日常开发中,我们往往需要格式化后端返回的数据,遍历多层循环拿到我们想要的结果,然后去构造数据。然而,这种遍历的方式不仅繁琐,而且费事。那么,有没有一种比较简单的方法去格式化后端返回的数据呢?答案是有的。
js
import { flatMapDeep, isPlainObject, isArray } from 'lodash'
`data可以为数组或者对象`
`mapArr必须为数组,记录从第二级(第一级为data),到需要遍历级的所需key值,且这些key必须具备父子嵌套关系`
export function flatMapDeepByArray(data, mapArr = []) {
let flatMapArr = []
if (!mapArr.length) return []
`如果data是对象,就取出第二级的key,并将data[key]变为数组`
if (isPlainObject(data)) {
const shiftData = data[mapArr.shift()]
flatMapArr = isArray(shiftData) ? shiftData : [shiftData]
} else flatMapArr = data
`遍历并递归,展开铺平后,得到flatMapArr`
mapArr.forEach((item, ind) => {
flatMapArr = flatMapDeep(flatMapArr, (n) => {
`关于$GET的定义,详见下文`
let arr = $GET(n, `${[mapArr[ind]]}`, [])
return arr
})
})
return flatMapArr
}
js
`给定数据:`
const data = {
id: 1,
orderCode: '202309271100',
orderList: [
{
id: 2,
orderItem: [{ id: 11, name: '城府', productCount: 34567, freightDTO: { freight: 3, realFreight: 1 } }]
},
{
id: 3,
orderItem: [
{ id: 21, name: '素颜', productCount: 45678, freightDTO: { freight: 4, realFreight: 2 } },
{ id: 22, name: '认错', productCount: 56789, freightDTO: { freight: 9, realFreight: 8 } }
]
},
{
id: 4,
orderItem: [{ id: 31, name: '多余的解释', productCount: 678910, freightDTO: { freight: 12, realFreight: 10 } }]
}
]
}
`需求: 假定我们需要将所有realFreight给取出来,并形成一个数组`
`测试:`
flatMapDeepByArray(data.orderList, ['orderItem', 'freightDTO', 'realFreight']) // [1, 2, 8, 10]
效果看上去很不错,对吗?但是如果此时需求有变更:除了这些信息外,我们还要保留其他信息,上述代码就不够看了。那么,我们又该如何解决这个问题呢?
js
`扁平化数组或对象方法封装`
import { flatMapDeep, isPlainObject, isArray, upperFirst } from 'lodash'
`* 对数组进行深度扁平化,并提取指定的属性路径值。`
`* @param {Array} data - 要扁平化的数组。`
`* @param {Array} mapArr - 属性路径数组。`
`* @param {Array} mapKeyArr - 需要填充的属性路径数组。`
`* @param {boolean} needFill - 是否需要填充属性值。`
`* @returns {Array} - 扁平化后的数组。`
export function flatMapDeepByArray(data, mapArr = [], mapKeyArr = [], needFill = false) {
let flatMapArr = []
if (!mapArr.length) return []
if (isPlainObject(data)) {
const shiftData = data[mapArr.shift()]
flatMapArr = isArray(shiftData) ? shiftData : [shiftData]
} else flatMapArr = data
mapKeyArr = mapKeyArr.slice(0, mapArr.length)
mapArr.map((item, ind) => {
flatMapArr = flatMapDeep(flatMapArr, (n) => {
let arr = $GET(n, `${[mapArr[ind]]}`, [])
if (!isArray(arr)) arr = [arr]
const sliceKeyArr = mapKeyArr.slice(0, ind + 1)
const sliceMapArr = mapArr.slice(0, ind + 1)
sliceKeyArr.map((key, k) => {
arr.map((nItem, index) => {
nItem.$index = index
if (k == sliceMapArr.length - 1) {
return (nItem[`$${key}`] = n)
}
nItem[`$${key}`] = n[`$${key}`]
})
})
return arr
})
})
if (needFill) flatMapArr.map((item) => fillProps(item, mapKeyArr))
return flatMapArr
}
`* 填充对象的属性值。`
`* @param {Object} obj - 要填充属性值的对象。`
`* @param {Array} props - 属性路径数组。`
export function fillProps(obj, props) {
if (!isArray(props)) props = [props]
props = props.map((prop) => `$${prop}`)
props.map((prop) => {
const val = obj[prop]
if (!isPlainObject(val)) return
for (let key in val) {
const valKey = obj[key] ? `${prop}${upperFirst(key)}` : key
obj[valKey] = val[key]
}
})
}
js
`测试:`
`在使用第三个参数的前提下,第二个参数的key不能为最后一层的key,否则会报错,这也是本代码的一个缺陷,欢迎完善`
`第三个参数,数组中的每个值,都对应我们需要保留的当前mapArr key所在层的数据`
`第四个参数,决定是否将所有数据都铺平到数组中的对象上,如果有重名变量,则会以$传入的key和重复的键名拼接`
flatMapDeepByArray(data.orderList, ['orderItem', 'freightDTO'], ['father', 'son'], true)
3. 善用第三方组件库提供的工具类方法
大部分第三方组件库都有一套属于自己的格式化日期的工具类方法,因此我们没必要二次手动封装,以Element
为例:
js
import { formatDate } from 'element-ui/src/utils/date-util'
`定义格式化日期的方法及默认显示年月日时分秒的格式`
export getFormatData(date = new Date(), format = 'yyyy-MM-dd hh:mm:ss') {
return formatDate(date, format)
}
new Date() // Tue Oct 10 2023 08:37:59 GMT+0800 (中国标准时间)
getFormatData() // 2023-10-10 08:37:59
getFormatData(new Date(), 'yyyyMMdd') // 20231010
4. 二次封装lodash
中的get
方法
首先,我们需要理解lodash
中的get
方法:
js
_.get(object, path, [defaultValue])
object
:要获取属性值的对象或者数组(如果是数组,则第二个参数需要使用索引的形式获取值,例如'[0].name'
)。path
:属性路径,可以是字符串或数组形式。例如,使用字符串形式可以是'a.b.c'
,使用数组形式可以是['a', 'b', 'c']
。(Tips: 如果找不到对应的值,且未给定默认值,则返回undefined
)defaultValue
(可选):属性值为undefined
时,返回的默认值。
针对实际需求:后端返回的数据可能不是一个数组,而是null
。再给定默认值[]
是不会生效的,这也是get
方法的弊端。 我们默认后端返回的数据是数组,并未照顾到代码的健壮性,此时如果强行使用数组的map
方法,肯定就会报错!为此,我们需要对lodash
中的get
方法 进行二次封装。
js
import { get } from 'lodash'
window.$GET = (object, path, defaultValue) => {
return get(object, path, defaultValue) || defaultValue
}
使用:
js
const data = {
id: 1,
name: 'cxk',
age: null
}
`vue中使用lodash原生的get方法:`
get(this.data, 'age', 18) // 输出:null
$GET(this.data, 'age', 18) // 输出:18
5. 忠告:使用vue
, 但思维不要太vue
vue
很多的底层原理,都是通过js
来实现的。不要离开vue
,就不知道该如何动态显示/隐藏数据了。在vue
中,是通过v-if
或者v-show
来实现。而在js
中,是通过filter
来实现。
假定有这么一个业务场景:
- 在
A
场景下,需要显示数组A
; - 在
B
场景下,需要显示数组B
; - 而数组
A
的内容是数组B
内容的真子集;
比较low
的处理方法是:
- 定义数组
A
作为公共数据; - 将公共数据
A
展开,并拼接上新增的内容,形成新的数组B
; - 利用计算属性和策略模式,在不同情况下,返回不同的数据
A
或者B
比较推荐的做法是:利用vue
的思想,数据驱动视图
- 在计算属性中,定义好数组
B
。并为多出的对象数据
上,添加与场景相关联的字段。这样,才能判断不同场景下,是否需要显示这条数据 - 利用计算属性,过滤掉需要隐藏的数据。这个计算属性,就是我们真正需要使用的数据
🏋️🌰:
js
<template>
<div class="app-container">
<el-button type="primary" size="small" @click="toggleHandler">切换</el-button>
<p>请欣赏Jay Chou歌曲:</p>
<div class="success mt10" v-for="{ song } in finalData" :key="song">
{{ song }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
hide: true
}
},
computed: {
data({ hide }) {
return [
{ singer: '周杰伦', song: '说了再见' },
{ singer: '周杰伦', song: '我落泪情绪零碎' },
{ singer: '周杰伦', song: '反方向的钟', hide },
{ singer: '周杰伦', song: '可爱女人' }
]
},
finalData({ data }) {
return data.filter(({ hide }) => !hide)
}
},
methods: {
toggleHandler() {
this.hide = !this.hide
}
}
}
</script>
<style>
.success {
color: green;
}
</style>
结果预览:
6. 在js
文件中使用vue
文件中定义的变量
假定需求场景是:项目中存在一个比较臃肿的vue
文件,为了使文件具有可读性,需要将定义在vue
中的部分变量和方法抽取到js
文件中。那么,定义在计算属性中(且与vue
文件存在较强的关联性 )的表单配置文件,该如何抽取到js
文件中呢?
🧠分析:
- 既然
js
文件需要使用定义在vue
文件中的变量,就一定要拿到vue
实例,也就是vue
文件中的this
对象; - 既然如此,我们可以在
js
文件中导出一个自定义函数,并在vue
文件中引入这个函数; - 在
vue
文件需要使用到表单配置文件时,将这个函数的this
指向vue
的实例,并执行该函数; - 那么,在我们自定义的函数中,就可以愉快地访问到
vue
文件中定义好的变量了;
🏋️🌰:
js
`const.js`
export function createData() {
return this.obj
}
js
`vue文件:`
<template>
<div class="app-container">
{{ data.name }}
</div>
</template>
<script>
import { createData } from './module/const'
export default {
data() {
return {
obj: {
name: 'cxk',
age: 18,
hobby: 'sing, dance and rap'
}
}
},
computed: {
data() {
return createData.call(this)
}
}
}
</script>
结果展示:
结语
往期精彩推荐(强势引流):
大概就这样吧~