JS对函数实现get和apply的拦截代理,调用call时会发生什么?

Azure_Chien
  • 97

问题描述

现有一个名为Animal的函数,它会打印出相关语句。然后使用代理,实现其中的getapply方法:

function Animal() {
  console.log("This is a " + this.species);
}

var animalProxy = new Proxy(Animal, {
  get: function(target, prop, recevier) {
    console.log("Proxy get was invoked!");
    return target[prop];   
  },
  apply: function(target, context, args) {
    console.log("Proxy apply was invoked!");
    return Reflect.apply(...arguments);
  }
});

animalProxy实现了对函数Animal的代理,现在,有如下语句

console.log( animalProxy.call({ 
    species: "Cat" 
} ));

它的运行结果也如预期,输出了

Proxy get was invoked!
Proxy apply was invoked!
This is a Cat
undefined

问题

  1. 发现对代理对象调用call方法时,getapply两个拦截方法都被调用了。但是注意到get方法中返回的是target[prop],在这里我认为返回的应该是原对象(即Animal)的call方法,和代理对象无关。既然已经返回了和代理对象无关的方法,这么说来后续对call进行函数调用也不是在代理对象上调用的,为什么还会触发apply拦截呢?
  2. 假设,对于代理对象使用callapply的行为都会被apply拦截,那么看起来get方法返回的值就不重要了,因为它的返回值不影响apply的使用。但是如果get方法返回一个非函数对象,例如返回1,那么运行时会触发一个错误:TypeError: animalProxy.call is not a function

延伸问题

假如我在每个拦截方法中都输出arguments参数列表,即:

var animalProxy = new Proxy(Animal, {
  get: function(target, prop, recevier) {
    console.log("Proxy get was invoked!",arguments);    //输出参数列表
    return target[prop];
  },
  apply: function(target, context, args) {
    console.log("Proxy apply was invoked!",arguments);    //输出参数列表
    return Reflect.apply(...arguments);
  }
});

那么使用console.log( animalProxy.call({species: "Cat" } ));语句时,在Chrome控制台环境下运行正常,但是在Nodejs(v10.6.0)中会爆栈,它会在get中的console语句中发生栈溢出:

<     console.log("Proxy get was invoked!",arguments);
<             ^
< RangeError: Maximum call stack size exceeded

然而如果注释掉get中的arguments但是保留apply中的arguments,那么在Nodejs(v10.6.0)环境中就运行正常。
这是为什么呢?是和Nodejs版本问题有关吗?

回复
阅读 3k
1 个回答

问题 1:

你在 get 中返回的是只是call函数,他的调用者还是proxy,所以还会触发apply

你可以用下面的例子运行试试看
add会输出 3 次get,也就是在add函数内部this指向应该还是proxy
用箭头函数的minus就只会执行一次,因为箭头函数默认绑定外层的this,不会因为调用者而改变,所以不会触发get

const obj = {
  a: 1,
  b: 2,
  add: function() {
    return this.a + this.b
  },
  minus: () => {
    return this.a - this.b
  },
}
const proxy = new Proxy(obj, {
  get: function(target, prop, recevier) {
    console.log('Proxy get was invoked!', prop)
    return target[prop]
  },
})
console.log(proxy.add())
console.log('========')
console.log(proxy.minus())

图片描述

问题 2:

animalProxy.call({ species: 'Cat' })
这句语句要分为两步来看,
第一步是 . 操作,也就是 get 操作,触发get拦截,
第二步是调用get返回的call,触发apply

第二步的 call 执行的是 get 返回的函数,所以如果返回一个非函数,却要执行肯定会报错的

你可以直接返回一个函数,这样执行的时候就不会报错,但不会执行apply拦截

get: function(target, prop, recevier) {
    console.log('Proxy get was invoked!', prop)
    return ()=>{}
  },

补充问题

爆栈是因为arguments里面的第三个参数recevier造成的,这个参数指向的是代理本身
cosole.log输出的时候,chromenode实现的好像不太一样
chrome会直接输出代理本身
node找到proxy所代理的对象obj,而在找obj的过程中会不断触发get,然后一直循环
所以chrome可以正常运行,而node会爆栈

这个问题的话,你可以用我第一个例子试试看,在add函数里面加一句

console.log('===== begin log this =====')
console.log('add this =>', this)
console.log('===== end log this =====')

然后在chromenode里面分别运行看看
chrome里面会直接输出proxy,get只触发 3 次,在ndoe里面会输出obj,get触发了 8 次

chrome
图片描述

node
图片描述


大概讲下我粗浅的理解,至于在深入的理解,现在功力还不够
你知道吗?

宣传栏