Как лучше всего создать JS функций с помощью собственного прототипа? - PullRequest
1 голос
/ 19 июня 2020

Моя цель - создать вызываемые JavaScript объекты (функции), которые имеют настраиваемые [[Prototype]], а могут (или не могут) наследовать от Function.prototype.

Этот вопрос задавали много раз (даже при переполнении стека, например this , this или this ), но ни у одного из них нет хороших решений (у некоторых нет ответы, в то время как у других есть решения, перечисленные ниже).

Некоторые способы пришли мне в голову. Вот они:

  1. Использование Object.create:

    Это стандартный способ создания пользовательских прототипов объектов. Однако он не может создавать вызываемые объекты (функции).

  2. Создание подклассов Function:

    Это, вероятно, самый простой способ, о котором думает большинство людей . Примерно так:

    class MyFunction extends Function{
      constructor(id){
        super(
          '...args',
          'return args.length'
        )
        this.id=id
      }
      logCall(...args){
        console.log('Calling MyFunction #%i with arguments: %o', this.id, args)
        return Reflect.apply(this, this, args)
      }
    }
    
    const fn = new MyFunction(1)
    console.log(fn('foo', 'bar'))   //2
    fn.logCall('foo', 'bar', 'baz') //Calling MyFunction #1 with arguments: ['foo', 'bar', 'baz']
    //Works, so far so good!

    Однако у этого есть несколько недостатков:

    • Использует eval -like Function конструктор
    • Компиляция во время выполнения тела функции, каждый раз, когда создается новый экземпляр
    • Отсутствие доступа к лексической / закрывающей области внутри тела функции
    • Отсутствие ошибок времени компиляции и подсветки синтаксиса в теле функции: сложнее для поддержания
    • Невозможно создать функции, которые не имеют Function.prototype (и, следовательно, Object.prototype) в их цепочке прототипов
  3. Object.setPrototypeOf():

    class MyFunction{
      constructor(id){
        const _this = function (...args){
          return args.length
        }
        Object.setPrototypeOf(_this, Object.getPrototypeOf(this))
        _this.id=id
        return _this
      }
      logCall(...args){
        console.log('Calling MyFunction #%i with arguments: %o', this.id, args)
        return Reflect.apply(this, this, args)
      }
    }
    
    const fn = new MyFunction(1)
    console.log(fn('foo', 'bar'))   //2
    fn.logCall('foo', 'bar', 'baz') //Calling MyFunction #1 with arguments: ['foo', 'bar', 'baz']
    //Works as well, so far so good!

    Минусы:

    • Страшное красное предупреждение на MDN о производительности:

      Предупреждение: Изменение [[Prototype]] объекта - это по своей природе то, как современные JavaScript механизмы оптимизируют доступ к свойствам , в настоящее время очень медленная операция во всех браузерах и JavaScript двигатель. Кроме того, эффекты изменения наследования тонкие и обширные и не ограничиваются просто временем, затраченным на инструкцию Object.setPrototypeOf(...), но могут распространяться на любой код , который имеет доступ к любому объекту, [[Prototype]] которого был изменен.

      Поскольку эта функция является частью языка, разработчикам движка по-прежнему приходится выполнять эту функцию эффективно (в идеале). Пока разработчики движка не решат эту проблему, если вы беспокоитесь о производительности, вам следует избегать установки [[Prototype]] объекта. Вместо этого создайте новый объект с желаемым [[Prototype]], используя Object.create().

    • Труднее понять, unsemanti c, hacky -просмотр кода

  4. Proxy с фальшивыми ловушками:

    class MyFunction{
      constructor(id){
        const _this = new Proxy(
          function (...args){
            return args.length
          }, 
          Object.assign(
            Reflect
              .ownKeys(Reflect)
              .reduce((handler, trap) => ( //Forward operations to real `this`
                handler[trap] = (...args) => Reflect[trap](this, ...args.slice(1)),
                handler
              ), {}),
            {
              apply(...args){
                return Reflect.apply(...args)
              }
            }
          )
        )
        _this.id=id
        return _this
      }
      logCall(...args){
        console.log('Calling MyFunction #%i with arguments: %o', this.id, args)
        return Reflect.apply(this, this, args)
      }
    }
    
    const fn = new MyFunction(1)
    console.log(fn('foo', 'bar'))   //2
    fn.logCall('foo', 'bar', 'baz') //Calling MyFunction #1 with arguments: ['foo', 'bar', 'baz']
    //Works as well, so far so good!

    Недостатки:

Итак, что касается Я знаю, нет хорошего способа сделать это.

Мой вопрос:

Есть ли альтернатива , которая лучше этих, или, если ее нет, какой из них имеет наибольшие преимущества с точки зрения производительности , удобочитаемости и поддержки?

...