JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

扯皮

开始正题之前给大伙拜个晚年😄

前两天突然发现掘金上收到这么多点赞和收藏,意识到应该是大伙都开工摸鱼刷文章了😆

作者本来也是过完大年初三就应该开卷写文章肝项目了,但因为一些家庭原因后面带了几天娃,现在想想是真的痛苦😣

刚开始还想着一边带一边刷刷文章看看视频教程,后来发现根本不现实:还没看两眼小孩可能就要哭闹,不哭就要带着去玩,不跟着玩就又要闹…后面几天视频教程是一点没看,倒是开始看小猪佩奇和汪汪队了☹️

不过现在回想起来这样才有年味啊~ 难道大过年天天闭门不出敲代码真的就好吗?😶

正文

不扯这么多了我们直接开始正题,关于 JSON 这俩方法的文章其实也很多,光过年期间我都已经见了好几篇专讲这个内容了,无非进阶一点就是关于后面的几个可选参数:replacer、space、reviver 的使用

那为什么我还要写这篇文章呢?(因为没活了)因为我喜欢结合实际业务场景来讲知识点,空讲知识点完全可以抄文档了没必要专门写文章出来

在最近自己的低代码项目中做拖拽功能时遇到了需要传输数据的需求:

我们知道 drag 事件可以通过 e.dataTransfer.setData(key, value)e.dataTransfer.getData(key) 来进行整个拖拽过程中的数据传输和获取

关键在于 setData 方法中的 value 参数要求是 string 类型,而我的业务当中是要传输物料组件的配置对象,如果直接以对象类型进行传输最终在 getData 里获取的是 [object Object] 字符串

到这里没有丝毫的犹豫,直接脱口而出 JSON.stringify、JSON.parse 解决问题 😄

确实解决了,但这是因为我的项目还没做完,我开始犹豫了:针对于一个物料组件后续还会有一些自定义函数的配置,只不过现在还没有实现🤔,这到后面要还无脑用 JSON.stringify 不就抓瞎了

后面就研究起了 JSON.stringify 的第二个参数 replacer 其实可以解决这个问题

研究着就又想到我之前写过的一篇文章👇:

我来给你写一个滴水不漏的深拷贝!面试官:怎么把getter和setter拷贝上?😅😅😅 – 掘金 (juejin.cn)

对啊,完全可以封装 JSON.parse、stringify 这俩方法来看看能不能实现一个完整的深拷贝!!

以后又可以和面试官装b了:光知道背八股文 JSON.parse、stringify 实现深拷贝的缺点,那你会解决吗?哈,哥们现在真会了!😁😁😁

可选参数:replacer、space 和 reviver

MDN 上关于这几个参数介绍的很清晰,所以不再详细讲解,如果用心往下翻翻就能看到:

JSON.stringify(replacer、space):JSON.stringify() – JavaScript | MDN (mozilla.org)

JSON.parse(reviver):JSON.parse() – JavaScript | MDN (mozilla.org)

我们先讲 space 参数,因为它不是本文的重点🤣,但是我在低代码项目的业务中也用到了

它的类型是 number | string | undefined,说白了就是针对于 JSON 数据每一行添加缩进美化,默认不填就是没有缩进,number 就是每行的缩进空格字符数,string 是将缩进的空格字符串换为字符。直接看下面的效果就懂了:

number 大小和 string 长度限制为 10

const data = {
  a: "abc",
  b: 20,
  c: {
    name: "c",
    age: 22,
  },
};
console.log(data);
console.log("无缩进:", JSON.stringify(data));
console.log("缩进 2 个空格:", JSON.stringify(data, null, 2));
console.log("缩进字符串:", JSON.stringify(data, null, "QQ"));

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

可以看到针对于对象它会自动再次缩进两个空格,还是挺人性化的

问题来了,这有什么用呢?🤔 如果单看这个参数功能好像没啥用:我就只是想要转成字符串,搞这缩进多此一举还占字符数

但既然文档说它起到美化作用,说明这个字符串是需要展示给用户看的

如果实际项目中遇到代码编辑器(codemirror、monaco-editor…)展示代码片段时就知道了,我们可能需要展示一个对象信息,这时候就需要用 JSON.stringify 先转化成字符串再传入对应的编辑器组件中

那么以 codemirror 为例,针对于上面不同的 JSON 字符串它的效果是这样的:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

现在知道缩进美化的重要性了吧😆

接下来就是 replacer、reviver 这两个重头戏了,它允许我们进行 stringify、parse 时针对于每个字段实现自定义操作

我们先来看 JSON.stringify 的 replacer,它的类型是函数或数组:

const data = {
  a: "abc",
  b: 20,
  c: {
    name: "c",
    age: 22,
  },
};

console.log(
  JSON.stringify(data, (key, value) => {
    console.log("check:", key, value);
    return value;
  })
);

通过传入一个回调函数,可以看到该函数可以拿到 data 里的每个 key 和 value,而返回值允许用户自定义,默认可以直接 return value:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

但有一个容易被忽略的点:回调第一次执行拿到的 key 为空,value 为 data 本身

所以假如我们进行这样的操作:

const data = {
  a: "abc",
  b: 20,
  c: {
    name: "c",
    age: 22,
  },
};

console.log(
  JSON.stringify(data, (key, value) => {
    console.log("check:", key, value);
  })
);

你可能以为它依然会遍历 data 属性打印出 key、value,无非是针对于每个属性都设置为 undefined,实际上并不是:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

正是由于第一次回调执行拿到的是原数据本身,如果第一次回调也返回 undefined,那就没有后面的属性遍历了,所以要想实现一开始的效果应该这样:

const data = {
  a: "abc",
  b: 20,
  c: {
    name: "c",
    age: 22,
  },
};

console.log(
  JSON.stringify(data, (key, value) => {
    console.log("check:", key, value);
    if (!key) return value;
  })
);

我们来看结果:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

而如果 replacer 类型是一个数组时,stringify 操作只会针对于数组元素作为 key 来执行,忽略其他 key:

const data = {
  a: "abc",
  b: 20,
  c: {
    name: "c",
    age: 22,
  },
};

console.log(JSON.stringify(data, ["a", "b"])); // {"a":"abc","b":20}

下面再来看看 JSON.parse 的 reviver,和 replacer 类似,但是又有很大的差别:

const data = {
  a: "abc",
  b: 20,
  c: {
    name: "c",
    age: 22,
  },
};

const str = JSON.stringify(data);

console.log(
  JSON.parse(str, (key, value) => {
    console.log(key, value);
    return value;
  })
);

最大的变化在于其遍历顺序上:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

reviver 回调函数的最后一次执行时拿到的 key 为空,value 为 parse 的最终数据,与 replacer 正好反过来,而且 reviver 还会针对于引用值进行深层遍历解析,这些 replacer 都不具备

用 MDN 的文档话来讲它的顺序是这样的:从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身

以上就是 replacer 和 reviver 的基本使用,我们后面也是利用这两个实现想要的功能

封装 JSON.stringify

简单了解过 replacer 和 reviver 的使用后我们就来看看 JSON.parse、JSON.stringify 实现深拷贝的弊端吧

到了我们最熟悉的八股文环节,而这一点我们主要关注到 JSON.stringify 上,因为 JSON.parse 无非就是接收一个字符串,如果不符合 JSON 格式直接就抛错了,而针对于特殊类型都需要 JSON.stringify 兜底

关于这部分 MDN 文档已经给我们总结了,直接背就行:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

因此我们就可以按照这上面的几条规则使用 replacer 进行转换操作

但是仔细看的话其实还是有一些难以避免的缺陷的,比如像 symbol 作为 key 时 replacer 也没法获取到等等,剩下的我们遇到再说

undefined、symbol 的处理

首先来点简单的,JSON.stringify 遇到 value 为 undefined 时会自动忽略这对 key-value(数组元素为 undefined 会被转换为 null),但是我们可以使用 replacer 来处理这种情况,我们以类的方式来封装这些方法:

const undefinedFlag = "$$-undefined";

class MyJsonHandle {
  constructor(data) {
    this._raw = data;
  }

  JSONStringify() {
    return JSON.stringify(this._raw, (key, value) => {
      if (value === undefined) return undefinedFlag;
      return value;
    });
  }
}

这里使用一个 flag 来标识 undefined,最好保证这个标识的唯一性,这样后面使用 JSON.parse 时可以针对于该 flag 再进行转换即可

我们来看效果:

const data = {
  a: "a",
  b: "b",
  c: 111,
  e: undefined,
};

const JsonHandle = new MyJsonHandle(data);

console.log(JSON.stringify(data)); // {"a":"a","b":"b","c":111}
console.log(JsonHandle.JSONStringify()); // {"a":"a","b":"b","c":111,"e":"$$-undefined"}

而且 JSON.stringify 内部针对于引用值会进行深度遍历,这样还减少了我们的工作量不用再手动递归处理了:

const data = {
  a: {
    test: undefined,
  },
  b: [undefined, 1, undefined, { test: undefined }],
};

const JsonHandle = new MyJsonHandle(data);

console.log(JSON.stringify(data)); // {"a":{},"b":[null,1,null,{}]}
console.log(JsonHandle.JSONStringify()); // {"a":{"test":"$$-undefined"},"b":["$$-undefined",1,"$$-undefined",{"test":"$$-undefined"}]}

这样 undefined 的情况就轻松解决了,下面来看 symbol 的情况

关于 symbol 的处理我们之前在深拷贝那篇文章中已经详细讨论过 👇:

我来给你写一个滴水不漏的深拷贝!面试官:怎么把getter和setter拷贝上?😅😅😅 – 掘金 (juejin.cn)

就是这三种类型的 symbol,我们先来看看它们在 JSON.stringify 下的表现:

const data = {
  s1: Symbol(),
  s2: Symbol("test"),
  s3: Symbol.for("global"),
};

console.log(
  JSON.stringify(data, (key, value) => {
    console.log(key, value);
    return value;
  })
);

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

嗯…虽然最终结果把 symbol 给忽略了,但是 replacer 全暴露出来了,这就好办了

直接把之前深拷贝的实现拿过来,遇到 symbol 时进行深拷贝创建新的 symbol,之后存储到一个对象当中

我们还需要一个标识 symbol 的 flag 作为该对象的 key,后面进行 JSON.parse 时直接根据 flag 获取到新的 symbol 即可

还有一个注意点就是考虑嵌套,我们使用对象进行存储可能会遇到同一个key的情况:后面的 key 会覆盖前一个 key 的 value,为了避免这种情况再增加一个 index 索引来保证其唯一性:

const undefinedFlag = "$$-undefined";
// symbol 标识,用 key、index 保证唯一性
const symbolFlag = (key, index) => `$${key}-${index}-symbol`;

class MyJsonHandle {
  constructor(data) {
    this._raw = data;
    this.symbolData = {
      index: 0,
      data: {},
    };
  }

  JSONStringify() {
    return JSON.stringify(this._raw, (key, value) => {
      if (value === undefined) return undefinedFlag;
      if (typeof value === "symbol") return this.handleSymbol(key, value);
      return value;
    });
  }

  handleSymbol(key, s) {
    // 针对于该 symbol 来创建对应的 flag
    const flag = symbolFlag(key, this.symbolData.index++);
    const globalKey = Symbol.keyFor(s);
    const desc = s.description;
    // 全局 symbol
    if (globalKey) this.symbolData.data[flag] = Symbol.for(globalKey);
    // 带有描述的 symbol
    else if (desc) this.symbolData.data[flag] = Symbol(desc);
    // 普通 symbol
    else this.symbolData.data[flag] = Symbol();
    // 最后将标识返回
    return flag;
  }
}

我们来看下结果,符合预期😁,symbol 的转换都进行了标识,symbolData 里也保存了该标识对应的新 symbol value,等着后续 JSON.parse 使用:

const data = {
  s1: Symbol.for("global"),
  s2: Symbol("desc"),
  s3: Symbol(),
};

const JsonHandle = new MyJsonHandle(data);

console.log(JsonHandle.JSONStringify());
console.log(JsonHandle.symbolData.data);

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

但是正如之前所说,symbol 作为 key 时 replacer 也无法获取到,那基本上没有好的办法了

循环引用的处理

一个已经刻入 DNA 的知识点,JSON.stringify 遇到循环引用的对象时会直接报错:

const data = {
  a: "a",
  b: "b",
};

data.self = data;

console.log(
  JSON.stringify(data, (key, value) => {
    console.log(key, value);
    return value;
  })
);

好消息是它依旧可以在 replacer 里获取到🧐:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

坏消息是无法像正常情况用 WeakMap 存储第二次遇到相同引用值时判断了,因为这里在第一次遇到就直接报错了🤔

我们先来简单尝试一下,同样给一个循环引用的标识,在 replacer 中判断如果当前 value 与原数据相同就说明有循环引用

需要注意还要判断一下 key 是否存在,这与之前我们讲解 replacer 第一次调用有关,不再过多解释:

const c = { a: 123 };

c.abc = c;

const data = {
  a: "123",
  b: "456",
};

data.self = data;

console.log(
  JSON.stringify(data, (key, value) => {
    if (key && value === data) return "cycle";
    return value;
  })
); // {"a":"123","b":"456","self":"cycle"}

可以看到这时候就不会报循环引用错误正常打印了,之后 JSON.parse 根据 cycle 标识赋值原始数据即可

但我们需要考虑嵌套的循环引用情况,如下面的例子:

const b = { a: 123 };
b.abc = b;

const data = {
  b,
};

console.log(
  JSON.stringify(data, (key, value) => {
    if (key && value === data) return "cycle";
    return value;
  })
);

这时候如果只按照上面的实现的思路就有问题了,因为 value 始终与原数据比较,所以 b 对象里的 abc 属性形成的循环引用依旧会报错

其实可以这样思考:JSON.stringify 会深度遍历原数据,如果遇到 value 为引用值时会继续遍历该引用值的属性,所以有点深度递归的意思,说到递归自然而然可以想到栈结构

我们可以声明一个栈来存储当前遍历的对象引用值,之后查看栈顶元素与当前的 value 比较,如果相同说明有循环引用,执行出栈操作,并返回循环应用的标识

const b = { a: 123 };
b.abc = b;

const data = {
  b,
};

const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
const cycleStack = [];
console.log(
  JSON.stringify(data, (key, value) => {
    // key: "b", value: b 对象,首次碰到引用值进行存储
    if (key && isObject(value) && cycleStack[cycleStack.length - 1] !== value) {
      cycleStack.push(value);
      return value;
    }
    // key: "abc", value: b 对象,遇到循环引用,进行判断处理
    if (key && isObject(value)) {
      if (cycleStack[cycleStack.length - 1] === value) {
        cycleStack.pop();
        return "cycle";
      }
    }
    return value;
  })
);

同样这种写法也解决了多重循环引用嵌套的问题:

const c = { a: 123 };
c.ttt = c;

const b = { a: 123, c };
b.abc = b;

const data = {
  b,
  c,
};

const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
const cycleStack = [];
console.log(
  JSON.stringify(data, (key, value) => {
    if (key && isObject(value) && cycleStack[cycleStack.length - 1] !== value) {
      cycleStack.push(value);
      return value;
    }
    if (key && isObject(value)) {
      if (cycleStack[cycleStack.length - 1] === value) {
        cycleStack.pop();
        return "cycle";
      }
    }
    return value;
  })
);

为了展示打印结果清晰,使用测试用例时设置了 space 参数为 2,后面不再提醒

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

但还要结合一下一开始原始对象就有循环引用的问题,所以补充这样的判断,这样就没什么问题了:


const c = { a: 123 };
c.ttt = c;

const b = { a: 123, c };
b.abc = b;

const data = {
  b,
  c,
};
data.self = data; // 原始数据自身循环引用

const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
const cycleStack = [];
console.log(
  JSON.stringify(data, (key, value) => {
    // new: 考虑一开始对象就有循环引用,与原始对象进行判断
    if (key && isObject(value) && value === data) return "cycle";
    if (key && isObject(value) && cycleStack[cycleStack.length - 1] !== value) {
      cycleStack.push(value);
      return value;
    }
    if (key && isObject(value)) {
      if (cycleStack[cycleStack.length - 1] === value) {
        cycleStack.pop();
        return "cycle";
      }
    }
    return value;
  })
);

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

现在可以准备集成到之前封装的类里了,我们可以直接沿用之前 symbol 的思路,依旧用一个对象存储循环引用的内容,同样以 key + index 进行唯一标识即可:

const undefinedFlag = "$$-undefined";
const symbolFlag = (key, index) => `$$-{key}-${index}-symbol`;
// new: 循环引用标识
const cycleFlag = (key, index) => `$$-{key}-${index}-cycle`;

// new: 判断对象
const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
class MyJsonHandle {
  constructor(data) {
    this._raw = data;
    this.cycleStack = [];
    this.symbolData = {
      index: 0,
      data: {},
    };
    this.cycleData = {
      index: 0,
      data: {},
    };
  }

  JSONStringify() {
    return JSON.stringify(this._raw, (key, value) => {
      if (value === undefined) return undefinedFlag;
      if (typeof value === "symbol") return this.handleSymbol(key, value);
      // 考虑原始数据循环引用的情况
      if (key && isObject(value) && value === data) {
        // 创建循环引用标识
        const flag = cycleFlag(key, this.cycleData.index++);
        // 将该引用保存
        this.cycleData.data[flag] = value;
        return flag;
      }
      // 存储当前遍历到的引用值,为后面比较做准备
      if (key && isObject(value) && this.cycleStack[this.cycleStack.length - 1] !== value) {
        this.cycleStack.push(value);
        return value;
      }

      if (key && isObject(value)) {
        // 如果栈中保存的引用值与当前遍历的引用值相同,说明出现了循环引用
        if (this.cycleStack[this.cycleStack.length - 1] === value) {
          this.cycleStack.pop();
          const flag = cycleFlag(key, this.cycleData.index++);
          this.cycleData.data[flag] = value;
          return flag;
        }
      }
      return value;
    });
  }

  handleSymbol(key, s) {
    const flag = symbolFlag(key, this.symbolData.index++);
    const globalKey = Symbol.keyFor(s);
    const desc = s.description;
    if (globalKey) this.symbolData.data[flag] = Symbol.for(globalKey);
    else if (desc) this.symbolData.data[flag] = Symbol(desc);
    else this.symbolData.data[flag] = Symbol();
    return flag;
  }
}

现在我们再来看看之前例子的效果,没什么大问题,而且还区分出了不同 key 对应的引用值 value 😎:

const c = { a: 123 };
c.ttt = c;

const b = { a: 123, c };
b.abc = b;

const data = {
  b,
  c,
};
data.self = data;

const JsonHandle = new MyJsonHandle(data);
console.log(JsonHandle.JSONStringify());
console.log(JsonHandle.cycleData.data);

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

到此循环引用的问题就结束了,就等着后面 JSON.parse 对接了😁😁😁

其实还有一种情况我没有测试,就是跨属性的循环引用,比如嵌套多层后某个属性值引用不是自己而是原数据对象,这种情况感觉是还需要遍历 cycleData 来对比查看,确实又麻烦了些

但有一说一循环引用的场景我见到的都少,这种的就更少了,所以先不考虑了😂

特殊引用类型的处理

这里的特殊引用类型特指 Map、Set、Date、RegExp、function 这些对象,我们直接写个例子看 JSON.stringify 的表现:

const data = {
  a: new Map([
    [1, 2],
    [3, 4],
  ]),
  b: new Set([1, 2, 3]),
  c: new Date(),
  d: new RegExp("test"),
  e: function test() {
    console.log("123");
  },
};

console.log(
  JSON.stringify(data, (key, value) => {
    console.log(key, value);
    return value;
  })
);

可以看到 Map、Set、RegExp 都被转换成 {} 字符串,而 Date 转换了日期字符串,function 直接被忽略

但它们的共同特点是都可以在 replacer 里获取到:

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

那其实就可以沿用之前的思路,我们针对于每个特殊引用类型先进行一次深拷贝,然后进行存储,之后 JSON.parse 取出赋值

而关于这些类型的深拷贝我们之前的文章已经详细介绍过了,直接拿来用即可

但这次我们就不考虑 Map 和 Set 里存储另外的引用类型然后需要递归拷贝的情况了,不然又回归到了原始深拷贝实现,这不是我们文章的主题🙃

需要补充一点的是关于日期类型,它在 replacer 获取的是字符串,因此我们增加判断字符串来区分出日期类型,再从原数据里获取到日期对象进行处理

但又要考虑嵌套的情况,不过我们之前解决循环引用里创建了 cycleStack 栈,它保存着当前遍历到的引用对象,可以利用它来解决嵌套的日期对象

因为流程和之前差不多,无非就是需要单独判断类型,所以直接贴代码了👇:

const undefinedFlag = "$$-undefined";
const symbolFlag = (key, index) => `$$-${key}-${index}-symbol`;
const cycleFlag = (key, index) => `$$-${key}-${index}-cycle`;
const MapFlag = (key, index) => `$$-${key}-${index}-Map`;
const SetFlag = (key, index) => `$$-${key}-${index}-Set`;
const DateFlag = (key, index) => `$$-${key}-${index}-Date`;
const RegExpFlag = (key, index) => `$$-${key}-${index}-RegExp`;
const FunctionFlag = (key, index) => `$$-${key}-${index}-Function`;
// new: 判断对象
const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
class MyJsonHandle {
constructor(data) {
this._raw = data;
this.cycleStack = [];
this.symbolData = {
index: 0,
data: {},
};
this.cycleData = {
index: 0,
data: {},
};
this.MapData = {
index: 0,
data: {},
};
this.SetData = {
index: 0,
data: {},
};
this.DateData = {
index: 0,
data: {},
};
this.RegExpData = {
index: 0,
data: {},
};
this.FunctionData = {
index: 0,
data: {},
};
}
JSONStringify() {
return JSON.stringify(this._raw, (key, value) => {
if (value === undefined) return undefinedFlag;
if (typeof value === "symbol") return this.handleSymbol(key, value);
if (key && isObject(value) && value === data) {
const flag = cycleFlag(key, this.cycleData.index++);
this.cycleData.data[flag] = value;
return flag;
}
if (key && isObject(value) && this.cycleStack[this.cycleStack.length - 1] !== value) {
this.cycleStack.push(value);
return value;
}
if (key && isObject(value)) {
if (this.cycleStack[this.cycleStack.length - 1] === value) {
this.cycleStack.pop();
const flag = cycleFlag(key, this.cycleData.index++);
this.cycleData.data[flag] = value;
return flag;
}
}
// 判断出日期对象,先从字符串入手
if (typeof value === "string") {
const current = this.cycleStack[this.cycleStack.length - 1];
// 栈中存在当前引用对象,从这里面取出对应的 value,再判断是否为日期对象处理
if (current) {
const flag = this.handleSpecialObject(key, current[key]);
if (flag) return flag;
} else {
// 栈中不存在,可能原数据里存在日期对象,再单独判断
const flag = this.handleSpecialObject(key, this._raw[key]);
if (flag) return flag;
}
}
const flag = this.handleSpecialObject(key, value);
if (flag) return flag;
return value;
});
}
handleSymbol(key, s) {
const flag = symbolFlag(key, this.symbolData.index++);
const globalKey = Symbol.keyFor(s);
const desc = s.description;
if (globalKey) this.symbolData.data[flag] = Symbol.for(globalKey);
else if (desc) this.symbolData.data[flag] = Symbol(desc);
else this.symbolData.data[flag] = Symbol();
return flag;
}
handleSpecialObject(key, value) {
const type = Object.prototype.toString.call(value);
let flag = "";
switch (type) {
case "[object Map]":
flag = MapFlag(key, this.MapData.index++);
this.MapData.data[flag] = new Map([...value]);
break;
case "[object Set]":
flag = SetFlag(key, this.SetData.index++);
this.SetData.data[flag] = new Set([...value]);
break;
case "[object Date]":
flag = DateFlag(key, this.DateData.index++);
this.DateData.data[flag] = new Date(value);
break;
case "[object RegExp]":
flag = RegExpFlag(key, this.RegExpData.index++);
this.DateData.data[flag] = new RegExp(value);
break;
case "[object Function]":
flag = FunctionFlag(key, this.FunctionData.index++);
let cloneFn = null;
eval(`cloneFn=${value.toString()}`);
this.FunctionData.data[flag] = cloneFn;
break;
default:
break;
}
return flag;
}
}

来上例子看结果,符合预期:

const data = {
a: new Map([
[1, 2],
[3, 4],
]),
b: new Set([1, 2, 3]),
c: new Date(),
d: new RegExp("test"),
e: function test() {
console.log("123");
},
f: {
ff: new Date(),
},
};
const JsonHandle = new MyJsonHandle(data);
console.log(JsonHandle.JSONStringify());
console.log(JsonHandle);

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

到此针对于特殊类型的处理就结束了,像关于 boolean、number、string 这些通过对应构造函数创造出来的对象就不再考虑了,都是一样的思路

封装 JSON.parse

最后进入 parse 的封装,parse 主要处理的就是根据之前 stringify 改造出的标识判断取值赋值即可,没什么难度,直接上核心代码:

JSONParse(DOMString) {
return JSON.parse(DOMString, (_, value) => {
if (value === undefinedFlag) return undefined;
else if (typeof value === "string" && value.includes("cycle") && value.includes("$$"))
return this.cycleData.data[value];
else if (typeof value === "string" && value.includes("Map") && value.includes("$$"))
return this.MapData.data[value];
else if (typeof value === "string" && value.includes("Set") && value.includes("$$"))
return this.SetData.data[value];
else if (typeof value === "string" && value.includes("Date") && value.includes("$$"))
return this.DateData.data[value];
else if (typeof value === "string" && value.includes("RegExp") && value.includes("$$"))
return this.RegExpData.data[value];
else if (typeof value === "string" && value.includes("Function") && value.includes("$$"))
return this.FunctionData.data[value];
return value;
});
}

当然这里判断类型可以再优化的,比如写个正则匹配,作者写到这懒了直接 includes 一把刷了😂

我们来个全一点的例子看看:

const data = {
a: new Map([
[1, 2],
[3, 4],
]),
b: new Set([1, 2, 3]),
c: new Date(),
d: new RegExp("test"),
e: function test() {
console.log("123");
},
f: {
ff: new Date(),
},
g: undefined,
h: 1,
i: "hello",
};
data.self = data;
const JsonHandle = new MyJsonHandle(data);
console.log(JsonHandle.JSONStringify());
const res = JsonHandle.JSONParse(JsonHandle.JSONStringify());
console.log(res);
console.log(testDeepClone());
function testDeepClone() {
for (const key in data) {
if (!["g", "h", "i", "self"].includes()) {
if (data[key] === res[key]) return false;
}
return true;
}
}

JSON.parse、stringify 封装:弥补实现深拷贝的所有缺陷!

看结果应该是没有什么大问题了,最后再加一个 deepClone 方法即可,后面就直接 JsonHandle.JSONDeepClone 调用了:

JSONDeepClone() {
return this.JSONParse(this.JSONStringify());
}

End

写到最后发现这回还真有点标题党了🤣,其实这只是我在做项目的时候发现一个比较小的知识点,没想到最后延伸出来这么多字…😅

JSON.parse、stringify 深拷贝本身还是有些局限性的,就比如最开始 symbol 作为 key 的时候,这个问题如果纯手写深拷贝还是能实现的

而且这些标识 flag 没法做到真正的唯一性,暴露给用户还是不太安全,当然自己封装的也就比较简陋,可以再优化一下

不过整篇文章写下来之后还是学到不少东西的,比如这里解决循环引用的方案也是花费了我不少时间研究,对 JSON 的这俩方法肯定有一个深刻认知,以后面试就不再无脑答就会个 JSON.parse、stringify 调用了,最起码也得把这几个参数答上🤪🤪🤪

最后完整源码奉上:

const undefinedFlag = "$$-undefined";
const symbolFlag = (key, index) => `$$-${key}-${index}-symbol`;
const cycleFlag = (key, index) => `$$-${key}-${index}-cycle`;
const MapFlag = (key, index) => `$$-${key}-${index}-Map`;
const SetFlag = (key, index) => `$$-${key}-${index}-Set`;
const DateFlag = (key, index) => `$$-${key}-${index}-Date`;
const RegExpFlag = (key, index) => `$$-${key}-${index}-RegExp`;
const FunctionFlag = (key, index) => `$$-${key}-${index}-Function`;
const isObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
class MyJsonHandle {
constructor(data) {
this._raw = data;
this.cycleStack = [];
this.symbolData = {
index: 0,
data: {},
};
this.cycleData = {
index: 0,
data: {},
};
this.MapData = {
index: 0,
data: {},
};
this.SetData = {
index: 0,
data: {},
};
this.DateData = {
index: 0,
data: {},
};
this.RegExpData = {
index: 0,
data: {},
};
this.FunctionData = {
index: 0,
data: {},
};
}
JSONStringify() {
return JSON.stringify(this._raw, (key, value) => {
if (value === undefined) return undefinedFlag;
if (typeof value === "symbol") return this.handleSymbol(key, value);
if (key && isObject(value) && value === data) {
const flag = cycleFlag(key, this.cycleData.index++);
this.cycleData.data[flag] = value;
return flag;
}
if (key && isObject(value) && this.cycleStack[this.cycleStack.length - 1] !== value) {
this.cycleStack.push(value);
return value;
}
if (key && isObject(value)) {
if (this.cycleStack[this.cycleStack.length - 1] === value) {
this.cycleStack.pop();
const flag = cycleFlag(key, this.cycleData.index++);
this.cycleData.data[flag] = value;
return flag;
}
}
if (typeof value === "string") {
const current = this.cycleStack[this.cycleStack.length - 1];
if (current) {
const flag = this.handleSpecialObject(key, current[key]);
if (flag) return flag;
} else {
const flag = this.handleSpecialObject(key, this._raw[key]);
if (flag) return flag;
}
}
const flag = this.handleSpecialObject(key, value);
if (flag) return flag;
return value;
});
}
JSONParse(DOMString) {
return JSON.parse(DOMString, (_, value) => {
if (value === undefinedFlag) return undefined;
else if (typeof value === "string" && value.includes("cycle") && value.includes("$$"))
return this.cycleData.data[value];
else if (typeof value === "string" && value.includes("Map") && value.includes("$$"))
return this.MapData.data[value];
else if (typeof value === "string" && value.includes("Set") && value.includes("$$"))
return this.SetData.data[value];
else if (typeof value === "string" && value.includes("Date") && value.includes("$$"))
return this.DateData.data[value];
else if (typeof value === "string" && value.includes("RegExp") && value.includes("$$"))
return this.RegExpData.data[value];
else if (typeof value === "string" && value.includes("Function") && value.includes("$$"))
return this.FunctionData.data[value];
return value;
});
}
JSONDeepClone() {
return this.JSONParse(this.JSONStringify());
}
handleSymbol(key, s) {
const flag = symbolFlag(key, this.symbolData.index++);
const globalKey = Symbol.keyFor(s);
const desc = s.description;
if (globalKey) this.symbolData.data[flag] = Symbol.for(globalKey);
else if (desc) this.symbolData.data[flag] = Symbol(desc);
else this.symbolData.data[flag] = Symbol();
return flag;
}
handleSpecialObject(key, value) {
const type = Object.prototype.toString.call(value);
let flag = "";
switch (type) {
case "[object Map]":
flag = MapFlag(key, this.MapData.index++);
this.MapData.data[flag] = new Map([...value]);
break;
case "[object Set]":
flag = SetFlag(key, this.SetData.index++);
this.SetData.data[flag] = new Set([...value]);
break;
case "[object Date]":
flag = DateFlag(key, this.DateData.index++);
this.DateData.data[flag] = new Date(value);
break;
case "[object RegExp]":
flag = RegExpFlag(key, this.RegExpData.index++);
this.DateData.data[flag] = new RegExp(value);
break;
case "[object Function]":
flag = FunctionFlag(key, this.FunctionData.index++);
let cloneFn = null;
eval(`cloneFn=${value.toString()}`);
this.FunctionData.data[flag] = cloneFn;
break;
default:
break;
}
return flag;
}
}

原文链接:https://juejin.cn/post/7337892215226007589 作者:討厭吃香菜

(0)
上一篇 2024年2月22日 下午4:26
下一篇 2024年2月22日 下午4:37

相关推荐

发表回复

登录后才能评论