Jak rozszerzyć funkcję o klasy ES6?

105

ES6 umożliwia rozszerzenie specjalnych obiektów. Więc możliwe jest dziedziczenie z funkcji. Taki obiekt można wywołać jako funkcję, ale jak mogę zaimplementować logikę dla takiego wywołania?

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

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

Każda metoda klasy otrzymuje odwołanie do instancji klasy za pośrednictwem this. Ale kiedy jest wywoływany jako funkcja, thisodnosi się do window. Jak mogę uzyskać odwołanie do instancji klasy, gdy jest wywoływana jako funkcja?

PS: To samo pytanie po rosyjsku.

Qwertiy
źródło
17
Ach, w końcu ktoś zapytał o to pytanie :-)
Bergi
1
Po prostu zrób super(x)(tj. Przekaż to dalej Function)? Nie jestem pewien, czy Functionrzeczywiście można go przedłużyć.
Felix Kling
Należy pamiętać, że nadal występują problemy z rozszerzaniem klas wbudowanych. Specyfikacja sugeruje, że powinno to być możliwe, ale miałem problemy z rozszerzeniem Errorm.in.
ssube
1
Pamiętaj, że Functionjest to po prostu konstruktor funkcji. Implementację funkcji należy przekazać konstruktorowi. Jeśli nie chcesz Smthakceptować implementacji, musisz podać ją w konstruktorze, tj super('function implementation here').
Felix Kling
1
@Qwertiy: Twierdzę, że to wyjątek , a nie przypadek ogólny. Jest to również bardzo specyficzne dla wyrażeń funkcyjnych , ale używasz Functionkonstruktora (środowiska wykonawczego), który bardzo różni się od wyrażenia funkcyjnego (składni).
Felix Kling

Odpowiedzi:

49

superWezwanie wywoła Functionkonstruktor, który oczekuje ciąg kodu. Jeśli chcesz uzyskać dostęp do danych instancji, możesz po prostu zakodować je na stałe:

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

ale to nie jest satysfakcjonujące. Chcemy użyć zamknięcia.

Posiadanie zwróconej funkcji jako zamknięcia, które może uzyskać dostęp do zmiennych instancji , jest możliwe, ale nie jest łatwe. Dobrą rzeczą jest to, że nie musisz dzwonić, superjeśli nie chcesz - nadal możesz returndowolne obiekty z konstruktorów klasy ES6. W takim przypadku zrobilibyśmy

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

Ale możemy zrobić jeszcze lepiej i wyabstrahować to z 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;
  }
}

Wprawdzie stwarza to dodatkowy poziom pośrednictwa w łańcuchu dziedziczenia, ale to niekoniecznie jest złe (możesz go rozszerzyć zamiast natywnego Function). Jeśli chcesz tego uniknąć, użyj

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

ale zauważ, że Smthnie odziedziczy dynamicznie Functionwłaściwości statycznych .

Bergi
źródło
Chcę uzyskać dostęp do stanu klasy z funkcji.
Qwertiy
2
@Qwertiy: W takim razie skorzystaj z drugiej sugestii Bergiego.
Felix Kling
@ AlexanderO'Mara: Nie omijasz mutacji prototypu funkcji, jeśli chcesz, aby Twoje Smthinstancje były instanceof Smth(tak jak wszyscy by się tego spodziewali). Możesz pominąć Object.setPrototypeOfwywołanie, jeśli nie potrzebujesz tej lub żadnej z metod prototypowych zadeklarowanych w Twojej klasie.
Bergi
@ AlexanderO'Mara: Również Object.setPrototypeOfnie jest to duże zagrożenie optymalizacyjne, o ile jest wykonywane zaraz po utworzeniu obiektu. Tylko wtedy, gdy mutujesz [[prototyp]] obiektu w tę iz powrotem podczas jego życia, będzie on zły.
Bergi
1
@amn Nie, nie musisz, gdy nie używasz thisi returnobiektu.
Bergi
32

Jest to podejście do tworzenia wywoływalnych obiektów, które poprawnie odwołują się do swoich składowych obiektów i zachowują prawidłowe dziedziczenie, bez ingerowania w prototypy.

Po prostu:

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

Rozszerz tę klasę i dodaj __call__metodę, więcej poniżej ...

Wyjaśnienie w kodzie i komentarzach:

// 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())

Zobacz na repl.it

Dalsze wyjaśnienie bind:

function.bind()działa podobnie function.call()i mają podobny podpis metody:

fn.call(this, arg1, arg2, arg3, ...);więcej na mdn

fn.bind(this, arg1, arg2, arg3, ...);więcej na mdn

W obu przypadkach pierwszy argument przedefiniowuje thiskontekst wewnątrz funkcji. Dodatkowe argumenty mogą być również powiązane z wartością. Ale tam, gdzie callnatychmiast wywołuje funkcję z powiązanymi wartościami, bindzwraca „egzotyczny” obiekt funkcji, który w przezroczysty sposób zawija oryginał, z thisustawionymi wszystkimi argumentami.

Więc kiedy definiujesz funkcję, to bindniektóre z jej argumentów:

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

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

Wywołujesz funkcję powiązaną z tylko pozostałymi argumentami, jej kontekst jest wstępnie ustawiony, w tym przypadku jako ['hello'].

// We pass in arg `b` only because arg `a` is already set.
foo(2);  // returns 4, logs `['hello']`
Adrien
źródło
Czy możesz dodać wyjaśnienie, dlaczego binddziała (tj. Dlaczego zwraca wystąpienie ExFunc)?
Bergi
@Bergi bindzwraca przezroczysty obiekt funkcyjny, który otacza obiekt funkcji, na którym został wywołany, czyli nasz wywoływalny obiekt, po prostu z thisodbiciem kontekstu. Więc naprawdę zwraca przezroczysto opakowaną instancję ExFunc. Post zaktualizowany o więcej informacji bind.
Adrien
1
@Bergi Wszystkie pobierające / ustawiaczy i metody są dostępne, właściwości / atrybuty muszą być przypisane w constructorpo bindw ExFunc. W podklasach ExFunc wszyscy członkowie są dostępni. Jeśli chodzi o instanceof; w es6 funkcje powiązane są określane jako egzotyczne, więc ich wewnętrzne działanie nie jest widoczne, ale myślę, że przekazuje wywołanie do swojego opakowanego celu za pośrednictwem Symbol.hasInstance. Przypomina proxy, ale jest prostą metodą osiągnięcia pożądanego efektu. Ich podpis jest podobny, a nie to samo.
Adrien
1
@Adrien, ale od wewnątrz nie __call__mam dostępu this.alub this.ab(). np repl.it/repls/FelineFinishedDesktopenvironment
rob
1
@rob dobrze zauważony, wystąpił błąd odniesienia, zaktualizowałem odpowiedź i kod o poprawkę i nowe wyjaśnienie.
Adrien,
20

Możesz opakować instancję Smth w proxy z apply(i być może construct) pułapką:

class Smth extends Function {
  constructor (x) {
    super();
    return new Proxy(this, {
      apply: function(target, thisArg, argumentsList) {
        return x;
      }
    });
  }
}
new Smth(256)(); // 256
Oriol
źródło
Fajny pomysł. Lubię to. Czy powinienem zaimplementować jakąś logikę zamiast umieszczać ją w aplikacji?
Qwertiy
4
Proxy spowodowałoby sporo kosztów ogólnych, prawda? Ponadto thisnadal jest pustą funkcją (sprawdź new Smth().toString()).
Bergi
2
@Bergi Nie mam pojęcia o wydajności. MDN ma duże, czerwone, pogrubione ostrzeżenie setPrototypeOfi nie mówi nic o serwerach proxy. Ale wydaje mi się, że serwery proxy mogą być równie problematyczne jak setPrototypeOf. Co więcej toString, można go umieścić w cieniu za pomocą niestandardowej metody w programie Smth.prototype. Natywny jest i tak zależny od implementacji.
Oriol
@Qwertiy Możesz dodać constructpułapkę, aby określić zachowanie new new Smth(256)(). I dodaj niestandardowe metody, które przesłaniają natywne metody, które uzyskują dostęp do kodu funkcji, jak toStringzauważył Bergi.
Oriol
Mam na myśli, czy twoja applymetoda została zaimplementowana w sposób, w jaki ma być używana, czy jest to tylko demonstracja i muszę przejrzeć więcej informacji na jej temat Proxyi Reflectwłaściwie z niej korzystać?
Qwertiy
3

Wziąłem radę z odpowiedzi Bergi i zawarłem ją w moduł 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 ]);
Ryan Patterson
źródło
3

Aktualizacja:

Niestety to nie do końca działa, ponieważ zwraca teraz obiekt funkcji zamiast klasy, więc wydaje się, że nie można tego zrobić bez modyfikacji prototypu. Kulawy.


Zasadniczo problem polega na tym, że nie ma możliwości ustawienia thiswartości dla Functionkonstruktora. Jedynym sposobem, aby to naprawdę zrobić, byłoby użycie .bindmetody później, jednak nie jest to zbyt przyjazne dla klas.

Moglibyśmy to zrobić w pomocniczej klasie bazowej, jednak thisstaje się ona dostępna dopiero po pierwszym superwywołaniu, więc jest to trochę trudne.

Przykład roboczy:

'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))());

(Przykład wymaga nowoczesnej przeglądarki lub node --harmony.)

Zasadniczo funkcja podstawowa ClassFunctionextends otacza Functionwywołanie konstruktora niestandardową funkcją, która jest podobna do .bind, ale umożliwia późniejsze powiązanie przy pierwszym wywołaniu. Następnie w samym ClassFunctionkonstruktorze wywołuje zwróconą funkcję, z superktórej jest teraz powiązana funkcja, przechodząc thisdo zakończenia konfigurowania niestandardowej funkcji wiązania.

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

To wszystko jest dość skomplikowane, ale pozwala uniknąć mutacji prototypu, który jest uważany za zły z powodów optymalizacji i może generować ostrzeżenia w konsolach przeglądarek.

Alexander O'Mara
źródło
1
Zbytnio komplikujesz rzeczy. boundbędzie odnosić się do funkcji, którą masz returnz tej anonimowej klasy. Po prostu nazwij go i odnieś się bezpośrednio do niego. Zalecałbym również unikanie przekazywania ciągów kodu, są one po prostu bałaganem do pracy (na każdym etapie procesu programowania).
Bergi
extendsWydaje się, że to nie działa zgodnie z oczekiwaniami, Function.isPrototypeOf(Smth)a także new Smth instanceof Functionjest fałszywe.
Bergi
@Bergi Jakiego silnika JS używasz? console.log((new Smth) instanceof Function);jest truedla mnie w Node v5.11.0 i najnowszym Firefoksie.
Alexander O'Mara
Ups, zły przykład. To new Smth instanceof Smthnie działa z twoim rozwiązaniem. Również żadne metody nie Smthbędą dostępne w twoich instancjach - po prostu zwracasz standard Function, a nie plik Smth.
Bergi
1
@Bergi Cholera, wygląda na to, że masz rację. Jednak rozszerzenie jakichkolwiek typów natywnych wydaje się mieć ten sam problem. extend Functionrównież new Smth instanceof Smthfałszuje.
Alexander O'Mara
1

Najpierw doszedłem do rozwiązania arguments.callee, ale było okropne.
Spodziewałem się, że zepsuje się w globalnym trybie ścisłym, ale wydaje się, że działa nawet tam.

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

(new Smth(90))()

To był zły sposób z powodu używania arguments.callee, przekazywania kodu jako łańcucha i wymuszania jego wykonania w trybie nieścisłym. Ale potem applypojawił się pomysł do zastąpienia .

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

A test pokazujący, że jestem w stanie uruchomić to jako funkcję na różne sposoby:

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"

Wersja z

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

w rzeczywistości zawiera bindfunkcjonalność:

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

Wersja z

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

robi calli applyna windowniespójnych:

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

więc czek należy przenieść do apply:

super('return arguments.callee.apply(this, arguments)');
...
me = me !== global && me || this;
Qwertiy
źródło
1
Co tak naprawdę próbujesz zrobić?
Dziękuję
2
Myślę, że zajęcia są zawsze w trybie ścisłym: stackoverflow.com/questions/29283935/…
Alexander O'Mara
@ AlexanderO'Mara, nawiasem mówiąc, thisjest oknem, nie jest niezdefiniowana, więc utworzona funkcja nie jest w trybie ścisłym (przynajmniej w chrome).
Qwertiy
Proszę, przestańcie zaniżać tę odpowiedź. Pisałem już, że to zły sposób. Ale to naprawdę jest odpowiedź - działa zarówno w FF, jak i Chrome (nie mam Edge do sprawdzania).
Qwertiy
Domyślam się, że to działa, ponieważ Functionnie jest w trybie ścisłym. Choć okropne, jest interesujące +1. Jednak prawdopodobnie nie byłbyś w stanie przejść dalej łańcucha.
Alexander O'Mara
1

To rozwiązanie, które wypracowałem, spełnia wszystkie moje potrzeby związane z rozszerzaniem funkcji i całkiem dobrze mi służyło. Zalety tej techniki to:

  • Podczas rozszerzania ExtensibleFunctionkod jest idiomatyczny z rozszerzaniem dowolnej klasy ES6 (nie, grzebanie w udawanych konstruktorach lub serwerach proxy).
  • Łańcuch prototypów jest zachowywany przez wszystkie podklasy i instanceof/ .constructorzwraca oczekiwane wartości.
  • .bind() .apply()i .call()wszystkie działają zgodnie z oczekiwaniami. Odbywa się to poprzez nadpisanie tych metod w celu zmiany kontekstu funkcji „wewnętrznej” w przeciwieństwie do ExtensibleFunctioninstancji (lub jej podklasy).
  • .bind()zwraca nowe wystąpienie konstruktora funkcji ( ExtensibleFunctionczy to lub podklasy). Używa Object.assign()do zapewnienia, że ​​właściwości przechowywane w powiązanej funkcji są zgodne z właściwościami funkcji źródłowej.
  • Zamknięcia są honorowane, a funkcje strzałek nadal zachowują właściwy kontekst.
  • Funkcja „wewnętrzna” jest przechowywana za pomocą elementu Symbol, który można zaciemnić za pomocą modułów lub IIFE (lub dowolnej innej powszechnej techniki prywatyzacji odniesień).

I bez zbędnych ceregieli kod:

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

Edytować

Ponieważ byłem w nastroju, pomyślałem, że opublikuję pakiet na ten temat na npm.

Aaron Levine
źródło
1

Istnieje proste rozwiązanie, które wykorzystuje możliwości funkcjonalne JavaScript: Przekaż "logikę" jako argument funkcji do konstruktora swojej klasy, przypisz metody tej klasy do tej funkcji, a następnie zwróć tę funkcję z konstruktora jako wynik :

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

Powyższe zostało przetestowane na Node.js 8.

Wadą powyższego przykładu jest to, że nie obsługuje on metod dziedziczonych z łańcucha nadklasy. Aby to obsługiwać, po prostu zamień „Object. GetOwnPropertyNames (...)” na coś, co zwraca również nazwy dziedziczonych metod. Jak to zrobić, moim zdaniem, jest wyjaśnione w innym pytaniu-odpowiedzi na Stack Overflow :-). BTW. Byłoby miło, gdyby ES7 dodało metodę do tworzenia dziedziczonych nazw metod ;-).

Jeśli potrzebujesz obsługi dziedziczonych metod, jedną z możliwości jest dodanie metody statycznej do powyższej klasy, która zwraca wszystkie odziedziczone i lokalne nazwy metod. Następnie wywołaj to z konstruktora. Jeśli następnie rozszerzysz tę klasę Funk, otrzymasz również dziedziczoną metodę statyczną.

Panu Logic
źródło
Myślę, że ten przykład daje prostą odpowiedź na pierwotne pytanie "... jak mogę zaimplementować logikę dla takiego połączenia". Po prostu przekaż go jako argument o wartości funkcji do konstruktora. W powyższym kodzie klasa Funk nie rozszerza w sposób jawny funkcji, chociaż mogłoby to zrobić, ale tak naprawdę nie musi. Jak widzisz, możesz wywoływać jego „instancje” jak zwykłe funkcje.
Panu Logic,