JavaScript框架设计
上QQ阅读APP看书,第一时间看更新

5.3 es5属性描述符对OO库的冲击

es5最瞩目的升级是为对象引入属性描述符,让我们对属性有了更精细的控制,如这个属性是否可以修改,是否可以在 for in 循环中枚举出来,是否可以删除。这些新增的 API 都集中定义在Object下,基本上除了Object.keys这个方法外,其他新API,旧版本IE都无法模拟。由于是新API,没有什么书介绍,我在这里就稍微讲解一下。

Object下总供添加以下几种新方法。

· Object.keys

· Object.getOwnPropertyNames

· Object.getPrototypeOf

· Object.defineProperty

· Object.defineProperties

· Object.getOwnPropertyDescriptor

· Object.create

· Object.seal

· Object.freeze

· Object.preventExtensions

· Object.isSealed

· Object.isFrozen

· Object.isExtensible

其中除Object.keys外,旧版本IE都无法模拟这些新API 。旧版式的标准浏览器,可以用__proto__实现Object.getPrototypeOf,结合__defineGetter__与__defineSetter__来模拟Object.defineProperty。

Object.keys用于收集当前对象的可遍历属性(不包括原型链上的),以数组形式返回。这个我在之章的章节已经给出兼容函数。

Object.getOwnPropertyNames 用于收集当前对象不可遍历属性与可遍历属性(不包括原型链上),以数组形式返回。

      var obj = {
        aa: 1,
        toString: function() {
          return "1"
        }
      }
      if(Object.defineProperty && Object.seal) {
        Object.defineProperty(obj, "name", {
          value: 2
        })
      }
      console.log(Object.getOwnPropertyNames(obj));//["aa", "toString", "name"]
      console.log(Object.keys(obj));//["aa", "toString"]
      function fn(aa, bb) {};
      console.log(Object.getOwnPropertyNames(fn));//["prototype","length","name","arguments
      ","caller"]
      console.log(Object.keys(fn));//[]
      var reg = /\w{2,}/i
      console.log(Object.getOwnPropertyNames(reg));//["lastIndex","source","global","ignore
      Case","mnltiline","sticky"]
      console.log(Object.keys(reg));//[]

Object.getPrototypeOf返回参数对象的内部属性[[Prototype]],它在标准浏览器中一直使用一个私有属性__proto__获取(IE9、IE10、Opera 没有)。需要补兖一下,Object 的新 API(除了Object.create 外)有个统一的规定,要求第一个参数不能为数字、字符串、布尔、null、undefined这五种的字面量,否则抛出一个TypeError异常。

      console.log(Object.getPrototypeOf(function() {}) == Function.prototype); //true
      console.log(Object.getPrototypeOf({}) === Object.prototype); //true

Object.defineProperty暴露了属性描述的接口,之前许多内建属性都是由JavaScript引擎在水下操作。如for in循环为何不能遍历出函数的arguments、length、name等属性名,delete window.a为何返回false,这些现象终于有个解释。它一共涉及六个可组合的配置项:是否可重写writable,当前值 value,读取时内部调用的函数 set,写入时内部调用函数get,是否可以遍历 enumerable,是否可让人家再次改动这些配置项configurable。比如我们随便写个对象:

      var obj = { x : 1 };

有了属性描述符,我们就清楚它在底下做的更多细节,它相当于es5的这个创建对象的式子:

      var obj = Object.create(Object.prototype,
        { x : {
          value : 1,
          writable : true,
          enumerable : true,
          configurable : true
        }}
      )

如果对比es3与es5,就很快明白,曾经的[[ReadOnly]]、[[DontEnum]]、[[DontDelete]]改换成[[Writable]]、[[Enumerable]]、[[Configurable]]了。因此 configurable 还有兼顾能否删除的职能。

这六个配置项将原有的本地属性拆分为两组,数据属性与访问器属性。我们之前的方法可以像数据属性那样定义。

es3时代,我们的自定义类的属性可以统统看作是数据属性。

像DOM中的元素节点的innerHTML、innerText、cssText,数组的length则可归为访问器属性,对它们赋值时不是单纯的赋值,还会引发元素其他功能的触发,而取值也不一定直接返回我们之前给予的值。

数据属性拥有1、2、5、6这四个配置项,访问器属性拥有3、4、5、6这四个配置项。如果你设置了value与writable,就不能设置set、get,反之亦然。如果没有设置,第2、3、4项默认为false,第1、5、6项默认为false。

      var obj = {};
      Object.defineProperty(obj, "a", {
        value: 37,
        writable: true,
        enumerable: true,
        configurable: true
      });
      console.log(obj.a);//37
      obj.a = 40;
      console.log(obj.a);//40
      var name = "xxx"
      for(var i in obj){
          name = i
      }
      console.log(name);//a
      Object.defineProperty(obj, "a", {
        value: 55,
        writable: false,
        enumerable: false,
        configurable: true
      });
      console.log(obj.a);//55
      obj.a = 50;
      console.log(obj.a);//55
      name = "b";
      for(var i in obj){
          name = i
      }
      console.log(name);//b
      var value = "RubyLouvre";
      Object.defineProperty(obj, "b", {
        set: function(a){
          value = a;
        },
        get: function(){
          return value + "!"
        }
      });
      console.log(obj.b);//RubyLouvre!
      obj.b = "bbb";
      console.log(obj.b);//bbb!
      var obj = Object.defineProperty( {} , 'a', {
        value: "aaa"
      });
      delete obj.a;//configurable默认为false,此属性不能删除
      console.log(obj.a);//aaa
        但这东西各浏览器也有差异,只怪ecmascript总爱作事后孔明。
      var arr = [];
      //添加一个属性,但由于是数字字面量,它又会作为数组的第一个元素
      Object.defineProperty(arr, '0', {value : "零"});
      Object.defineProperty(arr, 'length', {value : 10});
      //删除第一个元素,但由于length的writable在上面被我们设置为false(不写默认为false),因此改不了。
      arr.length = 0 ;
      alert([arr.length, arr[0]]);//正确应该输出“1,零”
      //IE9、IE10:“1,零”
      //Firefox4~Firefox19:抛内部错误,说当前不支持定义length属性
      //Safari5.0.1:“0, ”,第二值应该是 undefined,说明它忽略了 writable 为 false 的默认设置,让
      arr.length把第一个元素删掉了
      //Chrome14-:“0,零”,估计后面的“零”是作为属性打印出来,chrome24与标准保持一致

此外defineProperty的第三个参数配置对象好像没有使用hasOwnProperty进行取值,导致一旦Object.prototype被污染,就很容易程序崩溃。这情况好像所有现代浏览器都踩坑了。

      Object.prototype.set = undefined
      var obj = {};
      Object.defineProperty(obj, "aaa", { value: "OK" });
      //TypeError: property descriptor's getter field is neither undefined nor a function

或者:

      Object.prototype.get = function(){};
      var obj = {};
      Object.defineProperty(obj,  "aaa", { value: "OK" });
      //TypeError: property descriptors must not specify a value or be writable when a getter
      or setter has been specified

如果真的碰巧让你撞上这事,唯有自力更生了。

      function hasOwn(obj, key) {
        return Object.prototype.hasOwnProperty.call(obj, key);
      }
      function defineProperty(obj, key, desc) {
      //创建一个纯空对象,不继承Object.prototype,跳过那些粗糙的for in遍历BUG
        var d = Object.create(null);
        d.configurable = hasOwn(desc,"configurable");
        d.enumerable = hasOwn(desc, "enumrable");
        if (hasOwn(desc, "value")) {
          d.writable = hasOwn(desc, "writable")
          d.value = desc.value;
        } else {
          d.get = hasOwn(desc, "get") ? desc.get : undefined;
          d.set = hasOwn(desc, "set") ? desc.set : undefined;
        }
        return Object.defineProperty(obj, key, d);
      }
      var obj = {};
      defineProperty(obj, "aaa", { value: "OK" });//save!

在标准浏览器中,如果不支持Object.defineProperty,我们可以勉强模拟它出来。

      if(typeof  Object.defineProperty!=='function'){
          Object.defineProperty = function(obj, prop, desc) {
              if ('value' in desc) {
                  obj[prop] = desc.value;
              }
              if ('get' in desc) {
                  obj.__defineGetter__(prop, desc.get);
              }
              if ('set' in desc) {
                  obj.__defineSetter__(prop, desc.set);
              }
              return obj;
          };
      }

Object.defineProperties就是Object.defineProperty的加强版,它能一下子处理多个属性。因此如果你能模拟Object.defineProperty, 它就不是问题。

      if(typeof  Object.defineProperties!=='function'){
          Object.defineProperties = function(obj, descs) {
              for (var prop in descs) {
                  if (descs.hasOwnProperty(prop)) {
                      Object.defineProperty(obj, prop, descs[prop]);
                  }
              }
              return obj;
          };
      }

使用示例:

      var obj = {};
      Object.defineProperties(obj, {
        "value": {
          value: true,
          writable: false
        },
        "name": {
          value: "John",
          writable: false
        }
      });
      var a = 1;
      for(var p in obj) {
        a = p;
      }
      console.log(a);//1

Object.getOwnPropertyDescriptor用于获得某对象的本地属性的配置对象,其中

configurable,enumerable肯定包含其中,视情况再包含value,writable或set,get。

      var obj = {},
        value = 0
        Object.defineProperty(obj, "aaa", {
          set: function(a) {
              value = a;
          },
          get: function() {
              return value
          }
        });
      //一个包含set, get, configurable, enumerable的对象
      console.log(Object.getOwnPropertyDescriptor(obj, "aaa"));
      console.log(typeof obj.aaa);//number
      console.log(obj.hasOwnProperty("aaa"));//true
      (function() {
      //一个包含value, writable, configurable, enumerable的对象
        console.log(Object.getOwnPropertyDescriptor(arguments, "length"))
      })(1, 2, 3);

由于属性在现代浏览器划分两阵营了,如果我们想把一个对象的成员赋给另一个对象,原来的mixin就会捉襟见肘。这时getOwnPropertyDescriptor就大派用场了。

      function mixin(receiver, supplier) {
          if (Object.getOwnPropertyDescriptor) {
              Object.keys(supplier).forEach(function(property) {
                  Object.defineProperty(receiver,  property,  Object.getOwnPropertyDescriptor
      (supplier, property));
              });
          } else {
              for (var property in supplier) {
                  if (supplier.hasOwnProperty(property)) {
                      receiver[property] = supplier[property];
                  }
              }
          }
      }

Object.create用于创建一个子类的原型,第一个参数为父类的原型,第二个是子类另外要添加的属性的配置对象。如果我们能模拟Object.defineProperties,它也能模拟得到。

      if(typeof  Object.create !== 'function') {
        Object.create = function(prototype, descs) {
          function F() {}
          F.prototype = prototype;
          var obj = new F();
          if(descs != null) {
              Object.defineProperties(obj, descs);
          }
          return obj;
        };
      }

有了它,我们创建子类会更方便些。

      function Animal(name) {
        this.name = name
      }
      Animal.prototype.getName = function() {
        return this.name;
      }
      function Dog(name, age) {
        Animal.call(this, name);
        this.age = age;
      }
      Dog.prototype = Object.create(Animal.prototype, {
        getAge: {
          value: function() {
              return this.age;
          }
        },
        setAge: {
          value: function(age) {
              this.age = age;
          }
        }
      });
      var dog = new Dog("dog", 4);
      console.log(dog.name);//dog
      dog.setAge(6);
      console.log(dog.getAge());//6

Object.create(null)还能生成一种叫纯空对象的东西,没有toString, valueOf,什么也没有,空空荡荡,在Object.prototype被污染或极需节省内存的情况下有用。外国还是有人设法在旧版本IE中模拟出来。

      //https://github.com/kriskowal/es5-shim/blob/master/es5-sham.js
      var createEmpty;
      var supportsProto = Object.prototype.__proto__ === null;
      if(supportsProto || typeof document == 'undefined') {
        createEmpty = function() {
          return {
              "__proto__": null
          };
        };
      } else {
        // 因为我们无法让一个对象继承自一个不存在的东西,它最后肯定要回溯到
        //Object.prototype,那么我们就从一个新的执行环境中盗取一个Object.prototype,
        //把它的所有原型属性都砍掉,这样它的实例就既没有特殊属性,也没有什么原型属性
        //(只剩下一个__proto__,值为null)
        createEmpty = (function() {
          var iframe = document.createElement('iframe');
          var parent = document.body || document.documentElement;
          iframe.style.display = 'none';
          parent.appendChild(iframe);
          iframe.src = 'javascript:';
          var empty = iframe.contentWindow.Object.prototype;
          parent.removeChild(iframe);
          iframe = null;
          delete empty.constructor;
          delete empty.hasOwnProperty;
          delete empty.propertyIsEnumerable;
          delete empty.isPrototypeOf;
          delete empty.toLocaleString;
          delete empty.toString;
          delete empty.valueOf;
          empty.__proto__ = null;
          function Empty() {}
          Empty.prototype = empty;
          return function() {
              return new Empty();
          };
        })();
      }

但是在firebug下还是能区分出它们的不同。

Object.preventExtensions,它是三个封锁对象修改的API中程度最轻的那个,就是阻止添加本地属性,不过如果本地属性被删除了,也无法再加回来。以前JavaScript对象的属性都是随意添加、删除、修改其值,如果它的原型改动,我们访问它还会有“意外之喜”。

      var a = {
        aa: "aa"
      };
      Object.preventExtensions(a)
      a.bb = 2;
      console.log(a.bb);        //undefined 添加本地属性失败
      a.aa = 3;
      console.log(a.aa);        //3  允许它修改原有属性
      delete a.aa;
      console.log(a.aa);        //undefined 但允许它删除已有属性
      Object.prototype.ccc = 4;
      console.log(a.ccc);       //4  不能阻止它增添原型属性
      a.aa = 5;
      console.log(a.bb);        //undefined ,不吃回头草,估计里面是以白名单方式实现的

Object.seal比Object.preventExtensions更过分,它不准删除已有的本地属性。内部实现就是遍历一下,把每个本地属性的configurable改为false。

      var a = {
        aa: "aa"
      };
      Object.seal(a)
      a.bb = 2;
      console.log(a.bb);        //undefined 添加本地属性失败
      a.aa = 3;
      console.log(a.aa);        //3   允许它修改已有属性
      delete a.aa;
      console.log(a.aa);        //3  但不允许它删除已有属性

Object.freeze无疑是最专制的(因此有人说过程式程序很专制,OO程序则自由些郑晖著的《冒号课堂——编程范式与OOP思想》第41页。,显然道格拉斯主导的ecma262v5想把JavaScript引向前者),它连原有本地属性也不让修改了。内部实现就是遍历一下,把每个本地属性的writable也改为false。

      var a = {
        aa: "aa"
      };
      Object.freeze(a)
      a.bb = 2;
      console.log(a.bb);        //undefined 添加本地属性失败
      a.aa = 3;
      console.log(a.aa);        //aa   允许它修改已有属性
      delete a.aa;
      console.log(a.aa);        //aa  但不允许它删除已有属性
      Object.isExtensible(object);
      Object.isSealed(object);
      Object.isFrozen(object);

判定一个对象是否被锁定。锁定,意味着无法扩展。如果一个对象被冻结了,它肯定被锁定,也肯定无法扩展新本地属性了。

后面利用这些新API开发一个新工厂出来。

      (function(global) {
        function fixDescriptor(item, definition, prop) {
          // 如果以标准defineProperty的第三个参数的形式定义扩展包
          if(isPainObject(item)) {
              if(!('enumerable' in item)) {
              item.enumerable = true;
              }
          } else { //如果是以es3那样普通对象定义扩展包
              item = definition[prop] = {
              value: item,
              enumerable: true,
              writable: true
              };
          }
          return item;
        }
        function isPainObject(item) {
          if(typeof item === 'object' && item !== null) {
              var a = Object.getPrototypeOf(item);
              return a === Object.prototype || a === null;
          }
          return false;
        }
        var funNames = Object.getOwnPropertyNames(Function);
        global.Class = {
          create: function(superclass, definition) {
              if(arguments.length === 1) {
              definition = superclass;
              superclass = Object;
              }
              if(typeof superclass !== "function") {
              throw new Error("superclass must be a function");
              }
              var _super = superclass.prototype;
              var statics = definition.statics;
              delete definition.statics;
              //重新调整definition
              Object.keys(definition).forEach(function(prop) {
              var item = fixDescriptor(definition[prop], definition, prop);
              if(typeof item.value === "function" && typeof _super[prop] === "function") {
                var __super = function() { //创建方法链
                      return _super[prop].apply(this, arguments);
                  };
                var __superApply = function(args) {
                      return _super[prop].apply(this, args);
                  };
                var fn = item.value;
                item.value = function() {
                  var t1 = this._super;
                  var t2 = this._superApply;
                  this._super = __super;
                  this._superApply = __superApply;
                  var ret = fn.apply(this, arguments);
                  this._super = t1;
                  this._superApply = t2;
                  return ret;
                }
              }
              });
              var Base = function() {
                this.init.apply(this, arguments);
              };
              Base.prototype = Object.create(_super, definition);
              Base.prototype.constructor = Base;
              //确保一定存在init方法
              if(typeof Base.prototype.init !== "function") {
              Base.prototype.init = function() {
                superclass.apply(this, arguments);
              };
              }
              if(Object !== superclass) { //继承父类的类成员
              Object.getOwnPropertyNames(superclass).forEach(function(name) {
                if(funNames.indexOf(name) === -1) {
                  Object.defineProperty(Base,name,Object.getOwnPropertyDescriptor(superclass, name));
                }
              });
              }
              if(isPainObject(statics)) { //添加自身的类成员
              Object.keys(statics).forEach(function(name) {
                if(funNames.indexOf(name) === -1) {
                  Object.defineProperty(Base,name,fixDescriptor(statics[name], statics, name));
                }
              });
              }
              return Object.freeze(Base);
          }
        }
      })(this)

fixDescriptor方法用于修正扩展包的值的格式,让大家只描述最重要的部分。

isPainObject用于判定目标是不是纯净的JavaScript对象,且不是其他自定义类的实例。用法与Prototype.js的Class.create一样,并参照jQuery UI提供了完美的方法链与静态成员的继承。

      var Dog = Class.create(Animal, {
        statics: {
          Name: "Dog",
          type: "shepherd"
        },
        init: function(name, age) {
          this._super(name);
          //或者 this._superApply(arguments)
          this.age = age;
        },
        getName: function() {
          return this._super() + "!"
        },
        getAge: function() {
          return this.age
        },
        setAge: function(age) {
          this.age = age
        }
      });
      var dog = new Dog("dog", 12)
      console.log(dog.getName()); //dog!
      console.log(dog.getAge()); //12
      console.log(dog instanceof Animal); //true
      console.log(Dog.Name); //Dog

总结,es5对JavaScript的对象产生深刻影响。Object.create让原型继承更方便了,但在增添子类的专有原型成员或类成员时,如果它们的属性的enumerable 为false,单纯的for in循环已经不管用了,我们就要用到 Object.getOwnPropertyNames。另外,访问器属性的复制只有通过Object.getOwnPropertyDescriptor与Object.defineProperty才能完成。