一、Object.defineProperty()
它可以用来给一个对象定义一个属性或者修改一个现有的属性,并且返回这个对象。
Object.defineProperty(obj,props,descripter)
descripter: 需要定义或修改的属性描述符
Object.defineProperty(obj, 'name', {
// configurable: false, //默认为false,定义了属性描述符能否被修改以及该属性能否被删除
// enumerable: false, //默认为false,定义了该属性能否被枚举
value: 'kongzhi',
// writable: false,/默认为false
// get(){},
// set(v){}
});
属性描述符主要分为两大类:数据描述符和存取描述符。这两个描述符只能存在一个,不能同时存在。它们共有的属性有:configurable,enumerable,数据描述符只有value和writable,存取描述符只有set和get。
小栗子:
const obj = {};
Object.defineProperty(obj, 'name', {
// configurable: false,//默认为false
// enumerable: false,//默认为false
value: 'kongzhi',
// writable: false,/默认为false
// get(){},
// set(v){}
});
console.log(obj.name); // 输出 kongzhi
// 改写obj.name 的值
obj.name = 111;
console.log(obj.name); // 还是打印出 kongzhi,因为writable默认为false
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
});
注意两个例子的区别,如果直接定义o.a=1,那么默认writable,configurable,enumerable都为true,如果是通过defineProperty定义的属性值,那么这些属性值都为false.
不能监听数组的变化
数组的这些方法是无法触发set的:push, pop, shift, unshift,splice, sort, reverse. Vue 把会修改原来数组的方法定义为变异方法 (mutation method)
非变异方法 (non-mutating method):例如 filter, concat, slice 等,它们都不会修改原始数组,而会返回一个新的数组。
Vue 的做法是把这些方法重写来实现数组的劫持
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];
aryMethods.forEach((method)=> {
// 这里是原生 Array 的原型方法
let original = Array.prototype[method];
// 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上
// 注意:是实例属性而非原型属性
arrayAugmentations[method] = function () {
console.log('我被改变啦!');
// 调用对应的原生方法并返回结果
return original.apply(this, arguments);
};
});
let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log
list.__proto__ = arrayAugmentations;
list.push('d'); // 我被改变啦!
// 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。
let list2 = ['a', 'b', 'c'];
list2.push('d'); // 不输出内容
缺点:
a.不能拦截数组
b.必须挨个遍历对象的属性才能进行拦截
c.必须递归遍历对象的属性,防止对象的属性还是对象
二、Proxy
理解为在target对象前设置一层拦截,每次对target访问都得通过这层拦截,就可以对target进行一些改写;而handler就是一个装有改写target函数的对象,target就是需要被代理的对象。
const p = new Proxy(target,handler)
handler是一个对象,里面的属性都是函数(一些捕捉器trap,都是可选的,如果没有指定会保留源对象的默认行为)
handler.getPrototypeOf()
handler.setPrototypeOf()
handler.isExtensible()
handler.preventExtensions()
handler.getOwnPropertyDescriptor()
handler.defineProperty()
handler.has()//in 操作符的捕捉器。
handler.get(target, property)
handler.set(target, property, value)
handler.deleteProperty()//delete 操作符的捕捉器。
handler.ownKeys()
handler.apply()
handler.construct()//new 操作符的捕捉器。
其中:get() , set() , apply() , has() , contruct() ps: 用于拦截new操作
小栗子:
Proxy可以代理数组
let arr = [1,2,3]
let proxy = new Proxy(arr, {
get (target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver)
}
})
proxy.push(4)
// 能够打印出很多内容
// get push (寻找 proxy.push 方法)
// get length (获取当前的 length)
// set 3 4 (设置 proxy[3] = 4)
// set length 4 (设置 proxy.length = 4)
Proxy也只代理外面一层属性,需要递归代理属性嵌套的对象属性
let obj={a:1,b:{c:2}};
let handler={
get:function(obj,prop){
const v = Reflect.get(obj,prop);
return v; // 返回obj[prop]
},
set(obj,prop,value){
return Reflect.set(obj,prop,value);//设置成功返回true
}
};
let p=new Proxy(obj,handler);
p.a//会触发get方法
p.b.c//会触发get方法获取p.b,不会触发.c的set,因为c没被代理。
let obj={a:1,b:{c:2}};
let handler={
get:function(obj,prop){
const v = Reflect.get(obj,prop);
if(v !== null && typeof v === 'object'){
return new Proxy(v,handler);//代理内层
}else{
return v; // 返回obj[prop]
}
},
set(obj,prop,value){
return Reflect.set(obj,prop,value);//设置成功返回true
}
};
let p=new Proxy(obj,handler);
p.a//会触发get方法
p.b.c//会先触发get方法获取p.b,然后触发返回的新代理对象的.c的set。
虽然Proxy和Object.defineProperty一样都需要递归拦截属性的get,set,但是Proxy只在调用时递归,Object.defineProperty在一开始就递归,所以Proxy性能更高。
面试题:
1、什么样的 a 可以满足 (a === 1 && a === 2 && a === 3) === true 呢?(注意是 3 个 =,也就是严格相等)
解决:每次访问 a 返回的值都不一样,那么肯定会想到数据劫持(有其它解法)
let current = 0
Object.defineProperty(window, 'a', {
get () {
current++
console.log(current)
return current
}
})
console.log(a === 1 && a === 2 && a === 3) // true
2、实现多继承
Javascript 通过原型链实现继承,正常情况一个对象(或者类)只能继承一个对象(或者类)。但通过这两个方法允许一个对象继承两个对象。
let foo = {
foo () {
console.log('foo')
}
}
let bar = {
bar () {
console.log('bar')
}
}
// 正常状态下,对象只能继承一个对象,要么有 foo(),要么有 bar()
let sonOfFoo = Object.create(foo);
sonOfFoo.foo(); // foo
let sonOfBar = Object.create(bar);
sonOfBar.bar(); // bar
// 黑科技开始
let sonOfFooBar = new Proxy({}, {
get (target, key) {
return target[key] || foo[key] || bar[key];
}
})
// 我们创造了一个对象同时继承了两个对象,foo() 和 bar() 同时拥有
sonOfFooBar.foo(); // foo 有foo方法,继承自对象foo
sonOfFooBar.bar(); // bar 也有bar方法,继承自对象bar
总结:
1、Proxy代理了整个对象,Object.defineProperty()只代理对象上的某个属性
2、Proxy在调用时递归,Object.defineProperty()在一开始就全部递归,Proxy性能更优
3、Proxy可以代理数组,数组的一些增删改查它可以拦截掉,Object.defineProperty()不可以
4、对象新添加属性Proxy可以监听到,Object.defineProperty()不可以
5、Proxy不兼容IE,Object.defineProperty()不兼容IE8及以下