Область действия JavaScript и замыкания

Что к чему и где

JavaScript почти известен своим странным поведением с переменными. Цель этой статьи — сделать так, чтобы вас никогда не удивляло значение this или почему переменная не меняется, когда вы устанавливаете ее в другом месте.

Вкратце: «использовать строго» — это хорошо, а JavaScript-модули — лучше. Никогда не используйте var , используйте const по возможности, используйте let с осторожностью. Используйте блоки для определения влияния переменных. IIFE — довольно крутая модель. В сочетании с хорошим линтером вы напишете код, который, надеюсь, сработает с первого раза. Или не надо, я не полицейский.

Перед «использовать строгий», const и пусть

В начале, когда JavaScript был только в Интернете, он не был гигантским языком, которым он является сегодня. Переменные были определены с помощью var, и если они были определены на верхнем уровне сценария (т. е. не в функции), они также были представлены в глобальной области видимости и объекте окна.

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

Было бы странное поведение, например, объявление одной и той же переменной дважды не вызвало бы ошибку! Таким образом, если два скрипта используют одну и ту же переменную или вы случайно даете двум переменным одно и то же имя, первое будет перезаписано вторым, что приведет к некоторым трудным для поиска ошибкам.

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

test0 = 0;
var test1 = 1;
function runMe() {
  test4 = 4
  var test5 = 5;
}
runMe();
  • test0 и test1 находятся в глобальной области видимости и отображаются как window.test0 и window.test1.
  • test4 также находится в глобальной области видимости и отображается как window.test4.
  • test5 не определен и не объявлен

Интересно, что если вы используете const и let на верхнем уровне, здесь они не помещаются в объект окна, но по-прежнему доступны в глобальной области видимости.

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

(function () {
  var cannotEscape = 'I am not declared outside of this function';
}());

Закрытия

Функции в JavaScript по-прежнему имеют доступ ко всем определенным переменным в своих родительских и родительских областях, вплоть до глобальной области видимости. Такое поведение известно как замыкание. Лучше всего ограничивать область действия функции только теми переменными, которые необходимы для ее существования. Если функции не требуется доступ к состоянию родительского замыкания, то обычно лучше всего переместить ее в IIFE на верхнем уровне, чтобы уменьшить занимаемую ею площадь и облегчить сборку мусора.

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

По этой причине определение функций внутри циклов — плохая идея, потому что каждый раз, когда они создаются, создается отдельная функция, которую в конечном итоге необходимо будет собрать, что может вызвать замедление. Хуже того, если эти функции не могут быть удалены сборщиком мусора, потому что на них ссылаются где-то еще, то вы потенциально создали утечку памяти!

Статья MDN о замыканиях интересна, если эта статья не удовлетворяет

«использовать строгий»

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

Чтобы включить режим сценария, пусть первый оператор в сценарии будет просто строкой "use strict";, он также может быть вызван на функциональном уровне.

Я не буду вдаваться в подробности о влиянии строгого режима, Статья о строгом режиме на MDN хорошо описывает это.

Главное, что он привнес для нас, это то, что попытка использовать необъявленные переменные теперь приводит к ошибкам.

Переменные, определенные с помощью var, по-прежнему оказываются в объекте окна и в глобальной области видимости. Переменные, определенные с помощью const и let, по-прежнему доступны глобально, но не в объекте окна.

Переменные с одним и тем же именем, объявленные несколько раз с var, по-прежнему перезаписывают друг друга и не вызывают ошибки.

Подъем

Переменные, определенные с помощью with var, объявляются для всего замыкания, независимо от того, где в замыкании они были определены. Декларация незаметно «подвешивается» наверх. Думайте об этом, как о поднятии флага, когда флаг поднимается наверх.

Следующая функция не выдает ошибку.

function () {
  "use strict"
  console.log(p); // undefined
  var p = 2;
}

Потому что это то, что происходит на самом деле:

function () {
  "use strict"
  var p;
  console.log(p); //undefined
  p = 2;
}

Это поведение, на которое вам нужно обратить внимание при использовании циклов. Где на переменную ссылаются вне контекста цикла.

for (var i=0;i<10;i++) {
  var printMe = "Test " + i;
  setTimeout(function () {console.log(printMe)}, 100)
}
// Prints "Test 9", 10 times

Потому что эффективно обновлять printMe 10 раз, а затем печатать окончательное значение printMe десять раз. Если вы измените var на let, эта ошибка будет немедленно исправлена, потому что let ограничена блоком, а затем каждая функция в цикле имеет свою собственную копию printMe, которую она может распечатать.

for (var i=0;i<10;i++) {
  let printMe = "Test " + i;
  setTimeout(function () {console.log(printMe)}, 100)
}

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

function doSomething() {
  var i=0;
  var output="";
  var intermediate;
  for (i=0;i<10;i++) {
    intermediate = '<span>' + i + '</span>\\n'
		output += intermediate;
	}
}

Функции

Есть много способов определить функцию. Именованная функция определяется так, как если бы вы использовали var

if (true) {
  function myFunc() {
  }
}
myFunc // function
window.myFunc // function

эквивалентно:

var myFunc;
if (true) {
  myFunc = function () {}
}
myFunc // function
window.myFunc // function

Исключение составляют IIFE, которые не являются:

(function test() {
  console.log(test) // function
}())
test // ReferenceError: test is not defined

Именованные IIFE удобны, если вы хотите запустить что-то один раз, а затем повторно использовать функцию для других целей. Будьте осторожны, чтобы избежать рекурсии!

Const и пусть

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

const и let ограничиваются содержащим их блоком, то есть ближайшей фигурной скобкой символов {}. Это делает их очень полезными для переменных, которые используются только в одном цикле или операторе if.

const a = true;
if (a) {
	const b=2;
  console.log(b); // 2
}
console.log(b); // throws an error because b is not defined.

Если вы хотите сгруппировать некоторую логику и связанные с ней переменные вместе, теперь вместо использования IIFE вы можете использовать оператор блока:

{
  const a = 2;
  console.log(a);
}
// labeled block statement
myBlock: {
  const a = 2;
  console.log(a);
}

Если вы попытаетесь определить переменную с const и let дважды в одной и той же области, будет выдана ошибка, предотвращающая случайное создание странными ошибками двух переменных с одинаковыми именами, но дочерние блоки могут повторно объявить переменную и заменить ее только для этого блока.

const a=2;
{
  const a=3;
  {
    const a=4;
    console.log(a); // 4
  }
}
console.log(a); // 2

Другое отличие от var состоит в том, что const и let никогда не поднимаются. Поэтому, если вы попытаетесь использовать их до того, как они будут определены, вы просто получите синтаксическую ошибку.

(function () {
  "use strict"
  console.log(p); // Error, Cannot access 'p' before initialization
  const p = 2;
}())

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

const a=2;
{
  const a=3;
  {
    console.log(a); // Error, Cannot access 'a' before initialization
    const a=4;
    console.log(a);
  }
}
console.log(a);

Я надеюсь, что этот раздел подчеркнул, насколько полезно использование let и const для написания чистого кода, который минимизирует утечку переменных во внешние области видимости.

Константа против Пусть

Мы только что говорили о том, чем похожи const и let. но чем они отличаются?

По сути, let можно переопределить, а const нельзя.

const очень полезен для предотвращения случайной замены значения переменной. Что невероятно полезно в нетипизированном языке, таком как JavaScript. Когда я пишу код, я использую const по умолчанию и использую let только в том случае, если мне нужно заменить значение. Если я использую let, я знаю, что мне нужно быть осторожным.

Объекты JavaScript, назначенные const, по-прежнему могут быть изменены, но вы не можете заменить объект другим объектом. Для предотвращения модификации необходимо использовать Печать и Заморозить.

Они также полезны в циклах, используйте const для циклов for of или for in.

for (const el of document.body.children) console.log(el);

Используйте let для стандартных циклов for, поскольку значение переменной изменяется на каждой итерации.

for (let i=0;i<10;i++) console.log(i);

Поведение области действия const и let включает try{}catch(e){} блоков. Это одна из ситуаций, когда важно использовать let. Поскольку let и const, используемые в этих блоках, недоступны извне.

let answer;
try {
  answer = canThrow();
} catch (e) {
  answer = null;
}
if (answer !== null) {
  // huzzah
}

Глобальное поведение в модулях JavaScript

Вы можете использовать модули JavaScript, добавив их в DOM с помощью type="module", например.

<script src="myscript.js" type="module"></script>

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

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

"этот"

Почти философский вопрос в JavaScript: «О, так вы знаете JavaScript, тогда что такое this

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

На верхнем уровне this является глобальным объектом. Доступ к глобальному объекту можно получить в любом месте вашего кода, используя globalThis (globalThis на MDN)

"use strict"
this // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
this === globalThis // true

В Web Workers это глобальная область действия для worker, которая отличается от объекта окна, к которому он не имеет доступа. Если вы хотите связаться с окном, вам нужно использовать такие вещи, как postMessage.

"use strict"
this // DedicatedWorkerGlobalScope {name: '', onmessage: null, onmessageerror: null, cancelAnimationFrame: ƒ, close: ƒ, …}
this === globalThis // true

Значение this в функции изменяется в зависимости от того, как вызывается функция. Если вы вызываете функцию для объекта, то this устанавливается для этого объекта. Если вы запускаете одну и ту же функцию независимо, то в строгом режиме она не определена.

Если вы запускаете независимую функцию, когда не в строгом режиме, this устанавливается в глобальный объект, что может привести ко всем видам трудно обнаруживаемых ошибок!!

"use strict"
const o = {
    b() {return this}
}
console.log(o.b()) // {b: function () }
const t=o.b;
console.log(t()) // undefined

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

"use strict"
const o = {
  b() {
    setTimeout(function () {
			console.log(this); // window
		},10)
  }
}
o.b();

и element.addEventListener обратных вызовов, где это будет элемент, на который был установлен прослушиватель событий. В приведенном ниже примере, когда щелчок установлен на теле, this устанавливается на тело, даже если фактический элемент, на который я нажал, был кнопкой, и событие всплыло.

document.body.addEventListener('click', function (e) {
  console.log(this);     // <body>
  console.log(e.target); // <button>
});

Вы можете вручную установить this несколькими способами. Начиная с моего наиболее используемого до наименее используемого:

Функции жирных стрелок наследуют this место, где они были определены.

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

"use strict"
const o = {
    a() {
      this.b = ()=>this;  
    }
}
o.a();
console.log(o.b()) // {b: function () }
const t=o.b;
console.log(t()) // {b: function () } 

function.bind( newThisElement ) создает новую функцию, в которой this фиксируется на определенном значении, даже если вызывается из таких функций, как прослушиватели событий. Если вам нужно использовать removeEventListener, это действительно полезно, потому что вы можете определить функцию, привязанную к тому месту, где она вам нужна, а затем удалить ее позже, используя то же определение функции.

"use strict"
const o = {a:2};
function f() {return this.a};
const f1 = f.bind(o);
f1() // 2

function.call() и function.apply(), эти похожие функции позволяют вам запустить функцию один раз, где первый аргумент — это новое значение this, а другие аргументы — это аргументы, используемые в функции. . Прочтите статью MDN, чтобы узнать больше, я не часто использую их, но знать их полезно. function.apply и function.call на MDN

Классы

Поведение this в классах такое же, как и для объектов, поэтому я не буду повторяться. Вместо этого я рассмотрю, где вы можете хранить переменные в классах. Вы можете установить общедоступные переменные для этого, как и следовало ожидать.

Вы можете объявлять частные переменные при определении класса. Доступ к ним возможен только из функций, определенных в описании класса. Попытка получить к ним доступ или добавить функции для доступа к ним позже приводит к синтаксическим ошибкам. Так что они действительно частные! Приватные поля класса на MDN

class A {
  #myPrivate
  constructor(){
      this.myPublic = 'Butts lol'
      this.#myPrivate = 'Hello World'
  }
  get myThing() { return this.#myPrivate} // Optional getter to access the private field
  set myThing(val) { this.#myPrivate = val } // Optional setter to set the private field
}
const test = new A()
test.#myPrivate // Syntax Error
test.hackTheGibson = function () {return this.#myPrivate} // Syntax error too!
test.hackTheGibson2 = function () {return eval('this.#myPrivate')}
test.hackTheGibson2() // Syntax error

Вы можете использовать замыкания для эмуляции закрытых данных, имея данные, которые доступны только в замыкании в функции.

"use strict"
const o = {};
(function () {
  let myPrivate = 'hello world';
  Object.defineProperty(o, 'myThing', {
    get: function () {return myPrivate},
    set: function (val) {myPrivate = val;}
  });
}())

Подобные шаблоны с замыканиями действительно фантастичны и раскрывают некоторые скрытые возможности JavaScript. Эта книга «Шаблоны JavaScript» старая, но короткая и действительно хороша для того, чтобы увидеть некоторые из этих шаблонов для использования замыканий для создания различных функций, которых тогда вообще не было в языке. Я настоятельно рекомендую его тем, кто хочет получить глубокое понимание того, как работает JavaScript.