直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象;
备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。
/**
* obj { Object } 必传,目标对象(要定义属性的对象)
* prop { String | Symbol } 必传, 需定义或修改的属性的名字
* descriptor { Object } 必传, 要定义或修改的属性描述符
* configurable { Boolean } false 属性是否可删除,及除 value 和 writable 特性外的其他特性是否可以被修改。
* enumerable { Boolean } false 属性是否对象的枚举属性中(for in 或者 Object.keys()的遍历中)
* writable { Boolean } false 属性是否被 赋值运算符 改变 (值能否被修改)
* value { any } undefined 属性对应的值
* set { Function } undefined 属性设置值的时候触发的函数,设置的新值通过参数第一个值获取
* get { Function } undefined 属性获取值的时候触发的函数
* */
Object.defineProperty(obj, prop, descriptor)
数据描述符
configurable | enumerable | writable | value |
---|
存取描述符
configurable | enumerable | set | get |
---|
注意:一个描述符同时拥有 value 或 writable 和 get 或 set 键,则会产生一个异常。
configurable:
// 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
const obj = {}
Object.defineProperty(obj, 'a', {
value: 'a',
configurable: true
})
console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
// 当前obj.a 描述符 { value: 'a', writable: false, enumerable: false, configurable: true }
Object.defineProperty(obj, 'a', {
value: 'aa',
enumerable: true
})
console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
// 当前 obj.a 描述符 {value: 'aa', writable: false, enumerable: true, configurable: true}
Object.defineProperty(obj, 'a', {
writable: true
})
console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
// 当前 obj.a 描述符 {value: 'aa', writable: true, enumerable: true, configurable: true}
// 当configurable 为 false ,去修改 value, configurable, enumerable
Object.defineProperty(obj, 'a', { configurable: false })
console.log(Object.getOwnPropertyDescriptor(obj, 'a'))
// 当前 obj.a 描述符 {value: 'aa', writable: true, enumerable: true, configurable: false}
// 报错
//Object.defineProperty(obj, 'a', { configurable: true })
// 报错
//Object.defineProperty(obj, 'a', { enumerable: false })
// 报错
//Object.defineProperty(obj, 'a', { set (value) { obj.c = value } })
// 报错
//Object.defineProperty(obj, 'a', { get () { return obj.c } })
delete obj.a // false 无法删除
enumerable:
// 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举
const obj2 = {};
Object.defineProperty(obj2, "a", { value : 1, enumerable: true });
Object.defineProperty(obj2, "b", { value : 2, enumerable: false });
Object.defineProperty(obj2, "c", { value : 3 }); // enumerable 默认为 false
o.d = 4; // 如果使用直接赋值的方式创建对象的属性,则 enumerable 为 true
Object.defineProperty(obj2, Symbol.for('e'), {
value: 5,
enumerable: true
});
Object.defineProperty(obj2, Symbol.for('f'), {
value: 6,
enumerable: false
});
for (const i in obj2) {
console.log(i);
} // a d
Object.keys(obj2); // ['a', 'd']
obj2.propertyIsEnumerable('a'); // true
obj2.propertyIsEnumerable('b'); // false
obj2.propertyIsEnumerable('c'); // false
obj2.propertyIsEnumerable('d'); // true
obj2.propertyIsEnumerable(Symbol.for('e')); // true
obj2.propertyIsEnumerable(Symbol.for('f')); // false
// 使用扩展运算符 只有 enumerable 为true 才能被获取
var p = { ...obj2 }
p.a // 1
p.b // undefined
p.c // undefined
p.d // 4
p[Symbol.for('e')] // 5
p[Symbol.for('f')] // undefined
writable:
// 当 writable 属性设置为 false 时,该属性被称为“不可写的”。它不能被重新赋值。
const obj3 = {}
Object.defineProperty(obj3, 'a', {
value: 'a',
writable: true
})
obj3.a = 4
console.log(obj3.a) // 4
Object.defineProperty(obj3, 'b', {
value: 'b',
writable: false // 或者可以不设置 默认为false
})
obj3.b = 4
console.log(obj3.b) // b
// 如果在 use strict 模式下 不可写 属性 ,去修改其值 就会报错
(function() {
'use strict';
const obj4 = {};
Object.defineProperty(obj4, 'a', {
value: 2,
writable: false
});
obj4.a = 3; // Cannot assign to read only property 'a' of object '#
obj4.a; // 2
}());
重定义数组的 length 属性是可能的,但是会受到一般的重定义限制。(length 属性初始为 non-configurable,non-enumerable 以及 writable。对于一个内容不变的数组,改变其 length 属性的值或者使它变为 non-writable 是可能的。但是改变其可枚举性和可配置性或者当它是 non-writable 时尝试改变它的值或是可写性,这两者都是不允许的。)然而,并不是所有的浏览器都允许 Array.length 的重定义。
在 Firefox 4 至 22 版本中,尝试重定义数组的 length 属性都会抛出 TypeError 异常。
一些版本的 Chrome 中,Object.defineProperty() 在某些情况下会忽略不同于数组当前length属性的length值。有些情况下改变可写性并不起作用(也不抛出异常)。同时,比如Array.prototype.push的一些数组操作方法也不会考虑不可读的length属性。
一些版本的 Safari 中,Object.defineProperty() 在某些情况下会忽略不同于数组当前length属性的length值。尝试改变可写性的操作会正常执行而不抛出错误,但事实上并未改变属性的可写性。
只在Internet Explorer 9及以后版本和Firefox 23及以后版本中,才完整地正确地支持数组 length 属性的重新定义。目前不要依赖于重定义数组 length 属性能够起作用,或在特定情形下起作用。与此同时,即使你能够依赖于它,你也没有合适的理由这样做。
Internet Explorer 8 实现了 Object.defineProperty() 方法,但只能在 DOM 对象上使用。 需要注意的一些事情:
尝试在原生对象上使用 Object.defineProperty() 会报错。
属性特性必须设置一些特定的值。对于数据属性描述符,configurable, enumerable 和 writable 属性必须全部设置为 true;对于访问器属性描述符,configurable 必须设置为 true,enumerable 必须设置为 false。(?) 任何试图提供其他值(?)将导致一个错误抛出。
重新配置一个属性首先需要删除该属性。如果属性没有删除,就如同重新配置前的尝试。
Chrome 37(及以下)特别备注
Chrome 37(及以下)有一个 bug,使用 writable: false 定义原型 prototype 属性,或者函数时,不会像预期的那样工作。
// 完整代码(参考源码,个人理解)
/*判断当前浏览器是否支持__proto__这个非标准属性*/
const hasProto = '__proto__' in {}
/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function(item){
Object.defineProperty(arrayMethods,item,{
value: function mutator () {
//缓存原生方法,之后调用
console.log('array被访问')
const original = arrayProto[item]
const args = Array.from(arguments)
original.apply(this, args)
}
})
})
function Observer (value) {
this.value = value
/*
将Observer实例绑定到data的__ob__属性上面去,之前说过observe的时候会先检测是否已经有__ob__对象存放Observer实例了
*/
def(value, '__ob__', this)
if (Array.isArray(value)) {
/*
如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
这里如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
*/
const augment = hasProto
? protoAugment /*直接覆盖原型的方法来修改目标对象*/
: copyAugment /*定义(覆盖)目标对象或数组的某一个方法*/
augment(value, arrayMethods, arrayKeys)
/*如果是数组则需要遍历数组的每一个成员进行observe*/
this.observeArray(value)
} else {
/*如果是对象则直接walk进行绑定*/
this.walk(value)
}
}
const observerPrototype = Observer.prototype
// 设置 属性监听
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
/*直接覆盖原型的方法来修改目标对象或数组*/
function protoAugment (target, src) {
target.__proto__ = src
}
/*定义(覆盖)目标对象或数组的某一个方法*/
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
/*对一个数组的每一个成员进行observe*/
observerPrototype.observeArray = function (items) {
for (let i = 0, l = items.length; i < l; i++) {
/*数组需要遍历每一个成员进行observe*/
observe(items[i])
}
}
/*
遍历每一个对象并且在它们上面绑定getter与setter。这个方法只有在value的类型是对象的时候才能被调用
*/
observerPrototype.walk = function (obj) {
const keys = Object.keys(obj)
/*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
for (let i = 0; i < keys.length; i++) {
const itemValue = keys[i]
defineReactive(obj, itemValue, obj[itemValue])
}
}
//数据重复Observer
function observe(value){
if (typeof(value) !== 'object') return
return new Observer(value)
}
/*为对象defineProperty上在变化时通知的属性*/
function defineReactive (obj, key, val) {
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
/*如果之前该对象已经预设了getter以及setter函数则将其取出来,新定义的getter/setter中会将其执行,保证不会覆盖之前已经定义的getter/setter。*/
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
/*对象的子对象递归进行observe并返回子节点的Observer对象*/
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(key + '被访问')
/*如果原本对象拥有getter方法则执行*/
const value = getter ? getter.call(obj) : val
return value
},
set: function reactiveSetter (newVal) {
console.log(key + '被修改,新' + key + '=' + newVal)
/*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (setter) {
/*如果原本对象拥有setter方法则执行setter*/
setter.call(obj, newVal)
} else {
val = newVal
}
/*新的值需要重新进行observe,保证数据响应式*/
childOb = observe(newVal)
}
})
}
const data = {
testObj: {
a: 'a',
b: 'b'
},
testArr: [{ c: 'c', d: 'd' }, 2, 3, 4]
}
const app = new Observer(data).value
app.testArr.push(5)
app.testArr[0].c = 'c1'
app.testObj.a = 'a1'