拦截函数调用或改变函数行为的方法有哪些?

What are ways of intercepting function calls or changing a function's behavior?

我想在每次调用对象中的某些函数并执行完毕时执行一些代码。

对象:

{
    doA() {
        // Does A
    },
    doB() {
        // Does B
    }
}

是否可以扩展它,改变那些功能,让它们做它们做的事,然后再做其他事情?好像这是一个监听那些函数完成的事件?

{
    doA() {
        // Does A
        // Do something else at end
    },
    doB() {
        // Does B
        // Do something else at end
    }
}

也许这可以使用代理 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

尝试使用代理:

const ob = {
    doA() {
        console.log('a');
    },
    doB() {
        console.log('b');
    }
};

const ob2 = new Proxy(ob, {
  apply: function (target, key, value) {
    console.log('c');
  },
});

ob2.doA();

下面是一个将函数包装在对象中的直接方法示例。

这对调试很有用,但您不应该在生产环境中这样做,因为您(或稍后查看您的代码的任何人)将很难弄清楚为什么您的方法正在做一些不在原代码。

var myObj = {
  helloWorld(){
    console.log('Hello, world!');
  }
}


// get a refernce to the original function
var f = myObj.helloWorld;

// overwrite the original function
myObj.helloWorld = function(...args){
  
  // call the original function first
  f.call(this, ...args);
  
  // Then  do other stuff afterwards
  console.log('Goodbye, cruel world..');
};


myObj.helloWorld();

如果它是关于具有相同的事件侦听器机制,您可以创建一个包含存储函数并在需要时执行它们的对象

const emitter =  {
    events: {},    

    addListener(event, listener) {
        this.events[event] = this.events[event] || [];
        this.events[event].push(listener);
    },

    emit(event, data) {
        if(this.events[event]) {
            this.events[event].forEach(listener => listener(data));
        }
    }
}

//instead of the config object you could just type the string for the event
const config = {
    doA: 'doA',
    doB: 'doB'
}

//store first function for doA
emitter.addListener(config.doA, (data) => {
    console.log('hardler for Function ' + data + ' executed!');
});

//store second function for doA
emitter.addListener(config.doA, () => {
    console.log('Another hardler for Function A executed!');
});

//store first function for doB
emitter.addListener(config.doB, (data) => {
    console.log('hardler for Function ' + data + ' executed!');
});

let obj = {
    doA() {
        let char = 'A';
        console.log('doA executed!');
        //You can pass data to the listener
        emitter.emit(config.doA, char);
    },

    doB() {
        let char = 'B';
        console.log('doB executed!');
        emitter.emit(config.doB, char);
    }
}

obj.doA();
obj.doB();

//Output:
//doA executed!
//hardler for Function A executed!
//Another hardler for Function A executed!
//doB executed!
//hardler for Function B executed!

使用 Proxy 我们可以定位所有包含函数的 get,然后我们可以检查正在获取的是否是一个函数,如果是我们创建和 return 我们自己的函数来包装目标函数并调用它。

从我们的包装函数中调用对象函数后,我们可以执行任何我们想要的,然后 return 对象函数的 return 值。

const ob = {
  doA(arg1, arg2) {
    console.log(arg1, arg2);
    return 1;
  },
  doB() {
    console.log('b');
  }
};

const ob2 = new Proxy(ob, {
  get: function(oTarget, sKey) {
    if (typeof oTarget[sKey] !== 'function')    return oTarget[sKey];

    return function(...args) {
      const ret = oTarget[sKey].apply(oTarget, args);

      console.log("c");

      return ret;
    }
  }
});

console.log(ob2.doA('aaa', 'bbb'));

如果有改进或其他选项,请添加评论!

对于 JavaScript 应用程序,有时需要 拦截 and/or 修改 控制流程 由于其他原因不拥有或拥有的功能, 不允许触摸。

对于这种情况,除了通过包装其原始实现来保留和更改此类逻辑之外,别无他法。这种能力并不是 JavaScript 独有的。通过 Reflection[= 启用 元编程 的编程语言历史悠久56=] 和 自修改.

原因之一 could/should 为人们可以想到的所有可能的修饰符用例提供防弹但方便的抽象。

由于 JavaScript 已经实现了 Function.prototype.bind,它已经带有某种微小的修改功能,我个人不介意,如果有一天 JavaScript 正式推出定制和标准化的便捷 方法修饰符 工具集...... Function.prototype[before|around|after|afterThrowing|afterFinally].

// begin :: closed code
const obj = {
  valueOf() {
    return { foo: this.foo, bar: this.bar };
  },
  toString(link = '-') {
    return [this.foo, this.bar].join(link);
  },
  foo: 'Foo',
  bar: 'Bar',
  baz: 'BAAAZZ'
};
// end :: closed code

console.log(
  'obj.valueOf() ...',
  obj.valueOf()
);
console.log(
  'obj.toString() ...',
  obj.toString()
);


enableMethodModifierPrototypes();


function concatBazAdditionally(proceed, handler, [ link ]) {
  const result = proceed.call(this, link);
  return `${ result }${ link }${ this.baz }`;
}
obj.toString = obj.toString.around(concatBazAdditionally, obj);
// obj.toString = aroundModifier(obj.toString, concatBazAdditionally, obj)

console.log(
  '`around` modified ... obj.toString("--") ...',
  obj.toString("--")
);


function logWithResult(result, args) {
  console.log({ modifyerLog: { result, args, target: this.valueOf() } });
}
obj.toString = obj.toString.after(logWithResult, obj);
// obj.toString = afterModifier(obj.toString, logWithResult, obj)

console.log(
  '`around` and `after` modified ... obj.toString("##") ...',
  obj.toString("##")
);


function logAheadOfInvocation(args) {
  console.log({ stats: { args, target: this } });
}
obj.valueOf = obj.valueOf.before(logAheadOfInvocation, obj);
// obj.valueOf = beforeModifier(obj.valueOf, logAheadOfInvocation, obj)

console.log(
  '`before` modified ... obj.valueOf() ...',
  obj.valueOf()
);


restoreDefaultFunctionPrototype();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
  function isFunction(value) {
    return (
      typeof value === 'function' &&
      typeof value.call === 'function' &&
      typeof value.apply === 'function'
    );
  }

  function getSanitizedTarget(value) {
    return value ?? null;
  }

  function around(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function aroundType(...args) {
        const context = getSanitizedTarget(this) ?? target;

        return handler.call(context, proceed, handler, args);
      }
    ) || proceed;
  }
  around.toString = () => 'around() { [native code] }';

  function before(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function beforeType(...args) {
        const context = getSanitizedTarget(this) ?? target;

        handler.call(context, [...args]);

        return proceed.apply(context, args);
      }
    ) || proceed;
  }
  before.toString = () => 'before() { [native code] }';

  function after(handler, target) {
    target = getSanitizedTarget(target);

    const proceed = this;
    return (
      isFunction(handler) &&
      isFunction(proceed) &&

      function afterReturningType(...args) {
        const context = getSanitizedTarget(this) ?? target;
        const result = proceed.apply(context, args);

        handler.call(context, result, args);

        return result;
      }
    ) || proceed;
  }
  after.toString = () => 'after() { [native code] }';

  function aroundModifier(proceed, handler, target) {
    return around.call(proceed, handler, target);
  }
  function beforeModifier(proceed, handler, target) {
    return before.call(proceed, handler, target);
  }
  function afterModifier(proceed, handler, target) {
    return after.call(proceed, handler, target);
  }

  const { prototype: fctPrototype } = Function;

  const methodIndex = {
    around,
    before,
    after/*Returning*/,
    // afterThrowing,
    // afterFinally,
  };
  const methodNameList = Reflect.ownKeys(methodIndex);

  function restoreDefaultFunctionPrototype() {
    methodNameList.forEach(methodName =>
      Reflect.deleteProperty(fctPrototype, methodName),
    );
  }
  function enableMethodModifierPrototypes() {
    methodNameList.forEach(methodName =>
      Reflect.defineProperty(fctPrototype, methodName, {
        configurable: true,
        writable: true,
        value: methodIndex[methodName],
      }),
    );
  }
</script>

<!--
<script src="https://closure-compiler.appspot.com/code/jscd16735554a0120b563ae21e9375a849d/default.js"></script>
<script>
  const {

    disablePrototypes: restoreDefaultFunctionPrototype,
    enablePrototypes: enableMethodModifierPrototypes,
    beforeModifier,
    aroundModifier,
    afterModifier,

  } = modifiers;
</script>
//-->

下一个提供的示例代码使用上述测试对象及其测试用例,但 implements/provides 是基于代理的解决方案。从测试用例需要如何调整,可以看出直接方法修改,基于 method-modifiers 的干净实现,允许更灵活地处理不同的用例,而基于代理的方法仅限于每个拦截的方法调用一个处理函数...

// begin :: closed code
const obj = {
  valueOf() {
    return { foo: this.foo, bar: this.bar };
  },
  toString(link = '-') {
    return [this.foo, this.bar].join(link);
  },
  sayHi() {
    console.log('Hi');
  },
  foo: 'Foo',
  bar: 'Bar',
  baz: 'BAAAZZ'
};
// end :: closed code

console.log(
  'non proxy call ... obj.valueOf() ...',
  obj.valueOf()
);
console.log(
  'non proxy call ... obj.toString() ...',
  obj.toString()
);


function toStringInterceptor(...args) {
  const { proceed, target } = this;
  const [ link ] = args;

  // retrieve the original return value.
  let result = proceed.call(target, link);

  // modify the return value while
  // intercepting the original method call.
  result = `${ result }${ link }${ target.baz }`;

  // log before ...
  console.log({ toStringInterceptorLog: { result, args, target: target.valueOf() } });

  // ... returning the
  // modified value.
  return result;
}

function valueOfInterceptor(...args) {
  const { proceed, target } = this;

  // log before returning ...
  console.log({ valueOfInterceptorLog: { proceed, args, target } });

  // ... and save/keep the
  // original return value.
  return proceed.call(target);
}

function handleTrappedGet(target, key) {
  const interceptors = {
    toString: toStringInterceptor,
    valueOf: valueOfInterceptor,
  }
  const value = target[key];

  return (typeof value === 'function') && (

    interceptors[key]
      ? interceptors[key].bind({ proceed: value, target })
      : value.bind(target)

  ) || value;
}
const objProxy = new Proxy(obj, { get: handleTrappedGet });

console.log('\n+++ proxy `get` handling +++\n\n');

const { foo, bar, baz } = objProxy;
console.log(
  'non method `get` handling ...',
  { foo, bar, baz }
);
console.log('\nproxy call ... objProxy.sayHi() ... but not intercepted ...');
objProxy.sayHi();

console.log('\nintercepted proxy calls ...');
console.log(
  'objProxy.toString("--") ...',
  objProxy.toString("--")
);
console.log(
  'objProxy.valueOf() ...',
  objProxy.valueOf()
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

您当然可以使用代理来做到这一点。但是您也可以编写自己的通用函数装饰器来执行此操作。

基本的装饰器可能是这样工作的:

const wrap = (wrapper) => (fn) => (...args) => {
  (wrapper .before || (() => {})) (...args)
  const res = fn (...args)
  const newRes = (wrapper .after || (() => {})) (res, ...args)
  return newRes === undefined ? res : newRes
}

const plus = (a, b) => a + b

const plusPlus = wrap ({
  before: (...args) => console .log (`Arguments: ${JSON.stringify(args)}`),
  after: (res, ...args) => console .log (`Results: ${JSON.stringify(res)}`)
}) (plus)

console .log (plusPlus (5, 7))

我们在主体之前(使用相同的参数)和之后(使用结果以及初始参数)向 运行 提供可选函数,并将我们想要的函数传递给结果函数装饰。生成的函数将调用 before、主函数,然后调用 after,如果未提供则跳过它们。

要使用此包装对象的元素,我们可以编写一个处理所有功能的薄包装器:

const wrap = (wrapper) => (fn) => (...args) => {
  (wrapper .before || (() => {})) (...args)
  const res = fn (...args)
  const newRes = (wrapper .after || (() => {})) (res, ...args)
  return newRes === undefined ? res : newRes
}

const wrapAll = (wrapper) => (o) => Object .fromEntries (
  Object .entries (o) .map (([k, v]) => [k, typeof v == 'function' ? wrap (wrapper) (v) : v])
)

const o = {
    doA () {
        console .log ('Does A')
    },
    doB () {
        console .log ('Does B')
    }
}

const newO = wrapAll ({
  after: () => console .log ('Does something else at end')
}) (o)

newO .doA ()
newO .doB ()

当然这可以通过多种方式扩展。我们可能想要选择特定的函数属性来包装。我们可能想要流畅地处理 this。我们可能希望 before 能够 改变 传递给主函数的参数。我们可能想给生成的函数起一个有用的名字。等等。但很难为通用包装器设计签名,而 所有 这些事情都可以轻松完成。