Object.defineProperty 的使用
第一个参数,需要定义属性的对象
第二个参数,需要在第一个参数对象上定义的属性名
第三个参数,一些配置(描述符)
- configurable
- 描述符能否改变(能否被Object.defineProperty多次定义),是否能被 delete 操作符删除
- 默认 false,不可重定义,不可删除
// =============== 例1 ===============
const obj = {}
// 当其为 false 时,不可再次使用 Object.defineProperty 进行定义
Object.defineProperty(obj, 'a', {
configurable: false
})
// 且不可被删除
delete obj.a // 该表达式返回 false,删除失败
Object.defineProperty(obj, 'a', { // TypeError: Cannot redefine property: a
configurable: true
})
// =============== 例2 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
configurable: true
})
Object.defineProperty(obj, 'a', { // 可以重新设置描述符
configurable: true
})
// 为 true 时,可以通过 delete 操作符进行删除
delete obj.a // true,obj 为空对象
- enumerable
- 是否可被枚举
- 默认 false,不可被枚举
// =============== 例1 ===============
const obj = {}
// 当其为 false 时,不可被枚举
Object.defineProperty(obj, 'a', {
enumerable: false
})
obj.propertyIsEnumerable('a') // false
for (const key in obj) { // 没有打印任何东西
console.log(key)
}
// =============== 例2 ===============
const obj = {}
// 当其为 true 时,可被枚举
Object.defineProperty(obj, 'a', {
enumerable: true
})
obj.propertyIsEnumerable('a') // true
for (const key in obj) { // 打印了一个字符串 a
console.log(key)
}
- value
- 该属性对应的值
- 默认 undefined
- 注:不能与 get、set 共存
// =============== 例1 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
value: 'b'
})
console.log(obj.a) // 'b'
- writable
- 默认 false
- 注:不能与 get、set 共存
// =============== 例1 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
writable: false,
value: 'b'
})
obj.a = 'c'
console.log(obj.a) // 打印输出结果还是 'b'
// =============== 例2 ===============
const obj = {}
Object.defineProperty(obj, 'a', {
writable: true,
value: 'b'
})
obj.a = 'c'
console.log(obj.a) // 可以修改了,结果是 'c'
- get
- 取值时,调用 get 函数,取到的值就是 get 函数的返回值
- 默认 undefined
- 注:不能与 value、writable 共存
- set
- 赋值时,调用 set 函数,赋值符右侧的值会作为参数传入
- 默认 undefined
- 注:不能与 value、writable 共存
// =============== 例1 ===============
const obj = {}
// get set 不能与 value、writable 共存
Object.defineProperty(obj, 'a', { // Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
value: 'b',
get: () => 1,
set: (val) => void 0,
})
// =============== 例2 ===============
const obj = {}
let a = null // get 和 set 需要借助一个额外的变量
Object.defineProperty(obj, 'a', {
get: () => a,
set: (val) => (a = val),
})
obj.a = '123'
console.log(obj.a, a) // '123' '123'
// =============== 例3 ===============
const obj = {}
let a = null
Object.defineProperty(obj, 'a', {
get: () => {
console.log('this is get') // 在 get 时做一些操作
return a
},
set: (val) => {
console.log('this is set')
a = val
},
})
obj.a = '123'
console.log(obj.a, a)
// 按如下顺序打印 先 set 里,再 get 里的,再上面一行的
// 'this is set'
// 'this is get'
// '123' '123'
实现 Vue2.0 类似的 侦听器
// 有这样一个对象
const obj = {
a: 1,
b: '2',
c: {
c1: 3,
},
d: [1, 2, 3]
}
// 以及一个这样的函数
function update() {
console.log('update')
}
// 我想要执行下面代码的时候,能打印 update
// obj.a = 2
// obj.c.c1 = 4
// obj.d.push(4)
Object.defineProperty 一次只能定义一个对象上的属性,如果要定义 obj 的所有属性,就需要对其进行遍历
const obj = {
a: 1,
b: '2',
c: {
c1: 3,
},
d: [1, 2, 3]
}
function update() {
console.log('update')
}
/**
* 对象侦听
* @param {object} observedObject 需要被观测的对象
* @param {Function} callback 需要被观测对象属性值发生修改时,触发的回调函数
*/
function observer(observedObject, callback) {
// 首先判断它是不是对象,如果不是,直接返回它本身
if (observedObject === null) return observedObject
if (typeof observedObject !== 'object') return observedObject
// 对属性进行遍历
for (const key in observedObject) {
let otherValue = observedObject[key] // Object.defineProperty 的 get set 需要用到一个额外的变量,它的默认值为当前 observedObject 被遍历到的属性
Object.defineProperty(observedObject, key, {
configurable: true,
enumerable: true,
get() {
return otherValue
},
set(val) {
if (typeof callback === 'function')
callback()
otherValue = val
}
})
}
return observedObject
}
observer(obj, update)
// 接下来对 obj 上的属性进行赋值操作
// obj.a = 2
// obj.b = 3
// 都可以打印一次 update
// 但是 obj.c.c1 = 4 就触发不了了
// 因为我们还只处理了一层
我们需要通过一个递归来处理更深层的属性观测
// 在 observer 函数的 for 循环内部,最后面加上一个判断
// 如果 observedObject 的当前属性对应值仍为一个对象,则把它的值作为参数继续调用 observer 函数自身
for (const key in observedObject) {
let otherValue = observedObject[key]
Object.defineProperty(observedObject, key, {
configurable: true,
enumerable: true,
get() {
return otherValue
},
set(val) {
if (typeof callback === 'function')
callback()
otherValue = val
}
})
// =============== 新增逻辑 ===============
// 非对象,忽略后续步骤
if (otherValue === null) continue
if (typeof otherValue !== 'object') continue
observer(observedObject[key], callback)
}
// 这下执行下面代码,update 也会打印出来了
// obj.c.c1 = 4
// 但是有这么一种情况,如果给 obj.b 赋值一个新对象,新对象并不会被观测到
// obj.b = { e: 1 }; obj.b.e = 2; // 后者不会再打印 update
// 但是在赋值对象的过程中,我们可以在 set 函数的 val 参数上进行判断,如果 val 是一个对象,那么对 val 进行观测
那我们对 set 函数进行如下改进
set(val) {
if (typeof callback === 'function')
callback()
otherValue = val
// =============== 以下新增逻辑 ===============
if (val === null) return
if (typeof val !== 'object') return
observer(val, callback)
}
// 这样 obj.b = { e: 1 }; obj.b.e = 2; 就会打印两次 update 了
对于 obj 里的数组,obj.d[0] = 3 这样赋值,可以打印出 update ,因为数组的索引也属于它的属性,但是 obj.d.push(4) 并不能触发打印,我们需要对数组实例上的数组变更方法,进行包裹,这种处理方式就是面向切片的编程方式,例如装饰器
// 列举出所有的数组变更方法的函数名
const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arr = new Array()
function update() {
console.log('update')
}
arrChangeMethodsKeys.forEach(methodsKey => {
const prevMethods = arr[methodsKey] // 将原来的方法保存到 prevMethods 上
arr.__proto__[methodsKey] = function() { // 改写成新的函数
update() // 先调用 update
prevMethods.apply(arr, arguments) // 再调用原来的方法,注意,数组的实例方法中都使用了 this,要手动绑定一次 this 指向
}
})
arr.push(1) // 先打印了 update,后打印 arr 结果得到 [1]
上面这部分逻辑我们也需要加在 observer 函数的 for 循环后面
for (const key in observedObject) {
if (otherValue === null) continue
if (typeof otherValue !== 'object') continue
observer(observedObject[key], callback)
if (Array.isArray(observedObject[key])) {
const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arr = observedObject[key]
arrChangeMethodsKeys.forEach(methodsKey => {
const prevMethods = arr[methodsKey]
arr.__proto__[methodsKey] = function() {
if (typeof callback === 'function')
callback()
prevMethods.apply(arr, arguments)
}
})
}
}
// 这样执行数组的变更方法时,update 也会被打印了
// obj.d.push(4) // update 被打印
完整代码
- 上文中的完整示例代码
const obj = {
a: 1,
b: '2',
c: {
c1: 3,
},
d: [1, 2, 3]
}
function update() {
console.log('update')
}
/**
* 对象侦听
* @param {object} observedObject 需要被观测的对象
* @param {Function} callback 需要被观测对象属性值发生修改时,触发的回调函数
*/
function observer(observedObject, callback) {
// 首先判断它是不是对象,如果不是,直接返回它本身
if (observedObject === null) return observedObject
if (typeof observedObject !== 'object') return observedObject
// 对属性进行遍历
for (const key in observedObject) {
let otherValue = observedObject[key]
Object.defineProperty(observedObject, key, {
configurable: true,
enumerable: true,
get() {
return otherValue
},
set(val) {
if (typeof callback === 'function')
callback()
otherValue = val
if (val === null) return
if (typeof val !== 'object') return
observer(val, callback)
}
})
if (otherValue === null) continue
if (typeof otherValue !== 'object') continue
observer(observedObject[key], callback)
if (Array.isArray(observedObject[key])) {
const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arr = observedObject[key]
arrChangeMethodsKeys.forEach(methodsKey => {
const prevMethods = arr[methodsKey]
arr.__proto__[methodsKey] = function() {
if (typeof callback === 'function')
callback()
prevMethods.apply(arr, arguments)
}
})
}
}
return observedObject
}
observer(obj, update)
- 上图中一个函数中写的内容过于臃肿了,拆分一下
function isFunction(val) {
return typeof val === 'function'
}
function isObject(val) {
return typeof val === 'object' && val !== null
}
function defineArrayObserver(arr, callback) {
if (Array.isArray(arr)) {
const arrChangeMethodsKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
arrChangeMethodsKeys.forEach(methodsKey => {
const prevMethods = arr[methodsKey]
arr.__proto__[methodsKey] = function() {
if (isFunction(callback)) callback()
prevMethods.apply(arr, arguments)
}
})
}
}
function defineObjectObserver(obj, key, otherValue, callback) {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: () => otherValue,
set(val) {
if (isFunction(callback)) callback()
otherValue = val
if (!isObject(val)) return
observer(val, callback)
}
})
observer(obj[key], callback)
defineArrayObserver(obj[key], callback)
}
/**
* 对象侦听
* @param {object} observedObject 需要被观测的对象
* @param {Function} callback 需要被观测对象属性值发生修改时,触发的回调函数
*/
function observer(observedObject, callback) {
if (!isObject(observedObject)) return observedObject
for (const key in observedObject) {
defineObjectObserver(observedObject, key, observedObject[key], callback)
}
return observedObject
}