加载中...
  • ES6 的 Proxy loading

    ES6中新增了Proxy对象,从字面量上看可以理解为代理器,主要用于改变对象的默认访问行为,实际表现是在访问对象之间增加了一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,我们可以增加自定义的行为

    Proxy的基本语法:


    1
    let proxy = new Proxy(target, handler)

    target —— 是要包装的对象,可以是任何东西,包括函数。

    handler —— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如 get 捕捉器用于读取 target 的属性,set 捕捉器用于写入 target 的属性,等等。

    对 proxy 进行操作,如果在 handler 中存在相应的捕捉器,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。

    Proxy实例


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //我们用 get 来实现一个对象的默认值。
    //我们将创建一个对不存在的数组项返回 0 的数组。
    //通常,当人们尝试获取不存在的数组项时,他们会得到 undefined,
    //但是我们在这将常规数组包装到代理(proxy)中,以捕获读取操作,并在没有要读取的属性的时返回 0

    // 定义目标数组
    let numbers = [0, 1, 2];

    // 定义配置对象
    let handler = {
    get(target, prop) {
    return prop in target ? target[prop] : 0
    }
    }
    // 生成Proxy实例
    let numbersProxy = new Proxy(numbers, handler)

    // 执行结果
    console.log(numbersProxy[1]); // 1
    console.log(numbersProxy[123]); // 0(没有这个数组项)

    上面实例中,我们定义了一个包含get()函数的配置对象,表示的是对代理对象的属性进行读取操作,就会触发get()函数。因此在执行numbersProxy[1],即访问Proxy实例数组项为1的值,会触发get()函数,在控制台输出自定义的结果。

    使用Proxy时的注意事项


    必须通过代理实例访问

    如果需要配置对象的拦截行为生效,那么必须是对代理实例的属性进行访问,而不是直接对目标对象进行访问。

    Proxy实例函数及其基本使用


    在上面例子中,我们通过访问代理对象的属性来触发自定义配置对象的get()函数。而get()函数只是Proxy实例支持的总共13中函数中的一种,这13中函数汇总如下。

    get(target,property,receiver)


    用于拦截对象的读取属性操作。例如调用proxy.name或者proxy[name],其中target表示的是目标对象,property表示的是读取的属性值,receiver表示的是配置对象(最初被调用的对象。通常是 proxy 本身)。

    set(target,property,value,receiver)


    拦截对象属性的写入操作,即设置属性值,例如proxy.name = ‘xx’或者proxy[name] = ‘xx’,其中target表示目标对象,property表示的是将要设置的属性,value表示将要设置的属性值,receiver表示的是配置对象。

    has(target,prop)


    拦截hasProperty的操作,返回一个布尔值,最典型的表现形式是执行prop in target,其中target表示目标对象,prop表示判断的属性值。

    deleteProperty(target,property)


    拦截delete proxy[property]的操作,返回一个布尔值,表示是否执行成功,其中target表示目标对象,property表示将要删除的属性。

    ownkeys(target)


    拦截Object.getOwnPropertyNames(target)、Object.getOwnPropertySymbols(target)、Object.keys(target)、for…in循环等操作,其中target表示的是获取对象自身所有的属性名。

    拓展 ,

    Object.getOwnPropertyNames(obj) 返回非 Symbol 键。

    Object.getOwnPropertySymbols(obj) 返回 Symbol 键。

    Object.keys/values() 返回带有 enumerable 标志的非 Symbol 键/值。

    for..in 循环遍历所有带有 enumerable 标志的非 Symbol 键,以及原型对象的键。

    getOwnPropertyDecriptor(target,prop)


    拦截Object.getOwnPropertyDecriptor(proxy, prop)操作,返回属性的属性描述符构成的对象,其中target表示目标对象,prop表示需要获取属性描述符集合的属性。

    defineProperty(target,property,descriptor)


    拦截Object.defineProperty(proxy, property, decriptor)、Object.defineProperties(proxy, decriptors)操作,返回一个布尔值,其中target表示目标对象,property表示新增的属性,descriptor表示的是属性描述符对象。

    preventExtensions(target)


    拦截Object.preventExtensions(proxy)操作,返回一个布尔值,表示的是让一个对象变得不可扩展,不能再增加新属性,其中target表示目标对象。

    isExtensible(target)


    拦截Object.isExtensible(proxy),返回一个布尔值,表示对象是否可拓展。其中target表示目标对象。

    getPrototypeOf(target)


    拦截Object.getPrototypeOf(proxy)操作,返回一个对象,表示的是拦截获取对象原型属性,其中target表示目标对象。

    setPropertyOf(target,prototype)


    拦截Object.setPrototype(proxy, prototype)操作,返回一个布尔值,表示的是拦截设置对象的原型属性的行为,其中target表示目标对象,prototype表示新的原型对象。

    apply(target,object,args)


    拦截Proxy实例作为函数调用的操作,例如proxy(…args)、proxy.call(object, …args)、proxy.apply(…),其中target表示目标对象,object表示函数的调用方,args表示函数调用传递的参数。

    constructor(target,args)


    拦截Proxy实例作为构造函数调用的操作,例如new Proxy(…args),其中target表示目标对象,args表示函数调用传递的参数。

    这些函数都有一个通用的特性,即如果再target中使用了this关键字,再通过Proxy处理后,this关键之指向的是Proxy实例,而不是目标对象target

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const person = {
    getName() {
    console.log(this === proxy)
    }
    }

    const proxy = new Proxy(person, {})
    person.getName() // fasle
    proxy.getName() // true

    Proxy应用场景


    读取不存在属性


    通常,尝试读取不存在的属性会返回 undefined。

    创建一个代理,在尝试读取不存在的属性时,该代理抛出一个错误。

    这可以帮助及早发现编程错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    let user = {
    name: "xx"
    };

    function wrap(target) {
    return new Proxy(target, {
    get(target, prop) {
    if (prop in target) {
    return target[prop]
    } else {
    throw new ReferenceError(`Property doesn't exist: "${prop}"`)
    }
    }
    });
    }

    user = wrap(user);

    console.log(user.name); // xx
    console.log(user.age); // ReferenceError: Property doesn't exist: "age"

    读取负索引的值


    数组的索引值时从0开始依次递增的,正常情况下我们无法读取负索引的值,但是通过Proxy的get()函数可以做到这一点。

    负索引实际就是从数组的尾部元素开始,从后往前,寻找元素的位置。

    换句话说,array[-N] 与 array[array.length - N] 相同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let arr = [1, 2, 3, 4, 5]
    arr = new Proxy(arr, {
    get(target, index) {
    if (index > 0) {
    return target[index]
    } else {
    return target[+index + target.length]
    }
    }
    })
    console.log(arr[1]) // 2
    console.log(arr[2]) // 3
    console.log(arr[-1]) // 5
    console.log(arr[-2]) // 4

    禁止访问私有属性


    在一些约定熟成的写法中,私有属性都会以下划线(_)开头,事实上,我们并不希望用户能访问到私有属性。

    我们将需要以下捕捉器

    get 读取此类属性时抛出错误

    set 写入属性时抛出错误

    deleteProperty 删除属性时抛出错误

    has 在使用 in 方法时排除以 _ 开头的属性

    ownKeys 在使用 for..in 和像 Object.keys 这样的的方法时排除以 _ 开头的属性

    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
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    let user = {
    name: "xx",
    _age: "18"
    };

    user = new Proxy(user, {
    get(target, prop) { // 拦截属性读取
    if (prop.startsWith('_')) {
    throw new Error("Access denied")
    } else {
    return target[prop]
    }
    },
    set(target, prop, val) { // 拦截属性写入
    if (prop.startsWith('_')) {
    throw new Error("Access denied")
    } else {
    target[prop] = val
    return true
    }
    },
    deleteProperty(target, prop) { // 拦截属性删除
    if (prop.startsWith('_')) {
    throw new Error("Access denied")
    } else {
    delete target[prop]
    return true
    }
    },
    has(target, prop) {
    if (prop.startsWith('_')) { // 拦截 in 操作
    throw new Error("Access denied");
    } else {
    return prop in target
    }
    },
    ownKeys(target) { // 拦截读取属性列表
    return Object.keys(target).filter(key => !key.startsWith('_'))
    }
    });

    // "get" 不允许读取 _age
    try {
    user._age // Error: Access denied
    } catch(e) { console.log(e.message) }

    // "set" 不允许写入 _age
    try {
    user._age = "20" // Error: Access denied
    } catch(e) { console.log(e.message) }

    // "deleteProperty" 不允许删除 _age
    try {
    delete user._age // Error: Access denied
    } catch(e) { console.log(e.message) }

    // "has" 将 _age 过滤出去
    try {
    '_age' in user
    } catch (e) { console.log( e.message ) }

    // "ownKeys" 将 _age 过滤出去
    for(let key in user) console.log(key) // name

    Proxy访问属性的限制


    当我们期望使用Proxy对对象的属性进行代理,并修改属性的返回值时,我们需要这个属性不能同时为不可配置和不可写。如果这个属性同时为不可配置和不可写,那么在通过代理读取属性时,会抛出异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    let target = Object.defineProperties({}, {
    // 可配置的name
    name: {
    value: 'xx',
    configurable: true,
    writable: false
    },

    // 不可配置的age
    age: {
    value: 18,
    configurable: false,
    writable: false
    }
    })

    target = new Proxy(target, {
    get(target, prop) {
    return 'abc'
    }
    })

    console.log(target.name) // abc
    console.log(target.age) // Uncaught TypeError: 'get' on proxy: property 'age' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '18' but got 'abc')

    拦截属性赋值操作


    定义一个person对象,包含一个age属性,取值在0~100之间,只要设置的值不在区间内,就会抛出异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    let user = {
    name: 'xx',
    age: 18
    }

    user = new Proxy(user, {
    set(target, prop, number) {
    if (prop === 'age' && number > 100 || number < 0) {
    throw new Error("The age is invalid")
    }
    target[prop] = number
    }
    })

    user.name = 'yy'
    console.log(user.name) // yy
    user.age = 20
    console.log(user.age) // 20
    user.age = 200
    console.log(user.age) // Uncaught Error: The age is invalid

    函数的拦截


    Proxy中提供了apply()函数,用于拦截函数调用的操作,函数调用包括直接调用、call()函数调用、apply()函数调用3种方式

    通过对函数调用的拦截,可以加入自定义操作,从而得到新的函数处理结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function sum (num1, num2) {
    return num1 + num2
    }

    sum = new Proxy(sum, {
    apply(target, obj, args) {
    return target.apply(obj, args) * 2
    }
    })

    console.log( sum(1, 2) ) // 6
    console.log( sum.call(null, 1, 2) ) // 6
    console.log( sum.apply(null, [1, 2]) ) // 6

    上一篇:
    ES6 的 Reflect
    下一篇:
    性能优化:防抖和节流
    本文目录
    本文目录