• 技术文章 >web前端 >js教程

    详解JavaScript中的Proxy(代理)

    青灯夜游青灯夜游2020-11-30 17:55:34转载2024

    Proxy是什么

    首先,我们要清楚,Proxy是什么意思,这个单词翻译过来,就是 代理

    可以理解为,有一个很火的明星,开通了一个微博账号,这个账号非常活跃,回复粉丝、到处点赞之类的,但可能并不是真的由本人在维护的。

    而是在背后有一个其他人 or 团队来运营,我们就可以称他们为代理人,因为他们发表的微博就代表了明星本人的意思。

    P.S. 强行举例子,因为本人不追星,只是猜测可能会有这样的运营团队

    这个代入到JavaScript当中来,就可以理解为对对象或者函数的代理操作。

    JavaScript中的Proxy

    Proxy是ES6中提供的新的API,可以用来定义对象各种基本操作的自定义行为 (在文档中被称为traps,我觉得可以理解为一个针对对象各种行为的钩子),拿它可以做很多有意思的事情,在我们需要对一些对象的行为进行控制时将变得非常有效。

    Proxy的语法

    创建一个Proxy的实例需要传入两个参数

    1. target 要被代理的对象,可以是一个object或者function
    2. handlers对该代理对象的各种操作行为处理
    let target = {}
    let handlers = {} // do nothing
    let proxy = new Proxy(target, handlers)
    
    proxy.a = 123
    
    console.log(target.a) // 123

    在第二个参数为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝
    (Proxy必须是浅拷贝,如果是深拷贝则会失去了代理的意义)

    Traps(各种行为的代理)

    就像上边的示例代码一样,如果没有定义对应的trap,则不会起任何作用,相当于直接操作了target

    当我们写了某个trap以后,在做对应的动作时,就会触发我们的回调函数,由我们来控制被代理对象的行为。

    最常用的两个trap应该就是getset了。

    早年JavaScript有着在定义对象时针对某个属性进行设置gettersetter

    let obj = {
      _age: 18,
      get age ()  {
        return `I'm ${this._age} years old`
      },
      set age (val) {
        this._age = Number(val)
      }
    }
    
    console.log(obj.age) // I'm 18 years old
    obj.age = 19
    console.log(obj.age) // I'm 19 years old

    就像这段代码描述的一样,我们设置了一个属性_age,然后又设置了一个get ageset age

    然后我们可以直接调用obj.age来获取一个返回值,也可以对其进行赋值。

    这么做有几个缺点:

    1. 针对每一个要代理的属性都要编写对应的gettersetter
    2. 必须还要存在一个存储真实值的key(如果我们直接在getter里边调用this.age则会出现堆栈溢出的情况,因为无论何时调用this.age进行取值都会触发getter)

    Proxy很好的解决了这两个问题:

    let target = { age: 18, name: 'Niko Bellic' }
    let handlers = {
      get (target, property) {
        return `${property}: ${target[property]}`
      },
      set (target, property, value) {
        target[property] = value
      }
    }
    let proxy = new Proxy(target, handlers)
    
    proxy.age = 19
    console.log(target.age, proxy.age)   // 19,          age : 19
    console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

    我们通过创建getset两个trap来统一管理所有的操作,可以看到,在修改proxy的同时,target的内容也被修改,而且我们对proxy的行为进行了一些特殊的处理。

    而且我们无需额外的用一个key来存储真实的值,因为我们在trap内部操作的是target对象,而不是proxy对象。

    拿Proxy来做些什么

    因为在使用了Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。

    在下边列出了几个简单的适用场景。

    解决对象属性为undefined的问题

    在一些层级比较深的对象属性获取中,如何处理undefined一直是一个痛苦的过程,如果我们用Proxy可以很好的兼容这种情况。

    (() => {
      let target = {}
      let handlers = {
        get: (target, property) => {
          target[property] = (property in target) ? target[property] : {}
          if (typeof target[property] === 'object') {
            return new Proxy(target[property], handlers)
          }
          return target[property]
        }
      }
      let proxy = new Proxy(target, handlers)
      console.log('z' in proxy.x.y) // false (其实这一步已经针对`target`创建了一个x.y的属性)
      proxy.x.y.z = 'hello'
      console.log('z' in proxy.x.y) // true
      console.log(target.x.y.z)     // hello
    })()

    我们代理了get,并在里边进行逻辑处理,如果我们要进行get的值来自一个不存在的key,则我们会在target中创建对应个这个key,然后返回一个针对这个key的代理对象。

    这样就能够保证我们的取值操作一定不会抛出can not get xxx from undefined
    但是这会有一个小缺点,就是如果你确实要判断这个key是否存在只能够通过in操作符来判断,而不能够直接通过get来判断。

    普通函数与构造函数的兼容处理

    如果我们提供了一个Class对象给其他人,或者说一个ES5版本的构造函数。
    如果没有使用new关键字来调用的话,Class对象会直接抛出异常,而ES5中的构造函数this指向则会变为调用函数时的作用域。
    我们可以使用apply这个trap来兼容这种情况:

    class Test {
      constructor (a, b) {
        console.log('constructor', a, b)
      }
    }
    
    // Test(1, 2) // throw an error
    let proxyClass = new Proxy(Test, {
      apply (target, thisArg, argumentsList) {
        // 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可
        // throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
        return new (target.bind(thisArg, ...argumentsList))()
      }
    })
    
    proxyClass(1, 2) // constructor 1 2

    我们使用了apply来代理一些行为,在函数调用时会被触发,因为我们明确的知道,代理的是一个Class或构造函数,所以我们直接在apply中使用new关键字来调用被代理的函数。

    以及如果我们想要对函数进行限制,禁止使用new关键字来调用,可以用另一个trap:construct

    function add (a, b) {
      return a + b
    }
    
    let proxy = new Proxy(add, {
      construct (target, argumentsList, newTarget) {
        throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
      }
    })
    
    proxy(1, 2)     // 3
    new proxy(1, 2) // throw an error

    用Proxy来包装fetch

    在前端发送请求,我们现在经常用到的应该就是fetch了,一个原生提供的API。
    我们可以用Proxy来包装它,使其变得更易用。

    let handlers = {
      get (target, property) {
        if (!target.init) {
          // 初始化对象
          ['GET', 'POST'].forEach(method => {
            target[method] = (url, params = {}) => {
              return fetch(url, {
                headers: {
                  'content-type': 'application/json'
                },
                mode: 'cors',
                credentials: 'same-origin',
                method,
                ...params
              }).then(response => response.json())
            }
          })
        }
    
        return target[property]
      }
    }
    let API = new Proxy({}, handlers)
    
    await API.GET('XXX')
    await API.POST('XXX', {
      body: JSON.stringify({name: 1})
    })

    GETPOST进行了一层封装,可以直接通过.GET这种方式来调用,并设置一些通用的参数。

    实现一个简易的断言工具

    写过测试的各位童鞋,应该都会知道断言这个东西
    console.assert就是一个断言工具,接受两个参数,如果第一个为false,则会将第二个参数作为Error message抛出。
    我们可以使用Proxy来做一个直接赋值就能实现断言的工具。

    let assert = new Proxy({}, {
      set (target, message, value) {
        if (!value) console.error(message)
      }
    })
    
    assert['Isn\'t true'] = false      // Error: Isn't true
    assert['Less than 18'] = 18 >= 19  // Error: Less than 18

    统计函数调用次数

    在做服务端时,我们可以用Proxy代理一些函数,来统计一段时间内调用的次数。
    在后期做性能分析时可能会能够用上:

    function orginFunction () {}
    let proxyFunction = new Proxy(orginFunction, {
      apply (target, thisArg. argumentsList) {
        log(XXX)
    
        return target.apply(thisArg, argumentsList)
      }
    })

    全部的traps

    这里列出了handlers所有可以定义的行为 (traps)

    具体的可以查看MDN-Proxy
    里边同样有一些例子

    trapsdescription
    get获取某个key
    set设置某个key
    has使用in操作符判断某个key是否存在
    apply函数调用,仅在代理对象为function时有效
    ownKeys获取目标对象所有的key
    construct函数通过实例化调用,仅在代理对象为function时有效
    isExtensible判断对象是否可扩展,Object.isExtensible的代理
    deleteProperty删除一个property
    defineProperty定义一个新的property
    getPrototypeOf获取原型对象
    setPrototypeOf设置原型对象
    preventExtensions设置对象为不可扩展
    getOwnPropertyDescriptor获取一个自有属性 (不会去原型链查找) 的属性描述

    更多编程相关知识,请访问:编程学习网站!!

    以上就是详解JavaScript中的Proxy(代理)的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:segmentfault,如有侵犯,请联系admin@php.cn删除
    专题推荐:JavaScript Proxy
    上一篇:解决JavaScript运行环境 下一篇:5种JavaScript中解构(Destructuring)的使用方法
    线上培训班

    相关文章推荐

    • 详解JavaScript中async/await的使用方法• jQuery JavaScript ajax区别是什么• 了解JavaScript中的对象原型和原型链• javascript中const和Object.freeze()的差异

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网