IIFE и прототипное наследование

Часть 1 | Часть 2 | Часть 3 | Часть 4 | Часть 5 | Часть 6 | Часть 7

В моем предыдущем блоге о конструкторах функций мы превратили нашу библиотеку getTime в IIFE (или IIFC) и выяснили, как правильно импортировать и экспортировать ее в другие стандартные сценарии JS, а также в браузер. Нам также удалось выяснить, как заставить getTime.js работать одновременно и в VSC, и в браузере.

Мы добились большого прогресса, но я думаю, что есть возможности для дальнейшего улучшения.

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

Поговорим о наследовании.

НАСЛЕДОВАНИЕ ОБЪЕКТИВНОЙ ЦЕЛЬНОСТИ

Наследование можно определить как способ совместного использования методов и свойств объектов. Когда один объект получает доступ к свойствам и методам другого объекта или получает свойства от отдельного объекта, мы говорим, что он наследует их. Мы можем сделать это несколькими способами.

В одном примере используется метод .call(). Метод .call() вызывает функцию и позволяет повторно назначить переменную ‘this’. Метод .call() также позволяет передавать параметры между объектами.

В приведенном ниже примере у нас есть две функции, которые назначают свойства двум отдельным конструкторам функций с помощью ключевого слова ‘this’. Во втором объекте мы вызываем obj1 с методом .call(). Это позволяет нам передавать свойства obj1 в obj2, и мы можем отправить контекст выполнения obj1 в obj2, передав ключевое слово ‘this’ в качестве параметра.

Если мы сохраним каждый объект в переменной и console.log их, мы увидим, что obj2 наследует свойства obj1 благодаря методу .call():

function obj1() {
 this.prop1 = ‘string1’
 this.prop2 = ‘string2’
}
function obj2() {
 obj1.call(this)
 this.prop3 = 1
 this.prop4 = 2
}
let createObj1 = new obj1()
let createObj2 = new obj2()
console.log(createObj1)
console.log(createObj2)

Достаточно просто ... но что, если мы захотим добавить свойство или функцию к объекту после его создания?

Здесь мы можем использовать другую концепцию, которую мы часто увидим, под названием Прототипное наследование.

ВЫ ПРОТОТИП

Все объекты, функции и массивы имеют объект-прототип. Этот объект содержит предопределенный набор методов, созданных механизмом JavaScript для данного типа данных.

Важно то, что мы можем использовать метод под названием .prototype для назначения пользовательских методов объектам или функциям, которые мы создаем. Мы хотим сохранить наши собственные методы в объекте .prototype, чтобы не создавать новый набор методов каждый раз, когда новый объект создается с помощью конструктора функции. Помните, что мы обращаемся к объектам по ссылке, когда сохраняем их в переменных. Мы не создаем новые объекты. Это идеально подходит для того, как мы хотим использовать .prototype.

Это также экономит место в памяти, поскольку мы не создаем новый набор методов каждый раз, когда инициализируем новый объект. Вдобавок к этому замечательная особенность метода .prototype заключается в том, что он позволяет двум отдельным объектам совместно использовать один и тот же объект-прототип. Это называется прототипным наследованием.

Давайте посмотрим на несколько примеров.

Для начала давайте рассмотрим некоторые методы по умолчанию, которые предоставляет нам JavaScript.

Если мы вызовем .prototype для массива в браузере, мы увидим много знакомых лиц:

или объект:

или даже строка:

ВАЖНОЕ ПРИМЕЧАНИЕ: Конструкторы функций и .prototype нельзя использовать с функциями стрелок. Стрелочные функции ES6 автоматически связывают ключевое слово ‘this' с контекстом выполнения, в котором создается, и вызывают множество ошибок. Придерживайтесь хорошо знакомого function(){}.

Круто, а как насчет добавления нового метода, который мы хотим написать?

Что ж, давай попробуем.

Мы можем создать новый конструктор функции и на этот раз передать конструктору некоторые аргументы:

function createObj(a, b, c) {
 this.prop1 = a
 this.prop2 = b
 this.prop3 = c
}

Мы можем добавить новое свойство или функцию с .prototype. Мы можем установить имя метода и сохранить его в переменной или установить для другой функции.

Мы также можем получить доступ к свойствам объекта, к которому добавляется метод прототипа, с помощью ‘this’:

createObj.prototype.addProps = function(){
 return this.prop1 + this.prop2 + this.prop3
}

Затем создайте новый объект:

let obj1 = new createObj(1, 2, 3)

И console.log функция, которую мы к нему добавляем:

console.log(obj1.addProps())

Мы также можем создать еще один новый объект с createObject(), но передать некоторые другие переменные:

let obj2 = new createObj(4, 5, 6)

И console.log функция .addProps() для obj2 и получите другой результат:

В браузере мы можем использовать метод .__proto__ для доступа к прототипу наших двух объектов.

Мы должны увидеть .addProps() метод, который они наследуют:

obj1:

obj2:

Мы видим, что метод .addProps() присутствует как в obj1, так и в obj2, и он также присутствует в прототипе их конструктора, которым является createObj().

Это означает, что оба объекта совместно используют метод .addProps(), который находится в объекте-прототипе конструктора функции createObj().

Отлично.

ВАЖНОЕ ПРИМЕЧАНИЕ. При работе с прототипами ТОЛЬКО используйте .__proto__ для образцов / тестирования. ВСЕГДА используйте метод .prototype. Поскольку метод .__proto___ обрабатывает набор методов по умолчанию, предоставляемых механизмом JavaScript, изменение .__proto__ может иметь серьезные побочные эффекты.

ДОБАВЛЕНИЕ ПРОТОТИПА

Мы собираемся провести некоторый рефакторинг. Я хочу сосредоточиться на улучшении внутренней функциональности getTime.js с использованием прототипного наследования. В конечном итоге это улучшит синтаксис, который мы используем для вызова getTime.js и из других файлов.

На данный момент посмотрите файлы, с которыми мы работаем:

Файловая структура:

getTime.js:

(function(env) {
  var getTime = function(){
    return new getTime.init()
  }
  getTime.init = function(){
    this.date = new Date()
    this.now = function() {
      let hours = this.date.getHours() % 12
      let ampm = this.date.getHours() <= 12 ? ‘PM’ : ‘AM’
      let minutes = this.date.getMinutes()
      hours = hours ? hours : 12
      minutes = minutes < 10 ? ‘0’ + minutes : minutes
      return hours + ‘:’ + minutes + ‘ ‘ + ampm
    }
    this.day = function() {
      return this.date.getDate()
    }
    this.month = function() {
      return (this.date.getMonth() + 1)
    }
    this.year = function() {
      return this.date.getFullYear()
    }
    this.fullDate = function() {
      return ‘(‘ + this.now() + ‘)’ + ‘ ‘ + this.month() + ‘/’ + this.day() + ‘/’ + this.year()
    }
  }
  env.getTime = getTime
})(typeof window === “undefined” ? global : window)

main.js:

if (typeof window === “undefined”) {
  var getTime = require(‘./getTime’)
  getTime = global.getTime
} else {
  document.addEventListener(“DOMContentLoaded”, addTime)
  function addTime() {
    const timeDiv = document.querySelector(“.time”)
    timeDiv.innerText = `${getTime().now()}`
  }
}
console.log(getTime().now())

index.html:

<html>
  <head>
    <title>Example</title>
    <script src=’scripts/getTime.js’></script>
    <script src=’scripts/main.js’></script>
  </head>
  <body>
    <div class=’time’></div>
  </body>
</html>

Короче говоря, index.html загружает наши 2 сценария: getTime.js и main.js.

В зависимости от того, в какой среде main.js выполняется (VSC или браузер), она требует и / или импортирует getTime.js.

Затем main.js вызывает IIFC и создает новый объект со встроенными в него методами.

getTime.js затем возвращает созданный объект и присоединяет его к глобальному контексту выполнения. Мы можем выполнить все это, вызвав getTime().

Затем мы можем связать метод с этим вызовом (например, .now() или .fullDate()) и получить обратно текущее время, текущий день, текущий год и т. Д.

Мы вызываем с getTime.js по main.js, потому что хотим иметь возможность использовать getTime.js в любом другом ванильном JS-файле, над которым мы работаем, поэтому мы тестируем, правильно ли экспортируется / импортируется наш IIFC в другие ванильные JS-файлы. В противном случае мы могли бы удалить main.js и запустить getTime.js самостоятельно. В любом случае он должен работать без проблем.

Первое, что мы хотим сделать, это добавить объект-прототип в наш IIFC. Затем мы хотим, чтобы наша init() функция унаследовала объект-прототип, который мы создаем.

В конечном итоге мы переместим все наши временные методы в объект-прототип, а пока давайте просто добавим тестовый метод:

getTime.prototype = {
  test: function() {
    return ‘test string’
  }
}

Если мы попытаемся ввести console.log getTime.js в браузере, мы не сможем нигде найти функцию «test»:

Хм ... ну, объект, который мы возвращаем, называется getTime.init ... не getTime ... ну и что, если мы попробуем установить прототип getTime.init на прототип getTime, поскольку мы возвращаем вызов getTime.init?

getTime.prototype = {
  test: function() {
    return ‘test string’
  }
}
getTime.init.prototype = getTime.prototype

Эврика.

и если мы console.log тестовая функция:

Большой.

Мы устанавливаем getTime.init.prototype равным getTime.prototype, потому что функция, которую мы используем для создания объекта, называется getTime.init, а getTime.init в настоящее время ничего не имеет в своем прототипе объекта. Таким образом, мы можем установить getTime.init.prototype в getTime.prototype, чтобы настроить цепочку прототипов и унаследовать методы, которые мы храним в getTime.prototype.

Когда объект наследует свойства другого объекта без перезаписи свойств исходного объекта, это называется чистым прототипным наследованием.

Если мы установим getTime.init.prototype равным getTime.prototype, мы будем обращаться к getTime.prototype по ссылке вместо создания нового объекта.

Мы также можем получить доступ к функции .test() в объекте-прототипе без необходимости писать .prototype.test() благодаря цепочке прототипов JavaScript.

Цепочка прототипов - это цепочка объектов-прототипов, которые функция, объект или массив наследует от других функций, объектов или массивов. Механизм JavaScript будет искать свойства во всех объектах-прототипах в цепочке, но вернет undefined, если не сможет найти свойство или функцию, которые ищет.

Пример: если бы у нас была цепочка прототипов, которая выглядела бы так:

obj.prototype.prototype.prototype.prop1

Мы можем получить доступ к prop1, просто набрав obj.prop1

Теперь, когда у нас настроена наша базовая цепочка прототипов, давайте переместим все наши пользовательские методы в объект-прототип и отформатируем все в пары ключ / значение:

Единственное, что мы не переносим в прототип, - это свойство date. Это скорее эстетическая причина, и я хочу чем-то инициализировать объект init (впрочем, позже я это изменю). Мы могли бы переместить дату в объект-прототип, если бы действительно захотели.

Сохраняя свойство date в объекте init, мы можем console.log(getTime) и получить красивый чистый объект с датой в объекте init и всем остальным в нашем объекте-прототипе:

Это мило.

Но если мы хотим вызвать любой из этих методов, нам все равно придется делать что-то вроде этого:

getTime().now()
getTime().day()
getTime().month()
getTime().year()

… И, честно говоря, эти двойные скобки просто уродливы.

Что, если бы вместо того, чтобы связывать методы с getTime(), мы могли бы передать параметры в getTime(), и наш конструктор функции определил, какой метод вызывать?

Что-то вроде этого:

getTime(‘now‘)
getTime(‘day‘)
getTime(‘month‘)
getTime(‘year‘)

… И если мы не передадим в getTime() какой-либо параметр, мы получим метод по умолчанию или что-то вроде сообщения об ошибке?

Было бы неплохо, правда?

Давай сделаем это.

ДОБАВЛЕНИЕ АРГУМЕНТОВ КЛЮЧЕВЫХ СЛОВ

Первое, что мы собираемся сделать, это реорганизовать нашу init() функцию, чтобы принять ключевое слово, которое мы собираемся передать в нее, а затем отправить его по цепочке прототипов и найти соответствующий метод.

Итак, если мы вызовем getTime с ключевым словом:

getTime(‘now‘)

Мы хотим, чтобы функция init взяла это ключевое слово и нашла соответствующую пару ключ / значение в объекте-прототипе. Мы уже возвращаем вызов init(), поэтому мы также можем вернуть вызов функции внутри init.prototype().

Мы можем сделать это, сохранив вызов init() в переменную, затем передав ключевое слово, которое мы хотим, чтобы оно было найдено, через вычисляемый оператор доступа к члену init(), и вернули его:

var getTime = function(keyword){
  var init = new getTime.init(keyword)
  return init[keyword]
}

Итак, если мы передадим ‘now’ в getTime(), параметр ключевого слова будет равен ‘now’, который мы можем передать вниз по цепочке прототипов и вернуть вызов функции ‘now’, как и раньше.

В настоящий момент, если мы попытаемся console.log(getTime(‘now’)), все, что мы получим, - это определение функции. Мы действительно получили определение функции ‘now’, так что он работает:

Что, если мы попытаемся применить это определение?

Интересно…

Что, если мы попробуем вызвать определение функции из функции init()?

getTime.init = function(keyword){
  this.date = new Date()
  this[keyword]()
}

Мы снова получаем определение функции:

Хорошо ... странно ... давайте попробуем сделать то, что мы делали до добавления объекта-прототипа, и установим тестовую переменную в функции init():

getTime.init = function(keyword){
  this.date = new Date()
  this[keyword] = ‘test’
}

Что происходит теперь, когда мы пытаемся console.log getTime('now')?

Возвращаемся "тест".

Это хорошо.

Это означает, что мы создаем динамическое свойство в init().

Единственная проблема в том, что вызываемая функция не находит нужных свойств.

Мы знаем, что переданное нами ключевое слово принимается в init() функцию. Мы также знаем, что JavaScript всегда будет смотреть вниз по цепочке прототипов в поисках определений переменных. Мы знаем, что новый контекст выполнения создается каждый раз, когда вызывается функция, и это изменяет ссылку на то, куда указывает ключевое слово this. Поэтому нам нужно init() найти и вызвать метод в объекте-прототипе таким образом, чтобы и init(), и init.prototype использовали один и тот же контекст выполнения.

Так что, если мы установим динамическое свойство в init() равным вызову самого себя?

getTime.init = function(keyword){
  this.date = new Date()
  this[keyword] = this[keyword]()
}

… А если мы попробуем console.log getTime(‘now') еще раз?

Джекпрот.

Можно console.log(getTime.prototype) в браузере?

Бринго.

Что, если мы console.log(getTime()) без аргументов?

Хм… интересно. Что ж, в этом есть смысл.

Поскольку мы не передаем аргумент в getTime(), конструктору функции нечего найти в цепочке прототипов, и он выдает ошибку TypeError, поскольку не может найти метод с аргументами, которые мы ему даем (а это ничто).

Итак, давайте займемся обработкой ошибок.

Мы можем использовать тернарный оператор, чтобы проверить, является ли this[keyword] правдивым, и, если это не так, вернуть сообщение об ошибке. В противном случае вызовите динамический метод, который мы отправляем по цепочке прототипов:

getTime.init = function(keyword){
  this.date = new Date()
  this[keyword] = this[keyword] ? this[keyword]() : ‘Error. Please pass an argument into getTime()’
}

Теперь, если мы console.log(getTime()) без аргументов?

Красивый.

Давайте немного расширим это сообщение об ошибке.

Пусть наш тернарный оператор возвращает функцию ошибки для ложного утверждения:

getTime.init = function(keyword){
  this.date = new Date()
  this[keyword] = this[keyword] ? this[keyword]() : this.error(keyword)
}

И вернуть сообщение об ошибке, если аргумент не передан, а также сообщение об ошибке, если было передано недопустимое ключевое слово:

in getTime.prototype:

error: function(keyword) {
  if (keyword === ‘’ || keyword === null || keyword === undefined) {
    return ‘Please enter a keyword’
  } else {
    return `${keyword} is an invalid keyword`
  }
}

Итак, если мы снова console.log(getTime()) без аргументов ...

А как насчет недопустимого ключевого слова?

Великолепный.

Вот последний взгляд на наши 2 сценария (я переместил this.date в объект-прототип):

getTime.js:

(function(env) {
  var getTime = function(keyword){
    var init = new getTime.init(keyword)
    return init[keyword]
  }
  getTime.init = function(keyword){
    this[keyword] = this[keyword] ? this[keyword]() : this.error(keyword)
  }
  getTime.prototype = {
    date: new Date(),
    now: function() {
      let hours = this.date.getHours() % 12
      let ampm = this.date.getHours() <= 12 ? 'AM' : 'PM'
      let minutes = this.date.getMinutes()
      hours = hours ? hours : 12
      minutes = minutes < 10 ? '0' + minutes : minutes
      return hours + ':' + minutes + ' ' + ampm
    },
    day: function() {
      return this.date.getDate()
    },
    month: function() {
      return (this.date.getMonth() + 1)
    },
    year: function() {
      return this.date.getFullYear()
    },
    fullDate: function() {
      return '(' + this.now() + ')' + ' ' + this.month() + '/' + this.day() + '/' + this.year()
    },
    error: function(keyword) {
      if (keyword === '' || keyword === null || keyword === undefined) {
        return 'Please enter a keyword'
      } else {
        return `${keyword} is an invalid keyword`
      }
    }
  }
  getTime.init.prototype = getTime.prototype
  env.getTime = getTime
})(typeof window === "undefined" ? global : window)

main.js:

if (typeof window === "undefined") {
  var getTime = require('./getTime')
  getTime = global.getTime
} else {
  document.addEventListener("DOMContentLoaded", addTime)
  function addTime() {
    const timeDiv = document.querySelector(".time")
    timeDiv.innerText = `${getTime('now')}`
  }
}
console.log(getTime())

И все, что нам нужно сделать, это передать ключевое слово в функцию для доступа к методам прототипа:

console.log(getTime('now'))
console.log(getTime('day'))
console.log(getTime('month'))
console.log(getTime('year'))
console.log(getTime('fullDate'))

Чтобы получить желаемую прибыль:

Чудесно.

ЗАДАНИЕ ВЫПОЛНЕНО

Мы могли бы сделать гораздо больше с нашим getTime.js, но я думаю, что это хорошая остановка для этого блога.

Конструкторы функций чрезвычайно полезны и гибки, и благодаря тому, как мы реорганизовали getTime.js, мы используем их структуру для создания чрезвычайно общих конструкторов функций, которые абстрагируют много кода, который мы в противном случае повторяли бы (что мы сделаем в следующем блоге с помощью fetch Запросы).

В следующий блог, который я запланировал, будут внесены некоторые незначительные дополнения, а также руководство по импорту getTime.js в приложение React. Я надеюсь, что этот блог стал хорошим продолжением первых двух, и я с нетерпением жду возможности завершить эту тему в части 4 Конструирования функций JavaScript.

Конструкция функции JavaScript

Часть 1 | Часть 2 | Часть 3 | Часть 4 | Часть 5 | Часть 6 | Часть 7