头图

作者:沐浴在曙光下的贰货道士

前言

  在日常开发中,我们经常会面对各种挑战和繁琐的任务。不管是处理复杂的数据结构,还是解决棘手的编程问题,都可能让我们感到沮丧和无从下手。但是,幸运的是,有一些神奇的方法可以让我们的开发变得更加轻松、高效,甚至让我们在编码的过程中快乐地摸🐟。

  我们将介绍一些在日常开发中非常实用的方法,它们能够极大地提升我们的开发效率和幸福感。这些方法不仅仅是技术层面的技巧,更是关于如何优雅地解决问题、简化复杂性的智慧。通过掌握这些方法,我们能够更加从容地面对开发中的困难,并以更高的效率和质量完成我们的工作。有喜欢的朋友,欢迎一键三连。让我们一起快乐地摸🐟吧~

(顺便吆喝一句,技术大厂,前后端测试捞人,尤其东莞、深圳的岗位急缺,感兴趣来这里

  1. ⭐ 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。因为字符串不会存在精度丢失的问题,所以这些库的底层原理,就是将这些数字转换为字符串,然后按位进行计算的。

  1. 🌙 展开方法展万物——获取后端多层嵌套数据的绝对利器

  在日常开发中,我们往往需要格式化后端返回的数据,遍历多层循环拿到我们想要的结果,然后去构造数据。然而,这种遍历的方式不仅繁琐,而且费事。那么,有没有一种比较简单的方法去格式化后端返回的数据呢?答案是有的。

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)

图片

图片

图片
​编辑

  1. 善用第三方组件库提供的工具类方法

  大部分第三方组件库都有一套属于自己的格式化日期的工具类方法,因此我们没必要二次手动封装,以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

图片

  1. 二次封装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

图片

  1. 忠告:使用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>

图片

结果预览:

图片

图片
​编辑

图片

图片
​编辑

  1. 在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>

图片

结果展示:

图片

图片
​编辑

  1. 本应先执行的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>

图片

图片

图片
​编辑

  1. 参数归一思想

  当需要针对对象和数组进行分类讨论时,可以使用参数归一思想。

  比较low的处理方法是:

针对对象,使用A逻辑进行讨论;
针对数组,使用B逻辑进行讨论。

  比较推荐的做法是:参数归一

不管你传参是对象还是数组,先将参数归一化为数组,最后使用针对数组的B逻辑进行处理。


幸福的闹钟
53 声望13 粉丝