重学ES6(五)Proxy

280 阅读12分钟

基础知识

proxy 在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截。proxy 这个词本意是代理,用在这里表示由它来代理某些操作,可以翻译为代理器。

其实就是相当于在我们对所有目标要进行操作之前,全部先拦截了,然后在拦截后进行一系列其他操作。

let proxy = new Proxy(target, handler)

这里 new Proxy() 表示生成一个 proxy 的实例, target 参数表示所要拦截的目标对象, handler 参数也是一个对象,用来定制拦截行为。

let obj = new Proxy({}, {
	get: function (target, propKey, receiver) {
		console.log(`getting ${propKey}`)
        return Reflect.get(target, propKey, receiver)
	},
    set: function (target, propKey, value, receiver) {
    	console.log(`setting ${propKey}`)
        return Reflect.set(target, propKey, value, receiver)
    }
})

上面代码对一个空对象 {} 设置了拦截,重新定义了这个空对象的 get 和 set。我们这里在给这个空对象赋值和取值时候会打印什么呢。

obj.count = 1
// setting count
// 1
// 这里给上面的的obj赋值,会拦截后使用 set 方法

++obj.count
// getting count
// setting count
// 2
// 这里执行的时候会拦截后调用 get 方法获取值
// 然后在调用 set 方法,设置值

我们在看一个拦截读取属性的行为

let proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
})

proxy.name	// 35
proxy.time	// 35
proxy.age	// 35

这里就设置了 get 行为,并且Proxy的实例(proxy)上的所有属性都被拦截,都是返回 35,所以这个对象上不管是啥属性取值都是 35。

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

let target = {}
let handler = {}

let proxy = new Proxy(target, handler)
proxy.name = 'chencc'

target.name		// 'chencc'

因为上面 handler 没有设置任何拦截操作,所以访问 proxy 就等同于访问 target。

在阮一峰的 ES6 教程里面,还学到了一个技巧,将 Proxy 对象,设置到 object.proxy 属性,从而可以在 object 对象上调用。

let target = {}
let handler = {}
let obj = {
	proxy: new Proxy(target, handler)
}

同时,Proxy 的实例也可以作为其他对象的原型对象。

let proxy = new Proxy({}, {
	get: function(target, propKey) {
    	return 35
    }
})
let obj = Object.create(proxy)
obj.name 		// 35
obj.__proto__	// 35

let proxy1 = new Proxy({}, {})
let obj1 = Object.create(proxy1)

obj1.__proto__ === proxy1		// true

这里我们使用 Object.create 实现的是将 proxy 放到 obj 的原型链上,这里我们一开始寻找 name 属性的时候,obj 本身是没有 name 属性的,所以会根据原型链向上找到 proxy,然后被拦截返回 35。

但是大家要注意,我发现正常继承的实例化的对象,对象的 [[proto]] 属性是会指向继承的对象的,但是这里我们发现如果是 proxy 拦截的,会将 [[proto]] 也同时拦截了,然后返回 35。然后大家会看到后面一个,如果我们使用 proxy 不进行任何拦截操作的时候,obj1[[proto]] 就会指向 proxy1。

Proxy 实例的方法

get()

get 方法用于拦截目标对象属性的读取操作,可以接受三个参数,目标对象、属性名 和 proxy 实例本身(可选)。(target, propKey, receiver)

eg1:

let person = {name: 'chencc'}

let proxy = new Proxy(person, {
	get: function (target, propKey, receiver) {
    	// 如果获取值在 target 里面没有则抛出异常
		if (propKey in target) {
			return target[propKey]
		}else {
        	throw new ReferenceError('---undefined---')
        }
	}
})

proxy.name		// 'chencc'
proxy.age		// ReferenceError: ---undefined---

get 方法可以继承

let proto = new Proxy({}, {
	get (target, propertyKey, receiver) {
    	console.log('Get' + propertyKey)
    }
})

let obj = Object.create(proto)
obj.foo		// "Get foo"

上面代码中,拦截器定义在 obj 的 [[proto]] 属性上,所以如果读取的属性在obj上没有,会顺着原型链往上找的时候,就会拦截。

使用 get 拦截,实现数组读取负数的索引

function createArray (...elements) {
	let handler = {
    	get(target, propKey, receiver) {
        	let index = Number(propKey)
            if (index < 0) {
            	propKey = String(target.length + index)
            }
            return Reflect.get(target, propKey, receiver)
        }
    }
    
    let target = []
    target.push(...elements)
    
    return new Proxy(target, handler)
}

let arr = createArray('a', 'b', 'c')
arr[-1]		// "c"

利用 proxy 拦截实现的链式操作(get)

var pipe = function (value) {
	var funcStack = []
    var oproxy = new Proxy({}, {
    	get: function (pipeObject, fnName) {
        	if(fnName === 'get') {
            	return funcStack.reduce(function (val, fn) {
                	return fn(val)
                }, value)
            }
            funcStack.push(window[fnName])
            return oproxy
        }
    })
    
    return oproxy
}

let double = n => n* 2
let pow = n => n * n
let reverseInt = reverseInt = n => n.toString().split("").reverse().join("") | 0

pipe(3).double.pow.reverseInt.get	// 63

上面代码设置 Proxy 后,达到了将函数名链式使用,链式调用实现的方式是通过将前面的方法依次推入到数组中,最后当所有方法都进入后,按照FIFO的规则,先进先出。执行第一个推入的方法,然后依次执行所有的方法。

通过 get ,实现得生成各种 DOM 节点得 通用函数

const dom = new Proxy({}, {
  get(target, property) {
    return function(attrs = {}, ...children) {
      const el = document.createElement(property);
      // attrs 传入对象的,获取外部传入对象,设置DOM属性
      for (let prop of Object.keys(attrs)) {
        el.setAttribute(prop, attrs[prop]);
      }
      // 单独判断是 string,设置单独文本插入,其他直接插入DOM中
      for (let child of children) {
        if (typeof child === 'string') {
          child = document.createTextNode(child);
        }
        el.appendChild(child);
      }
      return el;
    }
  }
});

const el = dom.div({},
  'Hello, my name is ',
  dom.a({href: '//example.com'}, 'Mark'),
  '. I like:',
  dom.ul({},
    dom.li({}, 'The web'),
    dom.li({}, 'Food'),
    dom.li({}, '…actually that\'s it')
  )
);

document.body.appendChild(el);

// 这里方法的执行顺序是会先执行 dom.div 内部的每个熟悉方法
// 即先获取到内部函数的值,再得到值后,再执行外面的方法。
// dom.a 执行后得到结果值 el
// dom.li 顺序执行,然后得到 三个 return 出来的 el 结果
// 然后再执行 dom.ul({}, el, el, el)
// 最后再执行最外层的 
  dom.div({}, 
    'Hello, my name is ',
    el,
    '. I like:',
    el
  ))

set()

set 方法用于拦截属性赋值的操作,可以接受四个参数 目标对象、属性名、属性值和 Proxy 实例本身(可选)

使用 Proxy 设置属性时做校验

let validator = {
	set: function(obj, prop, value) {
    	if (prop === 'age') {
        	if (!Number.isInteger(value)) {
            	thorw new TypeError('age is not integer')
            }
            if (value > 200) {
            	thorw new TypeError('age is seem invalid')
            }
        }
        obj[prop] = value
    }
}

let person = new Proxy({}, validator)

person.age = 100

person.age = 'chencc'	// 报错
person.age = 500		// 报错

我们也可以通过 get 和 set 方法,可以做到对象上设置内部属性,防止对象内部属性被外部读写。

const handler = {
	get (target, key) {
    	invariant(key, 'get')
        return target[key]
    },
    set (target, key, value) {
    	invariant(key, 'set')
        target[key] = value
        return true
    }
}

function invariant(key, action) {
	if(key[0] === '_') {
    	throw new Error('Invalid action')
    }
}

let target = {}
let proxy = new Proxy(target, handler)

proxy._prop			//	报错
proxy._prop	= 'c'	//	报错

从上面代码中,只要读写属性第一个字符串是下划线,一律抛出错误,达到禁止读写对象内部属性的目的。

set 方法的第四个参数

const handler = {
	set: function(obj, prop, value, receiver) {
    	obj[prop] = receiver
    }
}
const proxy = new Proxy({}, handler)

proxy.foo = 'bar'
proxy.foo === proxy

apply()

apply 方法拦截函数的调用、 call 和 apply 操作

apply 方法接受三个参数,分别是目标对象、目标对象的上下文对象(this) 和目标对象的参数数组

var handler = {
	apply (target, ctx, args) {
		return Reflect.apply(...arguments)
	}
}

当 Proxy 的实例,当它作为函数调用的时候,就会被 apply 方法拦截,如下:

let target = function () {
	return 'I am the target'
}

let handler = {
	apply: function () {
    	return 'I am the proxy'
    }
}

let p = new Proxy(target, handler)
p()		//	'I am the proxy'

当执行 Proxy 对象的实例,或者调用 call 和 apply 时,也会被 apply 拦截的,如下:

let handler = {
	apply (target, ctx, args) {
    	return Reflect.apply(...arguments) * 2
    }
}

function sum (l, r) {
	return l + r
}

let proxy = new Proxy(sum, handler)
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

备注:直接调用 Reflect.apply 方法,也会被拦截

Reflect.apply(proxy, null, [9, 10]) // 38

对于阮一峰的 ES6 教程的例子,大家是不是有的第一遍看,还有点晕晕的,来下面再来几个例子,先理解下,再回看头来看上面的例子。

eg:

var p = new Proxy(function(){}, {
	apply: function (target, thisArg, argumentsList) {
    	console.log(argumentsList.join(","))
        return argumentsList[0] + argumentsList[1] + argumentsList[2]
    }
})

console.log(p(1, 2, 3))

// 1 2 3
// 6

这里直接调用 Proxy 的实例,也会被 apply 拦截,然后 target 就是传入的 function(){}, ctx 上下文 this 就是这个 function 的环境,最后这个参数 list 就是 p 实例调用时,传入的 1、2、3。

eg:

let obj = {
	name: 'chencc'
}
let p = new Proxy(function(){}, {
	apply: function(target, thisArg, argumentsList) {
    	console.log(thisArg.name)
    }
})

p.apply(obj, [4])

// 这里 target 就是 function(){}
// thisArg接受的就是 obj 的环境
// argumentsList 就是 apply 传入的参数 [4]

应该通过下面两个例子的详解,上面几个demo 看起来就很简单了吧。

has()

has 方法用来拦截 HasProperty 操作,即判断对象是否具有某个属性,这个方法会生效。典型的操作就是 in。

has 方法接收2个参数 目标对象和需查询的属性名称

使用 has 方法拦截,使某些属性,不被 in 发现。

let handler = {
	has (target, key) {
    	if (key[0] === '_') {
        	return false
        }
        return key in target
    }
}

let target = {
	_name: "chencc",
    name: "chencc"
}
let proxy = new Proxy(target, handler)
'_name' in target	// false

注意,如果原对象不可配置或者禁止扩展,这时 has 拦截就会报错的

let obj = {name: 'chencc'}
Object.preventExtensions(obj)

let p = new Proxy(obj, {
	has: function (target, prop) {
    	return false
    }
})

'name' in p		// TypeError

同时,我们还要注意的是 has 方法拦截的是 hasProperty 操作,而不是 hasOwnProperty 操作,即 has 方法不判断一个属性是对象自身的属性,还是继承的属性。

has 方法拦截对 for in 循环不生效

construct()

construct 方法用于拦截 new 命令

let handler = {
	construct (target, args, newTarget) {
    	return new target(...args)
    }
}

construct 接收三个参数 目标对象、构造函数的参数、 创造实例对象时,new 命令作用的构造函数

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

(new p(1).value)
// "called: 1"
// 10

construct 方法返回的必须是一个对象,否则会报错

let p = new Proxy(function(){}, {
	construct: function(target, argumentList) {
		return 1
	}
})

new p()		// TypeError

deleteProperty()

deleteProperty 方法用于拦截 delete 操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete。

let handler = {
	deleteProperty (target, key) {
    	invariant(key, 'delete')
        delete target[key]
        return true
    }
}

function invariant(key, action) {
	if (key[0] === '_') {
    	throw new Error('Invalid attempt to delete')
    }
}

let target = {_name: 'chencc'}
let proxy = new Proxy(target, handler)
delete proxy._name
// Error

defineProperty()

defineProperty() 方法拦截了 Object.defineProperty 操作。

let handler = {
	defineProperty (target, key, descriptor) {
    	// 只有返回下面这个才能返回值
    	// return target[key] = descriptor.value
    	return false
    }
}

let target = {}
let proxy = new Proxy(target, handler)
proxy.foo = 'bar'	// 不生效

上面例子里面,defineProperty 内部只是返回 false,导致添加新属性总是无效。注意,这里返回 false 只是提示操作失败,本身不能阻止添加新属性。 需要上面那个注释内部的内容返回,才能有值。

同时,如果目标对象不可扩展(non-extensible),则defineProperty()不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty方法不得改变这两个设置。

getOwnPropertyDescriptor()

getOwnPropertyDescriptor 方法拦截 Object.getOwnPropertyDescriptor,返回一个属性描述对象或者undefined。

let handler = {
	getOwnPropertyDescriptor (target, key) {
    	if (key[0] === '_') {
        	return
        }
        return Object.getOwnPropertyDescriptor(target, key)
    }
}
let target = {_foo: 'bar', baz: 'tar'}
let proxy = new Proxy(target, handler)

Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
{
	value: "tar", 
    writable: true, 
    enumerable: true, 
    configurable: true
}

getPrototypeOf()

getPropertyOf 方法用来拦截获取对象原型。拦截以下操作

~ Objec.prototype.__proto__
~ Object.prototype.isPrototypeOf()
~ Object.getPrototypeOf()
~ Reflect.getPrototypeOf()
~ instanceof

eg:

let proto = {}
let p = new Proxy({}, {
	getPrototypeOf(target) {
    	return proto
    }
})

Object.getPrototypeOf(p) === proto
// true

注意: getPrototypeOf 方法必须返回一个对象或者 null,否则报错。另外如果目标对象不可扩展(non-extensible),getPrototypeOf 方法必须返回目标对象的原型对象。

isExtensible()

Object.isExtensible() 方法判断一个对象是否是可扩展的

let empty = {}
Object.isExtensible(empty)		// true

// 不可扩展
Object.preventExtensions(empty)
Object.isExtensible(empty)		// false

// 密封对象不可扩展
let sealed = Object.seal({})
Object.isExtensible(sealed)		// false

// 冻结对象不可扩展
let frozen = Object.freeze({})
Object.isExtensible(frozen)		// false

isExtensible 方法拦截 Object.isExtensible()

let p = new Proxy({}, {
	isExtensible: function(target) {
    	console.log('called')
        return true
    }
})

Object.isExtensible(p)
// "called"
// true

这个方法中有一个限制,它的返回值必须与目标对象的 isExtensible保持一致,不然就会抛出错误。

Object.isExtensible === Object.isExtensible(target)

ownKeys()

ownKeys() 方法用来拦截对象自身属性的读取操作。

~ Object.getOwnPropertyName()
~ Object.getOwnPropertySymbols()
~ Object.keys()
~ for..in 循环

下面拦截 Object.keys()

let target = {
	a: 1,
    b: 2,
    c: 3
}
let handler = {
	ownKeys(target) {
    	return ['a']
    }
}
let proxy = new Proxy(target, handler)

Object.keys(proxy)	// ['a']

注意:使用 Object.keys() 方法时,有三类属性会被 ownKeys 自动过滤,不会返回。

  • 目标对象上不存在的属性
  • 属性名称为 Symbol 类型
  • 不可遍历(enumerable) 的属性
let target = {
	a: 1,
    b: 2,
    c: 3,
    [Symbol.for('secret')]: 4
}
Object.defineProperty(target, 'key', {
	enumerable: false,
    configurable: true,
    writable: true,
    value: 'static'
})

let handler = {
	ownKeys(target) {
    	return ['a', 'd', Symbol.for('secret'), 'key']
    }
}

let proxy = new Proxy(target, handler)
Object.keys(proxy)		// 'a'

preventExtensions()

preventExtensions 方法拦截 Object.preventExtensions()。该方法必须返回一个布尔值,否则会自动转为布尔值。

该方法有个限制,只有目标对象不可扩展时(即 Object.isExtensible(proxy) 为false),proxy.preventExtensions 才能返回true。所以为了防止出现这个问题,通常在 proxy.proventExtensions 方法里面调用一次 object.preventExtensions

let prxoy = new Proxy({}, {
	preventExtensions: function(target) {
    	console.log('called')
        Object.preventExtensions(target)
        return true
    }
})

Object.preventExtensions(proxy)
// "called"
// Proxy {}

setPrototypeOf()

setPrototypeOf 方法用来拦截 Object.setPrototypeOf

let handler = {
	setPrototypeOf (target, proto) {
    	throw new Errot('forbidden')
    }
}
let proto = {}
let target = function () {}
let proxy = new Proxy(target, handler)
Object.setPrototypeOf(proxy, proto)
// Error: forbidden

上面只要修改 target 原型对象,就会报错

Proxy.revocable()

Proxy.revocable() 方法返回一个可取消的 Proxy 实例。

let target = {}
let handler = {}

let {proxy, revoke} = Proxy.revocable(target, handler)

proxy.foo = 123
proxy.foo 	// 123

revoke()
proxy.foo	// TypeError

Proxy.revocable() 方法返回一个对象,该对象包含 Proxy 实例和 revoke 属性,可以取消 Proxy 实例。上面代码中,当执行 revoke 函数之后,再访问 Proxy 实例,就会抛出一个错误。

实战场景

私有化 API

let api = {
	_apiKey: '987654321',
    getUsersInfo: function(){},
    getUserInfo: function(){},
    setUserInfo: function(){}
}
console.log(api._apiKey)

const RESTRICTED = ['_apiKey']

api = new Proxy(api, {
    get (target, key, proxy) {
        if (RESTRICTED.indexOf(key) > -1) {
            throw Error('the Key is restricted.')
        }
        return Reflect.get(target, key, proxy)
    },
    set (target, key, value, proxy) {
        if (RESTRICTED.indexOf(key) > -1) {
            throw Error('the Key is restricted.')
        }
        return Reflect.set(target, key, value, proxy)
    }
})

// 下面两个操作都会抛出 Error
console.log(api._apiKey)
api._apiKey = '123'

设置只读属性值

class Component {
	constructor () {
    	this.proxy = new Proxy({
        	id: Math.random().toString(36).slice(-8)
        },{})
    }
    get id() {
    	return this.proxy.id
    }
}

let com = new Component()
com.id = '123'		// 设置只读了,这里修改无效

这里其实prxoy代理并没有拦截,只是在构造函数内部实例化一个proxy,然后通过class的get拦截,每次都返回的是同一个实例的proxy的id。

数据校验

表单提交的时候做数据校验,数据类型是否满足条件,非常适合用 Proxy

let dataStore = {
	count: 0,
    amount: 123,
    total: 10
}
dataStore = new Proxy(dataStore, {
	set(target, key, value, proxy) {
    	if (typeof value !== 'number') {
        	throw Error('Only Number')
        }
        return Reflect.set(target, key, value, proxy)
    }
})

// Error
dataStore.count = 'chencc'

dataStore.count = 666

这里是个简单的校验规则设置,对一个对象里面所有value,用了同一个规则,下面我们抽离出单独的校验逻辑。

function createValidator(target, validator) {
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if(Reflect.has(target, key)) {
            	// 获取到有key对应的校验方法
                let validator = this._validator[key]
                if(!!validator(value)) {
                    return Reflect.set(target, key, value, proxy)
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid.`)
                }
            } else {
                throw Error(`${key} is not a valid property`)
            }
        }
    })
}

const PersonValidators = {
    name (val) {
        return typeof val === 'string'
    },
    age (val) {
        return typeof val === 'number' && val > 18
    }
}

class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
        return createValidator(this, PersonValidators)
    }
}

const person = new Person('Bill', 25)

// 下面这几个值设置都会报错
person.name = 27
person.age = 'chencc'	

实现观察者模式

// 观察者队列 使用 set,不会重复添加
const queueObservers = new Set()
// 向队列中添加观察中方法
// 这样观察的对象改变,通知执行队列中的方法
const observe = fn => queueObservers.add(fn)
// 代理观察者
// 将需要观察的对象,代理起来,这样值改变就会被拦截
const observeable = obj => new Proxy(obj, {set, get})
// Proxy set
function set (target, key, value, receiver) {
    // 重制原来的属性
    const result = Reflect.set(target, key, value, receiver)
    // 遍历执行所有观察者方法
    queueObservers.forEach(observe => observe())
    // 这里为什么不直接最后赋值使用下面方法
    // 因为之前赋值,可以在观察者方法中就能拿到已经改变的值
    // return Reflect.set(target, key, value, receiver)
    return result
}
// Proxy get
function get (target, key, receiver) {
    return Reflect.get(target, key, receiver)
}

let user = {
    name: 'chencc',
    age: 27
}

const person = observeable(user)
// 观察者触发打印属性方法
let observeUser = () => {
    console.log(`${person.name}, ${person.age}`)
}

observe(observeUser)

person.name = 'chenhh'

// 'chenhh', 27

服务端的代理

function createWebService(baseUrl) {
	return new Proxy({}, {
    	get(target, key, receiver) {
        	return () => httpGet(baseUrl + '/' + key)
        }
    })
}

总结

上面对于 Proxy 的基础知识和对于哪些地方能用到做了一些详细介绍,对于原来的认知是 Proxy 是代替 defineProperty 属性的,可以支持实现双向数据绑定等监听属性变化以及触发一系列方法的一个新特性。但是我们上面看到,它不仅仅支持监听属性变化,监听 get 和 set 方法,还支持 apply 等一系列方法拦截函数。对于 Proxy 的理解,就是一个代理方法,对于对象和方法都可以进行代理拦截,进行其他操作。大家可以在实际代码中实操试试看呢。欢迎大家一起交流学习啦!