Skip to content

将 JavaScript 对象展开或还原

数据表示经常彼此不同,数据结构的要求也不同。有时,您需要将嵌套对象转换为扁平对象,或者反之亦然。这是一个非平凡的任务,但可以使用递归策略来解决。

展开对象

给定一个对象,您可以通过将每个叶节点转换为扁平路径节点来展开它。由于这可能很难理解,这里有一个示例:

const fileSizes = {
  package: 256,
  src: {
    index: 1024,
    styles: {
      main: 128,
      colors: 16
    },
  },
  assets: {
    images: {
      logo: 512,
      background: 512
    },
    fonts: {
      serif: 64
    }
  }
};

const flattenedFileSizes = {
  'package': 256,
  'src.index': 1024,
  'src.styles.main': 128,
  'src.styles.colors': 16,
  'assets.images.logo': 512,
  'assets.images.background': 512,
  'assets.fonts.serif': 64
};

[!NOTE]

生成的键的顺序可能与原始键的顺序不匹配,因为 JavaScript 对象是无序的。本文中的所有示例都保留了原始顺序,以便更容易理解。

为了构建一个递归解决方案,我们需要定义基本情况。在这种情况下,基本情况是值不是对象的键。在这种情况下,我们可以简单地将键值对添加到结果中

对于值是对象的键,我们需要递归调用函数,并将当前键作为前缀添加。对于更深层次的情况,我们必须将任何先前的前缀添加到当前键之前。使用这个算法,我们可以构建出展开的对象。

好的,那么我们如何在 JavaScript 中实现这个呢? 我们可以使用 Object.keys() 来获取对象的键,并使用 Array.prototype.reduce() 将每个叶节点转换为扁平路径节点。如果键的值是对象,我们使用适当的前缀递归调用函数来使用 Object.assign() 创建路径。否则,我们将适当的带前缀的键值对添加到累加器对象中。

在前面的示例中,我们使用了一个分隔符.来分隔键,但是这可以通过使用额外的参数进行自定义。最后一个参数用于递归调用,除非您希望每个键都有一个前缀,否则应该始终省略。

const flattenObject = (obj, delimiter = '.', prefix = '') =>
  Object.keys(obj).reduce((acc, k) => {
    const pre = prefix.length ? `${prefix}${delimiter}` : '';
    if (
      typeof obj[k] === 'object' &&
      obj[k] !== null &&
      Object.keys(obj[k]).length > 0
    )
      Object.assign(acc, flattenObject(obj[k], delimiter, pre + k));
    else acc[pre + k] = obj[k];
    return acc;
  }, {});

// 假设之前的对象 `fileSizes`
flattenObject(fileSizes, '/');
/* {
  'package': 256,
  'src/index': 1024,
  'src/styles/main': 128,
  'src/styles/colors': 16,
  'assets/images/logo': 512,
  'assets/images/background': 512,
  'assets/fonts/serif': 64
} */

反转对象的扁平化

反转这个过程有点不同。给定扁平化对象中的一个键,您需要在分隔符处拆分路径,并使用生成的数组来创建嵌套对象

为了做到这一点,我们可以使用String.prototype.split()来在分隔符处拆分键,并使用Array.prototype.reduce()来根据键添加对象。如果当前累加器已经包含了特定键的值,我们将其值作为下一个累加器返回。否则,我们将适当的键值对添加到累加器对象中,并将值作为累加器返回。最后,我们可以使用Object.keys()Array.prototype.reduce()将此应用于扁平化对象中的每个键。

const unflattenObject = (obj, delimiter = '.') =>
  Object.keys(obj).reduce((res, k) => {
    k.split(delimiter).reduce(
      (acc, e, i, keys) =>
        acc[e] ||
        (acc[e] = isNaN(Number(keys[i + 1]))
          ? keys.length - 1 === i
            ? obj[k]
            : {}
          : []),
      res
    );
    return res;
  }, {});

// 假设之前的对象 `flattenedFileSizes`
unflattenObject(flattenedFileSizes);
/* {
  package: 256,
  src: {
    index: 1024,
    styles: {
      main: 128,
      colors: 16
    },
  },
  assets: {
    images: {
      logo: 512,
      background: 512
    },
    fonts: {
      serif: 64
    }
  }
} */

[!WARNING]

对象的扁平化和反扁平化可能会导致数据丢失,特别是如果键包含分隔符。在处理和转换数据时要始终小心。