Javascript без this

в 15:40, , рубрики: javascript, javascript this, замыкания в javascript, привет хабр

Одна из тонкостей в Javascript это то, как работает this. Это отличается от правил лексического окружения, которые применяются к обычным переменным в Javascript. То, на что ссылается this часто может не относиться к лексическому окружению функции. Чтобы c этим можно было работать обычно используют похожий трюк:

function blah(){
  var that = this;
  somethingThatRebindsThings( function(){
    that.whatever();
  });
}

Каждый, кто много писал на Javascript прошел через это. А представьте себе жизнь без that. Как это возможно? Один из способов — это никогда не использовать this. Звучит абсурдно? Давайте посмотрим.

Почему this?

Причина по которой мы используем this обычно связана с одними из наиболее полезных абстракций в ОО парадигме: состояние и поведение. Точнее, объекты с переменными и методами. Вы могли подумать, что мы потеряем эту мощную абстракцию если прекратим использовать this. Как могут методы обращаться к переменными именно своего объекта без this? Возможно, вы уже знаете правильный ответ: замыкания.

Вы можете открыть для себя, что замыкания это еще один способ определить состояние и поведение объекта. Вот пример замены this-кода на код, основанный на замыканиях:

function Car(numberOfDoors){
  this.numberOfDoors = numberOfDoors;
  this.numberOfWheels = 4;

  this.describe = function(){
    return "I have " + this.numberOfWheels +
      " wheels and " + this.numberOfDoors + " doors.";
  }
}

var sportsCar = new Car(2);
console.log( sportsCar.describe() );

Вот как мы сделаем то же самое используя замыкания:

function createCar(numberOfDoors){
  var numberOfWheels = 4;

  function describe(){
    return "I have " + numberOfWheels +
      " wheels and " + numberOfDoors + " doors.";
  }

  return {
    describe: describe
  };
}

var suv = createCar(4);
console.log( suv.describe() );

Я написал функцию-конструктор createCar. Она описывает все состояния и поведения моего класса Car и возвращает объект который предоставляет только публичный метод. Все остальное, что определенно в функции-конструкторе, недоступно внешнему миру, но, благодаря замыканиям, все объявленое в пределах функции-конструктора может продолжать взаимодействовать друг с другом. Каждый вызов функции-конструктора создает новое замыкание, новую маленькую сумочку со своими переменными и методами.

Наследование

А что же наследование? Обычно мы реализовали бы его через прототипное наследование, что означало бы использование this. Мы не используем this и поэтому давайте будем креативными и используем другой способ наследования:

function createMiniVan(capacity){
  var car = createCar(4);
  car.capacity = function(){
    return "I have room for " + capacity + " passengers.";
  };
  return car;
}

var miniVan = createMiniVan(7);
console.log( miniVan.describe() );
console.log( miniVan.capacity() );

В этом выдуманном примере я создал новый класс MiniVan, который наследует все публичные методы класса Car и потом добавляет новый функционал индикации вместимости. Это похоже на миксыны используемые в CLOS и Ruby. Недостаток этого подхода в том что это не позволит дочерним или родительским классам получить доступ к внутреннему состоянию или поведению — другими словами нет концепции защищенной видимости. Впрочем, лично я редко встречал случаи когда защищенный доступ был бы полезен. Я бы отметил, что почти во всех случаях вы можете достичь тех же целей более модной «композицией» чем «наследованием».

Композиция

Вот как можно добавить классу новую функцию с помощью композиции:

function createOdometer(){
  var mileage = 0;

  function increment(numberOfMiles){ mileage += numberOfMiles; }
  function report(){ return mileage; }

  return {
    increment: increment,
    report: report
  }
}

function createCarWithOdometer(numberOfDoors){
  var odometer = createOdometer();
  var car = createCar(numberOfDoors);

  сar.drive = function(numberOfMiles){
    odometer.increment(numberOfMiles);
  }

  car.mileage = function(){
    return "car has driven " + odometer.report() + " miles";
  }

  return car;
}

Внутри моей функции-конструктора createCartWithOdometer я создаю odometer, чтобы использовать его функционал. Потом я создаю объект Car и добавляю этому объекту новую функцию — и мы имеем новый тип, который расширяет Car используя функционал odometer. Все это без прототипного наследованиея и this.

Неужели?

Да, действительно. Я работал в команде которая разработала довольно большое Javascript приложение таким способом. Мы использовали this всего раз 10 в мульти-тысячной кодобазе и мы счастливы.

Почему мы считаем этот подход успешым? Во-первых, это позволило нам обойти подводные камни, связанных с тем, как this работает в JavaScript. К примеру, больше нет путанницы с тем, что jQuery переопределяет this на объект который итерирует. Во-вторых, возможность композиции собственных классов внутри функции-конструктора оказалась очень мощной. Это дает полезную часть множественного наследования без проблем с зависимостями. Наконец, самое большое преимущество было в контроле над API каждого класса, возможности спрятать приватные методы в замыканиях функции-конструктора. Я уверен, что это было значительная причина нашего успеха в работе в команде над огромной кодовой базой.

Автор: quux

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js