幾乎全部使用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=(加我微信拉你進羣)
努力成爲優秀前端工程師!