当JSON.parse“遇上”非键值对

4,431 阅读7分钟

前言

在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案例,待完善

参考文档