如何使用ES6类扩展函数?

时间:2016-04-26 17:01:59

标签: javascript function inheritance ecmascript-6 javascript-inheritance

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

PS:Same question in Russian.

8 个答案:

答案 0 :(得分:40)

super调用将调用Function构造函数,该构造函数需要代码字符串。如果要访问实例数据,可以对其进行硬编码:

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

但这并不令人满意。我们想要使用一个闭包。

让返回的函数成为can access your instance变量可能的闭包,但并不容易。好消息是,如果你不想,你不必打电话给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;
  }
}

不可否认,这在继承链中创建了一个额外的间接层,但这不一定是坏事(你可以扩展它而不是本地Function)。如果您想避免它,请使用

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

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

答案 1 :(得分:20)

这是我创建可正确引用其对象成员并保持正确继承的可调用对象的方法, 没有弄乱原型。

简单地:

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, i.e. 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 ]




View on repl.it

bind

的进一步说明

function.bind()的工作原理与function.call()非常相似,并且它们共享类似的方法签名:

mdn

上再次

fn.call(this, arg1, arg2, arg3, ...);mdn

上再次

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

答案 2 :(得分:17)

您可以使用Proxy(可能是apply)陷阱将Smth实例包装在construct中:

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

答案 3 :(得分: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。)

基本函数ClassFunction extends将使用类似于Function的自定义函数包装.bind构造函数调用,但允许稍后在第一次调用时进行绑定。然后在ClassFunction构造函数本身,它从super调用返回的函数,该函数现在是绑定函数,传递this以完成自定义绑定函数的设置。

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

这一切都有点复杂,但它确实避免了改变原型,由于优化原因它被认为是坏形式,并且可以在浏览器控制台中生成警告。

答案 4 :(得分:2)

我接受了Bergi的回答,将其包装成NPM module

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

答案 5 :(得分:2)

概括Oriol's answer

RANK()

答案 6 :(得分:1)

首先我用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;

call上的applywindow不一致:

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;

答案 7 :(得分:1)

这是我已经解决的解决方案,它满足了我扩展功能的所有需求,并且对我很有帮助。这种技术的好处是:

  • 在扩展ExtensibleFunction时,代码是扩展任何ES6类的惯用语(不,使用假装构造函数或代理)。
  • 原型链通过所有子类保留,instanceof / .constructor返回预期值。
  • .bind() .apply().call()都按预期运行。这是通过重写这些方法来改变“内部”函数的上下文而不是ExtensibleFunction(或它的子类')实例来完成的。
  • .bind()返回函数构造函数的新实例(be 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} publish a package就是这样。