
幾乎所有使用Vue的開發(fā)者都知道,Vue的雙向綁定是通過Object.defineProperty()實現(xiàn)的,也知道在getter中收集依賴,在setter中通知更新。
那么除了知道getter和setter之外,Object.defineProperty()還有哪些值得我們去注意的地方呢?是不是有很多細節(jié)的東西不懂呢?
你可能會說,除了getter和setter之外,Object.defineProperty()還有value,writable,enumerable,configurable。
那么問題來了?
- value或writable與getter,setter可以共存嗎?與enumerable,configurable呢?
- 概括講下writable,enumerable,configurable分別是什么意思?
- enumerable在Object.keys()和for...in以及展開操作符...是如何表現(xiàn)的?
- configurable會限制哪些屬性不可redefine?value會被限制嗎?會限制屬性的刪除嗎?
- 通過obj.foo和Object.defineProperty(obj,foo)方式定義的屬性有何區(qū)別?
-
data descriptor、accessor descriptor、shared descriptor是什么?
如果看了上面這些問題一臉懵逼,不要驚慌,我們先來看一道非常直觀易懂的題目:
// 實現(xiàn)下面的邏輯
console.log(a+a+a); // 'abc'
題目看完了,帶著問題開始閱讀下面的內容吧。
如果能耐心看完的話對于個人的前端技術提升會非常大。
往近了說,不出意外上面這些問題全部可以迎刃而解,對于a+a+a題目的題解也會理解更加透徹。
往遠了說,可以去看懂Vue源碼相關的實現(xiàn),以及看懂任何使用到Object.defineProperty()這個API的庫的源碼實現(xiàn),甚至最后自己寫個小輪子。
- 初識Object.defineProperty()
- 語法
- 參數
- 返回值
- Object.defineProperty()概覽
- 基本知識點
- data和accessor兩種描述符
- 描述符必須是data, accessor之一,不能同時具有兩種特性
- 如何區(qū)分data descriptor和accessor descriptor?
- descriptor key概覽
- 共享descriptor key概覽
- data descriptor key概覽
- accessor descriptor key概覽
- 牢記屬性不僅僅是descriptor自己的屬性,還要考慮繼承屬性
- 三個很基礎但是很好的例子
- 默認descriptor:不可寫,不可枚舉,不可配置
- 重用同一對象記憶上一次的value值
- 凍結Object.prototype
- Object.defineProperty()詳解
- 創(chuàng)建一個property
- 修改一個property
- Writable attribute
- Enumerable attribute
- 知識點
- 在for...in中如何表現(xiàn)?
- 在Object.keys()中如何表現(xiàn)?
- 在展開操作符...中如何表現(xiàn)?
- 如何檢測屬性是否可以枚舉?
- Configurable attribute
- 增加屬性和默認值
- 自定義setter和getter
- properties的繼承
- 如何獲取屬性的descriptor?
-
console.log(a+a+a); // 'abc'題解- 解法1: Object.defineProperty() 外部變量
- 解法1(優(yōu)化版):Object.defineProperty() 內部變量
- 解法2: Object.prototpye.valueOf()
- 解法3:charCodeAt,charFromCode
- 解法3(優(yōu)化版一):內部變量this._count和_code
- 解法3(優(yōu)化版二):內部變量this._code
- 題目擴展: 打印
a...z - 題目擴展(優(yōu)化版): 打印
a...z
初識Object.defineProperty()
靜態(tài)方法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)
參數
- obj 需要定義屬性的對象
- prop 需要定義或者修改的property的名字或者Symbol
- descriptor 定義和修改的property的描述符
返回值
返回傳遞進函數的對象。
Object.defineProperty()概覽
- 基本知識點
- data和accessor兩種描述符
- 描述符必須是data, accessor之一,不能同時具有兩種特性
- 如何區(qū)分data descriptor和accessor descriptor?
- descriptor key概覽
- 共享descriptor key概覽
- data descriptor key概覽
- accessor descriptor key概覽
- 牢記屬性不僅僅是descriptor自己的屬性,還要考慮繼承屬性
- 三個很基礎但是很好的例子
- 默認descriptor:不可寫,不可枚舉,不可配置
- 重用同一對象記憶上一次的value值
- 凍結Object.prototype
基本知識點
- Object.defineProperty()允許精準添加或者修改對象上的一個屬性。
- 通過const obj = {};obj.foo = 1這種賦值方式增添的屬性,可以通過for...in或者Object.keys枚舉,他的值可能發(fā)生改變,也可能被刪除。
- Object.defineProperty()允許對對象屬性的默認方法做出改變。
- 默認情況下通過Object.defineProperty()是immutable(不可變的)。不能通過delete obj.foo刪除這個屬性。
- Object.defineProperty()具有data和accessor兩種描述符,描述符生效時只能是其中之一,不能同時生效。
- data和accessor兩種描述符都是object,dataDescriptor = {value, writable},accessorDescriptor={get(){}, set(){}}
- data和accessor有各自獨有的key,它們也有共享的key。data accessor特有的key為value和writable,accessor descriptor特有的key為get和set。共享的key為configurable和enumerable。
- 如果descriptor沒有value, writable, get和set,會被當做一個data descriptor;如果同時有value或writable和get或set,異常會拋出。
data和accessor兩種描述符
對象的屬性descriptor描述符主要有兩種:data descriptor和accessor descriptor。
data descriptor
數據描述符指的是value,writable,它可能是可寫的,也可能是不可寫的。
accessor descriptor
權限描述符指的是通過getter-setter函數get(),set()對property的描述。
描述符必須是data, accessor之一,不能同時具有兩種特性
下面的代碼會報錯的原因破案了:只能是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.`
如何區(qū)分data descriptor和accessor descriptor?
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(){ ... }
})
descriptor key概覽
默認情況下是通過Object.defineProperty()定義屬性的。
共享descriptor key概覽
configurable
- 默認值為false
- 當且僅當屬性的描述符類型可能發(fā)生變化以及屬性描述符可能從對象上刪除和這個屬性相關聯(lián)
- configurable為false時,非data descriptor的屬性不能被重定義,也就是說除value和writable之外的屬性不能定義,而且特別要注意,value可以隨意改,而writable僅能從true改為false。get(), set(), enumerable, configurable是都不能重新定義的。
- 而且不能切換descriptor的類型:data descriptor和accessor descriptor
- configurable 不僅僅影響屬性的修改,還影響到了屬性的刪除。configurable為false時
delete obj.o失效
為什么configurable設置為false時要這樣設計?
- 提升對象屬性可控性
- 提升安全性
這是因為get(), set(), enumerable, configurable是權限相關的屬性,為了避免引起不必要的bug。
很多庫的作者不允許自己修改這個屬性,讓它保持在一種可控的狀態(tài),從而代碼按照自己的預期去運行。而且這樣做也更加安全。
enumerable
- 默認值為false
- 當且僅當對象的屬性枚舉展示時會和這個屬性相關聯(lián)
data descriptor key概覽
value
- 默認值為undefined
- 屬性相關聯(lián)的value
- 可以是任何JavaScript值 number,object,function等等
writable
- 默認是false
- 當且僅當通過賦值操作符賦值時會和這個屬性相關聯(lián)
accessor descriptor key概覽
get
- 默認值為undefined
- 作為屬性的getter服務于屬性,如果沒有getter的話,get為undefined。
- 當property被訪問時,這個函數會在不傳參的情況下調用然后,并將this設置為訪問屬性的對象(this由于繼承可能不是定義屬性的對象。)
- 返回值會作為property的value。
set
- 默認值為undefined
- 作為屬性的setter服務于屬性,如果沒有setter的話,set為undefined。
- 當屬性重新賦值時,函數在傳遞一個參數的情況下調用,并將這個集合設置為屬性賦值的對象。
牢記屬性不僅僅是descriptor自己的屬性,還要考慮繼承屬性
為確保保留了這些默認值:
- 可以freeze Object.prototype
- 或者Object.create(null)
三個很基礎但是很好的例子
默認descriptor:不可寫,不可枚舉,不可配置
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'
});
重用同一對象記憶上一次的value值
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.prototype
Object.freeze(Object.prototype)
Object.defineProperty()詳解
創(chuàng)建一個property
屬性如果在對象上不存在的話,Object.defineProperty()會創(chuàng)建一個新的屬性。
可以省略很多描述符中字段,并且輸入這些字段的默認值。
// 創(chuàng)建對象
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做了強關聯(lián)。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)
修改一個property
- 當一個屬性在對象中存在時,Object.defineProperty()可以根據descriptor中的值和對象返回值的配置嘗試修改這個屬性。
- 如果舊的descriptor有configurable屬性,并且設置為false,意思是”不可配置“。
- 意味著不能修改任意共享descriptor和accessor descriptor的屬性的值
- 可以重定義data descriptor:value任意變,writable只能從true變?yōu)閒alse(不能從false改為true)。
- 而且不能切換descriptor的類型:data descriptor和accessor descriptor
- 違反規(guī)則報錯:Cannot redefine property: xxx;符合規(guī)則和沒有修改屬性的話不報錯。
Writable attribute
當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
}());
Enumerable attribute
- 知識點
- 在for...in中如何表現(xiàn)?
- 在Object.keys()中如何表現(xiàn)?
- 在展開操作符...中如何表現(xiàn)?
- 如何檢測屬性是否可以枚舉?
知識點
- enumerable屬性定義了屬性是否可以被Object.assign()或者spread(...) pick到。
- 對于非symbol的屬性,它還會影響到for...in和Object.keys()對屬性的pick。
- 可以用
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
});
在for...in中如何表現(xiàn)?
只有'a'和'd'打印了出來。
enumerable為true的都能被解構出來,不包括Symbol。
for (var i in o) {
console.log(i); // 'a','d'
}
在Object.keys()中如何表現(xiàn)?
只有'a'和'd'被搜集到。
enumerable為true的都能被解構出來,不包括Symbol。
Object.keys(o); // ['a', 'd']
在展開操作符...中如何表現(xiàn)?
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 attribute
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()是有區(qū)別的。
兩種賦初始值方式的區(qū)別如下
- 通過點操作符定義的屬性,writable,configurable,enumerable值都為true,value為賦入的值
- 通過Object.defineProperty只指定value的屬性,writable,configurable,enumerable值都為false
通過點操作符定義的屬性
通過點操作符定義的屬性等價于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只指定value的屬性
Object.defineProperty(o, 'a', { value: 1 });
// 等價于
Object.defineProperty(o, 'a', {
value: 1,
writable: false,
configurable: false,
enumerable: false
});
自定義setter和getter
下面的例子展示了如何實現(xiàn)一個自存檔的對象。
當temperature屬性設置后,archive數組會打印。
- 常見的一種gettter,setter使用方式
- 這個getter和setter總是返回相同的值
常見的一種gettter,setter使用方式
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}]
這個getter和setter總是返回相同的值
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
properties的繼承
- 如果一個accessor屬性是繼承的,它的get和set方法會在屬性被訪問時調用,并且在后代對象上被修改。如果這些方法使用變量來存儲這個值,這個值會在所有對象間共享(即使使用new仍然會共享)
- 如果一個value屬性是繼承的,它可以直接設置在對象上。但是,如果繼承了一個不可寫的值屬性,它仍然會阻止修改對象上的屬性。
主要為以下3個問題:
- Object.defineProperty與prototype的問題
- 如何解決 Object.defineProperty與prototype的問題
- Object.defineProperty的writable和proto
Object.defineProperty與prototype的問題
這個例子展示了繼承帶來的問題:
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
如何解決 Object.defineProperty與prototype的問題
如何解決這個問題呢?
可以將值存儲在另一個this屬性上。這樣使用new創(chuàng)建新實例時,可以為自己開辟單獨的屬性空間。
在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
Object.defineProperty的writable和proto
下面的例子,點操作符賦值的屬性可寫,但是繼承的myclass.prototype的初始值不會發(fā)生更改;不可寫的屬性不可寫。
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
值得分析一波的截圖:

- proto是通過Object.defineProperty(foo.prototype)實現(xiàn)的繼承
- 如果屬性的writable為true,會在proto的上一級創(chuàng)建新的屬性
如何獲取屬性的descriptor?
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'題解
/*
console.log(a + a + a); // 打印'abc'
*/
- 解法1: Object.defineProperty() 外部變量
- 解法1(優(yōu)化版):Object.defineProperty() 內部變量
- 解法2: Object.prototpye.valueOf()
- 解法3:charCodeAt,charFromCode
- 解法3(優(yōu)化版一):內部變量this._count和_code
- 解法3(優(yōu)化版二):內部變量this._code
- 題目擴展: 打印
a...z - 題目擴展(優(yōu)化版): 打印
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(優(yōu)化版):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(優(yōu)化版一):內部變量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(優(yōu)化版二):內部變量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"
/*
題目擴展(優(yōu)化版): 打印`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/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable
期待和大家交流,共同進步,歡迎大家加入我創(chuàng)建的與前端開發(fā)密切相關的技術討論小組:
- 微信公眾號: 生活在瀏覽器里的我們 / excellent_developers
- Github博客: 趁你還年輕233的個人博客
- SegmentFault專欄:趁你還年輕,做個優(yōu)秀的前端工程師
- Leetcode討論微信群:Z2Fva2FpMjAxMDA4MDE=(加我微信拉你進群)
努力成為優(yōu)秀前端工程師!