如何使用ES6类扩展Function?


105

ES6允许扩展特殊对象。因此可以从该函数继承。可以将此类对象称为函数,但是如何实现此类调用的逻辑呢?

class Smth extends Function {
  constructor (x) {
    // What should be done here
    super();
  }
}

(new Smth(256))() // to get 256 at this call?

任何类方法都可通过引用类实例this。但是,当将其称为函数时,this是指window。将类实例作为函数调用时,如何获得对它的引用?

PS:同样的俄语问题。


17
啊,最后有人问了这个问题:-)
Bergi '16

1
就是这样做super(x)(即将其传递给Function)?不确定是否Function可以扩展。
Felix Kling

请记住,扩展内置类仍然存在问题。规范建议应该可行,但是我遇到了扩展Error等问题。
ssube

1
请记住,这Function只是一个函数构造函数。该函数的实现必须传递给构造函数。如果您不想Smth接受实现,则必须在构造函数中提供它,即super('function implementation here')
Felix Kling

1
@Qwertiy:我认为这是例外,而不是一般情况。这也很特定于函数表达式,但是您使用的Function构造函数(运行时)与函数表达式(语法)有很大不同。
Felix Kling

Answers:


49

super调用将调用Function构造函数,该构造函数需要一个代码字符串。如果要访问实例数据,则可以对其进行硬编码:

class Smth extends Function {
  constructor(x) {
    super("return "+JSON.stringify(x)+";");
  }
}

但这并不令人满意。我们要使用闭包。

将返回的函数设为可以访问您的实例变量的闭包是可能的,但并不容易。好消息是,super如果您不想调用,则不必调用-您仍然可以return从ES6类构造函数中获取任意对象。在这种情况下,我们会

class Smth extends Function {
  constructor(x) {
    // refer to `smth` instead of `this`
    function smth() { return x; };
    Object.setPrototypeOf(smth, Smth.prototype);
    return smth;
  }
}

但是我们可以做得更好,并从以下内容中抽象出这一点Smth

class ExtensibleFunction extends Function {
  constructor(f) {
    return Object.setPrototypeOf(f, new.target.prototype);
  }
}

class Smth extends ExtensibleFunction {
  constructor(x) {
    super(function() { return x; }); // closure
    // console.log(this); // function() { return x; }
    // console.log(this.prototype); // {constructor: …}
  }
}
class Anth extends ExtensibleFunction {
  constructor(x) {
    super(() => { return this.x; }); // arrow function, no prototype object created
    this.x = x;
  }
}
class Evth extends ExtensibleFunction {
  constructor(x) {
    super(function f() { return f.x; }); // named function
    this.x = x;
  }
}

诚然,这在继承链中创建了一个额外的间接级别,但这并不一定是一件坏事(您可以扩展它而不是native Function)。如果要避免这种情况,请使用

function ExtensibleFunction(f) {
  return Object.setPrototypeOf(f, new.target.prototype);
}
ExtensibleFunction.prototype = Function.prototype;

但请注意,它Smth不会动态继承静态Function属性。


我想从函数访问类状态。
Qwertiy '16

2
@Qwertiy:然后使用Bergi的第二个建议。
Felix Kling

@ AlexanderO'Mara:如果您希望自己的Smth实例instanceof Smth(正如每个人所期望的那样),您就无法绕开该函数的原型。Object.setPrototypeOf如果不需要此调用或不需要在类中声明的任何原型方法,则可以忽略该调用。
Bergi 2016年

@ AlexanderO'Mara:Object.setPrototypeOf只要在创建对象之后立即完成优化,就不会有太大的优化危险。只是如果您在对象的生命周期中来回更改它的[[prototype]]就会很糟糕。
Bergi 2016年

1
@amn不,当您不使用thisreturn对象时,您不这样做。
Bergi

32

这是一种创建可调用对象的方法,该对象可正确引用其对象成员,并保持正确的继承,而不会弄乱原型。

只是:

class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)')
    var self = this.bind(this)
    this.__self__ = self
    return self
  }

  // Example `__call__` method.
  __call__(a, b, c) {
    return [a, b, c];
  }
}

扩展此类并添加__call__方法,更多信息在下面...

代码和注释中的解释:

// This is an approach to creating callable objects
// that correctly reference their own object and object members,
// without messing with prototypes.

// A Class that extends Function so we can create
// objects that also behave like functions, i.e. callable objects.
class ExFunc extends Function {
  constructor() {
    super('...args', 'return this.__self__.__call__(...args)');
    // Here we create a function dynamically using `super`, which calls
    // the `Function` constructor which we are inheriting from. Our aim is to create
    // a `Function` object that, when called, will pass the call along to an internal
    // method `__call__`, to appear as though the object is callable. Our problem is
    // that the code inside our function can't find the `__call__` method, because it
    // has no reference to itself, the `this` object we just created.
    // The `this` reference inside a function is called its context. We need to give
    // our new `Function` object a `this` context of itself, so that it can access
    // the `__call__` method and any other properties/methods attached to it.
    // We can do this with `bind`:
    var self = this.bind(this);
    // We've wrapped our function object `this` in a bound function object, that
    // provides a fixed context to the function, in this case itself.
    this.__self__ = self;
    // Now we have a new wrinkle, our function has a context of our `this` object but
    // we are going to return the bound function from our constructor instead of the
    // original `this`, so that it is callable. But the bound function is a wrapper
    // around our original `this`, so anything we add to it won't be seen by the
    // code running inside our function. An easy fix is to add a reference to the
    // new `this` stored in `self` to the old `this` as `__self__`. Now our functions
    // context can find the bound version of itself by following `this.__self__`.
    self.person = 'Hank'
    return self;
  }
  
  // An example property to demonstrate member access.
  get venture() {
    return this.person;
  }
  
  // Override this method in subclasses of ExFunc to take whatever arguments
  // you want and perform whatever logic you like. It will be called whenever
  // you use the obj as a function.
  __call__(a, b, c) {
    return [this.venture, a, b, c];
  }
}

// A subclass of ExFunc with an overridden __call__ method.
class DaFunc extends ExFunc {
  constructor() {
    super()
    this.a = 'a1'
    this.b = 'b2'
    this.person = 'Dean'
  }

  ab() {
    return this.a + this.b
  }
  
  __call__(ans) {
    return [this.ab(), this.venture, ans];
  }
}

// Create objects from ExFunc and its subclass.
var callable1 = new ExFunc();
var callable2 = new DaFunc();

// Inheritance is correctly maintained.
console.log('\nInheritance maintained:');
console.log(callable2 instanceof Function);  // true
console.log(callable2 instanceof ExFunc);  // true
console.log(callable2 instanceof DaFunc);  // true

// Test ExFunc and its subclass objects by calling them like functions.
console.log('\nCallable objects:');
console.log( callable1(1, 2, 3) );  // [ 'Hank', 1, 2, 3 ]
console.log( callable2(42) );  // [ 'a1b2', Dean', 42 ]

// Test property and method access
console.log(callable2.a, callable2.b, callable2.ab())

查看repl.it

进一步说明bind

function.bind()的工作方式与相似function.call(),并且它们共享类似的方法签名:

fn.call(this, arg1, arg2, arg3, ...);关于mdn的更多信息

fn.bind(this, arg1, arg2, arg3, ...);关于mdn的更多信息

在这两个参数中,第一个参数重新定义this了函数内部的上下文。其他参数也可以绑定到值。但是,在call立即使用绑定值调用该函数的情况下,bind将返回一个“异国”函数对象,该对象透明地包装原始对象,this预设的参数。

因此,当您定义一个函数时bind,它的一些自变量:

var foo = function(a, b) {
  console.log(this);
  return a * b;
}

foo = foo.bind(['hello'], 2);

您仅用剩余的参数调用绑定函数,其上下文已预先设置,在本例中为['hello']

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`

您能否添加一个说明为什么bind有效(即为什么返回的实例ExFunc)?
Bergi

@Bergi bind返回一个透明的函数对象,该对象包装了它被调用的函数对象,这是我们可调用的对象,只是带有this上下文反弹。因此,它实际上会返回的透明包装实例ExFunc。发布有关的更多信息的更新bind
阿德里安

1
@Bergi所有getter / setter和方法都是可访问的,必须在constructorafter bind中指定属性/属性ExFunc。在ExFunc的子类中,所有成员都是可访问的。至于instanceof; 在es6中,绑定函数被称为外来函数,因此它们的内部工作方式并不明显,但是我认为它会将调用通过传递给包装的目标Symbol.hasInstance。它非常类似于Proxy,但是它是实现所需效果的简单方法。他们的签名相似是不一样的。
阿德里安

1
@Adrien但无法从内部__call__访问this.athis.ab()。例如repl.it/repls/FelineFinishedDesktopenvironment
抢劫

1
@rob发现得很好,存在参考错误,我已经更新了答案和代码,并提供了修复和新的解释。
阿德里安

20

您可以将Smth实例包装在带有(可能是)陷阱的Proxy中applyconstruct

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256

好主意。像这样。我是否应该实施更多逻辑,而不是将其放置在apply内部?
Qwertiy '16

4
代理会产生很多开销,不是吗?另外,this仍然是一个空函数(选中new Smth().toString())。
Bergi '16

2
@Bergi不知道性能。MDN带有红色的大胆警告,setPrototypeOf并且不对代理进行任何说明。但是我想代理可能像setPrototypeOf。大约toString,它可以用中的自定义方法遮盖Smth.prototype。无论如何,本机是依赖于实现的。
Oriol

@Qwertiy您可以添加construct陷阱以指定的行为new new Smth(256)()。并添加自定义方法,toString以遮盖访问函数代码的本机方法,如Bergi所述。
Oriol

我是说您的apply方法是以应该使用的方式实现的,还是只是一个演示,我需要仔细阅读有关的更多信息ProxyReflect以正确的方式使用它?
Qwertiy '16

3

我从Bergi的答案中得到建议,并将其包装到NPM模块中

var CallableInstance = require('callable-instance');

class ExampleClass extends CallableInstance {
  constructor() {
    // CallableInstance accepts the name of the property to use as the callable
    // method.
    super('instanceMethod');
  }

  instanceMethod() {
    console.log("instanceMethod called!");
  }
}

var test = new ExampleClass();
// Invoke the method normally
test.instanceMethod();
// Call the instance itself, redirects to instanceMethod
test();
// The instance is actually a closure bound to itself and can be used like a
// normal function.
test.apply(null, [ 1, 2, 3 ]);

3

更新:

不幸的是,这并不是很有效,因为它现在返回的是函数对象而不是类,因此看来如果不修改原型实际上是无法完成的。瘸。


基本上问题是没有办法thisFunction构造函数设置值。真正做到这一点的唯一方法是.bind事后使用该方法,但是这不是非常友好的类。

我们可以在helper基类中执行此操作,但是this直到首次super调用之后该功能才可用,因此有点棘手。

工作示例:

'use strict';

class ClassFunction extends function() {
    const func = Function.apply(null, arguments);
    let bound;
    return function() {
        if (!bound) {
            bound = arguments[0];
            return;
        }
        return func.apply(bound, arguments);
    }
} {
    constructor(...args) {
        (super(...args))(this);
    }
}

class Smth extends ClassFunction {
    constructor(x) {
        super('return this.x');
        this.x = x;
    }
}

console.log((new Smth(90))());

(示例需要使用现代浏览器或node --harmony。)

基本上,基本功能ClassFunction扩展会Function使用类似于的自定义函数包装构造函数调用.bind,但允许在以后的第一次调用时进行绑定。然后,在ClassFunction构造函数本身中,它调用返回的函数super,现在从该函数中返回绑定函数,this以完成设置自定义绑定函数的过程。

(super(...))(this);

这一切都相当复杂,但是它确实避免了对原型的变异,出于优化原因,原型被认为是错误的形式,并且可以在浏览器控制台中生成警告。


1
您使事情变得过于复杂。bound将引用return该匿名类中的函数。只需命名,然后直接引用即可。我还建议避免传递代码字符串,因为它们只是一团糟(在开发过程的每个步骤中)。
Bergi '16

extends似乎并没有像预期的那样工作,Function.isPrototypeOf(Smth)而且也是new Smth instanceof Function错误的。
Bergi '16

@Bergi您正在使用什么JS引擎?console.log((new Smth) instanceof Function);true我在节点v5.11.0和最新的Firefox浏览器。
亚历山大·奥玛拉

糟糕,例子错误。这是new Smth instanceof Smth不与您的解决方案的工作。同样,Smth实例上没有可用的方法-因为您只返回标准Function而不是Smth
Bergi '16

1
@Bergi Darn,看来您是对的。但是,扩展任何本机类型似乎都存在相同的问题。extend Functionnew Smth instanceof Smth造假。
Alexander O'Mara

1

首先,我通过提出了解决方案arguments.callee,但是那太糟糕了。
我以为它会在全局严格模式下崩溃,但看起来即使在那里也能正常工作。

class Smth extends Function {
  constructor (x) {
    super('return arguments.callee.x');
    this.x = x;
  }
}

(new Smth(90))()

这是一个不好的方法,因为使用arguments.callee,将代码作为字符串传递,并以非严格模式强制执行。但是,apply出现了超越观念的想法。

var global = (1,eval)("this");

class Smth extends Function {
  constructor(x) {
    super('return arguments.callee.apply(this, arguments)');
    this.x = x;
  }
  apply(me, [y]) {
    me = me !== global && me || this;
    return me.x + y;
  }
}

测试表明,我能够以不同的方式运行此功能:

var f = new Smth(100);

[
f instanceof Smth,
f(1),
f.call(f, 2),
f.apply(f, [3]),
f.call(null, 4),
f.apply(null, [5]),
Function.prototype.apply.call(f, f, [6]),
Function.prototype.apply.call(f, null, [7]),
f.bind(f)(8),
f.bind(null)(9),
(new Smth(200)).call(new Smth(300), 1),
(new Smth(200)).apply(new Smth(300), [2]),
isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),
] == "true,101,102,103,104,105,106,107,108,109,301,302,true,true"

与版本

super('return arguments.callee.apply(arguments.callee, arguments)');

实际上包含bind功能:

(new Smth(200)).call(new Smth(300), 1) === 201

与版本

super('return arguments.callee.apply(this===(1,eval)("this") ? null : this, arguments)');
...
me = me || this;

使得callapplywindow不一致:

isNaN(f.apply(window, [1])) === isNaN(f.call(window, 1)),
isNaN(f.apply(window, [1])) === isNaN(Function.prototype.apply.call(f, window, [1])),

因此支票应移至apply

super('return arguments.callee.apply(this, arguments)');
...
me = me !== global && me || this;

1
您实际上想做什么?
谢谢您

2
我认为类是始终严格模式:stackoverflow.com/questions/29283935/...
亚历山大·奥马拉

顺便说一句,@ AlexanderO'Mara this是窗口,不是未定义的,因此创建的函数不是处于严格模式(至少在chrome中)。
Qwertiy '16

请停止对此答案的低估。我已经写过这是一个不好的方法。但这确实是一个答案-它在FF和Chrome中都可以使用(无需检查Edge)。
Qwertiy '16

我猜这行得通,因为Function它不在严格模式下。虽然很糟糕,但+1很有趣。但是,您可能将无法再走下去。
亚历山大·奥玛拉

1

这是我制定的解决方案,可满足我扩展功能的所有需求,并且为我提供了很好的服务。这种技术的好处是:

  • 扩展时ExtensibleFunction,该代码是扩展任何ES6类的惯用法(不,与假装的构造函数或代理混为一谈)。
  • 原型链通过所有子类保留,并且instanceof/ .constructor返回期望值。
  • .bind() .apply()并且.call()所有功能均按预期运行。这是通过重写这些方法来更改“内部”函数(而不是ExtensibleFunction(或它的子类))实例的上下文来完成的。
  • .bind()返回函数构造函数的新实例(无论是其ExtensibleFunction还是子类)。它用于Object.assign()确保存储在绑定函数上的属性与原始函数的属性一致。
  • 闭包是很荣幸的,箭头功能继续保持适当的上下文。
  • “内部”功能是通过来存储的Symbol,可以通过模块或IIFE(或私有化引用的任何其他常用技术)来混淆它。

事不宜迟,代码如下:

// The Symbol that becomes the key to the "inner" function 
const EFN_KEY = Symbol('ExtensibleFunctionKey');

// Here it is, the `ExtensibleFunction`!!!
class ExtensibleFunction extends Function {
  // Just pass in your function. 
  constructor (fn) {
    // This essentially calls Function() making this function look like:
    // `function (EFN_KEY, ...args) { return this[EFN_KEY](...args); }`
    // `EFN_KEY` is passed in because this function will escape the closure
    super('EFN_KEY, ...args','return this[EFN_KEY](...args)');
    // Create a new function from `this` that binds to `this` as the context
    // and `EFN_KEY` as the first argument.
    let ret = Function.prototype.bind.apply(this, [this, EFN_KEY]);
    // For both the original and bound funcitons, we need to set the `[EFN_KEY]`
    // property to the "inner" function. This is done with a getter to avoid
    // potential overwrites/enumeration
    Object.defineProperty(this, EFN_KEY, {get: ()=>fn});
    Object.defineProperty(ret, EFN_KEY, {get: ()=>fn});
    // Return the bound function
    return ret;
  }

  // We'll make `bind()` work just like it does normally
  bind (...args) {
    // We don't want to bind `this` because `this` doesn't have the execution context
    // It's the "inner" function that has the execution context.
    let fn = this[EFN_KEY].bind(...args);
    // Now we want to return a new instance of `this.constructor` with the newly bound
    // "inner" function. We also use `Object.assign` so the instance properties of `this`
    // are copied to the bound function.
    return Object.assign(new this.constructor(fn), this);
  }

  // Pretty much the same as `bind()`
  apply (...args) {
    // Self explanatory
    return this[EFN_KEY].apply(...args);
  }

  // Definitely the same as `apply()`
  call (...args) {
    return this[EFN_KEY].call(...args);
  }
}

/**
 * Below is just a bunch of code that tests many scenarios.
 * If you run this snippet and check your console (provided all ES6 features
 * and console.table are available in your browser [Chrome, Firefox?, Edge?])
 * you should get a fancy printout of the test results.
 */

// Just a couple constants so I don't have to type my strings out twice (or thrice).
const CONSTRUCTED_PROPERTY_VALUE = `Hi, I'm a property set during construction`;
const ADDITIONAL_PROPERTY_VALUE = `Hi, I'm a property added after construction`;

// Lets extend our `ExtensibleFunction` into an `ExtendedFunction`
class ExtendedFunction extends ExtensibleFunction {
  constructor (fn, ...args) {
    // Just use `super()` like any other class
    // You don't need to pass ...args here, but if you used them
    // in the super class, you might want to.
    super(fn, ...args);
    // Just use `this` like any other class. No more messing with fake return values!
    let [constructedPropertyValue, ...rest] = args;
    this.constructedProperty = constructedPropertyValue;
  }
}

// An instance of the extended function that can test both context and arguments
// It would work with arrow functions as well, but that would make testing `this` impossible.
// We pass in CONSTRUCTED_PROPERTY_VALUE just to prove that arguments can be passed
// into the constructor and used as normal
let fn = new ExtendedFunction(function (x) {
  // Add `this.y` to `x`
  // If either value isn't a number, coax it to one, else it's `0`
  return (this.y>>0) + (x>>0)
}, CONSTRUCTED_PROPERTY_VALUE);

// Add an additional property outside of the constructor
// to see if it works as expected
fn.additionalProperty = ADDITIONAL_PROPERTY_VALUE;

// Queue up my tests in a handy array of functions
// All of these should return true if it works
let tests = [
  ()=> fn instanceof Function, // true
  ()=> fn instanceof ExtensibleFunction, // true
  ()=> fn instanceof ExtendedFunction, // true
  ()=> fn.bind() instanceof Function, // true
  ()=> fn.bind() instanceof ExtensibleFunction, // true
  ()=> fn.bind() instanceof ExtendedFunction, // true
  ()=> fn.constructedProperty == CONSTRUCTED_PROPERTY_VALUE, // true
  ()=> fn.additionalProperty == ADDITIONAL_PROPERTY_VALUE, // true
  ()=> fn.constructor == ExtendedFunction, // true
  ()=> fn.constructedProperty == fn.bind().constructedProperty, // true
  ()=> fn.additionalProperty == fn.bind().additionalProperty, // true
  ()=> fn() == 0, // true
  ()=> fn(10) == 10, // true
  ()=> fn.apply({y:10}, [10]) == 20, // true
  ()=> fn.call({y:10}, 20) == 30, // true
  ()=> fn.bind({y:30})(10) == 40, // true
];

// Turn the tests / results into a printable object
let table = tests.map((test)=>(
  {test: test+'', result: test()}
));

// Print the test and result in a fancy table in the console.
// F12 much?
console.table(table);

编辑

由于心情不好,所以我想在npm上发布一个软件包


1

有一个简单的解决方案利用了JavaScript的功能:将“逻辑”作为函数参数传递给类的构造函数,将该类的方法分配给该函数,然后从构造函数返回该函数作为结果:

class Funk
{
    constructor (f)
    { let proto       = Funk.prototype;
      let methodNames = Object.getOwnPropertyNames (proto);
      methodNames.map (k => f[k] = this[k]);
      return f;
    }

    methodX () {return 3}
}

let myFunk  = new Funk (x => x + 1);
let two     = myFunk(1);         // == 2
let three   = myFunk.methodX();  // == 3

以上内容已在Node.js 8上进行了测试。

上面示例的一个缺点是它不支持从超类链继承的方法。为此,只需将“ Object。getOwnPropertyNames(...)”替换为还返回继承方法名称的内容即可。我相信如何做到这一点在有关Stack Overflow的其他一些问题中得到了解释:-)。顺便说一句。如果ES7还添加了一种方法来生成继承的方法的名称,也将很好;-)。

如果您需要支持继承的方法,则一种可能性是在上述类中添加一个静态方法,该方法返回所有继承的和本地方法的名称。然后从构造函数中调用它。如果然后扩展该类Funk,则也将继承该静态方法。


我认为该示例为原始问题“ ...如何实现此类调用的逻辑”提供了简单的答案。只需将其作为函数值参数传递给构造函数即可。在上面的代码中,Funk类没有显式扩展Function,尽管它确实可以,但实际上并不需要。如您所见,您可以像调用任何普通函数一样调用其“实例”提示。
Panu Logic
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.