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程序则自由些,显然道格拉斯主导的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才能完成。