[原文链接]http://feclub.cn/post/content/proxy_reflect

Proxy & Reflect

Reflect

先讲 Reflect 是因为 Proxy 要配合 Reflect 使用的

  1. Reflect 兼容性
  2. Reflect 是什么
  3. Reflect 对象的设计目的(为什么)
  4. Reflect 静态方法(怎么用)
  5. Reflect 操作对象与老方法的对比优势(优势对比)
  6. Reflect 操作对象能力扩展举个例子:(能力扩充举例)

Reflect 兼容性

caniuse 上居然没有 Reflect 的兼容性

MDN 上有有个兼容性表格,IE全跪了,enumerate 属性也全都不可用

1.Reflect 是什么

MDN: Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
阮一峰: Reflect对象是 ES6 为了操作对象而提供的新 API。

关键词: 对象, 操作对象

2.Reflect 对象的设计目的(为什么)

  • (1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上(Reflect.defineProperty)。未来新的方法将只部署在Reflect
  • (2)修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty在无法定义属性时,会报错,而Reflect.defineProperty则会返回false
Object.defineProperty([], 'length', { get() {return 10} }) 
//TypeError: Cannot redefine property: length

Reflect.defineProperty([], 'length', { get() {return 10} })
//false

兼容报错

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
  • (3)让Object命令式操作都变成函数行为
'assign' in Object // true 命令式操作
Reflect.has(Object, 'assign') // true 函数行为
const obj = { name: 'cg', age: 18 }
delete obj.name //命令式操作,删除 name 属性
Reflect.deleteProperty(obj, 'age') //函数式操作,删除age属性
  • (4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

举例:

const target = {}
const proxy = new Proxy(target, {
  set(target, name, value, receiver) {
    return Reflect.set(target, name, value, receiver)
  }
})

// 因为 Proxy 的 set 方法有4个参数 (target, name, value, receiver),
// 而 Reflect 的 set 方法也有这4个参数 (target, name, value, receiver)
// 它们一一对应,所以能很完美地用 Reflect 操作 Proxy 对象

Reflect 静态方法(怎么用)

阮一峰: 详细的api举例

MDN: 介绍

Reflect对象一共有 13 个静态方法。

Reflect.get(target, name, receiver)             
    //target[name]
Reflect.set(target, name, value, receiver)      
    //target[name] = value
Reflect.apply(func, thisArg, args)
    //Function.prototype.apply.call(func, thisArg, args)
Reflect.construct(target, args)                 
    //new target(...args)
Reflect.defineProperty(target, name, desc)
    //Object.defineProperty(target, name)
Reflect.deleteProperty(target, name)    
    //delete target[name]
Reflect.has(target, name)                       
    //name in target
Reflect.ownKeys(target) 
    //Object.getOwnPropertyNames(target) + Object.getOwnPropertySymbols(target)
Reflect.isExtensible(target)
    //Object.isExtensible 是否可扩展
Reflect.preventExtensions(target)   
    //Object.preventExtensions(target) 阻止扩展
Reflect.getOwnPropertyDescriptor(target, name)
    //Object.getOwnPropertyDescriptor(target, name) 
Reflect.getPrototypeOf(target)
    //Object.getPrototypeOf(target)  读取 __proto__
Reflect.setPrototypeOf(target, prototype)
    //Object.setPrototypeOf(target, prototype)  设置 __proto__

对于处理非法参数的错误机制,Reflect新方法和Object老方法的区别

1.defineProperty无法定义属性时,Reflect返回false,Object报错

Object.defineProperty([], 'length', { get() {return 10} }) 
//TypeError: Cannot redefine property: length

Reflect.defineProperty([], 'length', { get() {return 10} })  //false

2.getOwnPropertyDescriptor如果第一个参数不是对象,Object返回undefinedReflect报错

Object.getOwnPropertyDescriptor(1, 'foo')  // undefined

Reflect.getOwnPropertyDescriptor(1, 'foo')
// TypeError: Reflect.getOwnPropertyDescriptor called on non-object

3.isExtensible(对象是否可扩展)如果参数不是对象,Object返回false,Reflect报错

Object.isExtensible(1) // false
Reflect.isExtensible(1) 
// TypeError: Reflect.isExtensible called on non-object

4.preventExtensions(阻止对象扩展),如果参数不是对象,Object在ES5环境报错,在ES6环境返回原参数,Reflect则报错

// ES5 环境
Object.preventExtensions(1) // 报错

// ES6 环境
Object.preventExtensions(1) // 1

// 新写法
Reflect.preventExtensions(1) // 报错

Reflect操作对象与老方法的对比优势(优势对比)

  1. Reflect更加符合面向对象,操作对象的方法全部都挂在Reflect
Reflect操作对象 老方法操作对象
面向对象 全部挂在Reflect对象上,更加符合面向对象 各种指令方法,= in delete
函数式 所有方法都是函数 命令式、赋值、函数混用
规范报错 defineProperty无效返false,后面几个方法参数非法报错 defineProperty无效报错,后面几个方法参数非法不报错
方法扩展 参数receiver指定this指向 不能

Reflect 操作对象能力扩展举个例子:(能力扩充举例)

方法get, set中receiver参数指定this,获取、设置反射属性

//get
const Ironman = {
  firstName: 'Tony',
  lastName: 'Stark',
  get fullName() {return `${this.firstName} ${this.lastName}`}
}
//获取自身属性,新老方法都可以实现
Reflect.get(Ironman, 'firstName') //Tony
Reflect.get(Ironman, 'lastName') //Tony
Reflect.get(Ironman, 'fullName') //Tony Stark

const Spiderman = {
  firstName: 'Peter',
  lastName: 'Parker'
}

//获取反射属性,只有 Reflect 可以实现
Reflect.get(Ironman, 'fullName', Spiderman) //Peter Parker
//set
const Ironman = {
  hobbies: [],
  set like(value) {
    return this.hobbies.push(value)
  }
}
//设置自身属性,新老方法都可以实现
Reflect.set(Ironman, 'like', 'money')
Reflect.set(Ironman, 'like', 'girls')
Ironman.hobbies //["money", "girls"]

const Spiderman = {
  hobbies: []
}
//设置反射属性,只有 Reflect 可以实现
Reflect.set(Ironman, 'like', 'games', Spiderman)
Reflect.set(Ironman, 'like', 'animation', Spiderman)
Spiderman.hobbies //["games", "animation"]

Proxy 概述

  1. Proxy 的兼容性
  2. Proxy 是什么
  3. Proxy 为什么要设计
  4. Proxy 怎么用
  5. Proxy 的13种拦截方法
  6. Proxy 在双向绑定中比 Object.defineProperty 的优势
  7. 举例

Proxy 兼容性

Proxy 是什么

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

关键词:元编程

Proxy 为什么要设计

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

关键词:拦截操作代理对象

Proxy 怎么用

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

Proxy 拦截对象obj读取(get)和设置(set)行为:

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});

Proxy 与 Reflect 配合操作对象非常完美,因为它们的参数完全相同

Proxy 的用法的

var proxy = new Proxy(target, handler);
//target参数表示所要拦截的目标对象
//handler参数也是一个对象,用来定制拦截行为。

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。

举个简单例子,Proxy 拦截对属性的读取

const Ironman = {
  name: 'Stark',
  age: 18,
  hobbies: ["money", "girls"]
}
const proxy = new Proxy(Ironman, {
  get: function(target, property) {
    return 'I am Ironman';
  }
});

proxy.name // I am Ironman
proxy.age // I am Ironman
proxy.hobbies // I am Ironman
proxy.xxx   // I am Ironman

Proxy 的 handler 方法

Proxy 拦截的操作(handler)一共有13种

get(target, propKey, receiver)
    // 拦截 target[propKey]
set(target, propKey, value, receiver)
    // 拦截 target[propKey] = value
has(target, propKey)
    // 拦截 propKey in proxy
deleteProperty(target, propKey)
    // 拦截 delete proxy[propKey]
ownKeys(target)
    // 拦截 Object.getOwnPropertyNames(proxy)、
    // Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in
getOwnPropertyDescriptor(target, propKey)
    // 拦截 Object.getOwnPropertyDescriptor(proxy, propKey)
defineProperty(target, propKey, propDesc)
    // 拦截 Object.defineProperty(proxy, propKey, propDesc)、
    // Object.defineProperties(proxy, propDescs)
preventExtensions(target)
    // 拦截 Object.preventExtensions(proxy)
getPrototypeOf(target)
    // 拦截 Object.getPrototypeOf(proxy)
isExtensible(target)
    // 拦截 Object.isExtensible(proxy)
setPrototypeOf(target, proto)
    // 拦截 Object.setPrototypeOf(proxy, proto)
apply(target, object, args)
    // 拦截 Function.prototype.apply.call(func, thisArg, args)
construct(target, args)
    // 拦截 new target(...args)

可以看到,Proxy 的拦截操作正好和 Reflect 操作一一对应,所以 Proxy 配合 Reflect 拦截对象操作非常完美

Proxy 拦截读取、改写下划线"_"开头的属性

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {
  _prop: 'hello'
};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写自定义私有属性的目的。

Tips: 对于 Proxy 拦截下划线“_”开头的属性,在内部调用是否可行的问题,做了测试,结论如下:

  1. 代理对象 proxy 获取 _prop 会报错, 原对象 target 可以获取 _prop
  2. 对于内部用 this 调用自己的属性 _prop 时候,要看 this 的指向,如果 this 指向代理对象 proxy ,就会报错; 如果 this 指向原对象 target ,则可以正常调用。

代码如下

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {
  _prop: 'hello',
  get a() {
    console.log(this)
    return this._prop
  },
  say() {
    console.log(this)
    console.log(this._prop)
  }
};
const proxy = new Proxy(target, handler);

proxy._prop   //报错
proxy.a       //target  hello
proxy.say()   //proxy   报错

target._prop  //hello
target.a      //target  hello
target.say()  //target  hello

计算属性 a 内的 this, 固定指向了原对象 target, 而函数 say,内部的 this 需要看调用者了,如果是 proxy 调用,就是从 proxy 中获取 _prop ,自然就报错了

实现双向绑定Proxy 比 defineproperty 优劣如何?

Vue 中实现数据双向绑定是基于 Object.defineProperty,而Vue的作者宣称将在Vue3.0版本后加入Proxy从而代替Object.defineProperty

极简版的双向绑定

const obj = {};
Object.defineProperty(obj, 'text', {
  get: function() {
    console.log('get val');
  },
  set: function(newVal) {
    console.log('set val:' + newVal);
    document.getElementById('input').value = newVal;
    document.getElementById('p').innerHTML = newVal;
  }
});

const input = document.getElementById('input');
input.addEventListener('keyup', function(e){
  obj.text = e.target.value;
})

Object.defineProperty 的缺陷

1.Object.defineProperty , 无法监听数组变化。
  • 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法,vm.lists[indexOfLists]=newValue这种是无法检测的。
push()
pop()
shift()
unshift()
splice()
sort()
reverse()

其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了,以下是方法示例。

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等方法
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改变啦! 4

// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 4

由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的

2.Object.defineProperty 只能劫持对象的属性, 因此对需要双向绑定的属性需要显示地定义
var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

Proxy实现的双向绑定的特点

Proxy 提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,ProxyObject.defineProperty的全方位加强版

1.Proxy可以直接监听对象而非属性

Proxy 的极简版双向绑定

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup', function(e) {
  newObj.text = e.target.value;
});

我们可以看到,Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

2.Proxy可以直接监听数组的变化

Proxy 双向绑定渲染数组列表

const list = document.getElementById('list');
const btn = document.getElementById('btn');

// 渲染列表
const Render = {
  // 初始化
  init: function(arr) {
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < arr.length; i++) {
      const li = document.createElement('li');
      li.textContent = arr[i];
      fragment.appendChild(li);
    }
    list.appendChild(fragment);
  },
  // 我们只考虑了增加的情况,仅作为示例
  change: function(val) {
    const li = document.createElement('li');
    li.textContent = val;
    list.appendChild(li);
  },
};

// 初始数组
const arr = [1, 2, 3, 4];

// 监听数组
const newArr = new Proxy(arr, {
  get: function(target, key, receiver) {
    console.log(key);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key !== 'length') {
      Render.change(value);
    }
    return Reflect.set(target, key, value, receiver);
  },
});

// 初始化
window.onload = function() {
    Render.init(arr);
}

// push数字
btn.addEventListener('click', function() {
  newArr.push(6);
});

很显然,Proxy不需要hack就可以无压力监听数组的变化,我们都知道,标准永远优先于hack。

3.Proxy的其他优势
  • Proxy有多达13种拦截方法,不限于applyownKeysdeletePropertyhas等等是Object.defineProperty不具备的。
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
4.Proxy的劣势

当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。