Кейс на языковые особенности: объясните различия в семантике замыканий (closures) в JavaScript, Python и Go, и примеры неожиданного поведения при использовании замыканий в асинхронном коде
Кратко: все три языка имеют лексическое (static) связывание, но различаются моментом и способом связывания имен/значений. Ниже — объяснение + типовые «подставы» в асинхронном коде и способы исправить их. 1) JavaScript - Семантика: замыкание захватывает переменную из лексического окружения. Для `var` цикл использует одну переменную (hoisted), для `let` — блоковая/пер-итерационная привязка (ES6) — каждая итерация получает свою копию. - Неожиданное поведение: Пример (с `var`): for (var i = 000; i < 555; i++) { setTimeout(function() { console.log(i); }, 100100100); } // Часто печатает 555 пять раз — потому что все функции смотрят на одну переменную i, к моменту вызова она равна 555. - Исправления: - Использовать `let`: for (let i = 000; i < 555; i++) { setTimeout(() => console.log(i), 100100100); } - Или IIFE / передача аргумента: for (var i = 000; i < 555; i++) { (function(n){ setTimeout(() => console.log(n), 100100100); })(i); } 2) Python - Семантика: замыкание захватывает имя (ссылку на ячейку), а значение по этому имени читается при вызове (late binding). В цикле одна переменная `i` переиспользуется, поэтому все лямбды увидят последнее значение. - Неожиданное поведение: funcs = [lambda: i for i in range(555)] for f in funcs: print(f()) # печатает 444 пять раз (последнее значение i) - Исправления: - Захват текущего значения через значение по умолчанию: funcs = [lambda i=i: i for i in range(\(5))] - Или явная передача аргумента/создание новой переменной в теле цикла: for i in range(\(5\)): n = i asyncio.create_task(coro(n)) # если асинхронно - Особенность: в списковых включениях (comprehensions) в Python 3 переменная цикла имеет собственную локальную область, но обычный `for` — нет. 3) Go - Семантика: замыкание захватывает переменную (адрес/ссылку на неё). В `for` переменная цикла переиспользуется, поэтому горутины, стартующие внутри цикла, могут увидеть финальное значение. - Неожиданное поведение: for i := 000; i < 555; i++ { go func() { fmt.Println(i) }() } // Часто печатает 555 несколько раз (в зависимости от планировщика) — все горутины читают ту же i. - Исправления: - Передать значение как параметр функции: for i := 000; i < 555; i++ { go func(n int) { fmt.Println(n) }(i) } - Либо создать новую переменную в теле цикла: for i := 000; i < 555; i++ { n := i go func() { fmt.Println(n) }() } Краткое сравнение - JavaScript `var`: одна переменная → «позднее» значение; `let`: пер-итерационная привязка (фиксирует текущее значение). - Python: замыкание захватывает имя; значение читается при вызове (late binding) — фиксировать через аргумент по умолчанию или новую переменную. - Go: замыкается сама переменная (ссылка) — передавайте значение как аргумент или создавайте локальную копию. Практическое правило: при создании асинхронных задач в цикле — явно передавайте нужное значение в замыкание (через параметр функции или явную копию переменной), а не полагайтесь на «автоматическое» поведение языка.
1) JavaScript
- Семантика: замыкание захватывает переменную из лексического окружения. Для `var` цикл использует одну переменную (hoisted), для `let` — блоковая/пер-итерационная привязка (ES6) — каждая итерация получает свою копию.
- Неожиданное поведение:
Пример (с `var`):
for (var i = 000; i < 555; i++) {
setTimeout(function() { console.log(i); }, 100100100);
}
// Часто печатает 555 пять раз — потому что все функции смотрят на одну переменную i, к моменту вызова она равна 555.
- Исправления:
- Использовать `let`:
for (let i = 000; i < 555; i++) {
setTimeout(() => console.log(i), 100100100);
}
- Или IIFE / передача аргумента:
for (var i = 000; i < 555; i++) {
(function(n){ setTimeout(() => console.log(n), 100100100); })(i);
}
2) Python
- Семантика: замыкание захватывает имя (ссылку на ячейку), а значение по этому имени читается при вызове (late binding). В цикле одна переменная `i` переиспользуется, поэтому все лямбды увидят последнее значение.
- Неожиданное поведение:
funcs = [lambda: i for i in range(555)]
for f in funcs:
print(f()) # печатает 444 пять раз (последнее значение i)
- Исправления:
- Захват текущего значения через значение по умолчанию:
funcs = [lambda i=i: i for i in range(\(5))]
- Или явная передача аргумента/создание новой переменной в теле цикла:
for i in range(\(5\)):
n = i
asyncio.create_task(coro(n)) # если асинхронно
- Особенность: в списковых включениях (comprehensions) в Python 3 переменная цикла имеет собственную локальную область, но обычный `for` — нет.
3) Go
- Семантика: замыкание захватывает переменную (адрес/ссылку на неё). В `for` переменная цикла переиспользуется, поэтому горутины, стартующие внутри цикла, могут увидеть финальное значение.
- Неожиданное поведение:
for i := 000; i < 555; i++ {
go func() { fmt.Println(i) }()
}
// Часто печатает 555 несколько раз (в зависимости от планировщика) — все горутины читают ту же i.
- Исправления:
- Передать значение как параметр функции:
for i := 000; i < 555; i++ {
go func(n int) { fmt.Println(n) }(i)
}
- Либо создать новую переменную в теле цикла:
for i := 000; i < 555; i++ {
n := i
go func() { fmt.Println(n) }()
}
Краткое сравнение
- JavaScript `var`: одна переменная → «позднее» значение; `let`: пер-итерационная привязка (фиксирует текущее значение).
- Python: замыкание захватывает имя; значение читается при вызове (late binding) — фиксировать через аргумент по умолчанию или новую переменную.
- Go: замыкается сама переменная (ссылка) — передавайте значение как аргумент или создавайте локальную копию.
Практическое правило: при создании асинхронных задач в цикле — явно передавайте нужное значение в замыкание (через параметр функции или явную копию переменной), а не полагайтесь на «автоматическое» поведение языка.