记于:2022-02-18
前言
题目很简单,就是实现一个简单的 JSON.stringify() 甚至只需要考虑 number | string | array | object 这四种类型即可。
思路也很简单,和深克隆如出一辙,根据 value 不同,进行不同的处理
number | string:直接返回字符串array:遍历object:递归
然而我在面试过程中却没能写出来,究其原因,有两点:
- 紧张:心态影响了思路,这很重要,要学会调整自己的情绪,尽量让自己冷静,放松,无论面试还是工作中,总会遇到高压的情况,这是很不错的经验。
- 平时看得多,写得少,而且很多时候记笔记,跟着别人的代码敲一遍,理解了,就以为自己也能写出来了:这也是很大的问题,学习学习,【学】了还需要【习】,需要实践,放到 coding 中,就是需要自己独立多写一写。
实现
v1 - 简化版
先准备一些函数:
const isNumber = tar => Object.prototype.toString.call(tar) === "[object Number]";
const isString = tar => Object.prototype.toString.call(tar) === "[object String]";
const isArray = tar => Object.prototype.toString.call(tar) === "[object Array]";
const isObject = tar => Object.prototype.toString.call(tar) === "[object Object]";
const isBase = tar => isNum(tar) || isStr(tar)
递归这种操作,硬要在脑子里模拟调用栈,是很难理解的。
我们应该抽象,以人的视角去思考,把视角局限在函数内部,比如在这里就应该是:
number | string:我要处理基本数据array:我要处理数组object:我要处理对象
那么把它翻译成代码就是:
function stringify(target) {
let result = ""
if(isBase(target)){
result = dealBase(target)
}else if(isArray(target)) {
result = dealArray(target)
}else if(isObject(target)) {
result = dealObject(target)
}
return result
}
这样,我们不关心实现细节,只关心策略骨架就搭起来了,接下来还是以相同的思维去思考,要把自己局限于函数之内:
dealBase:我拿到了一个基本类型,很简单,直接返回它的字符串,并且给它加上引号就可以了dealArray:我拿到了一个数组,我应该遍历它,返回[a, b, c]的形式,至于每一个item怎么处理,关我啥事,问stringify去dealObject:我拿到了一个对象,那我应该遍历它,返回{key: value}的形式,啥?你说value可能是数组,也可能是对象?那关我啥事儿,找stringify呀
那么让我们带入角色吧:
function dealBase(target) {
return `"${target}"`
}
function dealArray(target) {
return `[${target.map(i=>stringify(i))}]`
}
function dealObject(target) {
let result = [];
Object.entries(target).forEach(([k, v]) => {
result.push(`"${k}": ${stringify(v)}`);
});
return `{${result.join(",")}}`;
}
v2 - 基础版
首先来看看原生的 JSON.stringify 能处理哪些类型吧:
| In | Out |
|---|---|
1 | "1" |
"a" | "a" |
true | "true" |
undefined | "undefined" |
null | "null" |
[1] | "[1]" |
{a: "a"} | '{"a": "a"}' |
new Set([1]) | "{}" |
new WeakSet([[1]]) | "{}" |
new Map([[1, 1]]) | "{}" |
new WeakMap([[[1], 1]]) | "{}" |
new RegExp(/1/) | "{}" |
new Date() | "2022-02-18T14:34:54.057Z" |
Symbol(1) | "undefined" |
BigInt(1) | TypeError: Do not know how to serialize a BigInt |
可以看到,stringify 其实要比深克隆简单,很多类型的处理方式都是一致的,这里只需要增加对应策略的处理就行了:
Set | WeakSet | Map | WeakMap | RegExp:这些是啥?我不认识,丢个"{}"出去吧base:这些类型直接转成字符串返回就行了
Date:这个类型我需要返回格式化之后的字符串Symbol:这啥呀?没见过,又不是引用类型,那还是返回undefined吧
BigInt:mua 的,这东西完全触及到我的知识盲区了,得报个错才行
按照上面的思路,这里就直接上代码吧。
const isNumber = tar => Object.prototype.toString.call(tar) === "[object Number]";
const isString = tar => Object.prototype.toString.call(tar) === "[object String]";
const isBoolean = tar => Object.prototype.toString.call(tar) === "[object Boolean]";
const isArray = tar => Object.prototype.toString.call(tar) === "[object Array]";
const isObject = tar => Object.prototype.toString.call(tar) === "[object Object]";
const isUndefined = tar => Object.prototype.toString.call(tar) === "[object Undefined]";
const isNull = tar => Object.prototype.toString.call(tar) === "[object Null]";
const isDate = tar => Object.prototype.toString.call(tar) === "[object Date]";
const isSymbol = tar => Object.prototype.toString.call(tar) === "[object Symbol]";
const isSet = tar => Object.prototype.toString.call(tar) === "[object Set]";
const isWeakSet = tar => Object.prototype.toString.call(tar) === "[object WeakSet]";
const isMap = tar => Object.prototype.toString.call(tar) === "[object Map]";
const isWeakMap = tar => Object.prototype.toString.call(tar) === "[object WeakMap]";
const isRegExp = tar => Object.prototype.toString.call(tar) === "[object RegExp]";
const isBigInt = tar => Object.prototype.toString.call(tar) === "[object BigInt]";
const isBase = tar =>
isNumber(tar) || isString(tar) || isBoolean(tar) || isUndefined(tar) || isNull(tar);
const isUnknowRef = tar =>
isSet(tar) || isMap(tar) || isWeakSet(tar) || isWeakMap(tar) || isRegExp(tar);
function dealBase(target) {
return `"${target}"`;
}
function dealArray(target) {
return `[${target.map(i => stringify(i))}]`;
}
function dealObject(target) {
let result = [];
Object.entries(target).forEach(([k, v]) => {
result.push(`"${k}": ${stringify(v)}`);
});
return `{${result.join(",")}}`;
}
function dealUnknownRef(_target) {
return `"{}"`;
}
function dealDate(target) {
return `${target}`;
}
function dealSymbol(_target) {
return `"undefined"`;
}
function dealBigInt(_target) {
throw new Error("TypeError: Do not know how to serialize a BigInt");
}
function stringify(target) {
let result = "";
if (isBase(target)) {
result = dealBase(target);
} else if (isObject(target)) {
result = dealObject(target);
} else if (isArray(target)) {
result = dealArray(target);
} else if (isUnknowRef(target)) {
result = dealUnknownRef(target);
} else if (isDate(target)) {
result = dealDate(target);
} else if (isSymbol(target)) {
result = dealSymbol(target);
} else if (isBigInt(target)) {
return dealBigInt(target);
}
return result;
}
const data = {
a: 1,
b: "2",
c: {
c1: { c11: "c11" }
},
d: [
1,
2,
3,
{
d1: {
d11: "d11"
}
}
],
e: undefined,
f: null,
h: new Date(),
i: Symbol(1),
j: new Set([1]),
k: new WeakSet([[1]]),
l: new Map([[1, 1]]),
m: new WeakMap([[[1], 1]]),
n: new RegExp(/1/)
// j: BigInt(1)
};
v3 - 增强版
从 v2 中的表格可以看到,其实很多类型 JSON.stringify 都是处理不了的,并且还有两个问题:
- 如果出现循环引用是会报错的
- 如果
object的key是一个Symbol,会被直接忽略掉
类型的问题很好处理,顺着递归的思维,把思维限制在函数之内,不要模拟调用栈,增加不同的策略就可以了,这里我们先解决特殊类型处理和 key 为 Symbol 的问题:
key为Symbol,在遍历object的时候,使用Reflect.ownKeys(target)即可,它会遍历对象上所有属性,无论是否为Symbol,是否可以迭代
const isNumber = tar => Object.prototype.toString.call(tar) === "[object Number]";
const isString = tar => Object.prototype.toString.call(tar) === "[object String]";
const isBoolean = tar => Object.prototype.toString.call(tar) === "[object Boolean]";
const isArray = tar => Object.prototype.toString.call(tar) === "[object Array]";
const isObject = tar => Object.prototype.toString.call(tar) === "[object Object]";
const isUndefined = tar => Object.prototype.toString.call(tar) === "[object Undefined]";
const isNull = tar => Object.prototype.toString.call(tar) === "[object Null]";
const isDate = tar => Object.prototype.toString.call(tar) === "[object Date]";
const isSymbol = tar => Object.prototype.toString.call(tar) === "[object Symbol]";
const isSet = tar => Object.prototype.toString.call(tar) === "[object Set]";
const isWeakSet = tar => Object.prototype.toString.call(tar) === "[object WeakSet]";
const isMap = tar => Object.prototype.toString.call(tar) === "[object Map]";
const isWeakMap = tar => Object.prototype.toString.call(tar) === "[object WeakMap]";
const isRegExp = tar => Object.prototype.toString.call(tar) === "[object RegExp]";
const isBigInt = tar => Object.prototype.toString.call(tar) === "[object BigInt]";
const isBase = tar =>
isNumber(tar) || isString(tar) || isBoolean(tar) || isUndefined(tar) || isNull(tar);
const isUnknowRef = tar =>
isSet(tar) || isMap(tar) || isWeakSet(tar) || isWeakMap(tar) || isRegExp(tar);
const isArrayLike = tar => isSet(tar) || isMap(tar);
const isWeak = tar => isWeakSet(tar) || isWeakMap(tar);
function dealBase(target) {
return `"${target}"`;
}
function dealArrayLike(target) {
const prefix = isSet(target) ? "<Set>" : isMap(target) ? "<Map>" : "";
return `${prefix}[${[...target].map(i => stringify(i))}]`;
}
function dealWeak(target) {
const prefix = isWeakSet(target) ? "<WeakSet>" : isWeakMap(target) ? "<WeakMap>" : "";
return `${prefix}{ <items unknown> }`;
}
function dealObject(target) {
let result = [];
// 遍历一个对象的所有属性,不管是否是 Symbol,是否可以枚举
Reflect.ownKeys(target).forEach(k => {
const item = isSymbol(k)
? `"${k.toString()}": ${stringify(target[k])}`
: `"${k}": ${stringify(target[k])}`;
result.push(item);
});
return `{${result.join(",")}}`;
}
function dealDate(target) {
return `"${target.toDateString()}"`;
}
function dealRegExp(target) {
return `"${target.toString()}"`;
}
function dealSymbol(target) {
return `"${target.toString()}"`;
}
function dealBigInt(target) {
return `${target.toString()}n`;
}
function stringify(target) {
let result = "";
if (isBase(target)) {
result = dealBase(target);
} else if (isObject(target)) {
result = dealObject(target);
} else if (isArray(target) || isArrayLike(target)) {
result = dealArrayLike(target);
} else if (isWeak(target)) {
result = dealWeak(target);
} else if (isDate(target)) {
result = dealDate(target);
} else if (isRegExp(target)) {
result = dealRegExp(target);
} else if (isSymbol(target)) {
result = dealSymbol(target);
} else if (isBigInt(target)) {
return dealBigInt(target);
}
return result;
}
那么最后一个问题,循环引用怎么解决呢?
要判断是否存在循环引用,就需要知道 key 是否与 object 指向了同一块内存,而能够用引用类型作为 key 的有 Map 和 WeakMap,考虑到垃圾回收,这里选择使用 WeakMap,那么带入函数的视角,应该怎么看呢:
stringify:我要准备一个WeakMap,方便它们到时查引用,其他的我可不知道,就交给doStringify吧,它可能干了doStringify:我要判断target的类型,具体的处理就交给deal方法吧,对了,如果target类型是Object,还要把WeakMap给它,方便它查一下有没有哪个小子偷偷来过一次了dealObject:我得看看有没有谁已经来过了,如果被我逮到,它就完了。怎么逮?简单呀,我把它的指针所指向的内存地址记在WeakMap里呀,至于其他的,那我可不知道,问问doStringify吧
根据上述思路 coding 即可:
const isObject = tar => Object.prototype.toString.call(tar) === "[object Object]";
const dealObject = (target, weakMap) => {
const queue = [];
const keys = Reflect.ownKeys(target);
for (let key of keys) {
const item = target[key];
if (weakMap.has(item)) {
return `circular reference`;
}
if (isObject(item)) {
weakMap.set(item, key);
}
queue.push(`"${key}":${doStringify(item, weakMap)}`);
}
return `{${queue.join(",")}}`;
};
const doStringify = (target, weakMap) => {
let result = "";
if (isObject(target)) {
result = dealObject(target, weakMap);
} else {
result = `"${target}"`;
}
return result;
};
const stringify = target => {
const weakMap = new WeakMap();
return doStringify(target, weakMap);
};