Лекция
В программировании перекрытие переменных происходит , когда переменная, объявленная в определенной области видимости (блоке принятия решений, методе или внутреннем классе ), имеет то же имя, что и переменная, объявленная во внешней области видимости. На уровне идентификаторов (имен, а не переменных) это называется маскированием имен . Говорят, что эта внешняя переменная перекрывается внутренней переменной, а внутренний идентификатор маскирует внешний идентификатор. Это может привести к путанице, поскольку может быть неясно, к какой переменной относятся последующие использования перекрываемого имени переменной, что зависит от правил разрешения имен в языке.
Одним из первых языков, представивших затенение переменных, был ALGOL , который впервые ввел блоки для установления областей видимости. Это также допускалось многими производными языками программирования, включая C , C++ и Java .
Язык C# нарушает эту традицию, допуская переопределение переменных между внутренним и внешним классами, а также между методом и содержащим его классом, но не между блоком if и содержащим его методом, или между операторами case в блоке switch .
Некоторые языки допускают переопределение переменных в большем количестве случаев, чем другие. Например, Kotlin позволяет внутренней переменной в функции переопределять переданный аргумент, а переменной во внутреннем блоке — другую переменную во внешнем блоке, в то время как Java этого не допускает (см. пример ниже ). Оба языка позволяют переданному аргументу в функцию/метод переопределять поле класса.
В некоторых языках полностью запрещено затенение переменных, например, в CoffeeScript и V (Vlang) .
Приведенный ниже код на Lua демонстрирует пример переопределения переменных в нескольких блоках.
v = 1 -- a global variable
do
local v = v + 1 -- a new local that shadows global v
print(v) -- prints 2
do
local v = v * 2 -- another local that shadows outer local v
print(v) -- prints 4
end
print(v) -- prints 2
end
print(v) -- prints 1
Следующий код на Python демонстрирует еще один пример переопределения переменных:
x = 0
def outer():
x = 1
def inner():
x = 2
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
# prints
# inner: 2
# outer: 1
# global: 0
Поскольку в Python нет объявления переменных, а есть только присваивание, ключевое слово `continuous`, nonlocalвведенное в Python 3, используется для предотвращения переопределения переменных и присваивания значений нелокальным переменным:
x = 0
def outer():
x = 1
def inner():
nonlocal x
x = 2
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
# prints
# inner: 2
# outer: 2
# global: 0
Это ключевое слово global используется для предотвращения переопределения переменных и присваивания значений глобальным переменным:
x = 0
def outer():
x = 1
def inner():
global x
x = 2
print("inner:", x)
inner()
print("outer:", x)
outer()
print("global:", x)
# prints
# inner: 2
# outer: 1
# global: 2
fn main() {
let x = 0;
{
// Shadow
let x = 1;
println!("Inner x: {}", x); // prints 1
}
println!("Outer x: {}", x); // prints 0
let x = "Rust";
println!("Outer x: {}", x); // prints 'Rust'
}
//# Inner x: 1
//# Outer x: 0
//# Outer x: Rust
#includeint main() { int x = 42; int sum = 0; for (int i = 0; i < 10; i++) { int x = i; std::cout << "x: " << x << '\n'; // prints values of i from 0 to 9 sum += x; } std::cout << "sum: " << sum << '\n'; // prints 45 std::cout << "x: " << x << '\n'; // prints 42 return 0; }
public class Shadow {
private int myIntVar = 0;
public void shadowTheVar() {
// Since it has the same name as above object instance field, it shadows above
// field inside this method.
int myIntVar = 5;
// If we simply refer to 'myIntVar' the one of this method is found
// (shadowing a second one with the same name)
System.out.println(myIntVar); // prints 5
// If we want to refer to the shadowed myIntVar from this class we need to
// refer to it like this:
System.out.println(this.myIntVar); // prints 0
}
public static void main(String[] args){
new Shadow().shadowTheVar();
}
}
Однако следующий код не скомпилируется:
public class Shadow {
public static void main(String[] args){
int a = 1;
for(int i = 0; i < 10; i++) {
// This causes a compilation error since redefining a variable
// inside a nested block in the same function is not allowed.
int a = i;
System.out.println(a);
}
}
}
letВ ECMAScript 6 введена возможность использования constблочной области видимости, что позволяет перекрывать переменные.
function myFunc() {
let my_var = 'test';
if (true) {
let my_var = 'new test';
console.log(my_var); // new test
}
console.log(my_var); // test
}
myFunc();

Чтобы понять shadowing, нужно помнить про scope.
var имеет область видимости функции:
var x = 1;
function test() {
var x = 2;
console.log(x);
}
test(); // 2
console.log(x); // 1
let и const имеют область видимости блока:
let value = 10;
if (true) {
let value = 20;
console.log(value); // 20
}
console.log(value); // 10
Блок — это обычно { ... }.
let count = 5;
function increment() {
let count = 0;
count++;
console.log(count);
}
increment(); // 1
console.log(count); // 5
Может быть ошибкой, если разработчик хотел изменить внешний count, но случайно создал новый.
Правильный вариант, если нужно менять внешнюю переменную:
let count = 5;
function increment() {
count++;
}
increment();
console.log(count); // 6
const status = "idle";
{
const status = "loading";
console.log(status); // loading
}
console.log(status); // idle
Это допустимо и иногда полезно, но может ухудшать читаемость.Shadowing параметров функции
const id = 100;
function getUser(id) {
console.log(id);
}
getUser(42); // 42
Параметр id затеняет внешний id.
Обычно это нормально, но если во внешнем scope тоже есть важный id, код может стать неочевидным.
Частая ошибка:
let isLoggedIn = false;
function login() {
let isLoggedIn = true;
}
login();
console.log(isLoggedIn); // false
Разработчик мог ожидать true, но внутри функции была создана новая переменная.
Исправление:
let isLoggedIn = false;
function login() {
isLoggedIn = true;
}
login();
console.log(isLoggedIn); // true
var x = 1;
if (true) {
var x = 2;
}
console.log(x); // 2
Здесь нет блочной области видимости, потому что var ограничен функцией, а не блоком.
С let поведение другое:
let x = 1;
if (true) {
let x = 2;
}
console.log(x); // 1
Поэтому в современном JavaScript почти всегда лучше использовать let и const, а не var.
В JavaScript есть случаи, когда затенение запрещено.
Например:
let x = 10;
{
var x = 20; // SyntaxError
}
Почему ошибка? Потому что var не ограничивается блоком и фактически пытается объявить x в той же или конфликтующей области видимости, где уже есть let.
Но такой код допустим:
var x = 10;
{
let x = 20;
console.log(x); // 20
}
console.log(x); // 10
let и const имеют Temporal Dead Zone — период от начала области видимости до фактического объявления переменной.
Пример:
let name = "Alice";
{
console.log(name); // ReferenceError
let name = "Bob";
}
Может показаться, что console.log(name) должен взять внешний name, но нет. Внутри блока уже существует локальная переменная name, просто она еще недоступна до строки объявления.
Правильно:
let name = "Alice";
{
console.log(name); // Alice
}
или:
let name = "Alice";
{
let localName = "Bob";
console.log(localName); // Bob
}
let i = 100;
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log(i);
Результат:
0 1 2 100
Это нормальный и часто приемлемый shadowing. Но в сложном коде одинаковые имена могут мешать понимать, о каком i идет речь.
const user = { name: "Alice" };
users.map(user => {
console.log(user.name);
});
Здесь параметр callback-функции user затеняет внешний user.
Лучше дать более точное имя:
const currentUser = { name: "Alice" };
users.map(listUser => {
console.log(listUser.name);
});
Особенно это важно в React, Node.js и при работе с массивами.
const error = "Global error";
try {
throw new Error("Local error");
} catch (error) {
console.log(error.message); // Local error
}
console.log(error); // Global error
Параметр catch (error) затеняет внешний error.
Лучше:
const globalError = "Global error";
try {
throw new Error("Local error");
} catch (caughtError) {
console.log(caughtError.message);
}
import { format } from "./utils";
function render() {
const format = "short";
console.log(format);
}
Здесь локальная переменная format затеняет импортированную функцию format. Это может привести к багам:
import { format } from "./utils";
function render() {
const format = "short";
return format(new Date()); // TypeError: format is not a function
}
Лучше:
import { format } from "./utils";
function render() {
const dateFormat = "short";
return format(new Date(), dateFormat);
}
Очень плохая практика — называть переменные как встроенные объекты:
const Array = [1, 2, 3]; const items = new Array(5); // TypeError
Или:
const console = "debug";
console.log("Hello"); // TypeError
Также лучше не использовать имена:
Object Array String Number Boolean Promise Date Math JSON console window document undefined NaN Infinity setTimeout
var поднимается наверх функции:
console.log(x); // undefined var x = 10;
Фактически JavaScript воспринимает это примерно так:
var x; console.log(x); x = 10;
С let и const иначе:
console.log(x); // ReferenceError let x = 10;
Связь с shadowing: из-за hoisting и одинаковых имен можно получить неожиданное поведение.
Если забыть let, const или var, можно случайно создать глобальную переменную:
function test() {
value = 123;
}
test();
console.log(value); // 123 в нестрогом режиме
Решение:
"use strict";
function test() {
const value = 123;
}
И всегда использовать let / const.
Иногда проблема обратная: разработчик думает, что создает новую переменную, но на самом деле меняет внешнюю.
let config = { theme: "light" };
function setup() {
config.theme = "dark";
}
setup();
console.log(config.theme); // dark
Это не shadowing, потому что новой переменной config нет. Это изменение объекта по ссылке.
Если нужна независимая копия:
let config = { theme: "light" };
function setup() {
const localConfig = { ...config, theme: "dark" };
console.log(localConfig.theme); // dark
}
console.log(config.theme); // light
Коллизия имен — более широкая проблема, когда разные сущности получают одинаковые имена.
function user() {
// ...
}
const user = {
name: "Alice"
}; // SyntaxError в одной области видимости
Решение — понятные имена:
function createUser() {
// ...
}
const currentUser = {
name: "Alice"
};
Shadowing может запутывать замыкания:
let value = "global";
function outer() {
let value = "outer";
return function inner() {
let value = "inner";
console.log(value);
};
}
outer()(); // inner
Если нужно обратиться к outer value, локальный value внутри inner мешает.
Исправление:
let value = "global";
function outer() {
let outerValue = "outer";
return function inner() {
console.log(outerValue);
};
}
outer()(); // outer
Shadowing не всегда ошибка. Иногда он нормален.
Например, в маленьком scope:
function normalize(user) {
return {
...user,
name: user.name.trim()
};
}
Или в callback:
const numbers = [1, 2, 3]; const doubled = numbers.map(number => number * 2);
Если имя очевидное и область видимости маленькая, это нормально.
Shadowing стоит избегать, если:
Плохой пример:
const data = await fetchUsers();
function process() {
const data = getLocalData();
return data.map(data => {
return data.value;
});
}
Лучше:
const usersResponse = await fetchUsers();
function process() {
const localUsers = getLocalData();
return localUsers.map(user => {
return user.value;
});
}
Частая проблема:
function UserCard({ user }) {
const [selectedUser, setSelectedUser] = useState(null);
return users.map(user => (
Здесь user из map затеняет user из props.
Лучше:
function UserCard({ user: currentUser }) {
const [selectedUser, setSelectedUser] = useState(null);
return users.map(listUser => (
let result;
async function load() {
const result = await fetchData();
}
await load();
console.log(result); // undefined
Внутри функции создан локальный result, внешний не изменился.
Исправление:
let result;
async function load() {
result = await fetchData();
}
await load();
console.log(result);
Но еще лучше — возвращать значение:
async function load() {
return await fetchData();
}
const result = await load();
Плохо:
const data = getData();
function save(data) {
// ...
}
Лучше:
const usersData = getData();
function save(formData) {
// ...
}
Плохо:
function handle(user) {
if (user.active) {
users.forEach(user => {
if (user.role === "admin") {
console.log(user);
}
});
}
}
Лучше:
function handle(currentUser) {
if (!currentUser.active) return;
users.forEach(teamUser => {
if (teamUser.role === "admin") {
console.log(teamUser);
}
});
}
Плохо:
var value = 1;
if (true) {
var value = 2;
}
Лучше:
let value = 1;
if (true) {
const localValue = 2;
}
Полезное правило:
{
"rules": {
"no-shadow": "error",
"no-redeclare": "error",
"no-undef": "error"
}
}
Для TypeScript:
{
"rules": {
"@typescript-eslint/no-shadow": "error"
}
}
Чем меньше функция, тем меньше риск перепутать переменные.
Плохо:
function process(data) {
// 100 строк кода
}
Лучше:
function normalizeUsers(users) {
return users.map(normalizeUser);
}
function normalizeUser(user) {
return {
...user,
name: user.name.trim()
};
}
Хороший ориентир:
Shadowing допустим, если область видимости маленькая и имя очевидное.
Shadowing опасен, если переменная из внешнего scope тоже нужна или код становится неоднозначным.
Перед тем как оставить одинаковое имя, спроси себя:
Плохо:
const user = getCurrentUser();
function render() {
users.map(user => {
console.log(user.name);
});
}
Хорошо:
const currentUser = getCurrentUser();
function render() {
users.map(listUser => {
console.log(listUser.name);
});
}
Итог: variable shadowing — это не всегда баг, но частый источник скрытых ошибок и плохой читаемости. Лучше избегать одинаковых имен в соседних и вложенных областях видимости, особенно в больших функциях, callback-ах, React-компонентах, async-коде и при работе с импортами.
Комментарии