首页 > 技术文章 > JS023. 不使用 JSON API 将 Object / Array 转换为 String (能处理 数组对象 / 对象数组 多层级嵌套)

97z4moon 2022-01-11 00:54 原文

阅前先知

* 能够在保留 <Boolean> (false) 的前提下过滤 <undefined> 与 <null> 类型。

* 无法识别 <Symbol> 与 <Function> 类型,这些类型将会与 <undefined> 、<null> 一样会被忽略。

效果预览

Object:

Array:

代码部分

/**
* [对象 -> 字符串]
* @param o [对象数据]
* @param o ({ key: value[] }: enabled)
*/
function obj2str(o){
  const result = []
  for (const prop in o) {
    if(hasValue(o[prop])) {
      if(o[prop].length && o[prop] instanceof Array) {
        result.push(`${prop}: ${arr2str(o[prop])}`)
      } else {
        switch (typeof o[prop]) {
          case 'string':
            result.push(`${prop}: '${o[prop]}'`)
            break
          case 'number':
            result.push(`${prop}: ${o[prop] + ''}`)
            break
          case 'boolean':
            result.push(`${prop}: ${o[prop].toString()}`)
            break
          case 'object':
            result.push(`${prop}: ${obj2str(o[prop])}`)
            break
        }
      }
    }
  }
  return `{ ${result.join(', ')} }`
}

/**
* [数组 -> 字符串]
* @param a [数组数据]
* @param a ([{ key: value }]: enabled)
*/
function arr2str(a) {
  const result = []
  a.forEach(item => {
    if(hasValue(item)) {
      if(item.length && item instanceof Array) {
        result.push(arr2str(item))
      } else {
        switch (typeof item) {
          case 'string':
            result.push(`'${item}'`)
            break
          case 'number':
            result.push(item + '')
            break
          case 'boolean':
            result.push(item.toString())
            break
          case 'object':
            result.push(obj2str(item))
            break
        }
      }
    }
  })
  return `[ ${result.join(', ')} ]`
}

/**
* [检验值是否存在]
* @param v [被检验值]
* @param v ([<boolean> (false): enabled)
*/
function hasValue(v) {
  let flag = !v && typeof v !== 'boolean'

  return !flag
}

应用场景

我在使用 ES6: class something extends HTMLElement 编写 shadowDOM 组件时踩的坑。

当我使用 innerHTML 创建一个 HTML 树结点: `<div onclick="myFn(${arg})">` 并点击触发时:

查看 DOM 树:

可预见的, 该参数仅仅被 innerHTML 转译成成了一段字符串,如下示例:

那么当我们想在 shadowDOM 上绑定自定义属性时,便难以将其传递给该函数的形参。

我们知道 onclick 事件的 this 会指向该 DOM 的结点,而不是继承了 HTMLElement 的 Class,无法获取类内部绑定的变量。

这时我尝试将 onclick 函数中传递的参数改为  `<div onclick="myFn({ msg: 'success!' })">`,浏览器反而能成功返回:

可以看出在 innerHTML API 中引用 Function 并传入实参本身就不被建议与支持,也许现在我们更加了解浏览器与 DOM 了。

但当我想要完整的传入一个对象 / 数组变量时,可能就要将其转换为字符串后传入,才可被浏览器解析与识别:

但如果无法改变 Class 内部的 props 变量,这样仅仅取值而不通知且无法改变宿主的拿来主义,反而让ShadowDOM变得更加局限。

需要另一个普通 Class 作为中继器来完成更多的处理,这并不能发挥 Class 的全部作用,这是一个非常浪费的行为,也是框架开发者们拥抱 Functional Component 而非 Class Component 的主要理由,当然大多框架的实现与 shadowDOM 无关,如 Vue1 的 template 与 Vue2 和 React 的 virtualDOM、JSX,shadowDOM 只是更便于原生 JS 实现组件化的手段之一。

由此可得在编写 ShadowDOM 组件时,应多利用 createElement 这样的 API 而非 innerHTML,尽可能地避免任务队列的滥用与队列栈指针的占用,虽然大部分场景中我们不会在客户端进行大量的数据访问与操作,也理应时刻谨记某些代码将会在数据量庞大或树形结构层级过深时发生不妙的结果,否则将会在滥用任务队列时看到以下报错,如某些函数的递归:

* 该报错由控制台 console.error() 模拟,本文提到的 "xxx2str" 函数并不会单独产生此结果(除非在其他代码中存在滥用队列的行为)。

- END -

推荐阅读