如何使用 ES6 类扩展函数?

IT技术 javascript function inheritance ecmascript-6 javascript-inheritance
2021-01-27 16:43:26

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:同样的问题俄语。

6个回答

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属性。

函数原型定义了子类最终继承的只读名称属性。我通过重新定义它来解决这个问题:Object.defineProperty(f, 'name', {value: null, writable: true})
2021-03-13 16:43:26
@amn 不,当您不使用thisreturn对象时,您不会
2021-03-20 16:43:26
我想从函数中访问类状态。
2021-03-24 16:43:26
@Qwertiy:然后使用 Bergi 的第二个建议。
2021-03-26 16:43:26
@AlexanderO'Mara:如果你想让你的Smth实例成为instanceof Smth(正如每个人所期望的),你就不会改变函数的原型Object.setPrototypeOf如果您不需要这个或您的类中声明的任何原型方法,您可以省略调用。
2021-03-27 16:43:26

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

简单地:

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']`
@Bergi所有的getter / setter和方法可访问时,属性/属性必须在分配constructor之后bindExFunc在 ExFunc 的子类中,所有成员都可以访问。至于instanceof; 在 es6 中绑定函数被称为外来函数,因此它们的内部工作原理并不明显,但我认为它会将调用传递给其包装目标,通过Symbol.hasInstance. 它很像一个代理,但它是一种实现预期效果的简单方法。他们的签名相似不一样。
2021-03-15 16:43:26
您能否添加一个解释为什么bind有效(即为什么它返回 的实例ExFunc)?
2021-03-17 16:43:26
@Bergibind返回一个透明的函数对象,它包装了它被调用的函数对象,它是我们的可调用对象,只是与this上下文重新绑定。所以它真的返回了一个透明包装的ExFunc. 发布更新,提供更多信息bind
2021-03-19 16:43:26
@rob 发现得很好,有一个参考错误,我已经用修复和新的解释更新了答案和代码。
2021-03-25 16:43:26
@Adrien 但从内部__call__我无法访问this.athis.ab(). 例如repl.it/repls/FelineFinishedDesktopenvironment
2021-04-08 16:43:26

您可以将 Smth 实例包装在带有(可能还有)陷阱代理中applyconstruct

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256
@Bergi 不知道性能。MDN 有一个setPrototypeOf关于代理的大红色粗体警告并且没有提及任何关于代理的内容。但我想代理可能和setPrototypeOf. 并且toString,可以使用Smth.prototype. 无论如何,本机是依赖于实现的。
2021-03-15 16:43:26
我知道你的apply方法是按照它应该使用的方式实现的,还是只是一个演示,我需要查看更多关于它的信息ProxyReflect以正确的方式使用它?
2021-03-20 16:43:26
@Qwertiy 您可以添加一个construct陷阱来指定new new Smth(256)(). 并添加自定义方法来toString隐藏访问函数代码的本机方法,就像Bergi 指出的那样。
2021-03-23 16:43:26
很酷的主意。像这样。我应该实现更多的逻辑而不是放在 apply 里面吗?
2021-03-24 16:43:26
代理会产生相当多的开销,不是吗?此外,this仍然是一个空函数(检查new Smth().toString())。
2021-03-31 16:43:26

我从 Bergi 的回答中得到了建议,并将其包装到NPM module中

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 ]);

更新:

不幸的是,这并不完全有效,因为它现在返回的是一个函数对象而不是一个类,所以如果不修改原型,这似乎实际上无法完成。瘸。


基本上问题是没有办法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.)

基本上,基函数ClassFunctionextends 将Function使用自定义函数包装构造函数调用,该函数类似于.bind,但允许稍后在第一次调用时进行绑定。然后在ClassFunction构造函数本身中,它调用super现在是绑定函数的返回函数,传递this以完成自定义绑定函数的设置。

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

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

@Bergi 该死,看起来你是对的。然而,扩展任何本机类型似乎都有同样的问题。extend Functionnew Smth instanceof Smth造假。
2021-03-17 16:43:26
@Bergi 你在使用什么 JS 引擎?console.log((new Smth) instanceof Function);true我在节点v5.11.0和最新的Firefox浏览器。
2021-03-24 16:43:26
糟糕,错误的例子。new Smth instanceof Smth不适用于您的解决方案。此外Smth,您的实例上也没有可用的方法- 因为您只返回一个标准Function,而不是一个Smth.
2021-03-26 16:43:26
你把事情复杂化了。bound将引用您return来自该匿名类的函数给它起个名字,直接引用就行了。我还建议避免传递代码字符串,它们只是一团糟(在开发过程的每个步骤中)。
2021-03-31 16:43:26
extends似乎并没有真正按预期工作,因为Function.isPrototypeOf(Smth)而且也是new Smth instanceof Function错误的。
2021-04-01 16:43:26