如何用 ES6类扩展函数?

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。当类实例作为函数调用时,如何获得对它的引用?

附注: 用俄语问同样的问题。

48245 次浏览

super调用将调用 Function构造函数,该构造函数需要一个代码字符串。如果你想访问你的实例数据,你可以硬编码它:

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

但这不是真正的满足。我们想用一个了结。

返回的函数是一个闭包,可以访问您的实例变量是可能的,但并不容易。好的一面是,如果你不想调用 super,你不必调用——你仍然可以从 ES6类构造函数中调用 return任意对象。在这种情况下,我们会做

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;
}
}

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

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

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

首先我想到了 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;

使 window上的 callapply不一致:

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;

你可以用 apply(或者 construct)陷阱把 Smth 实例包装在 代理中:

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

更新:

不幸的是,这并不能很好地工作,因为它现在返回的是一个函数对象,而不是一个类,所以看起来这实际上不能在不修改原型的情况下完成。逊毙了。


基本上问题是没有办法为 Function构造函数设置 this值。真正做到这一点的唯一方法是以后使用 .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);

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

我采纳了 Bergi 的建议,把它包装成了一个 ABc0。

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

这是我想出来的解决方案,它满足了我扩展函数的所有需求,并且非常好地满足了我的需求。这种技术的好处是:

  • 在扩展 ExtensibleFunction时,这段代码是扩展任何 ES6类的惯用方法(不,使用假冒的构造函数或代理)。
  • 原型链通过所有子类保留,instanceof/.constructor返回预期值。
  • .bind() .apply().call()都能正常工作。这是通过重写这些方法来改变“ inner”函数的上下文,而不是 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。

这是一种创建可调用对象的方法,可以正确引用它们的对象成员,并维护正确的继承, 而不会破坏原型机。

简单来说:

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, ...);更多关于 我不知道

fn.bind(this, arg1, arg2, arg3, ...);更多关于 我不知道

在这两个参数中,第一个参数都重新定义了函数内部的 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']`

有一个简单的解决方案可以利用 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.js8上进行了测试。

上面示例的一个缺点是它不支持从超类链继承的方法。要支持这一点,只需将“ Object”替换为“ Object”。GetOwnPropertyNames (...)”,它还返回继承方法的名称。如何做到这一点,我相信在关于堆栈溢出的其他问答中有解释: ——)。顺便说一句。如果 ES7也能添加一个方法来生成继承方法的名称就好了; ——)。

如果需要支持继承的方法,一种可能性是向上面的类添加一个静态方法,该方法返回所有继承的和本地的方法名。然后从构造函数调用它。然后,如果扩展 Funk 类,就会得到继承的静态方法。

推广 Oriol 的回答:

class Smth extends Function {
constructor(x) {
super();


this.x = x;


return new Proxy(this, {
apply: (target, that, args) => target.__call__(...args)
});
}


__call__(v) {
return this.x * v;
}
}

有点晚了,但让我把这个留在这里。

最近我不得不发现一种方法来子类化 Function,以便在不干扰 Function.prototype的情况下将正常函数转换为 Threadable/允诺函数。我认为这种特殊的必要性对于如何以及为什么可以使用类抽象来扩展 Function这个问题形成了一个非常合理的基础。

我们的想法是,创建一个 Threadable类,其成员函数是可线程化的。我的意思是,任何正常的函数都可以很容易地变成 Threadable,当派生的函数运行在一个单独的线程上时,我们可以根据 worker 操作的结果来解析或拒绝它。然而,如果需要,您仍然应该能够同步调用它。

class Threadable extends Function {
  

// just use super() to wrap the given f function with a transparent function layer
constructor(f){
super("...as",`return ${f.toString()}.apply(this,as)`);
}
  

// spawn is the only method of the Threadable class.
// Returns a promise and runs callee function on a separate thread.
spawn(...as){
var code = `self.onmessage = m => self.postMessage((${this.toString()}).apply(self,m.data));`,
blob = new Blob([code], {type: "text/javascript"}),
wrkr = new Worker(window.URL.createObjectURL(blob));
return new Promise( (v,x) => ( wrkr.onmessage = m => (v(m.data), wrkr.terminate())
, wrkr.onerror   = e => (x(e.message), wrkr.terminate())
, wrkr.postMessage(as)
)
);
}
}


function add(...ns) {
return ns.reduce((a,b) => a+b);
}


var addT = new Threadable(add);


addT.spawn(1,2,3,4)
.then(m => console.log(`Promisified thread returned ${m}`));


console.log(`Synchronous invocation of addT returned ${addT(1,2,3,4,5)}`);

在阅读了 这篇文章和这里的所有答案之后,我终于在 旧线索中找到了一个令人满意的答案。

这里有一个例子:

class Hey {
constructor() {
function hey() {
return "hey";
}
    

this.value = "yo";


Object.assign(hey, this);
Object.setPrototypeOf(hey, Object.getPrototypeOf(this));


return hey;
}
  

yo() {
return this.value;
}
}


const hey = new Hey();


console.log(hey());              // it's callable 👍
console.log(hey.yo());           // methods are correctly bound to `this` 👍


console.log(hey instanceof Hey); // it type-checks 👍

继承也有效:

class HeyHey extends Hey {
constructor() {
super();
}
  

yoyo() {
return this.value + "!";
}
}


const lol = new HeyHey();


console.log(lol());                 // it's callable 👍
console.log(lol.yo());              // inherited methods are correctly bound to `this` 👍
console.log(lol.yoyo());            // new methods are correctly bound to `this` as well 👍


console.log(lol instanceof Hey);    // it type-checks for the super class 👍
console.log(lol instanceof HeyHey); // it type-checks for the derived class 👍

你可以 在这里举个例子自己看。

这种方法:

  • 正确地将方法绑定到 this
  • 是类型安全的(可以使用 instanceof进行类型检查)
  • 正确地支持继承(正确地绑定和类型检查派生类)
  • 不依赖于任何比 class更新的特性-getPrototypeOfsetPrototypeOfclass之前几年就已经被广泛使用
  • 不依赖于 Function构造函数(避免在运行时解析源代码)
  • 不依赖于 Proxy(这对性能不是很好)

总而言之,这种方法肯定更简单、更容易实现,而且性能也应该更好。

(从理论上讲——请随意对此进行基准测试并公布您的结果。)

提出了一个不使用 Object.setPrototypeOf 的解决方案,因为 MDN 在它周围有很大的红色警告标志。可以运行 例子 JSFiddle 在这里。我无法理解的一个限制是,如何在任意执行中生成的函数的调用时访问 this上下文。

class ExtendedFunction extends Function {


// Arbitrary private properties
#foo
#baz() { return this.#foo + 'qux' }
  

// The thing that happens when you call your extended function
// context is optional if you want access to the `this`
// provides to your extended function at call time
#execute() {
// Arbitrary code that can call anything in closure here
return this.#baz()
}
  

constructor(a) {
// Set `this` to simple wrapper function
// that takes another function and returns its value
// Use super we get an instance of Function and ExtendedFucntion
super('execute', 'return execute()')
this.#foo = a
// Bind our arbitrary function to the current `this`
// allowing it to access private properties even when passed around
const boundExecute = this.#execute.bind(this)
// Bind the simple wrapper and the boundExecute together and return that
// When called our extended function will do whatever is in #execute
var self = this.bind(null, boundExecute)
return self
}


}


const a = new ExtendedFunction(256)
console.log(a instanceof Function) // true
console.log(a instanceof ExtendedFunction) // true
console.log(a()) // 256qux