На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: ElcnU, ANDLL, fatalist
  
> Иммитация классов в JS , Непонятки
    Написал свою версию имитации класса :D, но работает странно, если не выполнять вызов _super в конструкторе JavaDrocher, то вызов mark.getName() возвращает "Gregory"! Откуда тут Gregory? Как оно сюда попало? Должно undefined! :wacko:
    Где я накосячил?
    ExpandedWrap disabled
      Object.extends = function _extends(props) {
          var _super = this.prototype;
          var proto = new this();
       
          for (const key in props) {
              if (props.hasOwnProperty(key)) {
                  proto[key] = function (...args) {
                      this._super = _super[key];
                      return props[key](...args);  
                  };
              }
          }
       
          function Class(...args) {
              this.__constructor && this.__constructor(...args);
          }
       
          Class.prototype = proto;
          Class.extends = _extends;
       
          return Class;
      };
       
      var Person = Object.extends({
          __constructor(name) {
              this.name = name;
          },
          getName() {
              return this.name;
          }
      });
       
      var JavaDrocher = Person.extends({
          __constructor(name, grade) {
              // this._super(name); //<-- отключаем вызов _super
              this.grade = grade;
          },
          getGrade() {
              return this.grade;
          }
      });
       
      // tests
      var gregory = new Person('Gregory');
      var mark = new JavaDrocher('Mark', 10);
       
      console.log(gregory.getName()); // Gregory
      console.log(mark.getName()); // Gregory <-- должно undefined!
      console.log(mark.getGrade()); // 10
    Сообщение отредактировано: Cfon -
      Нашел баг, все дело было в контексте я вызывал методы внутри extends в не правильном контексте. Заменил простой вызов на вызов через call(this, ...args) теперь все пучком :D
      ExpandedWrap disabled
        Object.extends = function _extends(props) {
            var _super = this.prototype;
            var proto = new this();
         
            for (const key in props) {
                if (props.hasOwnProperty(key)) {
                    proto[key] = function (...args) {
                        this._super = _super[key];
                        // return props[key](...args); //<-- ошибка: вызов в неправильном контексте
                        return props[key].call(this, ...args); //<-- вызов в нужном контексте
                    };
                }
            }
         
            function Class(...args) {
                this.__constructor && this.__constructor(...args);
            }
         
            Class.prototype = proto;
            Class.extends = _extends;
         
            return Class;
        };
        - Маста, а можно без call? :huh:

        Да! Для этого надо просто скопировать функции в правильный контекст :D
        ExpandedWrap disabled
          Object.extends = function _extends(props) {
              var _super = this.prototype;
              var proto = new this();
           
              for (const key in props) {
                  if (props.hasOwnProperty(key)) {
                      proto[key] = function (...args) {
                          this._super = _super[key];
                          this[key] = props[key]; //<-- копируем функции в контекст
                          return this[key](...args); //<-- вызываем
                      };
                  }
              }
           
              function Class(...args) {
                  this.__constructor && this.__constructor(...args);
              }
           
              Class.prototype = proto;
              Class.extends = _extends;
           
              return Class;
          };
          Обнаружен косяк :D, конструктор класса в случае наследования вызывается два раза!
          Первый раз когда собственно определяется класс JavaDrocher
          ExpandedWrap disabled
            var JavaDrocher = Person.extends({
                __constructor(name, grade) {
                    this._super(name);
                    this.grade = grade;
                },
                getGrade() {
                    return this.grade;
                }
            });
          вызов Person.extends приводит к вызову Person.__constructor :blink:
          Ну а второй раз как полагается при создании объекта mark тут вопросов нет.
          Исследуя код обнаружил место где это происходит. Вот этот кусок
          ExpandedWrap disabled
            Object.extends = function _extends(props) {
                var _super = this.prototype;
                var proto = new this(); //<-- тут!
            ...

          Решение такое надо определять момент определения класса и момент создания объекта. Например можно определить флажок isCreatingObject и окружить им оператор создания объекта proto:
          ExpandedWrap disabled
            (function () {
                var isCreatingObject;
             
                Object.extends = function _extends(props) {
                    var _super = this.prototype;
             
                    isCreatingObject = false; //<-- раз
                    var proto = new this();
                    isCreatingObject = true; //<-- и два
                    
                    Object.keys(props).forEach(key => {          
                        proto[key] = function (...args) {
                            this._super = _super[key];
                            return props[key].call(this, ...args);
                        };
                    });
             
                    function Class(...args) {
                        isCreatingObject && this.__constructor && this.__constructor(...args); //<-- и три
                    }
             
                    Class.prototype = proto;
                    Class.extends = _extends;
                    return Class;
                };
            })();

          поскольку данный флажок проверяется также после определения класса в момент создания объекта, его необходимо вынести за пределы extends, но поскольку мы не хотим его делать глобальной, юзаем iife. Все теперь __constructor вызывается один раз при создании объекта
          ExpandedWrap disabled
            var mark = new JavaDrocher('Mark', 10);
          Сообщение отредактировано: Cfon -
            Есть еще одна оптимизация кода смотрите на след кусок кода
            ExpandedWrap disabled
              ...
                      Object.keys(props).forEach(key => {          
                          proto[key] = function (...args) {
                              this._super = _super[key]; //<-- создается ссылка на метод из прототипа
                              return props[key].call(this, ...args);
                          };
                      });
              ...

            тут при обходе каждый раз создается новая функция, которая подменяет оригинальную. Казалось бы ничего страшного, но если посмотреть на содержимое этой функции, то вы увидите, что единственое что она делает это выставляет ссылку на метод из прототипа. Спрашивается а зачем нам нужна ссылка на метод из прототипа, если мы ее не используем. Так в моем примере эта ссылка юзается только в методе __constructor
            ExpandedWrap disabled
              var JavaDrocher = Person.extends({
                  __constructor(name, grade) {
                      this._super(name); //<-- вот она
                      this.grade = grade;
                  },
                  getGrade() {
                      // а тут _super не юзается
                      return this.grade;
                  }
              });

            Делаем след оптимизацию, она не такая очевидная:
            ExpandedWrap disabled
              ...
                      Object.keys(props).forEach(key => {  
                          proto[key] = /_super/.test(props[key])
                              ? function (...args) {
                                  this._super = _super[key];
                                  return props[key].call(this, ...args);
                              }
                              : props[key];          
                      });
              ...

            Данный код требует комментария :D
            Строка /_super/.test(props[key]) тестирует тело метода на наличие в нем слова _super. Как это работает? Дело в том вызов test(props[key]) приводит к test(props[key].toString()), который возвращает содержимое метода в виде строки и таким образом мы имеем возможность проверить наличие обращения к _super. Если так то строим новый метод, если нет то просто ссылаемся на оригинальный метод.
            Тут еще остались косяки, но пока этого достаточно для общего развития :D

            Добавлено
            - Маста, а почему вы юзаете переменную _super:
            ExpandedWrap disabled
              ...
                  Object.extends = function _extends(props) {
                      var _super = this.prototype; //<-- тут
               
                      isCreatingObject = false;
                      var proto = new this();
                      isCreatingObject = true;
                      
                      Object.keys(props).forEach(key => {  
                          proto[key] = /_super/.test(props[key])
                              ? function (...args) {
                                  this._super = _super[key]; //<-- тут
                                  return props[key].call(this, ...args);
                              }
                              : props[key];  
                      });
              ...

            Новичок, это не просто переменная это замыкание, поскольку к ней мы обращаемся из другой функции. Это необходимо чтобы коректно определить контекст вызова метода из прототипа.

            - Как это? :huh:

            Смотри если ты напишешь просто this._super = this.prototype[key], то this.prototype[key] в момент вызова метода не будет указывать на метод прототипа, поскольку this будет ссылаться на созданный объект, а не функцию-конструктор.

            - Маста как же сразу не просек :wall:

            Спокойно Новичок! Голова нам еще пригодиться! :D
            Сообщение отредактировано: Cfon -
              а где:
              ExpandedWrap disabled
                class A extends B
                {
                    const A = 'aaa';
                 
                    private a;
                    protected b;
                    public c;
                 
                }


              ?
                Цитата K313 @
                а где:
                ExpandedWrap disabled
                  class A extends B
                  {
                      const A = 'aaa';
                   
                      private a;
                      protected b;
                      public c;
                   
                  }


                ?

                Цели написать полный аналог классов ESNext не было, цель была поупражняться с JS ну еще раз вспомнить прототипы, замыкания. К тому же лично я не юзаю эти дела, не прижилось в моей практике.
                Сообщение отредактировано: Cfon -
                  - Маста, а почему в вашем коде юзается то стрелочный синтаксис то обычный?
                  - с вашего позволения я убрал улучшения, чтобы код стал компактнее :D
                  ExpandedWrap disabled
                    Object.extends = function _extends(props) {
                        var _super = this.prototype;
                        var proto = new this();
                        
                        Object.keys(props).forEach(key => {  //<-- стрелочная
                            proto[key] = function (...args) { //<-- обычная
                                this._super = _super[key];
                                return props[key].call(this, ...args);
                            };  
                        });
                     
                        function Class(...args) {
                            this.__constructor && this.__constructor(...args);
                        }
                     
                        Class.prototype = proto;
                        Class.extends = _extends;
                        return Class;
                    };
                     
                    var Person = Object.extends({
                        __constructor(name) {
                            this.name = name;
                        },
                        getName() {
                            return this.name;
                        }
                    });
                     
                    var gregory = new Person('Gregory');
                    console.log(gregory.getName());

                  Хороший вопрос Новичок :D
                  Суть в контексте. Как ты наверное знаешь доступ к контексту выполняется через this. Функция extends имеет несколько зон обращения к контексту (я обозначил их комментариями):
                  ExpandedWrap disabled
                    Object.extends = function _extends(props) {
                        var _super = this.prototype;
                        var proto = new this(); //<-- 1
                      
                        Object.keys(props).forEach(key => {
                     
                            proto[key] = function (...args) {
                                this._super = _super[key]; //<-- 2
                                return props[key].call(this, ...args);
                            };  
                        });
                     
                        function Class(...args) {
                            this.__constructor && this.__constructor(...args); //<-- 3
                        }
                     
                        Class.prototype = proto;
                        Class.extends = _extends;
                        return Class;
                    };
                     
                    var Person = Object.extends({
                        __constructor(name) {
                            this.name = name; //<-- 4
                        },
                        getName() {
                            return this.name;
                        }
                    });

                  Так вот в первом случае контекстом будет Object, во втором - proto, в третьем - зависит от того как будет вызван Class, простой вызов Class() определит в качестве контекста объект Window или global, если же через new Class(), то это будет новый объект. В четвертом - также зависит от того как будут вызваны методы анонимного объекта, мы уже говорили об этом в предыдущих постах, если ты забыл я напомню. Посмотри на код сразу после комментария 2: props[key].call(this, ...args) тут как раз идет вызов методов из нашего анонимного объекта, как видишь я вызываю их через call с передачей контекста this, который как я уже написал является объектом proto.

                  - Понятно Маста! Но как это относится к стрелочной или обычной нотации функции? :huh:

                  Новичок дело в том что контекст в функции определяемой через function, если она не принадлежит объекту всегда является глобальный объект Window или global. Если же функция объявлена через стрелочную нотацию контекст внутри нее не меняется и является тем же что и в зоне ее определения, или (как возможно ты слышал) ее называют лексической областью видимости. Т.е. в нашем случае это будет функция Object.
                  На самом деле в данном случае это не имеет значения, т.к. внутри нашей стрелочной функции мы не юзаем конткест, поэтому ее можно заменить на обычную нотацию. А вот если заменить на стрелочную функцию, функцию которая присваивается объекту proto (см коммент 2), то контекст будет тем же что и в зоне ее определения, т.е. функция Object, что как ты понимаешь приведет к некорреткной работе кода вызова методов из анонимного объекта. Вот поэтому я юзал обычную нотацию в случае функции присваиваемой proto, а в случае колбэка forEach по привычке юзал стрелочную нотацию.

                  - ВАУ МАСТА! На это мне потребуется время, чтобы я все перевалил :wacko:
                  - СПАСИБО! Я перечитаю пост несколько раз :thanks:
                  Сообщение отредактировано: Cfon -
                    - Маста, я тут снова перечитал ваши посты и не понел откуда таки в примере поста #1 появляется Gregory? :huh:

                    Новичок ты меня таки поймал :D
                    Давай исследуем откуда этот Gregory взялся. В том примере у нас идет привязка к анонимному объекту, что передается в качестве параметра в extends, таким образом он и будет нашим контекстом при вызове __constructor:
                    ExpandedWrap disabled
                      var Person = Object.extends({
                          __constructor(name) {
                              this.name = name;
                          },
                          getName() {
                              return this.name;
                          }
                      });
                       
                      var gregory = new Person('Gregory'); //<-- вызов __constructor в контексте анонимного объекта

                    это приведет к появлению свойства name со значением "Gregory" в этом объекте, я визуализирую состояние объекта:
                    ExpandedWrap disabled
                      {
                          name: "Gregory", //<-- новое свойство name
                          __constructor(name) {
                              this.name = name;
                          },
                          getName() {
                              return this.name;
                          }
                      }

                    Далее идет вызов Person.extends, который приводит к тому что JavaDrocher "наследует" свойства Person, который как ты помнишь из-за неправильного контекста ссылается на наш анонимный объект, вместо Person.prototype:
                    ExpandedWrap disabled
                      var JavaDrocher = Person.extends({
                          __constructor(name, grade) {
                              this.grade = grade;
                          },
                          getGrade() {
                              return this.grade;
                          }
                      });

                    таким образом вызов mark.getName
                    ExpandedWrap disabled
                      var mark = new JavaDrocher('Mark', 10);
                      mark.getName();

                    приведет к обращению getName из нашего анонимного объекта, в котором свойство name уже определен со значением "Gregory", оно и будет возвращено. Вроде все верно отследил :D

                    - Маста дайте мне время я это переварю :jokingly:

                    Ок Новичок!
                    Сообщение отредактировано: Cfon -
                      Решил проблему двух вызовов конструкторов через копирование прототипа, вместо введения переменной isCreatingObject, также заюзал Object.defineProperty для восстановления связи прототипа с конструктором и заключил все в условие, согласно форме написания полифила :D:
                      ExpandedWrap disabled
                        if (!Object.extends) {
                            Object.extends = function _extends(props) {
                                var _super = this.prototype;
                                var proto = Object.create(this.prototype); //<-- копируем this.prototype
                         
                                Object.keys(props).forEach(key => {  
                                    proto[key] = /_super/.test(props[key])
                                        ? function (...args) {
                                            this._super = _super[key];
                                            return props[key].call(this, ...args);
                                        }
                                        : props[key];  
                                });
                            
                                function Class(...args) {
                                    this.__constructor && this.__constructor(...args);
                                }
                            
                                Class.prototype = proto;
                                Object.defineProperty(Class.prototype, 'constructor', {
                                    enumerable: false,
                                    value: Class
                                });
                                Class.extends = _extends;
                                return Class;
                            };
                        }
                        Цитата Cfon @
                        - Понятно Маста! Но как это относится к стрелочной или обычной нотации функции? :huh:

                        Новичок дело в том что контекст в функции определяемой через function, если она не принадлежит объекту всегда является глобальный объект Window или global. Если же функция объявлена через стрелочную нотацию контекст внутри нее не меняется и является тем же что и в зоне ее определения, или (как возможно ты слышал) ее называют лексической областью видимости. Т.е. в нашем случае это будет функция Object.
                        На самом деле в данном случае это не имеет значения, т.к. внутри нашей стрелочной функции мы не юзаем конткест, поэтому ее можно заменить на обычную нотацию. А вот если заменить на стрелочную функцию, функцию которая присваивается объекту proto (см коммент 2), то контекст будет тем же что и в зоне ее определения, т.е. функция Object, что как ты понимаешь приведет к некорреткной работе кода вызова методов из анонимного объекта. Вот поэтому я юзал обычную нотацию в случае функции присваиваемой proto, а в случае колбэка forEach по привычке юзал стрелочную нотацию.

                        Другое более понятное объяснение заключается в том, что каждое объявление функции неявно включает ссылку на контект this, значение которого зависит от того как была вызвана функция. В случае же со стрелкой this не определяется и как следствие юзается this внешней функции, если конечно она не является также стрелкой.
                          Цитата Cfon @
                          также заюзал Object.defineProperty для восстановления связи прототипа с конструктором

                          Можно сразу определить constructor в proto, Object.defineProperty уже не требуется:
                          ExpandedWrap disabled
                            ...
                                Object.extends = function _extends(props) {
                                    var _super = this.prototype;
                                    var proto = Object.create(this.prototype, {
                                        constructor: { //<-- определяем constructor в proto
                                            value: Class
                                        }
                                    });
                            ...
                          Сообщение отредактировано: Cfon -
                          0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                          0 пользователей:


                          Рейтинг@Mail.ru
                          [ Script execution time: 0,0534 ]   [ 16 queries used ]   [ Generated: 18.04.24, 00:51 GMT ]