几乎全部使用Vue的开发者都知道,Vue的双向绑定是经过Object.defineProperty()实现的,也知道在getter中收集依赖,在setter中通知更新。前端
那么除了知道getter和setter以外,Object.defineProperty()还有哪些值得咱们去注意的地方呢?是否是有不少细节的东西不懂呢?git
你可能会说,除了getter和setter以外,Object.defineProperty()还有value,writable,enumerable,configurable。github
那么问题来了?segmentfault
data descriptor、accessor descriptor、shared descriptor
是什么?若是看了上面这些问题一脸懵逼,不要惊慌,咱们先来看一道很是直观易懂的题目:数组
// 实现下面的逻辑 console.log(a+a+a); // 'abc'
题目看完了,带着问题开始阅读下面的内容吧。
若是能耐心看完的话对于我的的前端技术提高会很是大。
往近了说,不出意外上面这些问题所有能够迎刃而解,对于a+a+a题目的题解也会理解更加透彻。
往远了说,能够去看懂Vue源码相关的实现,以及看懂任何使用到Object.defineProperty()这个API的库的源码实现,甚至最后本身写个小轮子。浏览器
语法安全
Object.defineProperty()概览微信
descriptor key概览前端工程师
- 共享descriptor key概览 - data descriptor key概览 - accessor descriptor key概览
三个很基础可是很好的例子函数
- 默认descriptor:不可写,不可枚举,不可配置 - 重用同一对象记忆上一次的value值 - 冻结Object.prototype
Object.defineProperty()详解
修改一个property
Enumerable attribute
console.log(a+a+a); // 'abc'
题解
a...z
a...z
静态方法Object.defineProperty()会直接在一个对象上定义一个新的属性,或者修改对象上已经存在的属性,而后返回这个对象。
const obj = {}; Object.defineProperty(obj, 'prop', { value: 42, writable: true }); console.log(obj); // {prop: 42} obj.prop = 43; // {prop: 43}
Object.defineProperty(obj, prop, descriptor)
返回传递进函数的对象。
descriptor key概览
三个很基础可是很好的例子
对象的属性descriptor描述符主要有两种:data descriptor和accessor descriptor。
数据描述符指的是value,writable,它多是可写的,也多是不可写的。
权限描述符指的是经过getter-setter函数get(),set()
对property的描述。
下面的代码会报错的缘由破案了:只能是data,accessor 之一。
Object.defineProperty({this, 'a', { value: 'a', // data descriptor get(){ // access descriptor } }) // `Invalid property descriptor.Cannot both specify accessors and a value or writable attribue.`
data accessor特有的key为value和writable。
accessor descriptor特有的key为get和set。
// 典型的data descriptor Object.defineProperty({this, 'a', { value: 'a', writable: false }) // 典型的accessor descriptor Object.defineProperty({this, 'a', { get(){ ... } set(){ ... } })
默认状况下是经过Object.defineProperty()定义属性的。
delete obj.o
失效为何configurable设置为false时要这样设计?
这是由于get(), set(), enumerable, configurable是权限相关的属性,为了不引发没必要要的bug。
不少库的做者不容许本身修改这个属性,让它保持在一种可控的状态,从而代码按照本身的预期去运行。并且这样作也更加安全。
为确保保留了这些默认值:
var obj = {}; var descriptor = Object.create(null); // no inherited properties descriptor.value = 'static'; // not enumerable, not configurable, not writable as defaults Object.defineProperty(obj, 'key', descriptor); // being explicit Object.defineProperty(obj, 'key', { enumerable: false, configurable: false, writable: false, value: 'static' });
function withValue(value) { var d = withValue.d || ( // 记忆住上一次的值 withValue.d = { enumerable: false, writable: false, configurable: false, value: value } ); // 避免重复赋值 if (d.value !== value) d.value = value; return d; } Object.defineProperty(obj, 'key', withValue('static'));
Object.freeze(Object.prototype)
属性若是在对象上不存在的话,Object.defineProperty()会建立一个新的属性。
能够省略不少描述符中字段,而且输入这些字段的默认值。
// 建立对象 var o = {}; // 定义属性a而且传入data descriptor Object.defineProperty(o, 'a', { value: 37, writable: true, enumerable: true, configurable: true, }) // 定义属性b而且传入accessor descriptor // 伪造value(好处是更细粒度的value控制):外部变量和get() // 伪造writable(好处是更细粒度的writable控制):外部变量和set() // 在这个例子中,o.b的值与bValue作了强关联。bValue是什么值,o.b就是什么值。除非o.b被从新定义 var bValue = 38; Object.defineProperty(o, 'b', { get() { return bValue }, set(newValue) { bValue = newVlaue }, enumerable: true, configurable: true, }) // 不能够同时混合定义二者 Object.defineProperty(o, 'conflict', { value: 'a', get() { return 'a' } }) // 报错:Cannot both specify accessors and a value or writable // 从新解读报错:Cannot both specify accessors descriptor and data descriptor(a value or writable)
若是旧的descriptor有configurable属性,而且设置为false,意思是”不可配置“。
当writable设置为false时,属性是不可写的,意味着没法从新赋值。
Cannot assign to read only property 'b' of object '#<Object>'
// 非严格模式不会报错,只是赋值失败 var o = {}; Object.defineProperty(o, 'a', { value: 37, writable: false }); console.log(o.a); // logs 37 o.a = 25; // 不会报错 // (只会在strict mode报错,或者值没改变也不会报错) console.log(o.a); // logs 37. 从新赋值没有生效 // 严格模式会报错 // strict mode (function() { 'use strict'; var o = {}; Object.defineProperty(o, 'b', { value: 2, writable: false }); o.b = 3; // 抛出Cannot assign to read only property 'b' of object '#<Object>' return o.b; // 2 }());
obj.propertyIsEnumerable(prop)
检测属性是否可遍历。var o = {}; Object.defineProperty(o, 'a', { value: 1, enumerable: true }); Object.defineProperty(o, 'b', { value: 2, enumerable: false }); Object.defineProperty(o, 'c', { value: 3, // enumerable默认为false }); o.d = 4; // enumerable默认为true Object.defineProperty(o, Symbol.for('e'), { value: 5, enumerable: true }); Object.defineProperty(o, Symbol.for('f'), { value: 6, enumerable: false });
只有'a'和'd'打印了出来。
enumerable为true的都能被解构出来,不包括Symbol。
for (var i in o) { console.log(i); // 'a','d' }
只有'a'和'd'被搜集到。
enumerable为true的都能被解构出来,不包括Symbol。
Object.keys(o); // ['a', 'd']
enumerable为true的都能被解构出来,包括Symbol。
var p = { ...o } p.a // 1 p.b // undefined p.c // undefined p.d // 4 p[Symbol.for('e')] // 5 p[Symbol.for('f')] // undefined
能够用obj.propertyIsEnumerable(prop)检测属性是否可遍历
o.propertyIsEnumerable('a'); // true o.propertyIsEnumerable('b'); // false o.propertyIsEnumerable('c'); // false o.propertyIsEnumerable('d'); // true o.propertyIsEnumerable(Symbol.for('e')); // true o.propertyIsEnumerable(Symbol.for('f')); // false
configurable属性控制属性是否能够被修改(除value和writable外),或者属性被删除。
var o = {}; Object.defineProperty(o, 'a', { get() { return 1; }, configurable: false }); Object.defineProperty(o, 'a', { configurable: true }); // throws a TypeError Object.defineProperty(o, 'a', { enumerable: true }); // throws a TypeError Object.defineProperty(o, 'a', { set() {} }); // throws a TypeError (set初始值为undefined) Object.defineProperty(o, 'a', { get() { return 1; } }); // throws a TypeError // (即便set没有变化) Object.defineProperty(o, 'a', { value: 12 }); // throws a TypeError // ('value' can be changed when 'configurable' is false but not in this case due to 'get' accessor) console.log(o.a); // logs 1 delete o.a; // 不能删除 console.log(o.a); // logs 1
属性的默认值很值得思考一下。
经过点操做符.赋值和经过Object.defineProperty()是有区别的。
两种赋初始值方式的区别以下
经过点操做符定义的属性等价于Object.defineProperty的data descriptor和共享descriptor为true。
var o = {}; o.a = 1; // 等价于 Object.defineProperty(o, 'a', { value: 1, writable: true, configurable: true, enumerable: true });
Object.defineProperty(o, 'a', { value: 1 }); // 等价于 Object.defineProperty(o, 'a', { value: 1, writable: false, configurable: false, enumerable: false });
下面的例子展现了如何实现一个自存档的对象。
当temperature属性设置后,archive数组会打印。
function Archiver() { var temperature = null; var archive = []; Object.defineProperty(this, 'temperature', { get(){ console.log('get!'); return temperature; }, set(value) { temperature = value; archive.push({ val: temperature }); } }); this.getArchive = function(){ return archive; }; } var arc = new Archiver(); arc.temperature; // 'get' arc.temperature = 11; arc.temperature = 13; arc.getArchive(); // [{val: 11}, {vale: 13}]
var pattern = { get() { return 'I always return this string, ' + 'whatever you have assigned'; }, set() { this.myname = 'this is my name string'; } }; function TestDefineSetAndGet() { Object.defineProperty(this, 'myproperty', pattern); } var instance = new TestDefineSetAndGet(); instance.myproperty = 'test'; console.log(instance.myproperty); // I always return this string, whatever you have assigned console.log(instance.myname); // this is my name string
主要为如下3个问题:
这个例子展现了继承带来的问题:
function myclass() { } var value; Object.defineProperty(myclass.prototype, "x", { get() { return value; }, set(x) { value = x; } }); var a = new myclass(); var b = new myclass(); a.x = 1; console.log(b.x); // 1
如何解决这个问题呢?
能够将值存储在另外一个this属性上。这样使用new建立新实例时,能够为本身开辟单独的属性空间。
在get和set方法中,this指向使用、访问、修改属性的对象实例。
function myclass() { } Object.defineProperty(myclass.prototype, "x", { get() { return this._x; }, set(x) { this._x = x; // 用this._x来存储value } }); var a = new myclass(); var b = new myclass(); a.x = 1; console.log(b.x); // 1
下面的例子,点操做符赋值的属性可写,可是继承的myclass.prototype的初始值不会发生更改;不可写的属性不可写。
function myclass() { } myclass.prototype.x = 1; Object.defineProperty(myclass.prototype, "y", { writable: false, value: 1 }); var a = new myclass(); a.x = 2; console.log(a.x); // 2 console.log(myclass.prototype.x); // 1 a.y = 2; // Ignored, throws in strict mode console.log(a.y); // 1 console.log(myclass.prototype.y); // 1
值得分析一波的截图:
Object.getOwnPropertyDescriptor(obj,prop)
使用示例:
var o = {}; Object.defineProperty(o, 'a', { value: 1 }); Object.getOwnPropertyDescriptor(o,'a') // { // configurable: false // enumerable: false // value: 1 // writable: false // }
/* console.log(a + a + a); // 打印'abc' */
a...z
a...z
/** * 解法1: Object.defineProperty() 外部变量 */ let value = "a"; Object.defineProperty(this, "a", { get() { let result = value; if (value === "a") { value = "b"; } else if (value === "b") { value = "c"; } return result; }, }); console.log(a + a + a); /** * 解法1(优化版):Object.defineProperty() 内部变量 */ Object.defineProperty(this, "a", { get() { this._v = this._v || "a"; if (this._v === "a") { this._v = "b"; return "a"; } else if (this._v === "b") { this._v = "c"; return "b"; } else { return this._v; } }, }); console.log(a + a + a); /** * 解法2: Object.prototpye.valueOf() */ let index = 0; let a = { value: "a", valueOf() { return ["a", "b", "c"][index++]; }, }; console.log(a + a + a); /** * 解法3:charCodeAt,charFromCode */ let code = "a".charCodeAt(0); let count = 0; Object.defineProperty(this, "a", { get() { let char = String.fromCharCode(code + count); count++; return char; }, }); console.log(a + a + a); // 'abc' /** * 解法3(优化版一):内部变量this._count和_code */ Object.defineProperty(this, "a", { get() { let _code = "a".charCodeAt(0); this._count = this._count || 0; let char = String.fromCharCode(_code + this._count); this._count++; return char; }, }); console.log(a + a + a); // 'abc' /** * 解法3(优化版二):内部变量this._code */ Object.defineProperty(this, "a", { get() { this._code = this._code || "a".charCodeAt(0); let char = String.fromCharCode(this._code); this._code++; return char; }, }); console.log(a + a + a); // 'abc' /* 题目扩展: 打印`a...z` a+a+a; //'abc' a+a+a+a; //'abcd' */ /** * charCodeAt,charFromCode */ let code = "a".charCodeAt(0); let count = 0; Object.defineProperty(this, "a", { get() { let char = String.fromCharCode(code + count); if (count >= 26) { return ""; } count++; return char; }, }); // 打印‘abc’ console.log(a + a + a); // 'abc' // 打印‘abcd’ let code = "a".charCodeAt(0); let count = 0; // {...定义a...} console.log(a + a + a); // 'abcd' // 打印‘abcdefghijklmnopqrstuvwxyz’ let code = "a".charCodeAt(0); let count = 0; // {...定义a...} let str = ""; for (let i = 0; i < 27; i++) { str += a; } console.log(str); // "abcdefghijklmnopqrstuvwxyz" /* 题目扩展(优化版): 打印`a...z` a+a+a; //'abc' a+a+a+a; //'abcd' */ Object.defineProperty(this, "a", { get() { this._code = this._code || "a".charCodeAt(0); let char = String.fromCharCode(this._code); if (this._code >= "a".charCodeAt(0) + 26) { return ""; } this._code++; return char; }, }); // 打印‘abc’ console.log(a + a + a); // 'abc'
参考资料:
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
期待和你们交流,共同进步,欢迎你们加入我建立的与前端开发密切相关的技术讨论小组:
- 微信公众号: 生活在浏览器里的咱们 / excellent_developers
- Github博客: 趁你还年轻233的我的博客
- SegmentFault专栏:趁你还年轻,作个优秀的前端工程师
- Leetcode讨论微信群:Z2Fva2FpMjAxMDA4MDE=(加我微信拉你进群)
努力成为优秀前端工程师!