JS的众多小技巧之神奇的toString:你啥也能

161 阅读7分钟

吃货码农

toString真是一个让我爱不释手的方法,虽然它的职能定位很简单也很清晰,但是有的时候总是老忍不住使用它应用在一些其他的场景中。那么它能做什么,能帮助我们简化或者解决哪些复杂的问题呢?下面就让我为大家一一道来。

让我们开始吧

从字面意思来看toString表示的是转为字符串,问题就出在这,那么不同的对象他们的行为都是怎样的呢?先来看一下大概效果,然后再通过他们来做一些神奇的事情。

let a = "book"
let b = 5
let c = false
let d = {
  m: 1
}
let e = [1,2,3]
let f = function add(v1, v2) {
  return v1 + v2
}
let g = new RegExp("\d", "g")
let h = new Date()

基本列出了绝大部分常用的类型(其中null和undefined除外,其余类型类似),我们看一下执行toString之后得到的结果:

运行结果

我们可以看到,除了Object和Array类型,基本都按照定义时的样子直接输出了。

这就完啦?

就这?也没啥呀。[打脸]

接下来我们重点讨论一下toString的特殊之处↓↓↓

用于数值类型

对于数值类型来说,toString可以传入一个参数,用于指定转换之后的进制表示。

如:将十进制的5转化为二进制形式的字符串

十进制转二进制

由此我们可以将十进制的数值转换为任意合法进制的字符串表示形式。

我们注意到,在数值类型调用toString的时候先定义了一个变量来接收这个数值,然后再进行的转换,那么我们可不可以直接调用呢?

直接用数值调用

很遗憾!这样是不行的,那是怎么回事呢?我们又该怎么解决呢?

原来由于js表示数值方式的关系,所有的数值都会被解析为浮点数,也就是小数,所以5后面的点不是用来调用toString方法的,而是被解析成了小数点,看一下它的行为我们就能理解了。

正数后面点的含义

其实5.被解析成了5.0,只不过0可以省略,所以上面的代码就相当于(5.t)oString(),当然就报错啦。

我们有以下几种方式来处理这种现象:

  1. 用括号包裹:(5).toString()
  2. 写两个点:5..toString()
  3. 中间放一个空格:5 .toString()

用于对象类型

对于一个对象来说,执行完toString之后,我们几乎获取不到任何有用的信息,但是我们可以重写它的toString方法(虽然其他类型也可以这样做,但是没什么必要),来达到覆盖默认行为的效果,以便输出我们想要的。

除了我们显示的调用toString,其实对象也会有一些默认的隐式调用,比如在做相等运算符的时候。

隐式调用toString

(Tips: 其实这里在做隐式调用的时候,会优先检查该对象的valueOf方法是否存在,如果有重写,那么会优先调用valueOf并获取返回值,然后再做比较,否则才会调用toString方法。)

相信看到这里,小伙伴们都会想到那个经典的面试题,而且心中应该也已经有了思路,一起看一下:

覆盖默认行为

是不是很容易理解了呢?

通过这个例子,我们了解了它的运行机制,其实一通百通,其他的应用场景大家也应该是能够信手拈来的了

类似的还有一道题,其实原理也是这样的。

sort排序的默认行为

数组在调用sort方法进行排序的时候,如果不传入回调函数,默认行为会将每一项执行toString之后再进行比较,这个时候完全就是按照字符编码的顺序进行排列,因此会产生反直觉的结果。

因此,我们同样可以改写toString来改变默认行为:

sort排序改变默认行为

用于函数类型

这个是我比较钟爱的一种操作,他帮助我解决了很多难题,甚至除了这种方法我没有想到其他能解决的办法,请看下面的示例:

//DTtestone.js
function dataView() {
  [{
    className: "pd-5px",
    cols: [{
      span: 8,
      className: "text-right",
      elements: [{
        type: "string",
        descrip: "报销金额"
      }]
    }, {
      span: 16,
      elements: [{
        type: "input",
        model: true,
        name: "money",
        money: allDatas.money,
      }, {
        type: "string",
        descrip: "(元)"
      }]
    }]
  }]
}

乍一看,这个函数没有什么特殊之处,只是感觉有点奇怪,甚至觉得没有什么用,因为只是执行了一个数组,连一个接收值的变量都没有定义。

是的,函数的toString执行之后返回的结果就是定义的方法本身。我们把这个函数放在代码的任意一个地方,不去执行的话,永远不会报错。但是一旦执行的话,我们注意allDatas.money这里,就是抛出未定义的错误。

什么情况?

考虑这样一个场景。我的项目中为了做到组件化管理,我使用了数据视图来驱动组件的渲染,一个组件会有多种不同的呈现形态,即会对应很多个类似上面代码中dataView函数中的数组(即数据视图),甚至更复杂的数据视图。如果这些数据视图都写在一个文件里面,那么管理起来相当麻烦,而且维护成本非常高。

这可怎么办

那么我们能不能把数据视图与组件剥离开来,作为单独的模块进行管理呢?

自然而然的就会想到,可以使用export和import来对这些文件进行引用,每个文件都暴露出一个数组(即数据视图)。这时又会引发另一个问题,那就是这里面的allDatas是注入到组件里面的实例属性,直接在数据视图里面引用的话,在运行时会直接抛出错误。

直接报错影响程序运行

可是我们总不能每个数据视图的文件都引入一次吧,而且我们希望是在组件里面的数据全部初始化完毕之后再去解析数据视图,生成最终的状态。要是再有很多的组件呢?那么这种情况将变得不可控。

简直令人抓狂

因此我们既想要能在独立的文件中导出,又不想每个文件都引入组件,更不想在运行时报错,而且要能够只在指定的时机进行解析。那么此时我们就有请超牛气的toString登场,来帮我们解决这些问题。

请帮帮我吧

此时我们只需要按照上面的函数方式书写,然后在导出的时候做一下处理:

//DTtestone.js
export default {
  name: 'DTtestone',
  dataView: dataView.toString()
}

接下来在扫描文件的时候,我们再做进一步处理:

//importDataView.js
const elementsDataViews = require.context(
  // 视图目录的相对路径
  '@/dataViews',
  // 是否查询其子目录
  true,
  // 匹配数据视图文件名的正则表达式
  /DT.+.(js)$/
)
elementsDataViews.keys().forEach(fileName => {
  const componentConfig = elementsDataViews(fileName)
  let { name, dataView } = componentConfig.default
  allDataViews[name] = dataView.substring(21, dataView.length - 1)
})
export default allDataViews

其中对获取到的数据视图进行dataView.substring(21, dataView.length - 1)就拿到了函数体中的内容。

最后我们在组件文件里面引入这些数据视图,并且可以在适当的时机去执行它们:

//testone.vue
eval(allDataViews[name]);//name为引入的视图名称

到此为止,用这种方式就可以解决我们上面遇到的所有问题,甚至能使用当前组件内执行作用域链上的所有变量。

vue3提出了一个很重要的概念,也是它非常重要的一个特性,就是组合api。使得代码组织管理起来非常方便,提升了可读性与可维护性。

利用上面这种方法,我们甚至可以在代码块级别的粒度上高度抽象出来复用性强、耦合度低、易维护的功能。

前端路漫漫,不停往前看

由于奇技淫巧太多了,限于篇幅所制约,很多细节没有给大家深入去探讨,只挑出了一些典型的案例给大家讲解一下,希望能够帮助到前端路上的你!

感谢阅读

总的来说

js有很多好玩的特性与方法等待我们来挖掘,toString只是其中的一个,在我们的工作中熟练掌握这些,能帮助我们解决很多难题。毕竟生产力才是硬道理!!!期待与大家再见面。[比心]