likes
comments
collection
share

🔥🔥🔥自从学会这些奇技淫巧,日常开发可以快乐地摸🐟了

作者站长头像
站长
· 阅读数 27

前言

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

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

🔥🔥🔥自从学会这些奇技淫巧,日常开发可以快乐地摸🐟了

1. ⭐ toFixed方法的二次封装,解决toFixed精度问题

  在金钱计算中,精度问题是一个非常重要且常见的挑战。例如,当我们进行货币计算时,需要确保结果的精度准确,并且不出现舍入错误或精度损失。使用IEEE 754标准JavaScript语言内置的toFixed方法,是处理小数精度的一种常见方式,它用于将数字保留指定的小数位数。然而,toFixed方法在某些情况下可能会导致精度问题,特别是在处理金钱计算时。例如,当进行复杂的浮点数运算时,toFixed可能会产生不准确的结果或舍入错误,这可能会对最终的计算结果产生意想不到的影响。

  具体会有哪些影响呢?js中存在一个最大安全整数,可以使用Number.MAX_SAFE_INTEGER获取。值为9007199254740991,即253次方减1js的数值超过这个最大安全整数,就会出现精度丢失问题(对于采用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指定的显示数字位数,取值范围为1100。如果省略该参数,则调用Number.prototype.toString()方法,返回原始数字的字符串形式。如果参数不在1100(包括)之间,将会抛出一个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 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. 🌙 展开方法展万物——获取后端多层嵌套数据的绝对利器

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

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)

🔥🔥🔥自从学会这些奇技淫巧,日常开发可以快乐地摸🐟了

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

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

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

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。并为多出的对象数据上,添加与场景相关联的字段。这样,才能判断不同场景下,是否需要显示这条数据
  • 利用计算属性,过滤掉需要隐藏的数据。这个计算属性,就是我们真正需要使用的数据

  🏋️‍🌰:

<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文件中定义好的变量了;

🏋️‍🌰:

`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>

结果展示:

🔥🔥🔥自从学会这些奇技淫巧,日常开发可以快乐地摸🐟了

结语

往期精彩推荐(强势引流):

  大概就这样吧~