夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 第1章 什么是JavaScript
  • 第2章 HTML中的JavaScript
  • 第3章 语言基础
  • 第4章 变量、作用域与内存
  • 第5章 基本引用类型
  • 第6章 集合引用类型
  • 第7章 迭代器与生成器
  • 第8章 对象、类与面向对象编程
  • 第9章 代理与反射
    • 第10章 函数
    • 第11章 异步编程
    • 第12章 BOM
    • 第14章 DOM
    • 第15章 DOM扩展
    • 第16章 DOM2和DOM3
    • 第17章 事件
    • 第18章 动画与Canvas图形
    • 第19章 表单脚本
    • 第20章 JavaScript API
    • 第21章 错误处理与调试
    • 第23章 JSON
    • 第24章 网络请求与远程资源
    • 第25章 客户端存储
    • 第26章 模块
    • 第27章 工作者进程
    • 第28章 最佳实践
    • 《JavaScript高级程序设计》
    frost
    2021-11-28

    第9章 代理与反射

    # 代理基础

    # 创建空代理

    代理是使用 Proxy构造函数创建的。这个构造函数接收2个参数:目标对象和处理程序对象,均为必传。

    const target = {
      id:'hello'
    }
    
    const handler = {}
    
    const proxy = new Proxy(target,handler)
    
    console.log(proxy.id) // hello
    console.log(target.id) // hello
    // 结论1:id属性会访问同一个值
    
    target.id = 'foo'
    console.log(target.id) // foo
    console.log(proxy.id) // foo
    // 结论2:给目标属性赋值会反映到两个对象上,因为它们访问的是同一个值
    
    proxy.id = 'bar'
    console.log(target.id) // bar
    console.log(proxy.id) // bar
    // 结论3:给代理属性赋值会反映在两个对象上,因为这个赋值会转移到目标对象
    
    console.log(target.hasOwnProperty('id')) // true
    console.log(proxy.hasOwnProperty('id')) // true
    // 结论4:hasOwnProperty()方法都会应用到目标对象
    
    try{
      console.log(target instanceof Proxy)
    }catch(e){
      console.log('target-error',e) // target-error TypeError: Function has non-object prototype 'undefined' in instanceof check
    }
    try{
      console.log(proxy instanceof Proxy)
    }catch(e){
      console.log('proxy-error',e) // proxy-error TypeError: Function has non-object prototype 'undefined' in instanceof check
    }
    // 结论5:不能使用instanceof操作符
    
    console.log(target===proxy) // false
    // 结论6:严格相等可以区分代理和目标
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    # 定义捕获器

    捕获器(trap)是从操作系统中借用的概念,可以理解为拦截器,在js框架Vue中使用其特点来进行属性劫持。

    捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

    const target = {
      foo:'bar'
    }
    
    const handler = {
      get(){
        return 'handler override'
      }
    }
    
    const proxy = new Proxy(target,handler)
    
    console.log(target.foo) // bar
    console.log(proxy.foo) // handler override
    // 结论:在代理对象上执行操作才会触发捕获器,目标对象并不会触发
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 捕获器参数和反射API

    const target = {
      foo:'bar'
    }
    
    const handler ={
      get(trapTarget,property,receiver){
        console.log(trapTarget===target) // true
        console.log(property) // foo
        console.log(receiver===proxy) // true
      }
    }
    
    const proxy = new Proxy(target,handler)
    proxy.foo
    // 结论:get捕获器有3个参数,分别是目标对象、要查询的属性和代理对象
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    这些参数可以重建被捕获方法的原始行为。

    // ...
    get(trapTarget,property,receiver){
        return trapTarget[property]
      }
    // ...
    console.log(proxy.foo) // bar
    
    1
    2
    3
    4
    5
    6

    实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局Reflect对象上的 同名方法来轻松重建。

    处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方法。比如:

    // ...
    // return trapTarget[property]
        return Reflect.get(...arguments)
    // ...
    
    1
    2
    3
    4

    甚至更简洁

    // ...
    const handler ={
      get: Reflect.get
    }
    // ...
    
    1
    2
    3
    4
    5

    完整的例子

    const target = {
      foo:'bar',
      aaa:'bbb'
    }
    
    const handler ={
      get(){
        const decoration = '!!!'
        return Reflect.get(...arguments) + decoration
      }
    }
    
    const proxy = new Proxy(target,handler)
    console.log(proxy.foo) // bar!!!
    console.log(proxy.aaa) // bbb!!!
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    # 可撤销代理

    revocable方法支持撤销代理对象与目标对象的关联。撤销行为不可逆,撤销函数是幂等的。

    const target = {
      foo:'bar'
    }
    
    const handler = {
      get(){
        return 'hello'
      }
    }
    
    const {proxy,revoke} = Proxy.revocable(target,handler)
    
    console.log(target.foo) // bar
    console.log(proxy.foo) // hello
    
    revoke()
    console.log(proxy.foo) // TypeError
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    # 实用反射API

    注意:

    • 反射API并不限于捕获处理程序
    • 大多数反射API在Object类型上有对应的方法

    Object上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。

    状态标记

    下面通过2段代码进行说明,反射方法返回的“状态标记”有何作用。

    const o = {}
    try {
      Object.defineProperties(o, 'foo', 'bar')
      console.log('success')
    } catch (e) {
      console.log('fail')
    }
    
    // 重构后
    const o = {}
    const isSuccess = Reflect.defineProperty(o, 'foo', { value: 'bar' })
    if (isSuccess) {
      console.log('success')
    } else {
      console.log('fail')
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    替代操作符

    Reflect.get():可以替代对象属性访问操作符。

    Reflect.set():可以替代=赋值操作符。

    Reflect.has():可以替代in操作符

    Reflect.deletProperty():可以替代delete操作符。

    Reflect.construct():可以替代new操作符。

    # 代理的问题与不足

    代理的this

    代理与内部槽位

    比如Date内置对象,代理对象没有内部槽位[[NumberDate]],导致无法访问Date对象的方法。

    # 代理捕获器与反射方法

    代理可以捕获13种不同的基本操作。只要在代理上调用,所有捕获器都会拦截他们对应的反射API操作。

    # get()

    get()捕获器在代理对象获取属性值的操作中被调用。对应的反射API方法是Reflect.get()。

    const target = {
      foo: 1,
    }
    const proxy = new Proxy(target, {
      get(target, property, receiver) {
        console.log('getter') // getter
        return Reflect.get(...arguments)
      },
    })
    
    console.log(proxy.foo) // 1
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    # set()

    set()捕获器在设置属性值的操作中被调用。对应的反射API方法是Reflect.set()。

    const target = {
      foo: 1,
    }
    const proxy = new Proxy(target, {
      set(target, property, value, receiver) {
        console.log('setter')
        return Reflect.set(...arguments)
      },
    })
    
    proxy.foo = 2 // setter
    console.log(proxy.foo) // 2
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    # 其他捕获器

    其他11种捕获器如下

    捕获器 什么操作符中调用 对应反射API方法
    has() in Reflect.has()
    defineProperty() Object.defineProperty() Reflect.defineProperty()
    getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
    deleteProperty() delete Reflect.deleteProperty()
    ownKeys() Object.keys() Reflect.ownKeys()
    getPrototypeof() Object.getPrototypeOf() Reflect.getPrototypeOf()
    setPrototypeOf() Object.setPrototypeOf() Reflect.setPrototypeOf()
    isExtensible() Object.isExtensible() Reflect.isExtensible()
    preventExtensions() Object.preventExtensions() Reflect.preventExtensions()
    apply() 调用函数时 Reflect.apply()
    construct() new Reflect.construct()

    # 代理模式

    使用代理可以在代码中实现一些有用的编程模式。(注:这些模式也可以不使用Proxy来实现)

    # 跟踪属性访问

    通过捕获器如get、set和has,可以知道对象属性什么时候被访问、被查询。

    示例见上述get()和set()。

    # 隐藏属性

    代理的内部实现对外部代码是不可见的,因此很容易隐藏目标对象上的属性。如果不使用Proxy,我们也可以通过闭包的方式来实现(利用作用域访问规则)。

    const hiddenProperties = ['foo', 'bar']
    const targetObject = {
      foo: 1,
      bar: 2,
      baz: 3,
    }
    
    const proxy = new Proxy(targetObject, {
      get(target, property) {
        if (hiddenProperties.includes(property)) {
          return undefined
        } else {
          return Reflect.get(...arguments)
        }
      },
      set(target, property, value) {
        if (hiddenProperties.includes(property)) {
          return false
        } else {
          return Reflect.set(...arguments)
        }
      },
    })
    
    console.log(proxy.foo) // undefined
    console.log(proxy.baz) // 3
    
    console.log(targetObject.foo) // 1
    console.log(targetObject.baz) // 3
    
    proxy.foo = 10
    proxy.baz = 30
    
    console.log(proxy.foo) // undefined
    console.log(proxy.baz) // 30
    
    console.log(targetObject.foo) // 1
    console.log(targetObject.baz) // 30
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39

    # 属性验证

    const target = {
      onlyNumbersGoHere: 0,
    }
    
    const proxy = new Proxy(target, {
      set(target, property, value) {
        if (typeof value !== 'number') {
          return false
        } else {
          return Reflect.set(...arguments)
        }
      },
    })
    
    proxy.onlyNumbersGoHere = 1
    console.log(proxy.onlyNumbersGoHere) // 1
    proxy.onlyNumbersGoHere = '2'
    console.log(proxy.onlyNumbersGoHere) // 1
    proxy.onlyNumbersGoHere = 3
    console.log(proxy.onlyNumbersGoHere) // 3
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    # 函数与构造函数参数验证

    function max(...nums) {
      return Math.max(...nums)
    }
    
    const proxyMax = new Proxy(max, {
      apply(target, thisArg, argumentsList) {
        for (const arg of argumentsList) {
          if (typeof arg !== 'number') {
            throw 'Non-number argument provided'
          }
        }
        return Reflect.apply(...arguments)
      },
    })
    
    console.log(max(3, 6, 1)) // 6
    console.log(max(3, '6', 1)) // 6
    console.log(proxyMax(3, 6, 1)) // 6
    console.log(proxyMax(3, '6', 1)) //  throw 'Non-number argument provided'
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    注:《JS高级程序设计》示例有误,书上示例为

      apply(target, thisArg, ...argumentsList) {
      }
    
    1
    2

    这里的扩展运算符是多余的,要去掉。

    # 数据绑定与可观察对象

    比如,可以将被代理的类绑定到一个全局实例集合中。

    const userList = []
    class User {
      constructor(name) {
        this.name = name
      }
    }
    
    const UserProxy = new Proxy(User, {
      construct() {
        const newUser = Reflect.construct(...arguments)
        userList.push(newUser)
        return newUser
      },
    })
    
    new UserProxy('jack')
    new UserProxy('rose')
    console.log(userList) // [ User { name: 'jack' }, User { name: 'rose' } ]
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    上次更新: 2022/01/14, 08:52:26
    第8章 对象、类与面向对象编程
    第10章 函数

    ← 第8章 对象、类与面向对象编程 第10章 函数→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×