习题一
实现下面例子中的效果,需要怎么做?
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
的形式对应为{} + {} + {}
的形式应该处理?
{} + {} + {}
其实本质上就是隐式类型转换的问题,程序会如何进行隐式转换?
- 首先对象是无法进行加法计算的,
JS
会尝试将对象的形式转为原始类型,因为转为原始数据类型才能够运行加法运算。所以程序会首先寻找对象是否存在Symbol.toPrimitive
属性,Symbol.toPrimitive
可以将对象转为一个原始值。该函数被调用时,会被传递一个字符串参数hint
,表示要转换到原始值的预期类型。hint
参数的取值是number、string、default
中的任意一个。 - 如果该对象不存在
Symbol.toPrimitive
属性的话,那么就要去寻找toString/valueOf
方法。特别注意:这里存在一个顺序问题,是先调用toString
方法还是先调用valueOf
方法?其实这就牵扯到一个更细的知识点:如果是将对象转换为number
的话,那么就先调用valueOf
方法;如果是将对象转为string
类型的话,那么就先调用toString
方法。 {} + {} + {}
使用的是加法运算,那么自然就会触发number
的形式转换,也就是将对象的形式转为number
类型。
所以按照上面的说法,将{} + {} + {}
进行改造,看看程序是如何执行的:
在每个对象中分别都添加了toString,valueOf
方法,为什么str
输出的结果是'abc'
,而不是'123'
?特别注意下:并不是说,一看到+
运算符就认为程序直接调用toString()
方法。在隐式转换类型中,并不是这样的。上面说过,对象转为原始数据类型:那么首先要看Symbol.toPrimitive
,如果不存在Symbol.toPrimitive
属性的话,那么对象会通过不同的顺序调用valueOf
和toString
方法将其转为原始数据类型。
所以说,我们下面的例子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()
构造函数中的参数就是目标对象。
目标对象中的参数是什么意思?
values
:结果需要对应的数组集合。step
:计步器,既然结果要对应数组中的元素,就需要一个类似计步器的东西。valueOf/toString
:对象转为原始数据类型需要调用的方法,valueOf
和toString
方法都可以,二者只不过是调用的顺序不同。
get
函数是什么意思呢?其实get
函数就是getter
函数,也就是说当对代理对象进行属性读取操作时执行的方法。get
函数中的参数对应着什么意思呢?
target
:目标对象,也就是需要被代理的对象。(与new Proxy()
构造函数的参数相等)key
:读取操作时,获取到的属性名。receiver
:Proxy
或者继承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
的解决方案。这题也有几个需要注意的特点:
- 首先是
a[10]
的形式,这很显然是对象属性读取操作,并且是通过[]
中括号的方式进行的。 - 其次是
a[10][20]
的形式,这说明a[10]
返回的也是对象,这样才能够链式的调用。如果a[10]
返回的不是对象或者没有对象没有20
属性,程序肯定是抛出错误的。 - 然后是隐式转换的问题,因为
a[100][200] + 300
需要运算出结果,那么对于a[100][200]
来说,程序需要尝试对其进行数据的隐式转换。 - 最后是结果,我们需要将
[]
中括号传入的属性值都累加起来。显然需要一个累加器,用来保存最后返回的结果。
首先还是需要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
被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式:
Number
:该场合需要转为数值。String
:该场合需要转为字符串。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'