[javascript] 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. 함수로 호출 될 때 클래스 인스턴스에 대한 참조를 어떻게 얻을 수 있습니까?

추신 : 러시아어로도 같은 질문입니다.



답변

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속성을 상속하지는 않습니다 .


답변

이것은 프로토 타입을 엉망으로 만들지 않고 개체 멤버를 올바르게 참조하고 올바른 상속을 유지하는 호출 가능한 개체를 만드는 방법입니다.

간단히:

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']`


답변

당신은에 떨어지게 인스턴스를 래핑 할 수 프록시apply(아마도 construct) 트랩 :

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


답변

나는 Bergi의 답변에서 조언을 받아 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 ]);


답변

최신 정보:

불행히도 이것은 현재 클래스 대신 함수 객체를 반환하기 때문에 제대로 작동하지 않습니다. 따라서 프로토 타입을 수정하지 않고는 실제로 수행 할 수없는 것 같습니다. 절름발이.


기본적으로 문제는 생성자에 대한 this값을 설정할 방법이 없다는 것입니다 Function. 이 작업을 수행하는 유일한 방법은 .bind나중에 메서드 를 사용하는 것이지만 클래스 친화적이지 않습니다.

헬퍼베이스 클래스에서이 작업을 수행 할 수 있지만 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);

이것은 모두 상당히 복잡하지만 최적화상의 이유로 잘못된 형식으로 간주되고 브라우저 콘솔에서 경고를 생성 할 수있는 프로토 타입을 변경하는 것을 방지합니다.


답변

처음에는으로 해결책을 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;

callapplywindow일관성 :

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;


답변

이것은 기능 확장에 대한 모든 요구 사항을 충족하고 저에게 아주 잘 봉사 한 솔루션입니다. 이 기술의 이점은 다음과 같습니다.

  • 를 확장 할 때 ExtensibleFunction코드는 ES6 클래스를 확장하는 관용적입니다 (아니요, 가장 생성자 또는 프록시로 비웃음).
  • 프로토 타입 체인은 모든 하위 클래스를 통해 유지되며 instanceof/ .constructor예상 값을 반환합니다.
  • .bind() .apply()그리고 .call()모든 기능이 예상대로. 이는 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에 이것에 대한 패키지게시 할 것이라고 생각했습니다 .