如何扩展function与ES6类?
ES6允许扩展特殊对象。 所以可以从函数inheritance。 这样的对象可以被称为一个函数,但是我怎样才能实现这种调用的逻辑呢?
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: 同样的问题在俄罗斯。
super
调用将调用Function
构造函数,它需要一个代码string。 如果你想访问你的实例数据,你可以硬编码它:
class Smth extends Function { constructor(x) { super("return "+JSON.stringify(x)+";"); } }
但这并不令人满意。 我们想要使用闭包。
让返回的函数成为一个可以访问你的实例variables的闭包是可能的,但并不容易。 好的是,如果你不想要的话,你不必调用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 fx; }); // named function this.x = x; } }
无可否认,这在inheritance链中创造了一个额外的间接层次,但这不一定是坏事(你可以扩展它而不是本地Function
)。 如果你想避免它,使用
function ExtensibleFunction(f) { return Object.setPrototypeOf(f, new.target.prototype); } ExtensibleFunction.prototype = Function.prototype;
但请注意, Smth
不会dynamicinheritance静态Function
属性。
你可以用一个apply
(也可能是construct
)陷阱来把这个Smth实例封装在Proxy中 :
class Smth extends Function { constructor (x) { super(); return new Proxy(this, { apply: function(target, thisArg, argumentsList) { return x; } }); } } new Smth(256)(); // 256
这是我创build可正确引用其对象成员的可调用对象的方法,并保持正确的inheritance,而不会混淆原型。
只是:
class ExFunc extends Function { constructor() { super('...args', 'return this.__call__(...args)'); return this.bind(this); } // Example `__call__` method. __call__(a, b, c) { return [a, b, c]; } }
扩展这个类并添加一个__call__
方法,更多…
代码和意见的解释:
// A Class that extends Function so we can create // objects that also behave like functions, ie callable objects. class ExFunc extends Function { constructor() { // Here we create a dynamic function with `super`, // which calls the constructor of the parent class, `Function`. // The dynamic function simply passes any calls onto // an overridable object method which I named `__call__`. // But there is a problem, the dynamic function created from // the strings sent to `super` doesn't have any reference to `this`; // our new object. There are in fact two `this` objects; the outer // one being created by our class inside `constructor` and an inner // one created by `super` for the dynamic function. // So the reference to this in the text: `return this.__call__(...args)` // does not refer to `this` inside `constructor`. // So attempting: // `obj = new ExFunc();` // `obj();` // Will throw an Error because __call__ doesn't exist to the dynamic function. super('...args', 'return this.__call__(...args)'); // `bind` is the simple remedy to this reference problem. // Because the outer `this` is also a function we can call `bind` on it // and set a new inner `this` reference. So we bind the inner `this` // of our dynamic function to point to the outer `this` of our object. // Now our dynamic function can access all the members of our new object. // So attempting: // `obj = new Exfunc();` // `obj();` // Will work. // We return the value returned by `bind`, which is our `this` callable object, // wrapped in a transparent "exotic" function object with its `this` context // bound to our new instance (outer `this`). // The workings of `bind` are further explained elsewhere in this post. return this.bind(this); } // An example property to demonstrate member access. get venture() { return 'Hank'; } // 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 { get venture() { return 'Dean'; } __call__(ans) { return [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) ); // [ 'Dean', 42 ]
更新:
不幸的是,这并不是很有效,因为它现在正在返回一个函数对象而不是一个类,所以看起来这实际上不能在不修改原型的情况下完成。 瘸。
基本上问题是没有办法为Function
构造Function
设置this
值。 真正做到这一点的唯一方法是之后使用.bind
方法,但是这不是非常有用的。
我们可以在助手的基类中做到这一点,但是直到最初的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))());
我从Bergi的回答中得到了build议,并将其包装到一个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 ]);
首先,我用arguments.callee
来解决arguments.callee
,但是很糟糕。
我期望它在全球严格的模式下突破,但即使在那里,它似乎仍然有效。
class Smth extends Function { constructor (x) { super('return arguments.callee.x'); this.x = x; } } (new Smth(90))()
这是一个不好的方法,因为使用arguments.callee
,将代码作为string传递并强制其在非严格模式下执行。 但是,超越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; } }
testing,显示我能以不同的方式运行这个function:
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
function:
(new Smth(200)).call(new Smth(300), 1) === 201
版本与
super('return arguments.callee.apply(this===(1,eval)("this") ? null : this, arguments)'); ... me = me || this;
call
apply
window
不一致:
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;
这是我制定的解决scheme,可以满足我扩展function的所有需求,并为我提供了很好的服务。 这种技术的好处是:
- 当扩展
ExtensibleFunction
,代码是扩展任何ES6类的习惯用法(不,使用伪装的构造函数或代理)。 - 原型链通过所有子类保留,并且
instanceof
/.constructor
返回期望的值。 -
.bind()
.apply()
和.apply()
.call()
全部按预期运行。 这是通过重写这些方法来改变“内部”函数的上下文而不是ExtensibleFunction
(或它的子类)实例。 -
.bind()
返回函数构造函数的新实例(无论是ExtensibleFunction
还是子类)。 它使用Object.assign()
来确保存储在绑定函数中的属性与原始函数的属性一致。 - closures是荣幸的,箭头function继续保持适当的上下文。
- “内部”function是通过一个
Symbol
来存储的,这个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);