模拟JSON.stringfy三大参数
回复算法,加入前端编程面试算法每日一题群
来源:MSFee
https://juejin.cn/post/6952015403472125982
写在前面
编码的过程中常常需要对对象进行拷贝操作,一般单层对象使用Object.assign, 含有嵌套的话我都是直接JSON.parse(JSON.stringfy())一把梭,基本满足日常的开发需求了(当然主要是因为懒。。。)。久而久之,我突然对JSON.stringfy产生了好奇,打开MDN文档,好家伙。原来它可以接受三个参数。
于是我尝试自己来实现一个JSON.stringfy。
第一版:实现单层基本类型对象的拷贝
1.首先我们新建立一个index.ts文件,写入我们的代码
// 用于判断是字符串函数数字(目前我们只考虑字符串和数字两种类型)
const stringOrOtherVal = (data: string | number): string => {
if (typeof data === 'string') {
return `"${data}"`
} else{
return `${data}`
}
}
const myStringfy = (params: any): string => {
let resultStr = '{'
for (let key in params) {
resultStr += `"${key}":${stringOrOtherVal(params[key])},`
}
resultStr = resultStr.slice(0, resultStr.length - 1);
resultStr += '}'
return resultStr
}
复制代码
让我们来对比一下mystringfy和JSON.stringfy的功能
const obj = {
name: '张三',
age: 15,
}
console.log(myStringfy1(obj))
console.log(JSON.stringify(obj))
console.log(JSON.stringify(obj) === myStringfy1(obj))
复制代码
我们已经实现了最基础的对象转字符串功能,不过这当然是远远不够的。
第二版,通过递归 实现嵌套对象的字符串转化
在这一版中我们需要实现嵌套对象的字符串转化功能(依然只包含字符串和数字类型)。改进我们的myStringfy函数
const myStringfy = (params: any): string => {
let resultStr = '{'
for (let key in params) {
if (params[key] instanceof Object) {
resultStr += `"${key}":${myStringfy(params[key])},`;
} else {
resultStr += `"${key}":${stringOrOtherVal(params[key])},`
}
}
resultStr = resultStr.slice(0, resultStr.length - 1);
resultStr += '}'
return resultStr
}
const obj = {
name: '张三',
age: 15,
dog: {
size: 'large',
food: '骨头',
dogType: {
type1: '哈士奇',
nums: 3
}
}
}
console.log(myStringfy(obj))
console.log(JSON.stringify(obj))
console.log(JSON.stringify(obj) === myStringfy(obj))
复制代码
通过简单的递归,我们已经能够对嵌套对象进行转化,下面我们再来看看如果对象中存在数组该如何处理呢。
第三版 实现数组的字符串转化
对数组的处理步骤比较多,我们一方面需要判断数组中是否包含有对象,又或者是数组中嵌套有子数组等情况,所以我们将数组处理抽取为一个函数
const arrayDeail = (params: any[]): string => {
let resultStr = '[';
for (let i = 0; i < params.length; i++) {
if (Array.isArray(params[i])) {
resultStr += arrayDeail(params[i]) + ','
} else if (params[i] instanceof Object) {
resultStr += myStringfy(params[i]) + ',';
} else {
resultStr += stringOrOtherVal(params[i]) + ',';
}
}
resultStr = resultStr.slice(0, resultStr.length - 1)
resultStr += ']'
return resultStr
}
复制代码
上述代码中,通过遍历数组来处理数组中的每一个元素,同时判断类型,如果元素为数组,那么递归调用本身方法处理,如果为对象则调用myStringfy。
const myStringfy = (params: any): string => {
let resultStr = '{'
for (let key in params) {
if (Array.isArray(params[key])) {
resultStr += `"${key}":${arrayDeail(params[key])},`
}else if (params[key] instanceof Object) {
resultStr += `"${key}":${myStringfy(params[key])},`;
}else {
resultStr += `"${key}":${stringOrOtherVal(params[key])},`
}
}
resultStr = resultStr.slice(0, resultStr.length - 1);
resultStr += '}'
return resultStr
}
const obj = {
name: '张三',
age: 15,
arr: [1, 2, 3, 5, 'xxx', {
tem: {
name: '测试',
nums: [5, 6, 8],
}
}],
dog: {
size: 'large',
food: '骨头',
dogType: {
type1: '哈士奇',
nums: 3,
large: 'ccc',
}
}
}
复制代码
改进一下我们的myStringfy,当对象属性为数组的时候,调用arrayDeail函数进行处理。用上面的obj测试一下
可以看到,也是没有任何问题的,实现到这一步,距离我们使用这个函数去转化我们的对象只差最后一步啦。
第四版 实现其他类型的处理 (null undefined function Map Set WeakMap WeakSet Date)
这里我们需要判断多种类型,而后面的Map、Set等也都属性引用类型,使用typeof就不再合适了,所以我们选择使用Object.prototype.toString.call()方法
const toStringCheckType = (params: any): string => {
let resultStr = Object.prototype.toString.call(params);
resultStr = resultStr.slice(1, resultStr.length - 1)
const arr = resultStr.split(' ')
return arr[1];
}
复制代码
我们新建立一个函数,对toString方法得到的结果简单的处理,处理后的函数可以直接准确的返回数据的对应类型。改进一下arrayDeail和myStringfy,以及上面最开始定义的stringOrOtherVal函数
const arrayDeail = (params: any[]): string => {
let resultStr = '[';
for (let i = 0; i < params.length; i++) {
if (toStringCheckType(params[i]) === 'Array') {
resultStr += arrayDeail(params[i]) + ','
} else if (toStringCheckType(params[i]) === 'Object') {
resultStr += myStringfy(params[i]) + ',';
} else {
resultStr += stringOrOtherVal(params[i]) + ',';
}
}
resultStr = resultStr.slice(0, resultStr.length - 1)
resultStr += ']'
return resultStr
}
const myStringfy = (params: any): string => {
let resultStr = '{'
for (let key in params) {
if (toStringCheckType(params[key]) === 'Array') {
resultStr += `"${key}":${arrayDeail(params[key])},`
}
else if (toStringCheckType(params[key]) === 'Object') {
resultStr += `"${key}":${myStringfy(params[key])},`;
} else if (toStringCheckType(params[key]) === 'Number' || toStringCheckType(params[key]) === 'String' ||
toStringCheckType(params[key]) === 'Null' || toStringCheckType(params[key]) === 'Boolean') {
resultStr += `"${key}":${stringOrOtherVal(params[key])},`
} else if (toStringCheckType(params[key]) === 'Map' || toStringCheckType(params[key]) === 'Set' ||
toStringCheckType(params[key]) === 'WeakMap' || toStringCheckType(params[key]) === 'WeakSet') {
resultStr += `"${key}":{},`
} else if(toStringCheckType(params[key]) === 'Date') {
resultStr += `"${key}":"${params[key].toJSON()}",`
}
else {
continue
}
}
resultStr = resultStr.slice(0, resultStr.length - 1);
resultStr += '}'
return resultStr
}
const stringOrOtherVal = (data): string => {
if (typeof data === 'string') {
return `"${data}"`
} else if (typeof data === 'number' || typeof data === 'boolean') {
return `${data}`
} else {
return null
}
}
复制代码
让我们来自测一下:
const obj = {
name: '张三',
age: 15,
sss: true,
arr: [1, 2, 3, 5, 'xxx', {
tem: {
name: '测试',
nums: [5, 6, 8],
}
}],
map: new Map(),
set: new Set(),
null_: null,
undefined_: undefined,
date_: new Date(),
}
复制代码
下面,让我们来实现第二个参数
第五版 JSON.stringify第二个参数replacer
首先,我们看看MDN文档上,对replacer的描述
可以看到,replacer可以是一个数组,也可以是一个函数,所以我们需要分别对数组和函数进行不同的处理。首先根据MDN文档,我们定义一下参数可能的类型
type replacerType = any[] | Function | undefined
复制代码
将新定义的replacerType和之前的toStringCheckType以及stringOrOtherVal抽取 出来,重新定义一个文件,来引入使用。
改进一下我们之前写的arrDetail函数和myStringfy函数
const arrayDeail = (params: any[], replacer?: Function): string => {
let resultStr = '[';
let flag = toStringCheckType(replacer) === 'Function';
for (let i = 0; i < params.length; i++) {
params[i] = flag ? replacer(`${i}`, params[i]) : params[i]
if (toStringCheckType(params[i]) === 'Array') {
resultStr += flag ? arrayDeail(params[i], replacer) + ',' : arrayDeail(params[i]) + ','
} else if (toStringCheckType(params[i]) === 'Object') {
resultStr += myStringfy(params[i], replacer as replacerType) + ',';
} else {
resultStr += stringOrOtherVal(params[i]) + ',';
}
}
resultStr = resultStr.slice(0, resultStr.length - 1)
resultStr += ']'
return resultStr
}
const myStringfy = (params: any, replacer?: replacerType): string => {
let resultStr: string = '{'
let isFunction: number = 0; // 1 表示函数 2 表示数组
// 判断replacer 的类型
if(toStringCheckType(replacer) === 'Function') {
isFunction = 1;
}else if(toStringCheckType(replacer) === 'Array'){
isFunction = 2;
}else if(toStringCheckType(replacer) === 'Undefined' || toStringCheckType(replacer) === 'Null') {
}else {
throw new Error("replacer 只能为函数、数组或者null!")
}
for (let key in params) {
if(isFunction === 2 && (replacer as string[]).indexOf(key) === -1) { // 如果传递的是数组,判断当前key值是否在数组中
continue
}
if(isFunction === 1) {
params[key] = (replacer as Function)(key, params[key])
}
if (toStringCheckType(params[key]) === 'Array') {
resultStr += isFunction === 1 ? `"${key}":${arrayDeail(params[key], replacer as Function)},` : `"${key}":${arrayDeail(params[key])},`
}
else if (toStringCheckType(params[key]) === 'Object') {
resultStr += `"${key}":${myStringfy(params[key], replacer)},`;
} else if (toStringCheckType(params[key]) === 'Number' || toStringCheckType(params[key]) === 'String' ||
toStringCheckType(params[key]) === 'Null' || toStringCheckType(params[key]) === 'Boolean') {
resultStr += `"${key}":${stringOrOtherVal(params[key])},`
} else if (toStringCheckType(params[key]) === 'Map' || toStringCheckType(params[key]) === 'Set' ||
toStringCheckType(params[key]) === 'WeakMap' || toStringCheckType(params[key]) === 'WeakSet') {
resultStr += `"${key}":{},`
} else if(toStringCheckType(params[key]) === 'Date') {
resultStr += `"${key}":"${params[key].toJSON()}",`
}
else {
continue
}
}
resultStr = resultStr.slice(0, resultStr.length - 1);
resultStr += '}'
return resultStr
}
复制代码
我们通过isFunction这个变量来区分函数还是数组,数组的话,需要判断对象的key值是否在数组中,如果不在则直接跳过。如果是函数的话,需要注意的是,我们要传递两个参数到如果是对象中的函数调用replacer函数中,一个是 key, 一个是value, 如果是对象中的函数调用replacer,那么key就是对应的下标。
这里需要注意的是,当 replacer 为函数的时候,第一次会把整个对象塞进去,一般来说,我们只需要这个对象中的属性和值就可以,不需要整个对象
到这里,我们终于实现出了第二个参数,虽然代码比较凌乱,不过不需要担心,我们先实现功能,后面再调整代码。让我们用个例子来测试一下:
当replacer为函数的时
const obj = {
name: '张三',
age: 15,
sss: true,
arr: [1, 2, 3, 5, 'xxx', {
tem: {
name: '测试',
nums: [5, 6, 8],
}
}],
}
function test(key, obj) {
if(typeof obj === 'number') {
obj = obj + 10;
}
return obj
}
console.log(myStringfy(obj, test))
console.log(JSON.stringify(obj, test))
console.log(JSON.stringify(obj, test) === myStringfy(obj, test))
复制代码
当replacer为数组时:
const keyArr = ['name', 'arr', 'nums']
console.log(myStringfy(obj, keyArr))
console.log(JSON.stringify(obj, keyArr))
console.log(JSON.stringify(obj, keyArr) === myStringfy(obj, keyArr))
复制代码
可以看到,我们的返回结果和JSON.stringfy的结果并不一样,是因为我对这里JSON.stringfy中数组中的对象是否需要判断在replacer存在不同的看法。不过这并不是太大的问题,我们继续往下看。
第六版 实现JSON.stringfy第三个参数
让我们看看MDN文档对,第三个参数的描述
可以看到,第三个参数space可以是数字也可以是字符串,如果是字符串的话,只会取前10个,让我们看看代码:
class Stringfy {
addSpace: string = ''
constructor(space) {
this.addSpace = space;
}
arrayDeail = (params: any[], replacer?: Function, spaceStr?: string, count?: number, isObjFlag: boolean = true): string => {
spaceStr = getPrefixStr(this.addSpace, count)
const prefilxStr = !isObjFlag ? spaceStr.slice(0, spaceStr.length - this.addSpace.length) : ''
let resultStr = prefilxStr + '[' + `${spaceStr.length ? '\n' : ''}`;
let flag = toStringCheckType(replacer) === 'Function';
for (let i = 0; i < params.length; i++) {
params[i] = flag ? replacer(`${i}`, params[i]) : params[i]
if (toStringCheckType(params[i]) === 'Array') {
resultStr += flag ? this.arrayDeail(params[i], replacer, spaceStr, count + 1, false) + ',' : this.arrayDeail(params[i], null, spaceStr, count + 1, false) + ','
} else if (toStringCheckType(params[i]) === 'Object') {
resultStr += this.myStringfy(params[i], replacer as replacerType, spaceStr, count + 1) + ',';
} else {
resultStr += `${spaceStr}${stringOrOtherVal(params[i])}${spaceStr.length && i === params.length - 1 ? '' : ',' }${spaceStr.length ? '\n' : ''}`
}
}
resultStr = resultStr.slice(0, resultStr.length - 1)
resultStr += spaceStr.length ? `\n${spaceStr.slice(0, spaceStr.length - this.addSpace.length)}` : ''
resultStr += ']'
return resultStr
}
myStringfy = (params: any, replacer?: replacerType, space?: string, count?: number, isObjFlag: boolean = true): string => {
space = getPrefixStr(this.addSpace, count)
const prefilxStr = isObjFlag ? space.slice(0, space.length - this.addSpace.length) : ''
let resultStr: string = prefilxStr + '{' + `${space.length ? '\n' : ''}`
let isFunction: number = checkReplacerType(replacer); // 1 表示函数 2 表示数组
for (let key in params) {
if (isFunction === 2 && (replacer as string[]).indexOf(key) === -1) { // 如果传递的是数组,判断当前key值是否在数组中
continue
}
if (isFunction === 1) {
params[key] = (replacer as Function)(key, params[key])
}
switch (toStringCheckType(params[key])) {
case 'Array':
if(isFunction === 1) {
resultStr += `${space}"${key}":${this.arrayDeail(params[key], replacer as Function, space, count + 1)},${space.length ? '\n' : ''}`
}else if(isFunction === 2) {
resultStr += `${space}"${key}":${this.arrayDeail(params[key], null, space, count + 1)},${space.length ? '\n' : ''}`
}else {
resultStr += `${space}"${key}":${this.arrayDeail(params[key], null, space, count + 1)},${space.length ? '\n' : ''}`
}
break;
case 'Object':
resultStr += `${space}"${key}":${this.myStringfy(params[key], replacer, space, count + 1, false)},${space.length ? '\n' : ''}`;
break
case 'Number':
case 'String':
case 'Null':
case 'Boolean':
resultStr += `${space}"${key}":${stringOrOtherVal(params[key])},${space.length ? '\n' : ''}`
break
case 'WeakMap':
case 'Map':
case 'Set':
case 'WeakSet':
resultStr += `${space}"${key}":{},${space.length ? '\n' : ''}`
break
case 'Date':
resultStr += `${space}"${key}":"${params[key].toJSON()}",${space.length ? '\n' : ''}`
break
default:
continue
}
}
resultStr = resultStr.slice(0, space.length ? resultStr.length - 2 : resultStr.length - 1);
resultStr += space.length ? `\n${space.slice(0, space.length - this.addSpace.length)}` : ''
resultStr += '}'
return resultStr
}
}
复制代码
可以看到我们重新定义了一个Stringfy类,因为我们需要保留最开始传递的space内容,同时优化了一下代码结构。同时在类型文件中新增了两个函数
export const checkSpaceType = (space?: string | number): string | never => {
let spaceStr: string;
if (toStringCheckType(space) === 'String') {
if ((space as string).length > 10) {
spaceStr = (space as string).slice(0, 10)
} else {
spaceStr = space as string;
}
} else if (toStringCheckType(space) === "Number") {
if (space > 10) {
space = 10;
}
for (let i = 0; i < space; i++) {
spaceStr += ' '
}
} else if (toStringCheckType(space) === 'Undefined') {
spaceStr = '';
} else {
throw new Error("space 只能为数字或者字符串!")
}
return spaceStr
}
export const getPrefixStr = (str: string, count: number): string => {
let resultStr = str;
for(let i = 0; i < count; i++) {
resultStr += str;
}
return resultStr
}
复制代码
checkSpaceType是用来判断第三个参数的类型,并且通过类型来生成相应的字符串。getPrefixStr的作用是用来生成填充的字符串。在定义myStringfy函数来实例化我们的Stringfy
const myStringfy = (params: any, replacer?: replacerType, space?: string | number): string => {
let spaceStr = checkSpaceType(space)
const stringfy = new Stringfy(spaceStr);
return stringfy.myStringfy(params, replacer, spaceStr, 0)
}
复制代码
最后,让我们通过例子来测试一下:
console.log(myStringfy(obj, null, 'xxxxx'))
console.log(JSON.stringify(obj, null, 'xxxxx'))
复制代码
基本上可以看到结构是相同的。到这里我们基本上实现了JSON.stringfy的所有功能。
最终完整版代码如下:
typeAndFun.ts文件
export type replacerType = any[] | Function | undefined
export const toStringCheckType = (params: any): string => {
let resultStr = Object.prototype.toString.call(params);
resultStr = resultStr.slice(1, resultStr.length - 1)
const arr = resultStr.split(' ')
return arr[1];
}
export const stringOrOtherVal = (data): string => {
if (typeof data === 'string') {
return `"${data}"`
} else if (typeof data === 'number' || typeof data === 'boolean') {
return `${data}`
} else {
return null
}
}
export const checkReplacerType = (replacer: replacerType): number | never => {
let isFunction: number;
// 判断replacer 的类型
if (toStringCheckType(replacer) === 'Function') {
return isFunction = 1;
} else if (toStringCheckType(replacer) === 'Array') {
return isFunction = 2;
} else if (toStringCheckType(replacer) === 'Undefined' || toStringCheckType(replacer) === 'Null') {
return 0
} else {
throw new Error("replacer 只能为函数、数组或者null!")
}
}
export const checkSpaceType = (space?: string | number): string | never => {
let spaceStr: string;
if (toStringCheckType(space) === 'String') {
if ((space as string).length > 10) {
spaceStr = (space as string).slice(0, 10)
} else {
spaceStr = space as string;
}
} else if (toStringCheckType(space) === "Number") {
if (space > 10) {
space = 10;
}
for (let i = 0; i < space; i++) {
spaceStr += ' '
}
} else if (toStringCheckType(space) === 'Undefined') {
spaceStr = '';
} else {
throw new Error("space 只能为数字或者字符串!")
}
return spaceStr
}
export const getPrefixStr = (str: string, count: number): string => {
let resultStr = str;
for(let i = 0; i < count; i++) {
resultStr += str;
}
return resultStr
}
复制代码
index.ts文件
import {
replacerType,
toStringCheckType, stringOrOtherVal,
checkReplacerType, checkSpaceType,
getPrefixStr
} from "./typeAndFun";
class Stringfy {
addSpace: string = ''
constructor(space) {
this.addSpace = space;
}
arrayDeail = (params: any[], replacer?: Function, spaceStr?: string, count?: number, isObjFlag: boolean = true): string => {
spaceStr = getPrefixStr(this.addSpace, count)
const prefilxStr = !isObjFlag ? spaceStr.slice(0, spaceStr.length - this.addSpace.length) : ''
let resultStr = prefilxStr + '[' + `${spaceStr.length ? '\n' : ''}`;
let flag = toStringCheckType(replacer) === 'Function';
for (let i = 0; i < params.length; i++) {
params[i] = flag ? replacer(`${i}`, params[i]) : params[i]
if (toStringCheckType(params[i]) === 'Array') {
resultStr += flag ? this.arrayDeail(params[i], replacer, spaceStr, count + 1, false) + ',' : this.arrayDeail(params[i], null, spaceStr, count + 1, false) + ','
} else if (toStringCheckType(params[i]) === 'Object') {
resultStr += this.myStringfy(params[i], replacer as replacerType, spaceStr, count + 1) + ',';
} else {
resultStr += `${spaceStr}${stringOrOtherVal(params[i])}${spaceStr.length && i === params.length - 1 ? '' : ',' }${spaceStr.length ? '\n' : ''}`
}
}
resultStr = resultStr.slice(0, resultStr.length - 1)
resultStr += spaceStr.length ? `\n${spaceStr.slice(0, spaceStr.length - this.addSpace.length)}` : ''
resultStr += ']'
return resultStr
}
myStringfy = (params: any, replacer?: replacerType, space?: string, count?: number, isObjFlag: boolean = true): string => {
space = getPrefixStr(this.addSpace, count)
const prefilxStr = isObjFlag ? space.slice(0, space.length - this.addSpace.length) : ''
let resultStr: string = prefilxStr + '{' + `${space.length ? '\n' : ''}`
let isFunction: number = checkReplacerType(replacer); // 1 表示函数 2 表示数组
for (let key in params) {
if (isFunction === 2 && (replacer as string[]).indexOf(key) === -1) { // 如果传递的是数组,判断当前key值是否在数组中
continue
}
if (isFunction === 1) {
params[key] = (replacer as Function)(key, params[key])
}
switch (toStringCheckType(params[key])) {
case 'Array':
if(isFunction === 1) {
resultStr += `${space}"${key}":${this.arrayDeail(params[key], replacer as Function, space, count + 1)},${space.length ? '\n' : ''}`
}else if(isFunction === 2) {
resultStr += `${space}"${key}":${this.arrayDeail(params[key], null, space, count + 1)},${space.length ? '\n' : ''}`
}else {
resultStr += `${space}"${key}":${this.arrayDeail(params[key], null, space, count + 1)},${space.length ? '\n' : ''}`
}
break;
case 'Object':
resultStr += `${space}"${key}":${this.myStringfy(params[key], replacer, space, count + 1, false)},${space.length ? '\n' : ''}`;
break
case 'Number':
case 'String':
case 'Null':
case 'Boolean':
resultStr += `${space}"${key}":${stringOrOtherVal(params[key])},${space.length ? '\n' : ''}`
break
case 'WeakMap':
case 'Map':
case 'Set':
case 'WeakSet':
resultStr += `${space}"${key}":{},${space.length ? '\n' : ''}`
break
case 'Date':
resultStr += `${space}"${key}":"${params[key].toJSON()}",${space.length ? '\n' : ''}`
break
default:
continue
}
}
resultStr = resultStr.slice(0, space.length ? resultStr.length - 2 : resultStr.length - 1);
resultStr += space.length ? `\n${space.slice(0, space.length - this.addSpace.length)}` : ''
resultStr += '}'
return resultStr
}
}
const obj = {
arr: [1,2,[3,4,[5,6,[7,8]]]],
data: [{
ccc: {
sddsd: 'xxx',
arr: [1,2,3,4,5,6]
},
obk: {
name: 'xxx',
ahe: 21,
tem: [123, 4, 5,6,6]
}
}]
}
const myStringfy = (params: any, replacer?: replacerType, space?: string | number) => {
let spaceStr = checkSpaceType(space)
const stringfy = new Stringfy(spaceStr);
console.log(stringfy.myStringfy(params, replacer, spaceStr, 0))
}
console.log(JSON.stringify(obj))
myStringfy(obj)
复制代码
到此我们基本实现JSON.stringfy的所有功能。