當JSON.parse“遇上”非鍵值對

RobinsonZhang發表於2019-03-03

前言

在json大行其道並作為前後端主要通訊的資料格式之一時,對json本身的使用和了解多少人都會有些概念,當然隨之而來的也是對json的物件以及其字串形式的互相轉換。在歷史久遠的過去,前端是通過低能的eval來實現格式轉換的。

那麼作為常識,我們知道JSON提供了兩個常用的工具方法可以實現互相轉換,分別是JSON.parse(),以及JSON.stringfy();常識的另外一方面,我們也知道一般情況下,我們處理的後端返回的物件都是標準的鍵值對格式,比如:{code:200,message:`success`,data:{page:1,list:[]}}

那當後端或者其他場景下,我們將其他值型別轉換的時候會發生什麼呢?產生這個想法是因為在處理業務的時候發現,後端有個欄位,其圖片列表的欄位值,返回的是‘[url1,url2]’,很顯然其是陣列字串後的結果。開始我並沒有想到用parse方法,因為腦中侷限於這不是一個json資料。

什麼是json資料

我們知道json是js物件表示法的子集,其標準的定義裡有以下幾條規則:

  • 資料在名稱、值對中
  • 資料由逗號分隔
  • 花括號儲存物件
  • 方括號儲存陣列

那麼一些常見的資料型別,比如字串,布林型,null,undefined,數字,還有引用型別的函式、物件,陣列這些屬於json麼?或者說其字串化是否支援轉化麼?

我進行了一些案例驗證,這裡直接將結果公佈出來,大家有興趣的可以去校驗下是不是這樣的結果。

JSON.parse(`true`) //true
JSON.parse(`false`) //false
JSON.parse(`str`) //Uncaught SyntaxError: Unexpected token d in JSON at position 0
JSON.parse(`345str`) //Uncaught SyntaxError: Unexpected token d in JSON at position 3 ,其報錯的位置是出現字串非數字的時候
JSON.parse(`345`) //345
JSON.parse(`null`) //null
JSON.parse("undefined") //Uncaught SyntaxError: Unexpected token d in JSON at position 0
JSON.parse("[]") //[]
JSON.parse(`[1,"5"]`)//[1,"5"]
JSON.parse("{}")//{}
JSON.parse(`{1,5}`)//Uncaught SyntaxError: Unexpected token d in JSON at position 1
JSON.parse(`{1:1}`)//Uncaught SyntaxError: Unexpected token d in JSON at position 1
JSON.parse(`{"name":1}`)//{name:1}
複製程式碼

追根溯源

要想知道為什麼是這樣的結果,我們就要分析下其parse方法底層寫了哪些邏輯了。

這裡重點分析為什麼支援這些非鍵值對的型別,而有些為什麼又不支援。

首先我們要有個基本概念理解下:String在解析之前進行了一次字串格式的整理,來保證整體字元是有效的,然後根據第一個字元進行了分類,不符合預期情況的都會報未期待的字元錯誤。然後其會特徵性的區分的包括但不侷限於以下的特殊情況。

字元 呼叫函式
{ ParseJsonObject
f 判斷是否是false
t 判斷是否是true
n 判斷是否是null
含有數字,已0-9 或者 負數標識 – 開始的 檢視整體是不是數字

原始碼追蹤:整體邏輯

我們在原始碼角度找到了如下的程式碼:需要翻牆才可以看到。
其對應的原始檔地址為 :


// 情況一 :發現了首字元是字串標識的引號,用ParseJsonString實現 
if (c0_ == `"`) return ParseJsonString();

// 情況二 :發現是0-9 數字開始,或者 - 開始的,有可能是數字型別,用轉換為數字的方法進行轉換
 if ((c0_ >= `0` && c0_ <= `9`) || c0_ == `-`) return ParseJsonNumber();
 
 // 情況三 :發現開始是物件左側標記 { ,用json物件的解析方法
  if (c0_ == `{`) return ParseJsonObject();
  
// 情況四 :發現是 [ 開始的,嘗試用陣列轉換的方法去轉換
  if (c0_ == `[`) return ParseJsonArray();
  
  // 情況五 :排除特殊的一些資料型別,比如true,false,null的字串化
  if (c0_ == `f`) {
    if (AdvanceGetChar() == `a` && AdvanceGetChar() == `l` &&
        AdvanceGetChar() == `s` && AdvanceGetChar() == `e`) {
      AdvanceSkipWhitespace();
      return factory()->false_value();
    }
    return ReportUnexpectedCharacter();
  }
  if (c0_ == `t`) {
    if (AdvanceGetChar() == `r` && AdvanceGetChar() == `u` &&
        AdvanceGetChar() == `e`) {
      AdvanceSkipWhitespace();
      return factory()->true_value();
    }
    return ReportUnexpectedCharacter();
  }
  if (c0_ == `n`) {
    if (AdvanceGetChar() == `u` && AdvanceGetChar() == `l` &&
        AdvanceGetChar() == `l`) {
      AdvanceSkipWhitespace();
      return factory()->null_value();
    }
    return ReportUnexpectedCharacter();
  }

複製程式碼

原始碼追蹤:具體方法

ParseJsonString

因為這部分程式碼的語言自己沒有去學習過,就簡單分析下里面處理的一些邏輯吧,主要處理了內容是否是單位元組,以及一些特殊符號的處理。

template <bool seq_one_byte>
bool JsonParser<seq_one_byte>::ParseJsonString(Handle<String> expected) {
  int length = expected->length();
  if (source_->length() - position_ - 1 > length) {
    DisallowHeapAllocation no_gc;
    String::FlatContent content = expected->GetFlatContent();
    if (content.IsOneByte()) {
      DCHECK_EQ(`"`, c0_);
      const uint8_t* input_chars = seq_source_->GetChars() + position_ + 1;
      const uint8_t* expected_chars = content.ToOneByteVector().start();
      for (int i = 0; i < length; i++) {
        uint8_t c0 = input_chars[i];
        if (c0 != expected_chars[i] || c0 == `"` || c0 < 0x20 || c0 == `\`) {
          return false;
        }
      }
      if (input_chars[length] == `"`) {
        position_ = position_ + length + 1;
        AdvanceSkipWhitespace();
        return true;
      }
    }
  }
  return false;
}

ParseJsonString
複製程式碼

ParseJsonArray

核心是處理了當其結尾是否是]區分,陣列處理的前提是右邊的結束符必須是] .
如果不是,那麼就會按照ParseJsonValue進行轉換,當發現轉換為物件失敗,比如說發現是null,或者一些特殊情況的時候,就會報錯不可預期的字串錯誤;
如果右側是],則可能是陣列,按照簡單陣列以及複雜陣列分別處理,簡單陣列就會指定固定一個陣列然後返回這個陣列。

// Parse a JSON array. Position must be right at `[`.
template <bool seq_one_byte>
Handle<Object> JsonParser<seq_one_byte>::ParseJsonArray() {
  HandleScope scope(isolate());
  ZoneList<Handle<Object> > elements(4, zone());
  DCHECK_EQ(c0_, `[`);

  ElementKindLattice lattice;

  AdvanceSkipWhitespace();
  if (c0_ != `]`) {
    do {
      Handle<Object> element = ParseJsonValue();
      if (element.is_null()) return ReportUnexpectedCharacter();
      elements.Add(element, zone());
      lattice.Update(element);
    } while (MatchSkipWhiteSpace(`,`));
    if (c0_ != `]`) {
      return ReportUnexpectedCharacter();
    }
  }
  AdvanceSkipWhitespace();

  // Allocate a fixed array with all the elements.

  Handle<Object> json_array;
  const ElementsKind kind = lattice.GetElementsKind();

  switch (kind) {
    case PACKED_ELEMENTS:
    case PACKED_SMI_ELEMENTS: {
      Handle<FixedArray> elems =
          factory()->NewFixedArray(elements.length(), pretenure_);
      for (int i = 0; i < elements.length(); i++) elems->set(i, *elements[i]);
      json_array = factory()->NewJSArrayWithElements(elems, kind, pretenure_);
      break;
    }
    case PACKED_DOUBLE_ELEMENTS: {
      Handle<FixedDoubleArray> elems = Handle<FixedDoubleArray>::cast(
          factory()->NewFixedDoubleArray(elements.length(), pretenure_));
      for (int i = 0; i < elements.length(); i++) {
        elems->set(i, elements[i]->Number());
      }
      json_array = factory()->NewJSArrayWithElements(elems, kind, pretenure_);
      break;
    }
    default:
      UNREACHABLE();
  }

  return scope.CloseAndEscape(json_array);
}

ParseJsonArray
複製程式碼

ParseJsonNumber

核心判斷了一些負數,0,1-9,小數點等不同情況的處理,並對不符合情況的丟擲異常字元。

template <bool seq_one_byte>
Handle<Object> JsonParser<seq_one_byte>::ParseJsonNumber() {
  bool negative = false;
  int beg_pos = position_;
  if (c0_ == `-`) {
    Advance();
    negative = true;
  }
  if (c0_ == `0`) {
    Advance();
    // Prefix zero is only allowed if it`s the only digit before
    // a decimal point or exponent.
    if (IsDecimalDigit(c0_)) return ReportUnexpectedCharacter();
  } else {
    int i = 0;
    int digits = 0;
    if (c0_ < `1` || c0_ > `9`) return ReportUnexpectedCharacter();
    do {
      i = i * 10 + c0_ - `0`;
      digits++;
      Advance();
    } while (IsDecimalDigit(c0_));
    if (c0_ != `.` && c0_ != `e` && c0_ != `E` && digits < 10) {
      SkipWhitespace();
      return Handle<Smi>(Smi::FromInt((negative ? -i : i)), isolate());
    }
  }
  if (c0_ == `.`) {
    Advance();
    if (!IsDecimalDigit(c0_)) return ReportUnexpectedCharacter();
    do {
      Advance();
    } while (IsDecimalDigit(c0_));
  }
  if (AsciiAlphaToLower(c0_) == `e`) {
    Advance();
    if (c0_ == `-` || c0_ == `+`) Advance();
    if (!IsDecimalDigit(c0_)) return ReportUnexpectedCharacter();
    do {
      Advance();
    } while (IsDecimalDigit(c0_));
  }
  int length = position_ - beg_pos;
  double number;
  if (seq_one_byte) {
    Vector<const uint8_t> chars(seq_source_->GetChars() + beg_pos, length);
    number = StringToDouble(isolate()->unicode_cache(), chars,
                            NO_FLAGS,  // Hex, octal or trailing junk.
                            std::numeric_limits<double>::quiet_NaN());
  } else {
    Vector<uint8_t> buffer = Vector<uint8_t>::New(length);
    String::WriteToFlat(*source_, buffer.start(), beg_pos, position_);
    Vector<const uint8_t> result =
        Vector<const uint8_t>(buffer.start(), length);
    number = StringToDouble(isolate()->unicode_cache(), result,
                            NO_FLAGS,  // Hex, octal or trailing junk.
                            0.0);
    buffer.Dispose();
  }
  SkipWhitespace();
  return factory()->NewNumber(number, pretenure_);
}

ParseJsonNumber
複製程式碼

ParseJsonObject

核心判斷了末尾是不是}來保證json物件,以及嚴格校驗是否複核鍵值對的基本格式。

// Parse a JSON object. Position must be right at `{`.
template <bool seq_one_byte>
Handle<Object> JsonParser<seq_one_byte>::ParseJsonObject() {
  HandleScope scope(isolate());
  Handle<JSObject> json_object =
      factory()->NewJSObject(object_constructor(), pretenure_);
  Handle<Map> map(json_object->map());
  int descriptor = 0;
  ZoneList<Handle<Object> > properties(8, zone());
  DCHECK_EQ(c0_, `{`);

  bool transitioning = true;

  AdvanceSkipWhitespace();
  if (c0_ != `}`) {
    do {
      if (c0_ != `"`) return ReportUnexpectedCharacter();

      int start_position = position_;
      Advance();

      if (IsDecimalDigit(c0_)) {
        ParseElementResult element_result = ParseElement(json_object);
        if (element_result == kNullHandle) return Handle<Object>::null();
        if (element_result == kElementFound) continue;
      }
      // Not an index, fallback to the slow path.

      position_ = start_position;
#ifdef DEBUG
      c0_ = `"`;
#endif

      Handle<String> key;
      Handle<Object> value;

      // Try to follow existing transitions as long as possible. Once we stop
      // transitioning, no transition can be found anymore.
      DCHECK(transitioning);
      // First check whether there is a single expected transition. If so, try
      // to parse it first.
      bool follow_expected = false;
      Handle<Map> target;
      if (seq_one_byte) {
        key = TransitionArray::ExpectedTransitionKey(map);
        follow_expected = !key.is_null() && ParseJsonString(key);
      }
      // If the expected transition hits, follow it.
      if (follow_expected) {
        target = TransitionArray::ExpectedTransitionTarget(map);
      } else {
        // If the expected transition failed, parse an internalized string and
        // try to find a matching transition.
        key = ParseJsonInternalizedString();
        if (key.is_null()) return ReportUnexpectedCharacter();

        target = TransitionArray::FindTransitionToField(map, key);
        // If a transition was found, follow it and continue.
        transitioning = !target.is_null();
      }
      if (c0_ != `:`) return ReportUnexpectedCharacter();

      AdvanceSkipWhitespace();
      value = ParseJsonValue();
      if (value.is_null()) return ReportUnexpectedCharacter();

      if (transitioning) {
        PropertyDetails details =
            target->instance_descriptors()->GetDetails(descriptor);
        Representation expected_representation = details.representation();

        if (value->FitsRepresentation(expected_representation)) {
          if (expected_representation.IsHeapObject() &&
              !target->instance_descriptors()
                   ->GetFieldType(descriptor)
                   ->NowContains(value)) {
            Handle<FieldType> value_type(
                value->OptimalType(isolate(), expected_representation));
            Map::GeneralizeField(target, descriptor, details.constness(),
                                 expected_representation, value_type);
          }
          DCHECK(target->instance_descriptors()
                     ->GetFieldType(descriptor)
                     ->NowContains(value));
          properties.Add(value, zone());
          map = target;
          descriptor++;
          continue;
        } else {
          transitioning = false;
        }
      }

      DCHECK(!transitioning);

      // Commit the intermediate state to the object and stop transitioning.
      CommitStateToJsonObject(json_object, map, &properties);

      JSObject::DefinePropertyOrElementIgnoreAttributes(json_object, key, value)
          .Check();
    } while (transitioning && MatchSkipWhiteSpace(`,`));

    // If we transitioned until the very end, transition the map now.
    if (transitioning) {
      CommitStateToJsonObject(json_object, map, &properties);
    } else {
      while (MatchSkipWhiteSpace(`,`)) {
        HandleScope local_scope(isolate());
        if (c0_ != `"`) return ReportUnexpectedCharacter();

        int start_position = position_;
        Advance();

        if (IsDecimalDigit(c0_)) {
          ParseElementResult element_result = ParseElement(json_object);
          if (element_result == kNullHandle) return Handle<Object>::null();
          if (element_result == kElementFound) continue;
        }
        // Not an index, fallback to the slow path.

        position_ = start_position;
#ifdef DEBUG
        c0_ = `"`;
#endif

        Handle<String> key;
        Handle<Object> value;

        key = ParseJsonInternalizedString();
        if (key.is_null() || c0_ != `:`) return ReportUnexpectedCharacter();

        AdvanceSkipWhitespace();
        value = ParseJsonValue();
        if (value.is_null()) return ReportUnexpectedCharacter();

        JSObject::DefinePropertyOrElementIgnoreAttributes(json_object, key,
                                                          value)
            .Check();
      }
    }

    if (c0_ != `}`) {
      return ReportUnexpectedCharacter();
    }
  }
  AdvanceSkipWhitespace();
  return scope.CloseAndEscape(json_object);
}

ParseJsonObject
複製程式碼

我的方法重寫

假設如果瀏覽器底層沒有支援這些方法,我們該如何底層用js封裝一個函式呢?可以參考下我的一個案例。(僅供參考學習)

parse方法用js實現:codepen案例,待完善

參考文件

相關文章