JS Proxy的奇怪现象详解

482 阅读3分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

最近在使用Proxy的时候,发现了几个奇怪的现象,打算深入去了解下原因。不了解Proxy的小伙伴可以先来这里 JS Proxy对象的介绍与实践 看看。

set()和defineProperty()的触发

先上代码,Case 1

var foo = {
  b: 'lol'
}
var handler = {
  defineProperty(obj, prop, descriptor) {
    console.log(`defineProperty method starts foo.${prop} = ${descriptor.value}`)
    obj[prop] = descriptor.value
    return true
  },
  set(obj, prop, val) {
    console.log(`set method starts foo.${prop} = ${val}`)
    obj[prop] = val
  }
}

var newFoo = new Proxy(foo, handler);

newFoo.a = 1
// output: set method starts foo.a = 1
newFoo.b = 'Zzz'
// output: set method starts foo.b = Zzz

Object.defineProperty(newFoo, 'num', {
  value: 666
})
// output: defineProperty method starts foo.num = 666

console.log(foo.a, foo.b, foo.num)
// output: 1 "Zzz" 666

这符合我们的期望,然后来看看另一个例子,Case 2

var foo = {
  b: 'lol'
}
var handler = {
  defineProperty(obj, prop, descriptor) {
    console.log(`defineProperty method starts foo.${prop} = ${descriptor.value}`)
    obj[prop] = descriptor.value
    return true
  }
}

var newFoo = new Proxy(foo, handler);

newFoo.a = 1
// output: defineProperty method starts foo.a = 1
newFoo.b = 'Zzz'
// output: defineProperty method starts foo.b = Zzz

Object.defineProperty(newFoo, 'num', {
  value: 666
})
// output: defineProperty method starts foo.num = 666

console.log(foo.a, foo.b, foo.num)
// output: 1 "Zzz" 666

神奇的事情发生了,我们Case 2只是把Case 1set()劫持去掉,但结果发现常规的属性赋值语句,也触发defineProperty()的劫持,这跟我们通常的理解不太一致。而根据MDN上面,Proxy/defineProperty的描述,是劫持Object.defineProperty()的操作。

Screen Shot 2021-08-08 at 5.40.04 PM.png

原因

百思不得其解之际,我在ECMA官网上查找到了对象的set和defineProperty的内部调用逻辑,大体如下:

对象set操作

Screen Shot 2021-08-08 at 5.50.01 PM.png

对象defineProperty操作

Screen Shot 2021-08-08 at 5.51.29 PM.png

简而言之,set操作,会先执行内部的[[Set]]方法从而率先触发Proxy的Set劫持(如有),进而不再进行后续的一系列操作;如果没有定义Proxy的Set劫持,则会一直执行到内部的[[DefineOwnProperty]]方法,继而出发Proxy的DefineProperty劫持(如有)。

has()的触发

惯例先上代码

var foo = [233, 555, 666]
var handler = {
  has(obj, prop) {
    console.log(`has foo[${prop}]`)
    return prop in obj
  }
}

var proxy = new Proxy(foo, handler);

'4' in proxy
// output: has foo[4]
console.log('==========')
proxy.indexOf(666)
// output: has foo[0]
// output: has foo[1]
// output: has foo[2]
console.log('==========')
proxy.forEach((v, i) => {
  console.log(v, i)
})
// output: has foo[0]
// output: 233 0
// output: has foo[1]
// output: 555 1
// output: has foo[2]
// output: 666 2
console.log('==========')
proxy.concat([985, 211])
// output: has foo[0]
// output: has foo[1]
// output: has foo[2]

神奇的事情再次出现了,根据MDN上面,Proxy/has的描述,是劫持in操作符的执行,但是这里确是好几个数组方法都触发了。

Screen Shot 2021-08-08 at 8.19.16 PM.png

原因

有了前面的经验,我们再次来到ECMA的官网,找到in操作符的官方文档(要手动滚动下去找到in),可以找到Proxy处理函数中的has()所劫持Object的HasProperty方法

接着来到Array对象的原型链,查看原型链方法就会发现,以下的方法都会在遍历数组元素的时候调用到HasProperty():

  • concat()
  • copyWithin()
  • every()
  • filter()
  • flat()
  • forEach()
  • indexOf()
  • lastIndexOf()
  • map()
  • reduce()
  • reduceRight()
  • reverse()
  • shift()
  • slice()
  • some()
  • sort()
  • splice()
  • unshift()

我们可以发现这样的规律,涉及需要遍历数组元素或者改变数组元素排列顺序的方法,都会触发。

关于Proxy handlers

最后,关于Proxy Handlers,具体可以在这查看到每个处理函数对应所劫持的内部方法。

Screen Shot 2021-08-06 at 9.28.39 PM.png