如何理解Object.defineProperty()
前言
几乎所有使用Vue的开发者都知道,Vue的双向绑定是通过Object.defineProperty()实现的,也知道在getter中收集依赖,在setter中通知更新。
那么除了知道getter和setter之外,Object.defineProperty()还有哪些值得我们去注意的地方呢?是不是有很多细节的东西不懂呢?
你可能会说,除了getter和setter之外,Object.defineProperty()还有value,writable,enumerable,configurable。
那么问题来了?
- value或writable与getter,setter可以共存吗?与enumerable,configurable呢?
- 概括讲下writable,enumerable,configurable分别是什么意思?
- enumerable在Object.keys()和for…in以及展开操作符…是如何表现的?
- configurable会限制哪些属性不可redefine?value会被限制吗?会限制属性的删除吗?
- 通过obj.foo和Object.defineProperty(obj,foo)方式定义的属性有何区别?
- data descriptor、accessor descriptor、shared descriptor是什么?
如果看了上面这些问题一脸懵逼,不要惊慌,我们先来看一道非常直观易懂的题目:
// 实现下面的逻辑
console.log(a+a+a); // 'abc'
题目看完了,带着问题开始阅读下面的内容吧。 如果能耐心看完的话对于个人的前端技术提升会非常大。 往近了说,不出意外上面这些问题全部可以迎刃而解,对于a+a+a题目的题解也会理解更加透彻。 往远了说,可以去看懂Vue源码相关的实现,以及看懂任何使用到Object.defineProperty()这个API的库的源码实现,甚至最后自己写个小轮子。
- 初识Object.defineProperty()
-
语法
- 参数
- 返回值
-
Object.defineProperty()概览
- 基本知识点
- data和accessor两种描述符
- 描述符必须是data, accessor之一,不能同时具有两种特性
- 如何区分data descriptor和accessor descriptor?
- descriptor key概览
- 共享descriptor key概览
- data descriptor key概览
- accessor descriptor key概览
- 牢记属性不仅仅是descriptor自己的属性,还要考虑继承属性
- 三个很基础但是很好的例子
- 默认descriptor:不可写,不可枚举,不可配置
- 重用同一对象记忆上一次的value值
- 冻结Object.prototype
Object.defineProperty()详解
创建一个property 修改一个property
Writable attribute Enumerable attribute
知识点 在for…in中如何表现? 在Object.keys()中如何表现? 在展开操作符…中如何表现? 如何检测属性是否可以枚举? Configurable attribute 增加属性和默认值 自定义setter和getter properties的继承 如何获取属性的descriptor?
- console.log(a+a+a); // ‘abc’题解
解法1: Object.defineProperty() 外部变量 解法1(优化版):Object.defineProperty() 内部变量 解法2: Object.prototpye.valueOf() 解法3:charCodeAt,charFromCode 解法3(优化版一):内部变量this._count和_code 解法3(优化版二):内部变量this._code 题目扩展: 打印a…z 题目扩展(优化版): 打印a…z
Object.defineProperty()详解
创建一个property 属性如果在对象上不存在的话,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)
修改一个property
当一个属性在对象中存在时,Object.defineProperty()可以根据descriptor中的值和对象返回值的配置尝试修改这个属性。 如果旧的descriptor有configurable属性,并且设置为false,意思是”不可配置“。
意味着不能修改任意共享descriptor和accessor descriptor的属性的值 可以重定义data descriptor:value任意变,writable只能从true变为false(不能从false改为true)。 而且不能切换descriptor的类型:data descriptor和accessor descriptor 违反规则报错:Cannot redefine property: xxx;符合规则和没有修改属性的话不报错。
Writable attribute
当writable设置为false时,属性是不可写的,意味着无法重新赋值。
非严格模式不会报错,只是赋值失败 严格模式会报错Cannot assign to read only property ‘b’ of 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中如何表现? 在Object.keys()中如何表现? 在展开操作符…中如何表现? 如何检测属性是否可以枚举?
-
知识点 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
});
- 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()是有区别的。
两种赋初始值方式的区别如下
通过点操作符定义的属性,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
下面的例子展示了如何实现一个自存档的对象。 当temperature属性设置后,archive数组会打印。
- 常见的一种gettter,setter使用方式
- 这个getter和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
参考资料:
End