深拷貝系列 ———— 本身實現一個JSON.stringify和JSON.parse

簡介

在上篇文章咱們已經瞭解什麼是深拷貝淺拷貝,也着重介紹了淺拷貝相關的一下實現方法,或者本身實現一個淺拷貝等等。本篇文章主要介紹深拷貝的一種簡單實現方式JSON.parse/JSON.stringify。在日常開發時咱們能夠常常的看到別人使用,或者在不那麼瞭解深拷貝時本身也有使用。編程

JSON.parse/JSON.stringify實際上是用來序列化 JSON 格式的數據的方法。那它爲何能實現一個簡單的深拷貝呢? 在執行JSON.stringify會把咱們的一個對象序列化爲字符串,而字符串是基本類型。 再經過JSON.parse時,把字符串類型反序列化爲對象,這個時候由於在反序列化以前它是基本類型因此他會指向一個新的地址,在反序列化以後它是一個對象會再分配內存空間。 因此JSON.parse/JSON.stringify能夠實現一個簡單的深拷貝json

本篇文章首先實現一個JSON.stringify/JSON.parse,下一篇文章實現一個比較完整的深拷貝數組

實例

直接上代碼驗證一下函數

// 聲明原始對象
var old = {
  name: "old",
  attr: {
    age: 18,
    sex: "man"
  },
  title: ["M1", "P6"]
};

// 聲明一個新對象,經過SON.parse/JSON.stringify 實現對原始對象深拷貝,而且賦值給新對象
var newValue = JSON.parse(JSON.stringify(old));
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}

// 修改原始對象的name,新對象不受影響
old.name = "new";
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}
console.log(old); // {name: "new", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}

// 修改原始對象的引用類型,新對象也不受影響
old.attr.age = 20;
console.log(newValue); // {name: "old", attr: {age: 18, sex: "man"}, title: [['M1', 'P6']]}
console.log(old); // {name: "new", attr: {age: 20, sex: "man"}, title: [['M1', 'P6']]}
複製代碼

實際上是不是覺得用這個就能夠了,並無什麼問題啊,下面咱們就來一點點揭開它的面紗。測試

侷限性

其實JSON.parse/JSON.stringify仍是有不少侷限性,大體以下:ui

  • 會忽略 undefined
  • 會忽略 Symbol
  • 沒法序列化function,也會忽略
  • 沒法解決循環引用,會報錯
  • 深層對象轉換爆棧

直接上代碼驗證spa

// 聲明一個包含undefined、null、symbol、function的對象
var oldObj = {
  name: "old",
  age: undefined,
  sex: Symbol("setter"),
  title: function() {},
  lastName: null
};
var newObj = JSON.parse(JSON.stringify(oldObj));
// 能夠看到會忽略undefined、symbol、function的對象
console.log(newObj); // {name: "old", lastName: null}

var firstObj = {
  name: "firstObj"
};
firstObj.newKey = firstObj;
// Converting circular structure to JSON
var newFirstObj = JSON.parse(JSON.stringify(firstObj));
複製代碼

若是循環引用報錯以下圖所示: prototype

JSON.parse/JSON.stringify

一個生成任意深度、廣度對象方法。code

function createData(deep, breadth) {
  var data = {};
  var temp = data;

  for (var i = 0; i < deep; i++) {
    temp = temp["data"] = {};
    for (var j = 0; j < breadth; j++) {
      temp[j] = j;
    }
  }
  return data;
}
複製代碼

驗證JSON.stringify遞歸爆棧regexp

JSON.stringify(createData(10000));
// VM97994:1 Uncaught RangeError: Maximum call stack size exceeded
複製代碼

本身實現 JSON.stringify

  • 首先一個簡單的遞歸
  • 區分StringBooleanNumbernull
  • 過濾undefinedsymbolfunction
  • 循環引用警告

一個簡單的遞歸

實現目標

  • 遞歸調用
// 數據類型判斷
function getType(attr) {
  let type = Object.prototype.toString.call(attr);
  let newType = type.substr(8, type.length - 9);
  return newType;
}

// 轉換函數
function StringIfy(obj) {
  // 若是是非object類型 or null的類型直接返回 原值的String
  if (typeof obj !== "object" || getType(obj) === null) {
    return String(obj);
  }
  // 聲明一個數組
  let json = [];
  // 判斷當前傳入參數是對象仍是數組
  let arr = obj ? getType(obj) === "Array" : false;
  // 循環對象屬性
  for (let key in obj) {
    // 判斷屬性是否在對象自己上
    if (obj.hasOwnProperty(key)) {
      // 獲取屬性而且判斷屬性值類型
      let item = obj[key];
      // 若是爲object類型遞歸調用
      if (getType(obj) === "Object") {
        // consoarrle.log(item)
        item = StringIfy(item);
      }
      // 拼接數組字段
      json.push((arr ? '"' : '"' + key + '": "') + String(item) + '"');
    }
  }
  console.log(arr, String(json));
  // 轉換數組字段爲字符串
  return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}

// 測試代碼
StringIfy({ name: { name: "abc" } }); // "{"name": "{"name": "abc"}"}"
StringIfy([1, 2, 4]); // "["1","2","4"]"
複製代碼

在上面代碼中咱們基本的JSON序列化,能夠序列化引用類型基本類型

區分數據類型

我說的區分的類型,是JSON.stringify再序列化時,像NumberBooleannull它是不會加上雙引號的,只有在String類型或者Object中的key纔會帶雙引號

  • 增長一個判斷當前屬性類型
// 。。。省略代碼

// 轉換函數
function StringIfy(obj) {
  // 。。。省略代碼
  let IsQueto =
    getType(item) === "Number" ||
    getType(item) === "Boolean" ||
    getType(item) === "Null"
      ? ""
      : '"';
  // 拼接數組字段
  json.push((arr ? IsQueto : '"' + key + '": "') + String(item) + IsQueto);
  // 。。。省略代
}

// 測試代碼
StringIfy({ name: { name: "abc" } }); // "{"name": "{"name": "abc"}"}"
StringIfy([1, 2, 4]); // "[1,2,4]"
複製代碼

不處理部分值

  • 經過正則判斷過濾Symbol|Function|Undefined
  • 跳過當前循環
if (/Symbol|Function|Undefined/.test(getType(item))) {
        delete obj[key];
        continue;
    }
    let test = {
        name: 'name',
        age: undefined,
        func: function () {},
        sym: Symbol('setter')
    };
    let newTest = StringIfy(test);
    console.log(newTest); // {"name": "name"}
複製代碼

循環引用警告

  • 處理循環引用,警告而且退出循環
if (item === obj) {
  console.error(new TypeError("Converting circular structure to JSON"));
  return false;
}
複製代碼

JSON.stingify 其餘參數

JSON.stringify它能夠傳入三個參數。

語法JSON.stringify(value[, replacer [, space]])

參數

  • value:將要序列化成 一個 JSON 字符串的值。
  • replacer(可選):若是該參數是一個函數,則在序列化過程當中,被序列化的值的每一個屬性都會通過該函數的轉換和處理;若是該參數是一個數組,則只有包含在這個數組中的屬性名纔會被序列化到最終的 JSON 字符串中;
  • space:指定縮進用的空白字符串,用於美化輸出(pretty-print)

這裏主要記錄replacer的實現,首先咱們要知道replacer參數的使用才能本身實現。

replacer實例

let oJson = {
  name: "oJson",
  age: 20,
  sex: "man",
  calss: "one"
};
JSON.stringify(oJson, ["sex", "name"]); // "{"sex":"man","name":"oJson"}"
// 兩個參數 key/value的形式
JSON.stringify(oJson, function(key, value) {
  if (typeof value === "string") {
    return undefined;
  }
  return value;
}); // "{"age":20}"
複製代碼

實現

// 轉換函數
function StringIfy(obj, replacer) {
  // 若是是非object類型 or null的類型直接返回 原值的String
  if (typeof obj !== "object" || getType(obj) === null) {
    return String(obj);
  }
  // 聲明一個數組
  let json = [];

  // 判斷當前傳入參數是對象仍是數組
  let arr = obj ? getType(obj) === "Array" : false;
  // 循環對象屬性
  for (let key in obj) {
    // 判斷屬性是否可枚舉
    if (obj.hasOwnProperty(key)) {
      // console.log(key, item);

      // 獲取屬性而且判斷屬性值類型
      let item = obj[key];
      // <!-------修改開始-------!>
      let flag = true;
      // 處理第二個參數
      if (replacer) {
        // 判斷第二個參數類型
        switch (getType(replacer)) {
          case "Function":
            // 若是爲函數執行
            flag = replacer(key, item);
            break;
          case "Array":
            // 若是爲數組
            flag = replacer.indexOf(key) !== -1;
            break;
        }
      }
      // 判斷返回結果
      if (!flag) {
        continue;
      }
      // <!-------修改結束-------!>
      if (item === obj) {
        console.error(new TypeError("Converting circular structure to JSON"));
        return false;
      }
      if (/Symbol|Function|Undefined/.test(getType(item))) {
        delete obj[key];
        continue;
      }
      // 若是爲object類型遞歸調用
      if (getType(item) === "Object") {
        // consoarrle.log(item)
        item = StringIfy(item);
      }
      let IsQueto =
        getType(item) === "Number" ||
        getType(item) === "Boolean" ||
        getType(item) === "Null"
          ? ""
          : '"';
      // 拼接數組字段
      json.push((arr ? IsQueto : '"' + key + '": "') + String(item) + IsQueto);
    }
  }
  console.log(arr, String(json));
  // 轉換數組字段爲字符串
  return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}
複製代碼

咱們新增第二個參數的處理,第三個參數暫時就忽濾了,主要用於設置space的,下面直接測試上面的代碼:

let test = {
    name: "name",
    age: undefined,
    func: function() {},
    sym: Symbol("setter"),
    age: 30,
    sex: 'man'
  };
  console.log(StringIfy(test, ['name', 'sex'])); // {"name": "name","sex": "man"}
  let newTest = StringIfy(test, function (key, value) {
    if (typeof value === 'string') {
      return undefined;
    }
    return value;
  });
  console.log(newTest); // {"age": "30}
複製代碼

到此StringIfy的實現到此結束。

Stringify 總結

到此本身實現JSON.stringify到此結束了,完整代碼以下:

// 數據類型判斷
function getType(attr) {
  let type = Object.prototype.toString.call(attr);
  let newType = type.substr(8, type.length - 9);
  return newType;
}
// 轉換函數
function StringIfy(obj) {
  // 若是是非object類型 or null的類型直接返回 原值的String
  if (typeof obj !== "object" || getType(obj) === null) {
    return String(obj);
  }
  // 聲明一個數組
  let json = [];
  // 判斷當前傳入參數是對象仍是數組
  let arr = obj ? getType(obj) === "Array" : false;
  // 循環對象屬性
  for (let key in obj) {
    // 判斷屬性是否在對象自己上
    if (obj.hasOwnProperty(key)) {
      // console.log(key, item);
      // 獲取屬性而且判斷屬性值類型
      let item = obj[key];
      if (item === obj) {
        console.error(new TypeError("Converting circular structure to JSON"));
        return false;
      }
      if (/Symbol|Function|Undefined/.test(getType(item))) {
        delete obj[key];
        continue;
      }
      // 若是爲object類型遞歸調用
      if (getType(item) === "Object") {
        // consoarrle.log(item)
        item = StringIfy(item);
      }
      let IsQueto =
        getType(item) === "Number" ||
        getType(item) === "Boolean" ||
        getType(item) === "Null"
          ? ""
          : '"';
      // 拼接數組字段
      json.push((arr ? IsQueto : '"' + key + '": "') + String(item) + IsQueto);
    }
  }
  console.log(arr, String(json));
  // 轉換數組字段爲字符串
  return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}");
}
let aa = StringIfy([1, 2, 4]);
let test = {
  name: "name",
  age: undefined,
  func: function() {},
  sym: Symbol("setter")
};
let newTest = StringIfy(test);
console.log(aa, newTest);
var firstObj = {
  name: "firstObj"
};
firstObj.newKey = firstObj;
StringIfy(firstObj);
複製代碼

JSON.parse 實現

有兩種方法實現parse效果,第一種是eval實現,另外一種是Function實現,下面直接開始。

eval 實現

function ParseJson(opt) {
  return eval("(" + opt + ")");
}

let aa = StringIfy([1, 2, 4]);
ParseJson(aa); // [1, 2, 4]

let test = {
  name: "name",
  age: undefined,
  func: function() {},
  sym: Symbol("setter")
};
let newTest = StringIfy(test);
console.log(ParseJson(newTest)); // {name: "name"}
複製代碼

能夠看到上面的代碼能夠實現基本的反序列化。

避免在沒必要要的狀況下使用 eval,eval() 是一個危險的函數, 他執行的代碼擁有着執行者的權利。若是你用 eval()運行的字符串代碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴展程序的權限下,在用戶計算機上運行惡意代碼。

Function 實現

function ParseJsonTwo(opt) {
  return new Function("return " + opt)();
}

let aa = StringIfy([1, 2, 4]);
ParseJson(aa); // [1, 2, 4]

let test = {
  name: "name",
  age: undefined,
  func: function() {},
  sym: Symbol("setter")
};
let newTest = StringIfy(test);
console.log(ParseJson(newTest)); // {name: "name"}
複製代碼

evalFunction 都有着動態編譯js代碼的做用,可是在實際的編程中並不推薦使用。

處理 XSS

它會執行 JS 代碼,有 XSS 漏洞。

若是你只想記這個方法,就得對參數 json 作校驗。

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;

var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;

var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if (
	rx_one.test(
		json.replace(rx_two, "@").replace(rx_three, "]").replace(rx_four, "")
	);
) {
	var obj = ParseJson(json); // ParseJson(json) or ParseJsonTwo(json)
}
複製代碼

Parse 總結

其實不管在何時都不太推薦evalfunction,由於它很容形成入侵。 若是有興趣能夠去看一下JSON.parse 三種實現方式,它有涉及到遞歸實現,狀態機實現,講的也不錯。

總結

本篇文章主要講解了JSON.parse/JSON.stringify是怎麼實現的深拷貝,而且深刻了解一下JSON.parse/JSON.stringify深拷貝上的實現,其實還有怎麼加速JSON序列化的速度,會在另外一篇文章中講解。最後本身也簡單實現了一個ParseJson/StringIfy

參考

無敵祕籍之 — JavaScript 手寫代碼 JSON.parse 三種實現方式

相關文章
相關標籤/搜索