Конспект из You Don't know JS, book 1: Scope and Closure.
Переменная, которая встречается в коде, может интерпретироваться в одном из двух контекстов, называемых RHS и LHS - right-hand-side и left-hand-side, часто встречаются значения lvalue и rvalue. Стороны выбираются относительно оператора присваивания:
Переменная, которая встречается в коде, может интерпретироваться в одном из двух контекстов, называемых RHS и LHS - right-hand-side и left-hand-side, часто встречаются значения lvalue и rvalue. Стороны выбираются относительно оператора присваивания:
a = b;
Левая сторона (lvalue) представляет собой некий контейнер, в который помещаются значения и задача компилятора сводится только к определению адреса этого контейнера по имени переменной, по которому это значение можно разместить.
Правая сторона (rvalue) представляет собой контейнер хранящий значения и важность представляют сами значения. В правостороннем контексте компилятор пытается эти значения извлечь.
В вызове console.log(a) на a ссылаются в правостороннем контексте, значение аргумента не меняется в ходе вызова функции (метода).
В присваивании a = 2 на а ссылаются в левостороннем контексте, т.к. изначальное значение а не важно компилятору, оно меняется в процессе присваивания.
При вызове любой функции, например: f(2) используются оба контекста. Имя функции обрабатывается компилятором в правостороннем контексте, по имени f он получает некоторое значение (которое представляет собой некоторый адрес, указывающий на начало блока с некоторым кодом, представляющим собой тело функции). В процессе вызова происходит неявное присваивание аргументу (численный литерал 2) формальному параметру функции. Если формальные параметры не объявлены в заголовке, то значение получит свойство объекта arguments с ключом 0. Если параметры были объявлены, то значение arguments[0] дополнительно скопируется в первый из объявленных в заголовке формальных параметров.
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
RHS-ссылки: вызов foo, b = a, значение a и b при возврате результата выполнения функции.
LHS-ссылки: a = 2 при вызове foo, b = a при определении значения переменной b, c = foo() - присваивание результата вызова функции переменной.
Область видимости - набор правил в соответствии с которым движок может находить переменную по ее имени. При наличии вложенных функций каждая из них получает свою область видимости:
var x = 1, y = 3;
function a() {
var y = 2;
function b() {
function c() {
x = y;
}
}
}
Функции a, b, c имеют свои области видимости (с вложена в b, b вложена в а), кроме того существует глобальная область видимости (соответствующая объектам window или global).
Когда компилятор обнаруживает лексему y он последовательно проводит поиск в области видимости c, затем b, затем a, где и находит переменную. То же происходит с переменной x, но поиск завершается успехом в глобальной области видимости. Как только переменная найдена, поиск прекращается, что позволяет иметь переменные с одинаковыми именами на разных областях видимости. y, объявленная на глобальном уровне, будет "затенена" y, объявленной в области видимости a. Однако глобальные переменные доступны в любом месте программы как свойства глобального объекта.
В том случае, если RHS-поиск не увенчается успехом в глобальной области видимости, движок сгенерирует ошибку ReferenceError. LHS-поиск завершается в глобальной области видимости и если требуемое имя не находится и там, автоматически создается переменная с запрашиваемым именем. Однако в Strict mode поиск завершается ошибкой ReferenceError по аналогии с RHS.
TypeError генерируется в том случае, когда RHS-поиск завершился успешно, однако программа пытается выполнить действие, не предусмотренное для данного значения (например, вызывает как функцию переменную, не являющуюся ссылкой на функцию или пытается вызвать метод у переменной, содержащей значения undefined или null).
JavaScript использует лексическую область видимости, помимо этого существует динамическая область видимости, используемая, например, в языке сценариев Bash и некоторых режимах Perl. Лексическая область времени определяется на момент компиляции (выполнения), функция видит переменные в зависимости от того где она определена. В динамической области видимости функция видит переменные в зависимости от того где она вызвана.
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
Результатом выполнения bar() в JavaScript будет 2, потому что вызываемая в ней функция foo() определена в глобальном контексте и переменную a она ищет там же. Переменной а, объявленной внутри функции bar() для нее не существует. В языках с динамической областью видимости результатом выполнения будет 3, потому что функция foo() вызывается после определения переменной a внутри функции bar() и пользуется именно этой "версией" переменной. В то же время поведение переменной this подчиняется правилам динамической области видимости (она получает разные ссылки в зависимости от места вызова), но об этом в другом разделе.
Область видимости определяется для каждого блока в процессе компиляции приложения, однако eval и with позволяют "обмануть" компилятор. eval может внести изменения в уже сложившуюся область видимости, если в ходе его выполнения, например, будет объявлена новая переменная, затеняющая одну из ранее объявленных. with же создает новую область видимости в контексте того объекта, для которого вызывается.
И with, и eval замедляют скорость выполнения скрипта, делая невозможным ряд оптимизаций на этапе компиляции. Лучше не использовать их вообще, тем более, что они запрещены в strict mode (разрешен только непрямой eval).
В JavaScript отсутствует блочная видимость, она будет введена только со стандартом языка ES6. Однако конструкция try/catch позволяет локализовать переменные:
try {
undefined()
} catch(err) {
console.log(err);
}
console.log(err); // > ReferenceError
Для объявления переменной, видимой только в пределах блока (это может быть просто { }, if() { }, for() { ], while() { }) используется конструкция
let variable = value;
В отличие от объявления через var, объявление через let происходит строго в том месте, где расположен let. Поэтому:
console.log(a); // > undefined
var a = 5;
потому что объявление var a отделяется от присваивания компилятором и переносится в начало области видимости (hoisting - подъем), тогда как
console.log(a); // > ReferenceError - переменная считается необъявленной.
let a = 5;
Для эмуляции поведения let в ES5 можно использовать такой код:
try{throw value}
catch(variable){
// use variable;
}
Если работа ведется с крупным объектом, то сборщик мусора гарантированно очистит память, удалив из нее объект, сразу после окончания блока catch. Во всех остальных случаях этот код несколько громоздок. Однако использование вместо него IFFE тоже имеет ряд недостатков: внутри нее меняется значение this, а также усложняется использование return, break и continue.
Каждая функция обладает своей собственной областью видимости (она включает в себя аргументы функции, объявленные внутри нее переменные и все внешние по отношению к ней области видимости). Существует несколько разных способов транспортировки внутренней функции за пределы ее лексической области видимости (обычно внутренняя функция может быть вызвана только в пределах той функции, непосредственно внутри которой она объявлена). При каждом таком способе создается замыкание, имеющее доступ к той области видимости, к которой обычно мы не можем получить доступ.
function outer() {
var a = 2;
function inner() {
console.log( a );
}
return inner;
}
var clousure = outer();
clousre(); // > 2
Мы объявили функцию inner() внутри функции outer() и получили способ ее вызова вне той области видимости, в которой она была объявлена. На момент вызова closure() функция outer() уже прекратила свою работу и исчезла ее внутренняя переменная a. Однако closure() имеет доступ как к переменным, объявленным внутри функции inner(), так и внутри функции outer().
По сути, outer() в качестве результата возвращает ссылку на inner(). Таким образом можно реализовать полиморфную функцию, которая будет возвращать некоторую другую функцию в зависимости от входных данных. При этом непосредственный доступ к методам и переменным внешней функции будет невозможен:
function polymorph(arg) {
function ones() { console.log('I like ones!'); }
function twos() { console.log('I like twos!'); }
if (arg === 1) return ones;
if (arg === 2) return twos;
return function() { console.log('I like noones and notwos') };
}
onef = polymorph(0); onef(); // > I like ones!
nof = polymorph(0); nof(); // > I like noones and notwos
"Проброс" к внутренней функции может быть косвенным:
var fn;
function outer1() {
var a = 2;
function inner() {
console.log( a ); // 2
}
fn = inner;
}
function outer2() {
fn();
}
outer1();
outer2(); // 2
outer2() получает доступ к функции inner() через внешнюю переменную fn, с которой транспортируется ссылка на внутреннюю функцию. При этом вместе с кодом функции inner() fn выносит за пределы outer() и всю область видимости outer() со всеми определенными в ней переменными.
Рассмотрим следующий код:
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log(i);
}, i*1000 );
}
Функция timer() вызывается спустя некоторое время, которое достаточно для того, чтобы цикл успел завершиться. Поэтому на момент выполнения даже первой итерации i уже равно 6 и вся конструкция выведет последовательность 6 6 6 6 6
Кроме того каждая итерация создаёт новый экземпляр функции timer(), однако область видимости у всех них одна и та же и используют они одно и то же значение переменной i.
Для создания отдельной области видимости превратим внутреннюю часть цикла в функциональное выражение:
for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log(i);
}, j*1000 );
)(i);
}
IIFE получает дополнительный параметр, т.к. без него мы получим только изолированную область видимости внутри замыкания, но она будет пуста. Передавая параметр (присваивание можно выполнять непосредственно временной переменной внутри IIFE) мы тем самым сохраняем текущее значение i на каждой итерации цикла. При этом у каждой итерации будет свое собственное значение j, которое и используется отложенной функцией при ее вызове. IIFE становится ячейкой, консервирующей нужное нам значение.
В стандарте ES6 можно использовать let вместо var и замыкания:
"use strict";
for (var i = 1; i<=5; i++) {
let j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
}
Циклы в ES6 имеют собственную область видимости, а переменная, объявленная с помощью let, уникальная в пределах этой области видимости.
По стандарту переменная, объявленная через let, должна сохранять свое значение в пределах каждой итерации цикла. Поэтому при полном соблюдении стандарта будет работать и такой вариант:
"use strict";
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
var x = 1, y = 3;
function a() {
var y = 2;
function b() {
function c() {
x = y;
}
}
}
Функции a, b, c имеют свои области видимости (с вложена в b, b вложена в а), кроме того существует глобальная область видимости (соответствующая объектам window или global).
Когда компилятор обнаруживает лексему y он последовательно проводит поиск в области видимости c, затем b, затем a, где и находит переменную. То же происходит с переменной x, но поиск завершается успехом в глобальной области видимости. Как только переменная найдена, поиск прекращается, что позволяет иметь переменные с одинаковыми именами на разных областях видимости. y, объявленная на глобальном уровне, будет "затенена" y, объявленной в области видимости a. Однако глобальные переменные доступны в любом месте программы как свойства глобального объекта.
В том случае, если RHS-поиск не увенчается успехом в глобальной области видимости, движок сгенерирует ошибку ReferenceError. LHS-поиск завершается в глобальной области видимости и если требуемое имя не находится и там, автоматически создается переменная с запрашиваемым именем. Однако в Strict mode поиск завершается ошибкой ReferenceError по аналогии с RHS.
TypeError генерируется в том случае, когда RHS-поиск завершился успешно, однако программа пытается выполнить действие, не предусмотренное для данного значения (например, вызывает как функцию переменную, не являющуюся ссылкой на функцию или пытается вызвать метод у переменной, содержащей значения undefined или null).
JavaScript использует лексическую область видимости, помимо этого существует динамическая область видимости, используемая, например, в языке сценариев Bash и некоторых режимах Perl. Лексическая область времени определяется на момент компиляции (выполнения), функция видит переменные в зависимости от того где она определена. В динамической области видимости функция видит переменные в зависимости от того где она вызвана.
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
Результатом выполнения bar() в JavaScript будет 2, потому что вызываемая в ней функция foo() определена в глобальном контексте и переменную a она ищет там же. Переменной а, объявленной внутри функции bar() для нее не существует. В языках с динамической областью видимости результатом выполнения будет 3, потому что функция foo() вызывается после определения переменной a внутри функции bar() и пользуется именно этой "версией" переменной. В то же время поведение переменной this подчиняется правилам динамической области видимости (она получает разные ссылки в зависимости от места вызова), но об этом в другом разделе.
Область видимости определяется для каждого блока в процессе компиляции приложения, однако eval и with позволяют "обмануть" компилятор. eval может внести изменения в уже сложившуюся область видимости, если в ходе его выполнения, например, будет объявлена новая переменная, затеняющая одну из ранее объявленных. with же создает новую область видимости в контексте того объекта, для которого вызывается.
И with, и eval замедляют скорость выполнения скрипта, делая невозможным ряд оптимизаций на этапе компиляции. Лучше не использовать их вообще, тем более, что они запрещены в strict mode (разрешен только непрямой eval).
В JavaScript отсутствует блочная видимость, она будет введена только со стандартом языка ES6. Однако конструкция try/catch позволяет локализовать переменные:
try {
undefined()
} catch(err) {
console.log(err);
}
console.log(err); // > ReferenceError
Для объявления переменной, видимой только в пределах блока (это может быть просто { }, if() { }, for() { ], while() { }) используется конструкция
let variable = value;
В отличие от объявления через var, объявление через let происходит строго в том месте, где расположен let. Поэтому:
console.log(a); // > undefined
var a = 5;
потому что объявление var a отделяется от присваивания компилятором и переносится в начало области видимости (hoisting - подъем), тогда как
console.log(a); // > ReferenceError - переменная считается необъявленной.
let a = 5;
Для эмуляции поведения let в ES5 можно использовать такой код:
try{throw value}
catch(variable){
// use variable;
}
Если работа ведется с крупным объектом, то сборщик мусора гарантированно очистит память, удалив из нее объект, сразу после окончания блока catch. Во всех остальных случаях этот код несколько громоздок. Однако использование вместо него IFFE тоже имеет ряд недостатков: внутри нее меняется значение this, а также усложняется использование return, break и continue.
Каждая функция обладает своей собственной областью видимости (она включает в себя аргументы функции, объявленные внутри нее переменные и все внешние по отношению к ней области видимости). Существует несколько разных способов транспортировки внутренней функции за пределы ее лексической области видимости (обычно внутренняя функция может быть вызвана только в пределах той функции, непосредственно внутри которой она объявлена). При каждом таком способе создается замыкание, имеющее доступ к той области видимости, к которой обычно мы не можем получить доступ.
function outer() {
var a = 2;
function inner() {
console.log( a );
}
return inner;
}
var clousure = outer();
clousre(); // > 2
Мы объявили функцию inner() внутри функции outer() и получили способ ее вызова вне той области видимости, в которой она была объявлена. На момент вызова closure() функция outer() уже прекратила свою работу и исчезла ее внутренняя переменная a. Однако closure() имеет доступ как к переменным, объявленным внутри функции inner(), так и внутри функции outer().
По сути, outer() в качестве результата возвращает ссылку на inner(). Таким образом можно реализовать полиморфную функцию, которая будет возвращать некоторую другую функцию в зависимости от входных данных. При этом непосредственный доступ к методам и переменным внешней функции будет невозможен:
function polymorph(arg) {
function ones() { console.log('I like ones!'); }
function twos() { console.log('I like twos!'); }
if (arg === 1) return ones;
if (arg === 2) return twos;
return function() { console.log('I like noones and notwos') };
}
onef = polymorph(0); onef(); // > I like ones!
nof = polymorph(0); nof(); // > I like noones and notwos
"Проброс" к внутренней функции может быть косвенным:
var fn;
function outer1() {
var a = 2;
function inner() {
console.log( a ); // 2
}
fn = inner;
}
function outer2() {
fn();
}
outer1();
outer2(); // 2
outer2() получает доступ к функции inner() через внешнюю переменную fn, с которой транспортируется ссылка на внутреннюю функцию. При этом вместе с кодом функции inner() fn выносит за пределы outer() и всю область видимости outer() со всеми определенными в ней переменными.
Рассмотрим следующий код:
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log(i);
}, i*1000 );
}
Функция timer() вызывается спустя некоторое время, которое достаточно для того, чтобы цикл успел завершиться. Поэтому на момент выполнения даже первой итерации i уже равно 6 и вся конструкция выведет последовательность 6 6 6 6 6
Кроме того каждая итерация создаёт новый экземпляр функции timer(), однако область видимости у всех них одна и та же и используют они одно и то же значение переменной i.
Для создания отдельной области видимости превратим внутреннюю часть цикла в функциональное выражение:
for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log(i);
}, j*1000 );
)(i);
}
IIFE получает дополнительный параметр, т.к. без него мы получим только изолированную область видимости внутри замыкания, но она будет пуста. Передавая параметр (присваивание можно выполнять непосредственно временной переменной внутри IIFE) мы тем самым сохраняем текущее значение i на каждой итерации цикла. При этом у каждой итерации будет свое собственное значение j, которое и используется отложенной функцией при ее вызове. IIFE становится ячейкой, консервирующей нужное нам значение.
В стандарте ES6 можно использовать let вместо var и замыкания:
"use strict";
for (var i = 1; i<=5; i++) {
let j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
}
Циклы в ES6 имеют собственную область видимости, а переменная, объявленная с помощью let, уникальная в пределах этой области видимости.
По стандарту переменная, объявленная через let, должна сохранять свое значение в пределах каждой итерации цикла. Поэтому при полном соблюдении стандарта будет работать и такой вариант:
"use strict";
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
Комментариев нет :
Отправить комментарий