js的Proxy对象

1,285 阅读9分钟

前言

在js里面对一个js对象进行拦截和自定义操作是非常常见的场景。那么今天介绍一下如何通过Proxy对象进行创建一个对象的代理。

语法

const p = new Proxy(target, handler)

参数

  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

handler的捕获器

handler.getPrototypeOf()

当读取代理对象的原型时,该方法就会被调用。

  • 语法
const p = new Proxy(obj, {
  // target 被代理的目标对象。
  getPrototypeOf(target) {
  
  // ture,当 getPrototypeOf 方法被调用时,this 指向的是它所属的处理器对象。
  console.log(this === handler)
  
  // getPrototypeOf 方法的返回值必须是一个对象 或者 null。
  return null
  }
});

  • 以下五种方法可以触发 getPrototypeOf() 代理方法运行

    • Object.getPrototypeOf(),方法返回指定对象的原型(内部[[Prototype]]属性的值)。

    • Reflect.getPrototypeOf(),方法返回指定对象的原型 (即内部的 [[Prototype]] 属性的值) 。

    • __proto__ (该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。)Object.prototype 的 __proto__ 属性是一个访问器属性(一个getter函数和一个setter函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。

    • Object.prototype.isPrototypeOf(),用于测试一个对象是否存在于另一个对象的原型链上。

      let arr = []
      let obj = {}
      // true
      console.log(Array.prototype.isPrototypeOf(arr))
      // false
      console.log(Array.prototype.isPrototypeOf(obj))
      
    • instanceof,用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

      let arr = []
      let obj = {}
      // true
      console.log(arr instanceof Array)
      // false
      console.log(obj instanceof Array)
      
  • 例子

let monster1 = {
  eyeCount: 4,
  test: 1
};

let monsterPrototype = {
  eyeCount: 20,
  age: 10
};

let handler = {
  getPrototypeOf(target) {
    return monsterPrototype;
  }
};

let proxy1 = new Proxy(monster1, handler);

monsterPrototype.age = 30
let proxy1Prototype = Object.getPrototypeOf(proxy1)

// 30
console.log(proxy1Prototype.age, 1)
// 20
console.log(proxy1Prototype.eyeCount, 2)
// undefined
console.log(proxy1Prototype.test, 3)

handler.setPrototypeOf()

handler.setPrototypeOf() 方法主要用来拦截 Object.setPrototypeOf()、Reflect.setPrototypeOf()。

  • 语法
var p = new Proxy(target, {
  // target, 被拦截目标对象.
  // prototype, 对象新原型或为null.
  setPrototypeOf: function(target, prototype) {
    // 如果成功修改了[[Prototype]], setPrototypeOf 方法返回 true,否则返回 false.
    return true
  }
});

  • 例子 如果你不想为你的对象设置一个新的原型,你的处理者的setPrototypeOf方法可以返回false,也可以抛出异常。
var handlerReturnsFalse = {
    setPrototypeOf(target, newProto) {
        return false;
        // or
        // throw new Error('custom error');
    }
};

var newProto = {}, target = {};

var p1 = new Proxy(target, handlerReturnsFalse);
Object.setPrototypeOf(p1, newProto); // throws a TypeError
Reflect.setPrototypeOf(p1, newProto); // returns false

handler.isExtensible()

handler.isExtensible() 方法用于拦截对对象的Object.isExtensible()。(Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。)

  • 语法
var p = new Proxy(target, {
  // target,目标对象
  isExtensible: function(target) {
    // isExtensible方法必须返回一个 Boolean值或可转换成Boolean的值。
    return true
  }
});

  • 注意 Object.isExtensible(proxy) 必须同Object.isExtensible(target)返回相同值。
var p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});
// TypeError is thrown
Object.isExtensible(p);
var p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});
Object.preventExtensions(p);
// 返回false
Object.isExtensible(p);

handler.preventExtensions()

handler.preventExtensions() 方法用于设置对Object.preventExtensions()的拦截。(Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。)

  • 语法
var p = new Proxy(target, {
  // target, 所要拦截的目标对象.
  preventExtensions: function(target) {
    // 返回布尔值, 如果目标对象是可扩展的,那么只能返回 false
    return true
  }
});

  • 注意 handler.preventExtensions() 拦截 Object.preventExtensions()返回一个布尔值.
  • 例子
var p = new Proxy({}, {
  preventExtensions: function(target) {
    console.log('called');
    Object.preventExtensions(target);
    return true;
  }
});
// 返回p
console.log(Object.preventExtensions(p)); 

handler.getOwnPropertyDescriptor()

handler.getOwnPropertyDescriptor()用于对Object.getOwnPropertyDescriptor()方法的拦截( Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性) )

  • 语法
var p = new Proxy(target, {
  // target: 目标对象,prop: 返回属性名称的描述
  getOwnPropertyDescriptor: function(target, prop) {
    // 返回一个 object 或 undefined。
    return object
  }
});

  • 例子
var handler = {
  getOwnPropertyDescriptor (target, key) {
     if (key[0] === '_') {
       return undefined;
     }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'foo', baz: 'baz' };

var proxy = new Proxy(target, handler);

console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'))
// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'))
// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'))

handler.defineProperty()

handler.defineProperty() 用于拦截对对象的 Object.defineProperty() 操作。(Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。)

  • 语法
var p = new Proxy(target, {
  // target: 目标对象,property: 待检索其描述的属性名, descriptor: 待定义或修改的属性的描述符。
  defineProperty: function(target, property, descriptor) {
    // 返回一个 Boolean 值,表示定义该属性的操作成功与否。
    return true
  }
});

  • 对以下方法进行拦截
    • Object.defineProperty()
    • Reflect.defineProperty()
    • proxy.property='value'
  • 例子
var p = new Proxy({}, {
  defineProperty: function(target, prop, descriptor) {
    console.log('called: ' + prop);
    return true;
  }
});

var desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p, 'a', desc); // "called: a"

handler.has()

handler.has()用于in操作符拦截(in操作符:如果指定的属性在指定的对象或其原型链中,则in 运算符返回true。)

  • 语法
var p = new Proxy(target, {
  // target: 目标对象, prop: 需要检查是否存在的属性
  has: function(target, prop) {
    // 返回一个boolean值
    return true
  }
});

  • 对以下方法进行拦截

    • 属性查询: foo in proxy
    • 继承属性查询: foo in Object.create(proxy)
    • with 检查: with(proxy) { (foo); }
    • Reflect.has()
  • 例子

var obj = { a: 10 };

var p = new Proxy(obj, {
  has: function(target, prop) {
    return true;
  }
});

'a' in p; 
  • 注意 如果违反了下面这些规则, proxy 将会抛出 TypeError:
  • 如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏.
  • 如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
  has: function(target, prop) {
    return false;
  }
});

'a' in p; // TypeError is thrown

handler.get()

handler.get() 方法用于拦截对象的读取属性操作。

  • 语法
var p = new Proxy(target, {
  // target: 目标对象,property: 被获取的属性名 ,receiver:Proxy或者继承Proxy的对象
  get: function(target, property, receiver) {
    // get方法可以返回任何值。
    return any
  }
});

  • 对以下方法进行拦截
  • 访问属性: proxy[foo]和 proxy.bar
  • 访问原型链上的属性: Object.create(proxy)[foo]
  • Reflect.get()
  • 例子
var p = new Proxy({
  age: 10,
  name: 'lili'
}, {
  get: function(target, prop, receiver) {
   if(prop === 'age') { return 18 }
   return target[prop]
  }
});
// 18
console.log(p.age)
// lili
console.log(p.name)

handler.set()

handler.set() 方法是设置属性值操作的捕获器。

  • 语法
const p = new Proxy(target, {
 // target: 目标对象,property: 将被设置的属性名或 Symbol, value: 新属性值, receiver: 最初被调用的对象
  set: function(target, property, value, receiver) {
    // 返回一个布尔值。
    return true
  }
});
  • 对以下方法进行拦截

    • 指定属性值:proxy[foo] = bar 和 proxy.foo = bar
    • 指定继承者的属性值:Object.create(proxy)[foo] = bar
    • Reflect.set()
  • 例子

var p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    target[prop] = value
    return true;
  }
})

p.a = 102;               
// 102
console.log(p.a); 

handler.deleteProperty()

handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。

  • 语法
var p = new Proxy(target, {
  // target: 目标对象,property: 待删除的属性名
  deleteProperty: function(target, property) {
    // 返回一个 Boolean 类型的值,表示了该属性是否被成功删除。
    return true
  }
});

  • 对以下方法进行拦截

    • 删除属性: delete proxy[foo] 和 delete proxy.foo
    • Reflect.deleteProperty()
  • 例子

var p = new Proxy({a: 123}, {
  deleteProperty: function(target, prop) {
    return false;
  }
});

delete p.a; 
// 123
console.log(p.a)

handler.ownKeys()

handler.ownKeys() 方法用于拦截 Reflect.ownKeys()

  • 语法
var p = new Proxy(target, {
  // target: 目标对象
  ownKeys: function(target) {
    // 返回一个可枚举对象
    return obj 
  }
});

  • 对以下方法进行拦截
    • Object.getOwnPropertyNames()
    • Object.getOwnPropertySymbols()
    • Object.keys()
    • Reflect.ownKeys()
  • 例子
var p = new Proxy({
 a: 1,
 b: 2
}, {
  ownKeys: function(target) {
    return Reflect.ownKeys(target)
  }
});
// ['a', 'b']
console.log(Object.keys(p))

handler.apply()

handler.apply() 方法用于拦截函数的调用。

  • 语法
var p = new Proxy(target, {
  // target: 目标对象,thisArg: 被调用时的上下文对象, argumentsList: 被调用时的参数数组
  apply: function(target, thisArg, argumentsList) {
    // 可以返回任何值
    return any
  }
});

  • 对以下方法进行拦截

    • proxy(...args)
    • Function.prototype.apply() 和 Function.prototype.call()
    • Reflect.apply()
  • 例子

var p = new Proxy(function() {}, {
  apply: function(target, thisArg, argumentsList) {
    return argumentsList[0] + argumentsList[1] + argumentsList[2];
  }
});
// 6
console.log(p(1, 2, 3)); 
                         

handler.construct()

handler.construct() 方法用于拦截new 操作符. 为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。

  • 语法
var p = new Proxy(target, {
  // target: 目标对象,argumentsList: constructor的参数列表, newTarget: 最初被调用的构造函数,就这个例子而言是p。
  construct: function(target, argumentsList, newTarget) {
    // 必须返回一个对象
    return obj
  }
});

  • 对以下方法进行拦截
    • new proxy(...args)
    • Reflect.construct()
  • 例子
let p = new Proxy(function () {}, {
  construct: function(target, args) {
    return { value: args[0] * 10 };
  }
});
// { value: 10 }
console.log(new p(1))

场景

看一下proxy的使用例子,加深理解

当对象中不存在属性名时,默认返回'该属性未被定义'

例子:

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : '该属性未被定义';
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

// 打印结果:1 undefined "该属性未被定义"
console.log(p.a, p.b, p.c);  

// 'c' in p // 查询p上是否有'c'属性,无论属性在原型还是在实例中都会返回true
// 打印false
console.log('c' in p);

验证向一个对象的传值

例如person对象有一个age的属性,我们加个条件限制,age只能是number类型,并且不能超过100

例子:

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 100) {
        throw new RangeError('The age seems invalid');
      }
    }

    obj[prop] = value;

    // 表示成功
    return true;
  }
};

let person = new Proxy({}, validator);

person.age = 17;

console.log(person.age);
// 100

person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer

person.age = 200;
// 抛出异常: Uncaught RangeError: The age seems invalid

操作DOM节点

例如设置最后选中的dom元素的aria-selected属性为true,之前选中的则全为false

  • html代码
<!DOCTYPE html>
<html>
<head>
<title>文档的标题</title>
</head>

<body>
<div class="class1" id="1">aaa</div>
<div class="class2" id="2">bbb</div>
<div class="class3" id="3">ccc</div>
</body>

</html>
<style>
    .class1 {
        color: aqua;
    }
    .class2 {
        color: blue;
    }
    .class3 {
        color: pink;
    }
</style>
  • js代码
let view = new Proxy({
  selected: null
}, {
  set: function(obj, prop, newval) {
    let oldval = obj[prop];

    if (prop === 'selected') {
      if (oldval) {
        oldval.setAttribute('aria-selected', 'false');
      }
      if (newval) {
        newval.setAttribute('aria-selected', 'true');
      }
    }

    // 默认行为是存储被传入 setter 函数的属性值
    obj[prop] = newval;

    // 表示操作成功
    return true;
  }
});

let i1 = view.selected = document.getElementById('1');
let i2 = view.selected = document.getElementById('2');
let i3 = view.selected = document.getElementById('3');
  • 最后执行结果

image.png

值修正及附加属性

加上products对象的browsers是存储浏览器名称的数组,browsers.latestBrowser表示最新的浏览器,我们如何实现products.browsers = xxx, xxx是string的情况,如何默认转化成数组

let products = new Proxy({
  browsers: ['Internet Explorer', 'Netscape']
}, {
  get: function(obj, prop) {
    // 附加一个属性
    if (prop === 'latestBrowser') {
      return obj.browsers[obj.browsers.length - 1];
    }

    // 默认行为是返回属性值
    return obj[prop];
  },
  set: function(obj, prop, value) {
    // 附加属性
    if (prop === 'latestBrowser') {
      obj.browsers.push(value);
      return;
    }

    // 如果不是数组,则进行转换
    if (typeof value === 'string') {
      value = [value];
    }

    // 默认行为是保存属性值
    obj[prop] = value;

    // 表示成功
    return true;
  }
});

console.log(products.browsers); // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox';  // 如果不小心传入了一个字符串
console.log(products.browsers); // ['Firefox'] <- 也没问题, 得到的依旧是一个数组

products.latestBrowser = 'Chrome';
console.log(products.browsers);      // ['Firefox', 'Chrome']
console.log(products.latestBrowser); // 'Chrome'

vue3.0为何使用Proxy来代替Object.defineProperty

首先vue采用数据劫持结合发布-订阅者模式实现双向绑定

Object.defineProperty实现数据劫持的缺点

  • Object.defineProperty的第一个缺陷,无法监听数组变化
  • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

Proxy实现数据劫持的优点

  • Proxy可以直接监听对象而非属性
  • Proxy可以直接监听数组的变化
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

Proxy的缺点

Proxy的劣势就是兼容性问题,而且无法用polyfill磨平

关于这个话题

关于"vue3.0为何使用Proxy来代替Object.defineProperty"这个话题,推荐下juejin.cn/post/684490… 这篇文章,介绍的非常详细。

最后

感谢大家阅读,如有问题欢迎纠正!