javascript - Hur man utökar Function med ES6-klasser?
ES6 tillåter att utöka speciella objekt. Så det är möjligt att ärva från funktionen. Ett sådant objekt kan kallas som en funktion, men hur kan jag implementera logiken för ett sådant anrop?
class Smth extends Function {
constructor (x) {
// What should be done here
super();
}
}
(new Smth(256))() // to get 256 at this call?
Alla metoder i en klass får referens till klassinstansen via this. Men när den anropas som en funktion hänvisar this till window. Hur kan jag få referensen till klassinstansen när den anropas som en funktion?
PS: Samma fråga på ryska.
Svar #1
Anropet super kommer att anropa Function-konstruktören, som förväntar sig en kodsträng. Om du vill komma åt dina instansdata kan du helt enkelt hårdkoda dem:
class Smth extends Function {
constructor(x) {
super("return "+JSON.stringify(x)+";");
}
}
men det är inte riktigt tillfredsställande. Vi vill använda en closure.
Att ha den returnerade funktionen som en closure som kan komma åt dina instansvariabler är möjligt, men inte lätt. Det bra är att du inte behöver kalla super om du inte vill - du kan fortfarande return godtyckliga objekt från dina ES6-klasskonstruktörer. I det här fallet skulle vi göra
class Smth extends Function {
constructor(x) {
// refer to `smth` instead of `this`
function smth() { return x; };
Object.setPrototypeOf(smth, Smth.prototype);
return smth;
}
}
Men vi kan göra ännu bättre och abstrahera ut den här saken från 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;
}
}
Visserligen skapar detta en ytterligare nivå av indirektion i arvskedjan, men det är inte nödvändigtvis en dålig sak (du kan utöka den istället för den inbyggda Function). Om du vill undvika det, använd
function ExtensibleFunction(f) {
return Object.setPrototypeOf(f, new.target.prototype);
}
ExtensibleFunction.prototype = Function.prototype;
men notera att Smth inte dynamiskt kommer att ärva statiska Function-egenskaper.
Svar #2
Detta är ett tillvägagångssätt för att skapa anropbara objekt som korrekt refererar till sina objektsmedlemmar och bibehåller korrekt arv, utan att störa prototyper.
Enkelt:
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];
}
}
Utöka denna klass och lägg till en __call__-metod, mer nedan...
En förklaring i kod och kommentarer:
// 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())Visa på repl.it
Ytterligare förklaring av bind:
function.bind() fungerar mycket som function.call(), och de delar en liknande metodsignatur:
fn.call(this, arg1, arg2, arg3, ...); mer på mdn
fn.bind(this, arg1, arg2, arg3, ...); mer på mdn
I båda fallen omdefinierar det första argumentet this-kontexten inuti funktionen. Ytterligare argument kan också bindas till ett värde. Men där call omedelbart anropar funktionen med de bundna värdena, returnerar bind ett "exotiskt" funktionsobjekt som transparent omsluter originalet, med this och eventuella argument förinställda.
Så när du definierar en funktion och sedan binder några av dess argument:
var foo = function(a, b) {
console.log(this);
return a * b;
}
foo = foo.bind(['hello'], 2);
Du anropar den bundna funktionen med endast de återstående argumenten, dess kontext är förinställd, i detta fall till ['hello'].
// We pass in arg `b` only because arg `a` is already set.
foo(2); // returns 4, logs `['hello']`
Svar #3
Du kan lägga Smth-instansen i en Proxy med en apply-fälla (och kanske construct)-fälla:
class Smth extends Function {
constructor (x) {
super();
return new Proxy(this, {
apply: function(target, thisArg, argumentsList) {
return x;
}
});
}
}
new Smth(256)(); // 256
