JavaScript中的魔幻代理Proxy

时间: 2018-09-21阅读: 130标签: js知识

什么是代理?

让我们看三个案例:

A: 上小学的时候,李小红来你家叫你出去玩,第一个回应的不是你自己,是你妈:“王小明在家写作业,今天不出去!”

B: 上中学的时候,赵二虎带着小弟们放学在校门口等着揍你,走在前面的不是你自己,是二虎他爸:“考试没及格还学会装黑社会了!”拎起二虎就是一顿胖揍。

C: 上了大学,躺在宿舍里的床上,好饿。出门买饭并交代好不要葱蒜多放辣最后还直接端到床上的不是你自己,是快递小哥。

这些都是代理。


什么是 JavaScript 代理?

用官方的洋文来说,是 Proxy

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

通过 Proxy 我们可以拦截并改变一个对象的几乎所有的根本操作,包括但不限于属性查找、赋值、枚举、函数调用等等。

在生活中,通过代理我们可以自动屏蔽小红的邀请、自动赶走二虎的威胁、自动买好干净的饭端到床上。在 JavaScript 世界里,代理也可以帮你做类似的事情,接下来让我们一起琢磨一番。


初识代理:Hello World

以小学经历为例子,心里是喜欢小红的,于是我们定义:

const me = { name: '小明', like: '小红' }

这个时候如果调用 console.log(me.like),结果必然是 小红。然而生活并不是这样,作为一个未成年人,总是有各种的代理人围绕在你身边,比如这样:

const meWithProxy = new Proxy(me, {
  get(target, prop) {
    if (prop === 'like') {
      return '学习';
    }
    return target[prop];
  }
});

这个时候如果调用 console.log(me.like) 依然是 小红 ,因为真心不会说谎。但当我们调用 console.log(meWithProxy.like) 的时候,就会可耻的输出 学习 ,告诉大家说我们喜欢的是 学习 。


小试牛刀:不要停止我的音乐

刚才我们简单了解了代理能够拦截对象属性的获取,可以隐藏真实的属性值而返回代理想要返回的结果,那么对于对象属性的赋值呢?让我们一起来看看。

假设你正在听音乐:

const me = { name: '小明', musicPlaying: true }

此时如果我们执行 me.musicPlaying = false 这样就轻而易举地停止了你的音乐,那么如果我们挂上代理人:

const meWithProxy = new Proxy(me, {
  set(target, prop, value) {
    if (prop === 'musicPlaying' && value !== true) {
      throw Error('任何妄图停止音乐的行为都是耍流氓!');
    }
    target[prop] = value;
  }
});

这时候如果我们执行 me.musicPlaying = false,就会被毫不留情地掀了桌子:

> meWithProxy.musicPlaying = false
Error: 任何妄图停止音乐的行为都是耍流氓!
    at Object.set (repl:4:13)
>

释放魔法:封装全宇宙所有 RESTful API

现在我们已经知道通过 Proxy 可以拦截属性的读写操作,那然后呢?没什么用?

仅仅是拦截属性的读写操作,的确没有太大的发挥空间,或许可以方便的做一些属性赋值校验工作等等。但是,或许你还没有意识到一个惊人的秘密:Proxy 在拦截属性读写操作时,并不在乎属性是否真的存在!

那么,也就是说:利用 Proxy,我们可以拦截并不存在的属性的读取。

再进一步思考:利用 Proxy,我们可以在属性读取的那一瞬间,动态构造返回结果。

然而,属性并不局限于字符串、布尔值,属性可以是对象、函数、任何东西。

至此,你想到了什么?

没想到?不要紧!根据刚才的分析,让我们一起通过下面 17 行代码,来封装全宇宙所有的 RESTful API !

import axios from 'axios';
const api = new Proxy({}, {
  get(target, prop) {
    const method = /^[a-z]+/.exec(prop)[0];
    const path = '/' + prop
          .substring(method.length)
          .replace(/([a-z])([A-Z])/g, '$1/$2')
          .replace(/\$/g, '/$/')
          .toLowerCase();
    return (...args) => { // <------ 返回动态构造的函数!
      const url = path.replace(/\$/g, () => args.shift());
      const options = args.shift() || {};
      console.log('Requesting: ', method, url, options);
      return axios({ method, url,  ...options });
    }
  }
});

定义了 api 这个代理之后,我们就可以像下面这样调用:

api.get()
// GET /

api.getUsers()
// 获取所有用户
// GET /users

api.getUsers$Books(42)
// 获取 ID 为 42 的用户的所有书籍
// GET /users/42/books

api.getUsers$Books(42, { params: { page: 2 } })
// 获取 ID 为 42 的用户的所有书籍的第二页
// GET /users/42/books?page=2

api.postUsers({ data: { name: '小明' } })
// 创建名字为 小明 的用户
// POST /users Payload { name: '小明' }

以上所有的函数都在你调用的那一瞬间,通过代理人的魔法之手动态生成,供我们随意取用。

简洁、优雅,哇~ 真是太棒啦!


终极魔幻:通读代理人的魔法秘笈

到此,我们仅仅使用 Proxy 改造了对象的属性获取、赋值操作,而对于 Proxy 来说,只是冰山一角。

Proxy 的基本语法如下:

new Proxy(target, handler)

其中 target 是即将被代理的对象(比如:想要出门找小红玩耍的 me),handler 就是代理的魔法之手,用来拦截、改造 target 的行为。

对于 handler 对象,我们刚才仅仅用到了 get、set 函数,而实际上一共有 13 种可代理的操作:

  • handler.getPrototypeOf()

    在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。

  • handler.setPrototypeOf()

    在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。

  • handler.isExtensible()

    在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。

  • handler.preventExtensions()

    在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。

  • handler.getOwnPropertyDescriptor()

    在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, “foo”) 时。

  • handler.defineProperty()

    在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, “foo”, {}) 时。

  • handler.has()

    在判断代理对象是否拥有某个属性时触发该操作,比如在执行 “foo” in proxy 时。

  • handler.get()

    在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。

  • handler.set()

    在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。

  • handler.deleteProperty()

    在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。

  • handler.ownKeys()

    在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。

  • handler.apply()

    在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。

  • handler.construct()

    在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。

对于以上 13 种可代理的操作,还需要读者自行研究并实践方可踏上终极魔幻之旅。同学,我看好你。


参考链接:

本文来源链接:https://knownsec-fed.com/2018-01-27-javascript-mo-huan-dai-li/


js常见知识点整理总结

一些常用的JavaScript 知识点整理,包括:两个函数是否等价、NaN是什么?它是什么类型?如何检测一个变量是否是NaN?作用域相关问题?js小数计算不准确的bug,js算法/思路相关,js类型强制转换

适配器在JavaScript中的体现

适配器设计模式在JavaScript中非常有用,在处理跨浏览器兼容问题、整合多个第三方SDK的调用,都可以看到它的身影。适配器模式是一种软件设计模式,允许从另一个接口使用现有类的接口。它通常用于使现有的类与其他类一起工作,而无需修改其源代码。

JavaScript中的循环和作用域

JavaScript有一个特点,也许会让开发者头痛, 是与循环和作用域相关的.const。最简单的方案是用 let 声明、另外一个非常普遍的解决这个问题是使用pre-ES6代码, 同时它被称作即时调用函数表达式(IIFE)

JavaScript中的行为委托

行为委托简单来说就是一种设计模式,不同于传统的构造函数的类式设计。这里每个例子会通过构造函数,class和行为委托来不同实现,不过不会评论class,是否使用class取决于你的观点。

打造自己的JavaScript武器库

作为战斗在业务一线的前端,要想少加班,就要想办法提高工作效率。这里提一个小点,我们在业务开发过程中,经常会重复用到日期格式化、url参数转对象、浏览器类型判断、节流函数等一类函数,这些工具类函数

(...)这三个点在JavaScript中意味着什么?

解释JavaScript中三个点的作用:数组/对象扩展运算符、rest运算符(使用函数的参数时,无论是完全替换参数还是与函数的参数一起替换参数,这三个点也称为rest运算符)

await在forEach不起作用解决

我们知道await这个机制肯定是没问题的,如果真的有问题肯定不会轮到我测出来,那么其实剩下来的问题只能是for遍历的原因了。lodash的forEach和[].forEach不支持await,如果非要一边遍历一边执行await,可使用for-of

你不知道的 js 保留字

先是笼统的说一下有什么保留字,保留字的话根据犀牛书的划分,可有分为以下几类:基础保留字、严格模式下的保留字、严格模式下的不完全保留字、ECMAScript3的保留字、ECMAScipt 5 的保留字、全局变量和函数

js实现获取手机相册并上传

当初有很多人说使用form方法将文件封装来上传,可是因为要照顾到从相机中选择图片,所以一直没有去做。 后来看到了Uploader的方法来传文件,感觉自己找到了 。是使用plus.uploader来完成的

js中offset、scroll、event、client的区别和使用

用一句话概述:offset用于获取元素的实际显示尺寸 , scroll用于获取滚动后全部尺寸 , client用于获取不包括滚动条的实际显示尺寸,event用于获取鼠标的坐标位置。下面就详细介绍它们之间的使用和区别