ES6学习笔记: 代理和反射 上

简介: ES6学习笔记: 代理和反射 上

原文链接: www.w3cplus.com

前端时间学习Vue的时候,碰到Proxy,当时就一脸蒙逼了。所以返过头来补一下相关的知识。在JavaScript中有ProxyReflect的两个概念。最近几天一直在学习这两个概念,今天整整这方面的相关知识点。

术语介绍

Proxy又称为代理。在现实生活中,大家对代理二字并不会太陌生,比如某产品的代理。打个比方来说,我们要买一台手机,我们不会直接到一家手机厂去买,会在手机的代理商中买。

在JavaScript中,Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程,即对编程语言进行编程

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

Reflect称为反射。它也是ES6中为了操作对象而提供的新的API,用来替代直接调用Object的方法。Reflect是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

Reflect与大多数全局对象不同,Reflect没有构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。

简单的看一个小示例:

let target = {}
let handler = {
    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)
    }
}
let proxy = new Proxy(target, handler)
console.log(proxy.name)
proxy.name = '大漠'
console.log(proxy.name)
proxy.count = 1
proxy.count++
console.log(proxy.count)

运行的结果如下:

上面代码对一个空对象架设了一层拦截,重新定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。

在上面的示例中,我们看到了targethandlertrap。简单对介绍一下:

  • target代表了被代理的对象。这是你需要控制对其访问的对象。它始终作为Proxy构造器的第一个参数被传入,同时它也会被传入每个trap
  • handler是一个包含了你想要拦截和处理的操作的对象。它会被作为Proxy构造器的第二个参数传入。它实现了Proxy API(比如:getsetapply等等)。
  • 一个trap代表了handler中一个被处理的函数。因此,如果要拦截get请求你需要创建一个gettrap。以此类推。

基本语法

前面我们了解了相关的术语,我们来看看他们的基本语法。首先来看看Proxy的基本语法格式:

// @param {Object} target 用来被代理的对象
// @param {Object} handler 用来设置代理的对象
let proxy = new Proxy(target, handler)

ES6原生提供Proxy构造函数,用来生成Proxy实例。在使用过程当中,Proxy都像上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

let proxy = new Proxy({}, {
    get: function(target, property) {
        return property in target ? target[property] : `Error: ${target} object not has ${property} property! `
    }
})
console.log(proxy.time)
console.log(proxy.name)
console.log(proxy.title)
proxy.time = new Date()
proxy.name = '大漠'
proxy.title = '切图仔'
console.log(proxy.time)
console.log(proxy.name)
console.log(proxy.title)

上面代码中,作来构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(一个空对象{}),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代码的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面的代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是target(目标对象)和property(所要访问的属性)。

注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

Reflect只是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同(稍后会介绍)。Reflect不是一个函数对象,因此它是不可构造的。

new Reflect() // 错误的写法

Reflect提供了一些静态方法,静态方法是指只能通过对象自身访问的方法:

处理器对象

处理器对象handler一共提供了14种可代理操作,每种操作的代号(属性名/方法名)和触发这种操作的方式如下:

  • 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()

Reflect对象拥有对应的可以控制各种元编程任务的静态方法。这些功能和Proxy一一对应。

下面的这些名称你可能看起来很眼熟(因为他们也是Object上的方法):

  • Reflect.getOwnPropertyDescriptor(..)
  • Reflect.defineProperty(..)
  • Reflect.getPrototypeOf(..)
  • Reflect.setPrototypeOf(..)
  • Reflect.preventExtensions(..)
  • Reflect.isExtensible(..)

这些方法和在Object上的同名方法一样。然后,一个区别在于,Object上这么方法的第一个参数是一个对象,Reflect遇到这种情况会扔出一个错误。

如果handler没有设置任何拦截,那就等同于直接通向原对象。

let target = {}
let handler = {}
let proxy = new Proxy(target, handler)
proxy.name = '大漠'
console.log(`Proxy: ${proxy.name}`)   // => Proxy: 大漠
console.log(`Target: ${target.name}`) // => Target: 大漠

上面代码中,handler是一个空对象,没有任何拦截效果,访问handler就等同于访问target

Proxy支持的拦截操作

handler.get()

get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']Reflect.get(...),返回类型不限。最后一个参数receiver可选,当target对象设置了propKey属性的get函数时,receiver对象会绑定get函数的this对象

let person = {
    name: '大漠'
}
let handler = {
    get: function (target, propKey, receiver) {
        if (propKey in target) {
            return target[propKey]
        } else {
            throw new ReferenceError(`Property ${propKey} does not exist!`)
        }
    }
}
let proxy = new Proxy(person, handler)
console.log(proxy.name)
console.log(proxy.age)

上面的代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined

get()方法是可以继承的。

let proxy = new Proxy({},{
    get(target, propKey, receiver) {
        console.log(`GET: ${propKey}`)
        return target[propKey]
    }
})
let obj = Object.create(proxy)
console.log(obj.name)
复制代码

上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。

下面的例子,使用get拦截,实现数组读取负数的索引。

function createArray(...items) {
    let handler = {
        get(target, propKey, receiver) {
            let index = parseInt(propKey)
            if (index < 0) {
                propKey = String(target.length + index)
            }
            return Reflect.get(target, propKey, receiver)
        }
    }
    let target = []
    target.push(...items)
    return new Proxy(target, handler)
}
let array = createArray('大漠', 'w3cplus.com', '切图仔')
console.log(array[-1]) // => 切图仔

上面代码中,数组的位置参数是-1,就会输出数组的倒数第一个item值,也就是切图仔

handler.set()

set(target, propKey, value, receiver)用来拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = vReflect.set(...),返回一个布尔值。

假定Person对象有一个age属性,该属性应该是一个不大于100的整数,那么可以使用Proxy对象保证age的属性值符合要求。

let validator = {
    set: function (target, propKey, value, receiver) {
        if (propKey === 'age') {
            if (!Number.isInteger(value)) {
                throw new TypeError('age不是一个整数')
            }
            if (value > 100) {
                throw new RangeError('age不是有效值')
            }
        }
        // 对于age以外的属性,直接保存
        target[propKey] = value
    } 
}
let Person = new Proxy({}, validator)
Person.age = 36
Person.name = '大漠'
console.log(Person.age)  // => 36
console.log(Person.name) // => 大漠
Person.age = 200
console.log(Person.age) // => age不是有效值

上面的示例中,由于设置了存值函数set(),任何不符合要求的age属性赋值,都会抛出一个错误。利用set()方法,还可以数据绑定,即每当对象发生变化时,会自动更新。在使用JavaScript对表单进行验证时,非常有用。

利用proxy拦截不符合要求的数据

function validator(target, validator, errorMsg) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            let errMsg = errorMsg
            if (value == '') {
                alert(`${errMsg[key]}不能为空!`)
                return target[key] = false
            }
            let va = this._validator[key]
            if (!!va(value)) {
                return Reflect.set(target, key, value, proxy)
            } else {
                alert(`${errMsg[key]}格式不正确`)
                return target[key] = false
            }
        }
    })
}

负责校验的逻辑代码

const validators = {
    name(value) {
        return value.length > 6
    },
    passwd(value) {
        return value.length > 6
    },
    moblie(value) {
        return /^1(3|5|7|8|9)[0-9]{9}$/.test(value)
    },
    email(value) {
        return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)
    }
}

客户端调用代码

const errorMsg = { 
    name: '用户名', 
    passwd: '密码', 
    moblie: '手机号码', 
    email: '邮箱地址' 
}
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener(
    'submit', 
    function() {
        let validatorNext = function*() {
            yield vali.name = registerForm.userName.value
            yield vali.passwd = registerForm.passWord.value
            yield vali.moblie = registerForm.phoneNumber.value
            yield vali.email = registerForm.emailAddress.value
        }
        let validator = validatorNext()
        validator.next();
        !vali.name || validator.next(); //上一步的校验通过才执行下一步
        !vali.passwd || validator.next();
        !vali.moblie || validator.next();
    }, 
false)

上面这部分验证表单的代码来自于@jawil的《探索两种优雅的表单验证

handler.has()

has(target, propKey)用来拦截propKey in proxyReflect.has(...)的操作,返回一个布尔值。

has()方法可以隐藏某些属性,不被in操作符发现。

let handler = {
    has: function(target, key) {
        if (key[0] === '_') {
            return false
        }
        return key in target
    }
}
let target = {
    _prop: '大漠',
    prop: 'W3cplus'
}
let proxy = new Proxy(target, handler)
console.log('_prop' in proxy)  // => false
console.log('prop' in proxy)   // => true

如果原对象的属性名的第一个字符是下划线,proxy.has就会返回false,从而不会被in运算符发现。如果原对象不可配置或者禁止扩展,这个时候,has()拦截就会报错。

let target = {
    age: 30,
    name: '大漠'
}
Object.preventExtensions(target)
let handler = {
    has: function (target, propKey) {
        return false
    }
}
let proxy = new Proxy(target, handler)
console.log(proxy.age)
console.log(proxy.name)
console.log('age' in proxy)

上例中,target对象禁止扩展,结果使用has()拦截就会报错。

handler.construct()

construct(target, args, proxy)用来拦截Proxy实例作为构造函数调用的操作,比如new proxy(...args)Reflect.construct(...)

construct()方法用来拦截new命令。

let handler = {
    construct: function (target, args) {
        console.log('Called:' + args.join(','))
        return {value: args[0] * 10}
    }
}
let target = function (){}
let proxy = new Proxy(target, handler)
console.log(new proxy(2).value)

如果construct()方法返回的不是对象,就会抛出错误。

let handler = {
    construct: function (target, args) {
        return 'w3cplus'
    }
}
let target = function () {}
let proxy = new Proxy(target, handler)
console.log(new proxy())

handler.deleteProperty()

deleteProperty(target, propKey)用于拦截delete proxy[propKey]Reflect.deleteProperty(...)的操作,返回一个布尔值。如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

let target = {
    _prop: '大漠',
    prop: 'w3cplus'
}
let handler = {
    deleteProperty: function (target, propKey) {
        invariant(propKey, 'delete')
        return true
    }
}
function invariant(key, action) {
    if (key[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private '${key}' property!`)
    }
}
let proxy = new Proxy(target, handler)
console.log(delete proxy.prop)
console.log(delete proxy._prop)

上面示例中,deleteProperty()方法拦截了delete操作符,删除第一个字符为下划线的属性会报错。


目录
相关文章
|
2天前
|
Java
反射&代理
反射&代理
37 0
|
6月前
|
Java
反射--JavaJEE基础
反射--JavaJEE基础
46 0
|
8月前
|
设计模式 Java
反射和代理
反射和代理
49 0
|
8月前
|
JavaScript 前端开发 API
ES6 拾遗:理解 Reflect 反射对象
理解 Reflect 反射对象
69 0
ES6 拾遗:理解 Reflect 反射对象
|
10月前
|
API
不同场景下如何使用易路代理?
不同场景下如何使用易路代理?
es6 代理(Reflect)和反射(Proxy)的学习总结
es6 代理(Reflect)和反射(Proxy)的学习总结
|
监控
代理与反射(二)
代理与反射(二)
79 0
代理与反射(一)
代理与反射(一)
76 0
|
JavaScript 前端开发 API
ES6学习笔记: 代理和反射 下
ES6学习笔记: 代理和反射 下
90 0
|
JavaScript 前端开发 物联网
ES6躬行记(24)——代理和反射
代理和反射是ES6新增的两个特性,两者之间是协调合作的关系,它们的具体功能将在接下来的章节中分别讲解。