作者:沐浴在曙光下的贰货道士
前言
在日常开发中,我们经常会面对各种挑战和繁琐的任务。不管是处理复杂的数据结构,还是解决棘手的编程问题,都可能让我们感到沮丧和无从下手。但是,幸运的是,有一些神奇的方法可以让我们的开发变得更加轻松、高效,甚至让我们在编码的过程中快乐地摸🐟。
我们将介绍一些在日常开发中非常实用的方法,它们能够极大地提升我们的开发效率和幸福感。这些方法不仅仅是技术层面的技巧,更是关于如何优雅地解决问题、简化复杂性的智慧。通过掌握这些方法,我们能够更加从容地面对开发中的困难,并以更高的效率和质量完成我们的工作。有喜欢的朋友,欢迎一键三连。让我们一起快乐地摸🐟吧~
(顺便吆喝一句,技术大厂,前后端测试捞人,尤其东莞、深圳的岗位急缺,感兴趣来这里)
- ⭐ toFixed方法的二次封装,解决toFixed精度问题
在金钱计算中,精度问题是一个非常重要且常见的挑战。例如,当我们进行货币计算时,需要确保结果的精度准确,并且不出现舍入错误或精度损失。使用IEEE 754标准的JavaScript语言内置的toFixed方法,是处理小数精度的一种常见方式,它用于将数字保留指定的小数位数。然而,toFixed方法在某些情况下可能会导致精度问题,特别是在处理金钱计算时。例如,当进行复杂的浮点数运算时,toFixed可能会产生不准确的结果或舍入错误,这可能会对最终的计算结果产生意想不到的影响。
具体会有哪些影响呢?js中存在一个最大安全整数,可以使用Number.MAX_SAFE_INTEGER获取。值为9007199254740991,即2的53次方减1。当js的数值超过这个最大安全整数,就会出现精度丢失问题(对于采用IEEE 754标准的数值,最好使用字符串去表示,才不会丢失精度):
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
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。
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的精度问题,以及分析为什么会出现这一情况:
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方法呢?
利用网上封装的方法:
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。因为字符串不会存在精度丢失的问题,所以这些库的底层原理,就是将这些数字转换为字符串,然后按位进行计算的。
- 🌙 展开方法展万物——获取后端多层嵌套数据的绝对利器
在日常开发中,我们往往需要格式化后端返回的数据,遍历多层循环拿到我们想要的结果,然后去构造数据。然而,这种遍历的方式不仅繁琐,而且费事。那么,有没有一种比较简单的方法去格式化后端返回的数据呢?答案是有的。
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
}
给定数据:
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]
效果看上去很不错,对吗?但是如果此时需求有变更:除了这些信息外,我们还要保留其他信息,上述代码就不够看了。那么,我们又该如何解决这个问题呢?
扁平化数组或对象方法封装
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]
}
})
}
测试:
在使用第三个参数的前提下,第二个参数的key不能为最后一层的key,否则会报错,这也是本代码的一个缺陷,欢迎完善
第三个参数,数组中的每个值,都对应我们需要保留的当前mapArr key所在层的数据
第四个参数,决定是否将所有数据都铺平到数组中的对象上,如果有重名变量,则会以$传入的key和重复的键名拼接
flatMapDeepByArray(data.orderList, ['orderItem', 'freightDTO'], ['father', 'son'], true)
编辑
- 善用第三方组件库提供的工具类方法
大部分第三方组件库都有一套属于自己的格式化日期的工具类方法,因此我们没必要二次手动封装,以Element为例:
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
- 二次封装lodash中的get方法
首先,我们需要理解lodash中的get方法:
_.get(object, path, [defaultValue])
object:要获取属性值的对象或者数组(如果是数组,则第二个参数需要使用索引的形式获取值,例如'[0].name')。
path:属性路径,可以是字符串或数组形式。例如,使用字符串形式可以是 'a.b.c',使用数组形式可以是 ['a', 'b', 'c']。(Tips: 如果找不到对应的值,且未给定默认值,则返回undefined)
defaultValue(可选):属性值为undefined时,返回的默认值。
针对实际需求:后端返回的数据可能不是一个数组,而是null。再给定默认值[]是不会生效的,这也是get方法的弊端。 我们默认后端返回的数据是数组,并未照顾到代码的健壮性,此时如果强行使用数组的map方法,肯定就会报错!为此,我们需要对lodash中的get方法 进行二次封装。
import { get } from 'lodash'
window.$GET = (object, path, defaultValue) => {
return get(object, path, defaultValue) || defaultValue
}
使用:
const data = {
id: 1,
name: 'cxk',
age: null
}
vue中使用lodash原生的get方法:
get(this.data, 'age', 18) // 输出:null
$GET(this.data, 'age', 18) // 输出:18
- 忠告:使用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。并为多出的对象数据上,添加与场景相关联的字段。这样,才能判断不同场景下,是否需要显示这条数据
利用计算属性,过滤掉需要隐藏的数据。这个计算属性,就是我们真正需要使用的数据
🏋️🌰:
<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>
结果预览:
编辑
编辑
- 在js文件中使用vue文件中定义的变量
假定需求场景是:项目中存在一个比较臃肿的vue文件,为了使文件具有可读性,需要将定义在vue中的部分变量和方法抽取到js文件中。那么,定义在计算属性中(且与vue文件存在较强的关联性)的表单配置文件,该如何抽取到js文件中呢?
🧠分析:
既然js文件需要使用定义在vue文件中的变量,就一定要拿到vue实例,也就是vue文件中的this对象;
既然如此,我们可以在js文件中导出一个自定义函数,并在vue文件中引入这个函数;
在vue文件需要使用到表单配置文件时,将这个函数的this指向vue的实例,并执行该函数;
那么,在我们自定义的函数中,就可以愉快地访问到vue文件中定义好的变量了;
实现方法:
直接将this作为形参传给函数,函数接收这个形参
如果不想使用函数接收这个形参,就使用call执行这个方法,并将this作为call的第一个形参,指向该函数
🏋️🌰:
const.js
export function createData() {
return this.obj
}
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>
结果展示:
编辑
- 本应先执行的A方法,需要在B方法执行后触发
混入封装:
export default {
methods: {
createPromise(name = 'value') {
return this[`${name}Promise`] = new Promise((resolve, reject) => {
this[`${name}Resolve`] = resolve
this[`${name}Reject`] = reject
})
}
}
}
<template>
<div class="app-container system-home">
<el-button type="primary" size="small" @click="firstClickHandler">
我先点击,但我需要后触发
</el-button>
<el-button size="small" @click="secondClickHandler">
我后点击,但我需要先触发
</el-button>
</div>
</template>
<script>
import promise from '@/extend/mixins/dialog/promise'
export default {
mixins: [promise],
methods: {
async firstClickHandler() {
const validate = await this.createPromise()
if (!validate) return
console.log('先点击的后触发了')
},
secondClickHandler() {
console.log('后点击的先触发了')
this.valueResolve(true)
}
}
}
</script>
编辑
- 参数归一思想
当需要针对对象和数组进行分类讨论时,可以使用参数归一思想。
比较low的处理方法是:
针对对象,使用A逻辑进行讨论;
针对数组,使用B逻辑进行讨论。
比较推荐的做法是:参数归一
不管你传参是对象还是数组,先将参数归一化为数组,最后使用针对数组的B逻辑进行处理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。