随着web应用的发展,直接操作dom的应用已渐行渐远,取而代之的是时下越来越流行的MVVM框架,dom操作几乎绝迹,这里面自然是框架底层封装的结果。MVVM框架的双向数据绑定使开发效率大大提高;然后在实现这些双向数据绑定时,使用ES7原生的Object.observe方法则是完美解决方案,但是遗憾的是该方法目前还是ES7的草案阶段,各浏览器还不支持,目前chrome 36+支持该方法。
既然Object.observe不被支持,但是其替代方案是ECMAScript 262v5带来的新东西Object.defineProperty和Object.defineProperties方法也可以很优雅的实现双向数据绑定。主要是利用在定义一个对象的属性时,可以设置其set和get方法,用于对对象的该属性进行设置或者读取时的“拦截”操作,类似于"钩子",即在一定的情况下执行特定的逻辑;目前使用该方案实现的MVVM框架有avalon、vue等,但是目前比较流行的angularjs框架缺使用为人诟病的"脏检查"机制实现,这也是其性能遭人吐槽低的一个原因,据说目前angularjs2会改造这一实现机制。
好了,言归正传,接下来看看defineProperty的使用
1、用法
语法: Object.defineProperty(obj, attr, descriptor)
从语法可以看出,该方法接受三个参数:
obj 为属性attr所属的对象;
attr 为obj对象新定义或者修改的属性名;
descriptor 为该对象属性的描述符,其中其有6个配置项:
- value: 属性的值,默认undefined
- configurable: 默认为false,true表示当前属性是否可以被改变或者删除,其中”改变“是指属性的descriptor的配置项configurable、enumerable和writable的修改
- enumerable:默认为false,true表示当前属性能否被for...in或者Objectk.keys语句枚举
- writable:默认为false,true表示当前属性的值可以被赋值重写
- get:默认undefined,获取目标属性时执行的回调方法,该函数的返回值作为该属性的值
- set:默认undefined,目标属性的值被重写时执行的回调
从上面的用法可以知道:可以通过设置get和set方法对属性的读取和修改进行拦截,通过将实现数据和视图同步的逻辑置于这两个方法中,从而实现数据变更视图也可以跟着同步,反之则需要配合事件了;
var obj = {}; Object.defineProperty(obj, 'name', { "value": "wonyun", "configurable": false
});
Object.getOwnPropertyDescriptor(obj, 'name') //Object {value: "wonyun", writable: false, enumerable: false, configurable: false}
2、注意事项
上面介绍了Object.defineProperty的用法,但是其使用还是有很多注意的地方,一不留心就会出错。
- 不允许同一个属性存在两个及以上的存取访问器配置,所以writable或者value不能与get/set同时配置,否则报错。
var obj = {}; Object.defineProperty(obj, 'name', { "writable": false, "get": function(){}, "set": function(){} });
执行上面代码,会发现控制台报错”Uncaught TypeError: Invalid property. A property cannot both have accessors and be writable or have a value“,
- configurable和writable设置为false的区别是:前者不能在对目标属性的configurable、enumerable和writable进行修改,修改也是无效;
后者不能对目标属性重写赋值,赋值也无效; - 在使用Obejct.defineProperty方法定义对象属性时,若是属性描述符没有配置的,configurable、enumerable和writable默认为false,value默认为undefined,而get/set则不会配置;而在使用非Object.defineProperty方法定义对象属性,其configurable、enumerable和writable均为true,value为赋的值,get/setb不会配置;
3、兼容性
就如本文开头说的,Obejct.defineProperty方法ECMAScript 262v5带来的新特性,支持ES5的浏览器才可使用该方法;当然这就涉及到其使用的浏览器兼容性问题;目前非IE浏览器的标准浏览器都支持该特性了,当然在天朝,你懂得还有些不少ie6-8的用户,这些低版本的IE浏览器是不支持Object.defineProperty的。话说回来,随着微软自己放弃维护这些低版本的ie浏览器,ie6-8的使用占比是越来越低,这也是当前一些前端框架不在支持ie6、7、8浏览器的一大原因,像angularjs、vuejs都不支持低版本ie浏览器,看来低版本的ie6也快走到尽头。
Feature | Firefox (Gecko) | Chrome | Internet Explorer | Opera | Safari |
---|---|---|---|---|---|
Basic support | 4.0 | 5 | 9 | 11.60 | 5.1 |
4、Object.defineProperties(obj, descriptors)
Object.defineProperty方法只能定义单个对象的属性,当然也就有批量定义对象属性的方法,Object.defineProperties方法就是干这个的:在一个对象添加或者修改一个或者多个属性。它与Object.defineProperty不同的地方是它只有两个参数,其中第二参数为一个配置对象,其键值为对象的属性名,其值为属性名的配置项,如:
var obj = {} Object.defineProperties(obj, { name: { value: "wonyun", writable: true, enumerable: true, configurable: true }, age: { value: 20, writable: true } })
5、Object.defineProperty在avalon中的运用
avalon中的利用Object.defineProperty,为其定义一个accessor来同步视图,每次改变或者读取vmodel对象中的属性时候,会调用accessor方法用于同步视图和其他操作。
1 function modelFactory($scope, $special, $model) { 2 3 //省略初始化代码 4 ... 5 ... 6 var $vmodel = {} //要返回的对象, 它在IE6-8下可能被偷龙转凤 7 $model = $model || {} //vmodels.$model属性 8 var $events = {} //vmodel.$events属性 9 var watchedProperties = {} //监控属性 10 var computedProperties = [] //计算属性 11 for (var i in $scope) { 12 (function(name, val) { 13 $model[name] = val 14 ... //省略其他无关代码 15 16 //总共产生三种accessor 17 var accessor 18 var valueType = avalon.type(val) 19 $events[name] = [] 20 //总共产生三种accessor 21 if (valueType === "object" && isFunction(val.get) && Object.keys(val).length <= 2) { 22 var setter = val.set 23 var getter = val.get 24 //第1种对应计算属性, 因变量,通过其他监控属性触发其改变 25 accessor = function(newValue) { 26 var $events = $vmodel.$events 27 var oldValue = $model[name] 28 if (arguments.length) { 29 if (stopRepeatAssign) { 30 return 31 } 32 if (isFunction(setter)) { 33 var backup = $events[name] 34 $events[name] = [] //清空回调,防止内部冒泡而触发多次$fire 35 setter.call($vmodel, newValue) 36 $events[name] = backup 37 } 38 } else { 39 if (avalon.openComputedCollect) { // 收集视图刷新函数 40 collectSubscribers($events[name]) 41 } 42 } 43 newValue = $model[name] = getter.call($vmodel) //同步$model 44 if (!isEqual(oldValue, newValue)) { 45 withProxyCount && updateWithProxy($vmodel.$id, name, newValue) //同步循环绑定中的代理VM 46 notifySubscribers($events[name]) //同步视图 47 safeFire($vmodel, name, newValue, oldValue) //触发$watch回调 48 } 49 return newValue 50 } 51 52 .... // 省略无关代码 53 54 } else if (rcomplexType.test(valueType)) { 55 //第2种对应子ViewModel或监控数组 56 accessor = function(newValue) { 57 var childVmodel = accessor.child 58 var oldValue = $model[name] 59 if (arguments.length) { 60 if (stopRepeatAssign) { 61 return 62 } 63 if (!isEqual(oldValue, newValue)) { 64 childVmodel = accessor.child = updateChild($vmodel, name, newValue, valueType) 65 newValue = $model[name] = childVmodel.$model //同步$model 66 var fn = rebindings[childVmodel.$id] 67 fn && fn() //同步视图 68 safeFire($vmodel, name, newValue, oldValue) //触发$watch回调 69 } 70 } else { 71 collectSubscribers($events[name]) //收集视图函数 72 return childVmodel 73 } 74 } 75 var childVmodel = accessor.child = modelFactory(val, 0, $model[name]) 76 childVmodel.$events[subscribers] = $events[name] 77 } else { 78 //第3种对应简单的数据类型,自变量,监控属性 79 accessor = function(newValue) { 80 var oldValue = $model[name] 81 if (arguments.length) { 82 if (!isEqual(oldValue, newValue)) { 83 $model[name] = newValue //同步$model 84 withProxyCount && updateWithProxy($vmodel.$id, name, newValue) //同步代理VM 85 notifySubscribers($events[name]) //同步视图 86 safeFire($vmodel, name, newValue, oldValue) //触发$watch回调 87 } 88 } else { 89 collectSubscribers($events[name]) 90 return oldValue 91 } 92 } 93 } 94 watchedProperties[name] = accessor 95 })(i, $scope[i]) 96 } 97 98 ... //省略无关代码 99 100 $vmodel = defineProperties($vmodel, descriptorFactory(watchedProperties), $scope) //生成一个空的ViewModel 101 102 ... //省略无关代码 103 return $vmodel 104 } 105 106 //生成avalon的vmodel的Object.defineProperty方法 107 108 var descriptorFactory = W3C ? function(obj) { 109 var descriptors = {} 110 for (var i in obj) { 111 descriptors[i] = { 112 get: obj[i], 113 set: obj[i], 114 enumerable: true, 115 configurable: true 116 } 117 } 118 return descriptors 119 } : function(a) { 120 return a 121 }
参考文献:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- http://segmentfault.com/a/1190000003109392