数据劫持(一)由浅入深理解Object.defineProperty

Object.defineProperty

  • 看了几篇网上相关博客,发现存在很多问题,有很多误导性,特此梳理总结一下

理解数据劫持

  • 数据劫持,其实就是数据代理。
  • 数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

了解Object.defineProperty() 方法

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
  • Object.defineProperty(obj, prop, descriptor)
    • obj 要定义属性的对象。
    • prop 要定义或修改的属性的名称或 Symbol 。
    • descriptor 要定义或修改的属性描述符。
  • 属性描述符有两种主要形式:数据描述符和存取描述符。
    • 数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。有configurableenumerablevaluewritable四个配置属性。
    • 存取描述符是由 getter 函数和 setter 函数所描述的属性。有configurableenumerablegetset四个配置属性。
    • 一个描述符只能是这两者其中之一;不能同时是两者。

当使用了存取描述符get或set方法,不允许使用writable和value这两个属性,同理当使用了数据描述符writable和value,不允许使用get或set方法。

添加对象属性

通过数据描述符创建

const person = {};
Object.defineProperty(person , 'name', {
    value: '小陈'
})
console.log(person.name) // 小陈

通过存取描述符修改

const person = {};
Object.defineProperty(person ,'name',{
	get:function(){
		console.log('触发get');
		return '小陈'
	},
})
console.log(person.name)  //触发get 小陈

修改对象属性值

通过数据描述符修改

writable
  • 通过配置writable为true可修改属性的值
const person = {
    name: '小陈'
};
Object.defineProperty(person, 'name', {
    writable: true,
})
console.log(person.name) // 小陈
person.name = '小王';
console.log(person.name) //小王

当不配置writable时,其默认值是false,属性的值(value)是不能被赋值运算符改变;
当配置writable为true时,属性的值(value)才能被运算符改变;
所以再给name属性值重新赋值,并不会改变name的值。

通过存取描述符修改

const person = {
    name: '小陈'
};
let nameValue = person.name
Object.defineProperty(person, 'name', {
    configurable: true,
    get: function() {
        console.log('触发get');
        return nameValue
    },
    set: function(newValue) {
        console.log('触发set')
       // person['name'] = newValue  错误写法,会导致栈溢出
        nameValue = newValue
    }
})
console.log(person.name) //触发get 小陈
person.name = '小王'; //触发set 
console.log(person.name) //触发get 小王

配置对象属性

  • configurable 、enumerable配置对数据描述符和存取描述符设置效果一致。

configurable

  • 表示对象的属性是否可以被删除,以及除 value 和 writable 特性外的其他特性是否可以被修改。
  • configurable为默认值或者配置为false时,则该属性相关的配置不能再被更改,也不能被删除
const person = {};
Object.defineProperty(person, 'name', {
    value: '小陈',
    configurable: false,
})
console.log(person.name);
Object.defineProperty(person, 'name', {
    writable: true,  // Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
    configurable: true, //  Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
    enumerable: true, //  Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
     value: '小王' //  Cannot redefine property: name
})
Object.defineProperty(person, 'name', {
    get() {
        return '小王' //  Cannot redefine property: name
    }
})
Object.defineProperty(person, 'name', {
    set() {} //Cannot redefine property: name
})
delete person.name  // 由于前边抛错,不会执行到这里,即使执行到这里也不会删除该属性
console.log(person); 
  • 以上将configurable设为true时,则不会抛出任何错误,最后该属性会被删除。

注意:configurable: true时,可以通过Object.defineProperty给同一属性重复定义配置。同时数据描述符配置属性writable为false,则不能通过赋值方式改变属性的值,但是可以通过Object.defineProperty重复定义的方法改变。

const person = {};
Object.defineProperty(person, 'name', {
    value: '小陈',
    configurable: true,
    writable: false,
})
console.log(person.name); // 小陈
person.name = '小王';
console.log(person.name); // 小陈
Object.defineProperty(person, 'name', {
    value: '小王',
})
console.log(person.name); // 小王

enumerable

  • enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举。默认值为 false。
const person = {};
Object.defineProperty(person, "name", {
    value: '小陈',
    enumerable: true
});
Object.defineProperty(person, "age", {
    value: '20',
    enumerable: false
});
Object.defineProperty(person, "sex", {
    value: '男',
    enumerable: true
});
Object.defineProperty(person, "hobby", {
    value: '睡觉',
    enumerable: true
});
for (var i in person) {
    console.log(i); // name sex hobby
}
const result = Object.keys(person);
console.log(result); // ['name', 'sex', 'hobby']
  • 通过propertyIsEnumerable方法确定属性是否可枚举
console.log(person.propertyIsEnumerable('age'));   // false

数据描述符中的属性默认值问题

  • 使用点运算符和 Object.defineProperty() 为对象的属性赋值时,描述符中的属性默认值是不同的
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
});
  • 即通过点运算符创建的对象属性是可修改可删除可枚举, writable、configurable、enumerable都为true

自定义一个getters和setters

function Archiver() {
  var temperature = null;
  var archive = [];

  Object.defineProperty(this, 'temperature', {
    get: function() {
      console.log('get!');
      return temperature;
    },
    set: function(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 }, { val: 13 }]

Object.defineProperty的应用

监听对象上的多个属性

错误写法
  const person = {
	name:'小陈',
	age:20,
	sex:'男',
  }
 Object.keys(person).forEach(function (key) {
    Object.defineProperty(person, key, {
        enumerable: true,
        configurable: true,
        get() {
            return person[key]
        },
        set(val) {
            console.log(`对person中的${key}属性进行了修改`)
            person[key] = val
        }
    })
})
console.log(person.age)   // Uncaught RangeError: Maximum call stack size exceeded

这种写法在get/set里写person[key],进入递归调用状态,反复访问属性值。最终造成栈溢出。

正确写法
const person = {
    name: '小陈',
    age: 20,
	sex:'男'
}
// 实现一个响应式函数
function defineProperty(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问了${key}属性`)
            return val
        },
        set(newVal) {
            console.log(`${key}属性被修改为${newVal}了`)
            val = newVal
        }
    })
}
// 实现一个遍历函数Observer
function observer(obj) {
    Object.keys(obj).forEach((key) => {
        defineProperty(obj, key, obj[key])
    })
}
Observer(person);
console.log(person.age); // 访问了age属性 20
person.age = 24;  // age属性被修改为24了
console.log(person.age);  // 访问了age属性 24

深度监听一个对象

  • 借助Object.defineProperty与递归的思想可达到深度监听的效果
function defineProperty(obj, key, val) {
    //如果某对象的属性也是一个对象,递归进入该对象,进行监听
    if (typeof val === "object") {
        observer(val);
    }
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问了${key}属性`);
            return val;
        },
        set(newVal) {
            // 如果newVal是一个对象,递归进入该对象进行监听
            if (typeof val === 'object') {
                observer(key)
            }
            console.log(`${key}属性被修改为${newVal}了`);
            val = newVal;
        },
    });
}
function observer(obj) {
    //如果传入的不是一个对象,return
    if (typeof obj !== "object" || obj === null) {
        return;
    }
    Object.keys(obj).forEach((key) => {
        defineProperty(obj, key, obj[key]);
    });
}

Object.defineProperty的不足

不支持监听数组的变化

let arr = [1, 2, 3]
let obj = {}
    //把arr作为obj的属性监听
Object.defineProperty(obj, 'arr', {
    get() {
        console.log('触发了get')
        return arr
    },
    set(newVal) {
        console.log('触发了set', newVal)
        arr = newVal
    }
})
console.log(obj.arr) //触发了get [1,2,3] 
obj.arr = [1, 2, 3, 4] //输出set [1,2,3,4] 正
obj.arr.push(5) //触发了get
obj.arr.unshift() //触发了get
obj.arr.pop() // //触发了get
obj.arr.shift() 触发了get

当使用Object.defineProperty监听的对象属性是数组时,使用push、unshift、pop、shift、splice, ‘sort’, reverse是监听是触发不了set的。
只要不是重新赋值一个新的数组对象,任何对数组内部的修改都不会触发set方法的执行。

  • 在Vue2.x中,通过重写Array原型上的方法解决了这个问题的。
通过重写数组操作方法实现数组监听
function arrMethods() {
    const orginalProto = Array.prototype;
    const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
    const methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ]
    methodsToPatch.forEach(method => {
        arrayProto[method] = function() {
            // 执行原始操作
            orginalProto[method].apply(this, arguments)
            console.log('监听成功', method)
        }
    })
    return arrayProto
}

function observer(data) {
    if (Array.isArray(data)) {
        // 如果是数组, 重写原型
        data.__proto__ = arrMethods()
            // 传入的数据可能是多维度的,也需要执行响应式
        for (let i = 0; i < data.length; i++) {
            observer(data[i])
        }
    }
}
const arr = [1, 2, 3, 4]
observer(arr);
arr.push(5);  //监听成功
实现监听对象数组
//重写数组操作方法
function arrMethods() {
    const orginalProto = Array.prototype;
    const arrayProto = Object.create(orginalProto); // 先克隆一份Array的原型出来
    const methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
    ]
    methodsToPatch.forEach(method => {
        arrayProto[method] = function() {
            // 执行原始操作
            orginalProto[method].apply(this, arguments)
            console.log('监听成功', method)
        }
    })
    return arrayProto
}
// 劫持
function defineProperty(obj, key, val) {
    //如果某对象的属性也是一个对象,递归进入该对象,进行监听
    if (typeof val === "object") {
        observer(val);
    }
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问了${key}属性`);
            return val;
        },
        set(newVal) {
            // 如果newVal是一个对象,递归进入该对象进行监听
            if (typeof val === 'object') {
                observer(key)
            }
            console.log(`${key}属性被修改为${newVal}了`);
            val = newVal;
        },
    });
}
// 多属性监听
function observer(data) {
    //如果传入的不是一个对象,return
    if (typeof data !== "object" || data === null) {
        return;
    }
    if (Array.isArray(data)) {
        // 如果是数组, 重写原型
        data.__proto__ = arrMethods()
            // 传入的数据可能是多维度的,也需要执行响应式
        for (let i = 0; i < data.length; i++) {
            observer(data[i])
        }
    } else {
        // 如果是对象
        Object.keys(data).forEach((key) => {
            defineProperty(data, key, data[key]);
        });
    }
}
// 数组
const arr = [1, 2, 3, 4]
observer(arr)
arr.push(5) // 监听成功 push
// 对象
const person = {
    name: '小陈',
    age: 20,
    sex: '男',
    arr: [1, 2, 3, 4]
}
observer(person)
console.log(person.age); // 访问了age属性 20
person.age = 24; // age属性被修改为24了
console.log(person.age); // 访问了age属性 24
person.arr.push(5)  // 监听成功 push

不能监听属性新增和删除操作

const person = {
    name: '小陈'
};
let nameValue = person.name
Object.defineProperty(person, 'name', {
    configurable: true,
    get: function() {
        console.log('触发get');
        return nameValue
    },
    set: function(newValue) {
        console.log('触发set')
        nameValue = newValue
    }
})
person.age = 20;  // 并没有触发set
console.log(person);  
delete person.age;  //并没有触发set
console.log(person);

什么样的 a 可以满足 (a === 1 && a === 2 && a === 3) === true 呢?

  • 每次访问 a 返回的值都不一样,那么肯定会想到数据劫持。
let current = 0
Object.defineProperty(window, 'a', {
        get() {
            current++
            return current
        }
})
// console.log(a);  1
// console.log(a);  2
// console.log(a);  3
console.log(a === 1 && a === 2 && a === 3)  // true
  • 5
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值