Затенение в JavaScript

от автора

В статье о глобальной области видимости в JavaScript, мы коротко коснулись темы затенений (бурж. variable shadowing), в данной статье мы рассмотрим это явление подробнее.

В одной области видимости, не может быть переменных или аргументов с одинаковыми именами. Нарушение данного правила ведёт к ошибке:

function greetingUser(userName) {    let userName = 'Васятка' // Uncaught SyntaxError: Identifier 'userName' has already been declared      console.log(`Привет ${userName}`)}greetingUser('Пётр')

Оно и понятно, если есть две одинаковых переменных, как интерпретатор поймёт с какой из них ему работать дальше? Поэтому он сразу пресекает это дело, выдавая ошибку. Но если переменные были созданы в разных областях видимости, то ошибке не произойдёт, просто использоваться будет ближайшая к вызову переменная:

function greetingUser(userName, isForeign = false) {    let greeting = 'Привет'    if (isForeign) {        let greeting = 'Hola' // Затеняем greeting переменную из верхней области видимости        return (`${greeting} ${userName}!`)    }    return (`${greeting} ${userName}!`)}console.log(greetingUser('Pedro', true))

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

Затенение глобальных переменных

Затенению подвержены и переменные объявленные в глобальной области видимости:

var userName = 'Пётр'function greetingUser() {    let userName = 'Васятка' // Затеняем глобальную переменную    console.log(`Привет ${userName}!`) // Привет Васятка!}greetingUser()

Но если к простым смертным переменным, обратиться после затенения невозможно, то к глобальным можно получить доступ через свойство объекта window:

var userName = 'Пётр'function greetingUser() {  let userName = 'Васятка'  let globalUser = window.userName    console.log(`Привет ${userName}! И тебе тоже привет ${globalUser}!`) // Привет Васятка! И тебе тоже привет Пётр!}greetingUser()

В примере выше window.student, является синонимом глобальной переменной student, а не её копией. Изменения в сделанные при помощи одной формы обращений будут отображаться и в другой:

var userName = 'Пётр'function greetingUser() {  window.userName = 'Ивасик' // Изменяем глобальную переменную  let userName = 'Васятка'  let globalUser = window.userName  console.log(`Привет ${userName}! И тебе тоже привет ${globalUser}!`) // Привет Васятка! И тебе тоже привет Ивасик!}greetingUser()

Но обращение к глобальной переменной, как к свойству объекта window, сработает только если она была объявлена через var. А вот если использовать для объявления let или const, то ничего не выйдет:

let userName = 'Пётр'function greetingUser() {  let userName = 'Васятка'  let globalUser = window.userName  console.log(`Привет ${userName}! И тебе тоже привет ${globalUser}!`) // Привет Васятка! И тебе тоже привет undefined!}greetingUser()

При вызове window.userName, ошибки не возникнет, просто значение свойства будет равно undefined. Таким образом если для объявления, использовалось ключевое слово let или const, затенение обойти не получится.

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

let, var и затенение

Затенение по разному работает, в зависимости от через какое ключевое слово была объявлена переменная. Переменная объявленная при помощи let, спокойно затеняет переменную объявленную через var:

function greetingUser(userName, isForeign = false) {    var greeting = 'Привет'    if (isForeign) {        let greeting = 'Hola'         return (`${greeting} ${userName}!`)    }    return (`${greeting} ${userName}!`)}console.log(greetingUser('Pedro', true)) // Hola Pedro! - let затенило var

А вот var не может затенить let. Попытка сделать это, вызовет ошибку:

function greetingUser(userName, isForeign = false) {    let greeting = 'Привет'    if (isForeign) {        var greeting = 'Hola' // Uncaught SyntaxError: Identifier 'greeting' has already been declared        return (`${greeting} ${userName}!`)    }    return (`${greeting} ${userName}!`)}console.log(greetingUser('Pedro', true))

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

При этом этом var может затенить let, не вызывая ошибок, если они будут находится в разных функциях. Такое например возможно при использовании замыканий:

function greetingUser(userName, isForeign = false) {    let greeting = 'Привет'    if (isForeign) {        return () => {            var greeting = 'Hola' // Ошибки как в прошлый раз не будет!            return (`${greeting} ${userName}!`)        }    }      return () => {        return (`${greeting} ${userName}!`)    }}const greeting = greetingUser('Pedro', true)console.log(greeting())

О замыканиях у меня кстати тоже есть подробная статья.

Плюсы и минусы использования затенения в коде

Затенение двояко влияет на код. С одной стороны она позволяет инкапсулировать переменные, а с другой может привести к путанице. Рассмотрим плюсы и минусы замыканий подробнее.

Плюсы затенения

Локализация и изоляция

Когда в начала блока или внутренней функции локальная переменная, затеняет функциональную, это сразу говорит о том что тут инкапсулирована своя логика:

function greetingUser(userName, greeting) {    return {        say: () => {            console.log(`${greeting} ${userName}!`)        },        sayFr: () => {            let greeting = 'Bonjour'            console.log(`${greeting} ${userName}!`)        },        saySp: () => {            let greeting = 'Hola'            console.log(`${greeting} ${userName}!`)        },        sayEn: () => {            let greeting = 'Hola'            console.log(`${greeting} ${userName}!`)        }    }}const greeting = greetingUser('Васятка', 'Привет')greeting.say()greeting.sayEn()greeting.sayFr()greeting.saySp()

Зашита от конфликтов имён

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

Упрощение рефакторинга и отладки

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

Минусы затенения

Путаница при беглом чтении кода

При быстром чтении большого объёма кода, наличии нескольких переменных с одинаковыми именами именами, может так не слабо подпалить седалище разработчика:

function greetingUser(userName, isForeign = false) {    let greeting = 'Привет'    // Много, много кода    return () => {        var greeting = 'Hello'        // Много, много при много кода        if (isForeign) {            let greeting = 'Hola'            return (`${greeting} ${userName}!`)        }        // Ещё много, много при много кода        greeting = 'Hello' // В итоге какая из переменных меняется тут?        return (`${greeting} ${userName}!`)    }}const greeting = greetingUser('Pedro', true)console.log(greeting())

Пример выше, это сильно упрощённый псевдокод, но даже в нём нужно остановиться и потратить немало мыслетоплива, на то чтобы банально разобраться, во всех приключениях одной переменной. В недрах реальных проектов, встречаются чудища, куда страшнее.

Ухудшение читаемости при глубокой вложенности

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

function greetingUsers(users) {    let greeting = 'Hello'    if (Array.isArray(users)) {        for (let user of users) {            let isForeign = user[1]            let greeting = 'Привет'            if (isForeign) {                let greeting = 'Hola'                console.log(`${greeting} ${user[0]}!`)            }            console.log(`${greeting} ${user[0]}!`) // Какая из переменных greeting используется тут?        }    } else {        console.log(`${greeting} ${users}!`) // А тут?    }}const users = [    ['Васятка', false],    ['Pedro', true],    ['Пётр', false]] greetingUsers(users)

Неочевидность поведения

Выше мы описывали различия в работе затенения, в зависимости от того какое ключевое слово использовалось для объявления: var или let. Соответственно если в коде, затенение дополняется комбинацией, из различных способов объявление переменных, то понимание происходящего становится задачей со звёздочкой:

function greetingUser(userName, isForeign = false) {    let greeting = 'Привет'    return () => {        if (isForeign) {            var greeting = 'Hola'            return (`${greeting} ${userName}!`)        }        return (`${greeting} ${userName}!`)    }}const greetingForeign = greetingUser('Pedro', true)const greeting = greetingUser('Васятка')  console.log(greetingForeign()) // Hola Pedro!console.log(greeting()) // undefined Васятка!

В примере выше если аргумент isForeign равен true, то всё работает штатно и первый вывод в консоль обходится без сюрпризов. Но если значение isForeign, меняется на false, то переменная greeting, объявленная через var, не куда не девается. Она повинуясь законам всемирного тяготения JavaScript, всплывает в самый вверх внутренней функции, затеняя собой переменную greeting, из внешней функции. В этот момент движок JS, присваивает ей значение: undefined, до того момента пока не отработает строка с присвоением. Но в данном случае, присвоение находится в условии, которое не сработает, поэтому переменная greeting, так и останется со значение undefined! Это мы и увидим во втором выводе. Опять-же все подробности, почему так происходит, есть в статье о всплытии.

Поиск проблемных затенений в коде

Как мы убедились выше, затенения могут стать причиной множества неочевидных багов, поэтому лучше избегать их использования в коде. Помочь в этом может линтер: ESLint, имеющий специально обученное правило: no-shadow, ищущие проблемные затенения. Чтобы воспользоваться данным инструментом, нужно установить его в свой проект, при помощи пакетного менеджера:

npm install eslint --save-dev

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

npx eslint --init

В нём нужно будет, ответить на вопросы, о стеке и среде проекта. В итоге конфигуратор соберёт файл: eslint.config.mjs и сохранит его в корне проекта. Останется только добавить в него правило no-shadow:

import js from "@eslint/js"import globals from "globals"import {defineConfig} from "eslint/config"export default defineConfig([  {    files: ["**/*.{js,mjs,cjs}"],    plugins: { js },    extends: ["js/recommended"],    languageOptions: {      globals: globals.browser    },    rules: {      "no-shadow": "error" // Запрещаем затенения    }  }])

Теперь прогоним через ESLint файл с последним примером, где как раз из-за затенения возникала ошибка:

npx eslint js/script.js

После некоторых раздумий, ESLint выдаст в консоли сообщения о найденных им проблемах:

  2:6  error  'greeting' is assigned a value but never used                         no-unused-vars  6:8  error  'greeting' is already declared in the upper scope on line 2 column 6  no-shadow✖ 2 problems (2 errors, 0 warnings)

В данном случае он видит 2 проблемы:

  1. Переменная let greeting, на второй строке, объявлена, но не используется из-за затенения на 6-й строке;

  2. Переменная var greeting, на 6-й строке, создаёт затенение, нарушая правило no-shadow.

Современные IDE умеют считывать правила из конфига eslint.config.mjs, используя их в своём статическом анализаторе. В VSCode для потребуется установить расширение ESLint, которое в совокупности с правилом no-shadow, будет подсвечивать проблемные затенения прямо в коде:

Работа ESLint в VSCode

Работа ESLint в VSCode

Вывод

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

ссылка на оригинал статьи https://habr.com/ru/articles/1028948/