proxy的应用实例

习题一

实现下面例子中的效果,需要怎么做?

var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
console.log(a + a.a + a.a.a + a.a.a.a + a.a.a.a.a); // abcdef

看到a + a.a + a.a.a + a.a.a.a + a.a.a.a.a的形式,第一个出现的解决办法就是:Proxy代理。其次就是toString()方法,因为我们最后的结果需要对应数组中的元素,并且为字符串的形式abcdef
特别注意:开始想a + a.a + a.a.a的形式,只要我将结果对应成'a' + 'b' + 'c'的形式不就可以了吗?其实这个想法是错误的,因为如果a.a对应的结果是字符串'b'的话,那么a.a.a对应的结果就会是'b'.a的形式,这样程序肯定是会抛出异常的,因为字符串'b'不存在属性a。所以这样思路是错误的。
正确的思路:数据类型隐式转换,其实a + a.a + a.a.a这样的形式中牵扯着数据计算时的隐式转换问题。什么意思呢?其实就是按照我们上面的思路来处理,虽然我们上面的思路是错误的,但是错误的原因是我们将a + a.a + a.a.a的形式转为字符串的形式相加。如果我们现在让a + a.a + a.a.a的形式对应为{} + {} + {}的形式应该处理?
{} + {} + {}其实本质上就是隐式类型转换的问题,程序会如何进行隐式转换?

  1. 首先对象是无法进行加法计算的,JS会尝试将对象的形式转为原始类型,因为转为原始数据类型才能够运行加法运算。所以程序会首先寻找对象是否存在Symbol.toPrimitive属性,Symbol.toPrimitive可以将对象转为一个原始值。该函数被调用时,会被传递一个字符串参数hint,表示要转换到原始值的预期类型。hint参数的取值是number、string、default中的任意一个。
  2. 如果该对象不存在Symbol.toPrimitive属性的话,那么就要去寻找toString/valueOf方法。特别注意:这里存在一个顺序问题,是先调用toString方法还是先调用valueOf方法?其实这就牵扯到一个更细的知识点:如果是将对象转换为number的话,那么就先调用valueOf方法;如果是将对象转为string类型的话,那么就先调用toString方法。
  3. {} + {} + {}使用的是加法运算,那么自然就会触发number的形式转换,也就是将对象的形式转为number类型。

所以按照上面的说法,将{} + {} + {}进行改造,看看程序是如何执行的:
在每个对象中分别都添加了toString,valueOf方法,为什么str输出的结果是'abc',而不是'123'?特别注意下:并不是说,一看到+运算符就认为程序直接调用toString()方法。在隐式转换类型中,并不是这样的。上面说过,对象转为原始数据类型:那么首先要看Symbol.toPrimitive,如果不存在Symbol.toPrimitive属性的话,那么对象会通过不同的顺序调用valueOftoString方法将其转为原始数据类型。
所以说,我们下面的例子str输出的是字符串'abc'

var str = {
	valueOf() {
		return 'a'
	},
	toString() {
		return '1'
	}
} + {
	valueOf() {
		return 'b'
	},
	toString() {
		return '2'
	}
} + {
	valueOf() {
		return 'c'
	},
	toString() {
		return '3'
	}
};
console.log(str); // abc

明白了对象转为原始值类型的顺序之后就很简单了。a + a.a + a.a.a + a.a.a.a这种形式,我们可以用Proxy代理对象,很显然被代理的对象是a
首先我们要明白Proxy到底有什么用呢?Proxy用于创建一个目标对象的代理对象,从而实现目标对象基本操作的拦截与自定义(如属性查找、赋值、枚举、函数)。
所以下面例子中的变量a就是代理对象,而new Proxy()构造函数中的参数就是目标对象。
目标对象中的参数是什么意思?

  1. values:结果需要对应的数组集合。
  2. step:计步器,既然结果要对应数组中的元素,就需要一个类似计步器的东西。
  3. valueOf/toString:对象转为原始数据类型需要调用的方法,valueOftoString方法都可以,二者只不过是调用的顺序不同。

get函数是什么意思呢?其实get函数就是getter函数,也就是说当对代理对象进行属性读取操作时执行的方法。get函数中的参数对应着什么意思呢?

  1. target:目标对象,也就是需要被代理的对象。(与new Proxy()构造函数的参数相等)
  2. key:读取操作时,获取到的属性名。
  3. receiverProxy或者继承Proxy对象。

为什么我们在读取操作的时候,需要判断key值呢?因为在读取操作的时候,受到一些其它属性的影响,例如key的值可能是Symbol.toPrimitive、valueOf、toString之类的,所以我们要进行排除。当读取操作获取到的属性名不存在目标对象的身上时,此时我们就要返回receiver代理对象。
这样做的目的,是将a + a.a + a.a.a的形式转换为{} + {} + {}的形式,为什么step能够实现不停的自增操作呢?因为我们在get函数中判断key值,当key值满足条件时,返回的是代理对象a引用。也就是说a.a, a.a.a, a.a.a.a指向的都是同一个引用,也就是代理对象a,所以step肯定会自增。

let obj = {
  step:0,
  arr:['a','b','c','d','e','f'],
  valueOf:function(){
    return this.arr[this.step++]
  }
}
let a = new Proxy(obj,{
  get(target, key, receiver){
    if(!Reflect.has(target,key)&& typeof key!=='symbol'){
      return receiver
    }else{
      return target[key]
    }
  }
});
console.log(a + a.a + a.a.a + a.a.a.a + a.a.a.a.a+a.a.a.a.a.a); // abcdef

习题二

console.log(a[10][20] + 30); // 60
console.log(a[100][200] + 300); // 600
console.log(a[200][300] + 1000); // 1500

看到这样的形式,我们需要马上想到Proxy的解决方案。这题也有几个需要注意的特点:

  1. 首先是a[10]的形式,这很显然是对象属性读取操作,并且是通过[]中括号的方式进行的。
  2. 其次是a[10][20]的形式,这说明a[10]返回的也是对象,这样才能够链式的调用。如果a[10]返回的不是对象或者没有对象没有20属性,程序肯定是抛出错误的。
  3. 然后是隐式转换的问题,因为a[100][200] + 300需要运算出结果,那么对于a[100][200]来说,程序需要尝试对其进行数据的隐式转换。
  4. 最后是结果,我们需要将[]中括号传入的属性值都累加起来。显然需要一个累加器,用来保存最后返回的结果。

首先还是需要Proxy代理对象,让每一次对代理对象进行读取操作的时候,我们都要累加sum的值,因为最后我们要返回这个累计的结果。
为什么要在get函数最后返回receiver呢?因为receiver表示的是代理对象引用,所以我们返回代理对象就能够实现a[10][20]的调用方式。
为什么有key === Symbol.toPrimitive的判断呢?因为当程序执行到a[10][20] + 30的时候,程序会尝试将a[10][20]从代理对象的形式转为原始数据类型。当进行数据类型转换的时候,首先会去对象上寻找Symbol.toPrimitive的属性,也就是说明程序需要调用Symbol.toPrimitive方法将对象转为原始数据类型。所以我们需要返回一个函数用于Symbol.toPrimitive方法。
注意我们需要重置target对象中sum的值,否则会影响到后面的程序。

const a = new Proxy({
	// 累加器
	sum: 0
}, {
	get(target, key, receiver) {
		// 当key等于Symbol.toPrimitive的时候,说明a[10][20]需要从对象转换为原始数据类型
		if (key === Symbol.toPrimitive) {
			let { sum } = target;
			// 重置sum值
			target.sum = 0;
			// 返回Symbol.toPrimitive方法
			return (hint) => sum;
		} else {
			target.sum += Number(key);
		}
		return receiver;
	}
})

Symbol.toPrimitive()

对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式:

  1. Number:该场合需要转为数值。
  2. String:该场合需要转为字符串。
  3. Default:该场合可以转为数值,也可以转为字符串。

为什么3 + obj会走default模式呢?因为+运算符的特殊性,因为它有可能是字符串拼接操作,也有可能是数值的加法运算。所以只能走default模式。

let obj = {
	[Symbol.toPrimitive](hint) {
		switch (hint) {
			case 'number':
				return 123;
			case 'string':
				return 'str';
			case 'default':
				return 'default';
			default:
				throw new Error();
		}
	}
};

2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

️不倒翁

你的鼓励就是我前进的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值