前端

关于ECMAScript JSON实体的若干缺陷与解决方案

2019-01-05  本文已影响27人  Lyrieek

实现实体在OOP语言中通常要通过建立类来完成,类有完整而系统的内存分配,复用性,构造性,带来了极大的便利,而ECMAScript的本质是FP,并不追求类的诸多效果,通常处于低复用,无构造条件,追求便捷易读的应用环境下,但ECMAScript做页面渲染,数据交互传输时少不了实体,这时ECMAScript通常选择JSON来建立实体,如下

// e.g. 1
var foo = {
  bar: 'value'
}

e.g. 1 几乎无处不在,语法简洁,易读性高,相比之下OOP语言以类建立实体的语言就稍显繁琐,例如c#

// e.g. c#
public class Foo
{
  private string bar
  {
    get; set;
  }
}

以代码冗长而闻名的Java:

// e.g. java
public class FooObject {

  private String bar;

  public void setBar(String bar){
    this.bar = bar;
  }

  public String getBar(){
    return bar;
  }
}

看上去OOP十分繁琐,其实不然,e.g. c#e.g. java都是通过对象建立实体的,与之对应的ECMAScript实际上应该是这样的:

// e.g. 2
var Foo = function(){

  var _bar = "value";

  this.setBar = function(bar){
    _bar = bar;
  }

  this.getBar = function(){
    return _bar;
  }

}

建立对象无法避免繁琐,同时,e.g. c#e.g. java有一点是e.g. 2做不到的,即Foo的作用域问题,ECMAScript中,Foo的作用域其实难以控制,只能将其写在内部作用域中,使其与外部作用域隔离,达到外部不可见的效果,而java有四种可访问级别,c#更是有七种不同的可访问级别,这也是OOP在对象隔离上的优势,而EMCAScript没有实现真正意义上的class,在ES6中加入的class关键字只是prototype的语法糖,其衍生的转译语言CoffeeScript,TypeScript同样没有实现类作用域修饰。

通常e.g. 1的应用范围更广,而e.g. 2只作为组件框架出现,那么相比较,e.g. 1的简洁牺牲了什么?或者说e.g. 2多出的代码意义何在?

首先,e.g. 1的Foo无法复用,赋值只会移交指针,在e.g. 1下执行:

var clone = foo;
clone.bar = "clone";
console.log(foo.bar);
// console
clone

e.g. 2不存在这种问题,每次new Foo()都会得到不同的对象,e.g. 1可以如下方式clone JSON

var clone = Object.create(foo);
clone.bar = "clone";
console.log(foo.bar);
// console
value

成功隔离fooclone,但如果要用clone调用hasOwnProperty函数,就会出现问题,Object.create(foo)得到JSON的与foo并不相同

{
  bar: "value",
  __proto__: Object
}
{
  __proto__: {
    bar: "value",
    __proto__: Object
  }
}

这会导致hasOwnProperty无法判断,出现如下问题

var clone = Object.create(foo);
console.log(clone.bar);
console.log(clone.hasOwnProperty('bar'));
clone.bar = "clone";
console.log(clone.hasOwnProperty('bar'));
// console
value
false
true

Object.create将对象拷贝到__proto__中,导致hasOwnProperty检测不到拷贝来的对象,那么可否这样呢?

clone = Object.create(foo);
clone=clone.__proto__

看起来JSON相同了,clone的结构如下

{
  bar: "value",
  __proto__: Object
}

接着判断clonefoo是否隔离

clone.bar="clone"
console.log(foo.bar);
// console
clone

clone的指针仍然指向foo,不可行,只能以循环赋值的方式解决

var clone = {};
for(item in foo){
  clone[item] = foo[item];
}

这是成功的,看接下来的问题。

bar外部可见(内部字段访问权限无法控制,破坏字段安全性),无法控制get/set(无法实现只读/只写,无法封装字段逻辑)

依次解决以上问题。

bar外部可见有3种方法,最常见的是一种常见的编码规范约定,使用_开头的字段标示私有字段:

var foo = {
  _bar: 'value'
}

但这只是一种规范约定,很容易被打破,无法标识只读只写,当然ECMAScript 也有get & set访问器,可以用来实现只读

var foo = {
  get bar(){return 12;}
}
console.log(foo.bar);
foo.bar = 10;
console.log(foo.bar);

返回

12
12

但是使用get & set无法实现只写功能,尝试:

var foo = {
  set bar(param){
    this.bar = param;
  }
}
foo.bar = "value";

将会立即抛出错误Maximum call stack size exceeded,因为对this.bar赋值一样会触发set bar函数,导致递归调用而陷入死循环,get同理

var foo = {
  get bar(){
    return this.bar;
  }
}
foo.bar;

同上触发栈溢出,所以通常情况,会结合_命名规范与get&set对字段进行管理

//e.g. 2
var foo = {
  _bar: "value",
  get bar(){
    return this._bar;
  },
  set bar(param){
    this._bar = param;
  }
}

这是一种常见的解决方案,当然无法避免直接对foo._bar的操作,仍然不能保证安全性与内部封闭,不过这也是e.g. 1的最优解了。

当然这样不停的打补丁不如最开始就做出正确的选择,良好的程序结构设计需要有宏观的思维与对未来需求复杂变化的预判。

上一篇下一篇

猜你喜欢

热点阅读