Анализ области действия и замыканий фразы «Вы не знаете JavaScript»

внешний интерфейс JavaScript

Обновление: Спасибо за вашу поддержку. Недавно я бросил сводку данных для всех, чтобы прочитать систему. В будущем будет больше контента и больше оптимизации.Нажмите здесь, чтобы просмотреть

------ Далее идет текст ------

Глава 1. Что такое объем

  • Вопрос 1: Где хранятся переменные?
  • Вопрос 2: Как программы находят их, когда они им нужны?

1.1 Принцип составления

Язык JavaScript является «динамическим» или «интерпретируемым языком выполнения», но на самом деле является компилируемым языком. Но он не компилируется заранее, и скомпилированные результаты нельзя переносить между распределенными системами.

В традиционном скомпилированном языковом процессе программа проходит три этапа перед выполнением, которые в совокупности называются «компиляцией».

  • Токенизация/лексинг

    Разбивает строку символов на осмысленные (для языка программирования) фрагменты кода.

    var a = 2;
    

    Приведенная выше программа будет разложена на следующие лексические единицы: var, a, =, 2, ;.

    Рассматриваются ли пробелы как лексические единицы, зависит от того, значимы ли пробелы в языке.

  • Парсинг/Парсинг (Парсинг)

    Преобразует поток токенов (массив) в ряд элементов, вложенных по одному уровню за раз и представляющих грамматическую структуру программы. Этот номер называется抽象语法树(Абстрактное синтаксическое дерево, AST).

    var a = 2;
    

    Абстрактное синтаксическое дерево приведенного выше кода выглядит так:

    • Верхний узел VariableDeclarative
      • Идентификатор дочернего узла, значение
      • Дочерний узел AssignmentExpression
        • Дочерний узел NumericLiteral, слово равно 2

  • генерация кода

    БудуASTПроцесс преобразования в исполняемый код. Процесс зависит от языка, целевой платформы и т. д.

    Проще говоря, можноvar a = 2;AST преобразуется в набор машинных инструкций. Используется для создания переменной с именем a (включая выделение памяти и т. д.) и сохранения значения в a.

1.2 Понимание масштаба

1.2.1 Список актеров
  • Движок: отвечает за компиляцию и выполнение всей программы JavaScript от начала до конца.
  • Компилятор: отвечает за синтаксический анализ, генерацию кода и т. д.
  • Область применения: Отвечает за сбор и поддержку набора запросов, состоящего из всех объявленных идентификаторов (переменных, функций) и за соблюдение очень строгого набора правил, определяющих права доступа к этим идентификаторам исполняемым в данный момент кодом.
1.2.2 Диалог

var a = 2;Есть 2 разные декларации.

  • 1. Компилятор обрабатывает во время компиляции (var a): объявляет переменную в текущей области (если она не была объявлена ​​ранее).

    st=>start: Start
    e=>end: End
    op1=>operation: 分解成词法单元
    op2=>operation: 解析成树结构AST
    cond=>condition: 当前作用域存在变量a?
    op3=>operation: 忽略此声明,继续编译
    op4=>operation: 在当前作用域集合中声明新变量a
    op5=>operation: 生成代码
    st->op1->op2->cond
    cond(yes)->op3->op5->e
    cond(no)->op4->op5->e
    
  • 2. Движок обрабатывает во время выполнения (a = 2): Найдите переменную в области видимости и присвойте ей значение, если она найдена.

st=>start: Start
e=>end: End
cond=>condition: 当前作用域存在变量a?
cond2=>condition: 全局作用域?
op1=>operation: 引擎使用这个变量a
op2=>operation: 引擎向上一级作用域查找变量a
op3=>operation: 引擎把2赋值给变量a
op4=>operation: 举手示意,抛出异常
st->cond
cond(yes)->op1->op3->e
cond(no)->cond2(no)->op2(right)->cond
cond2(yes)->op4->e
1.2.3 Запрос LHS и RHS

Lа такжеRПредставляет левую и правую части присваивания соответственно, когда переменная появляется в левой части присваивания.LHSзапрос, который появляется в ** операции присваивания非左侧**RHSЗапрос.

  • Запрос LHS (левая сторона): найти контейнер самой переменной, затем присвоить ему значение
  • Запрос RHS (не левая сторона): найти значение переменной, которое можно понимать какretrieve his source valueТо есть получить его исходное значение
function foo(a) {
    console.log( a ); // 2
}

foo(2);

В приведенном выше коде есть 1 запрос LHS и 3 запроса RHS.

  • LHS-запросы:

    • скрытыйa = 2в, в2передается в качестве параметраfoo(…)функция, параметрaСделать запрос LHS
  • Запросы RHS:

    • последняя строкаfoo(...)Для вызова функции требуется запрос RHS на foo

    • console.log( a );средняя параaСделать запрос RHS

    • console.log(...)сам по себе правconsoleобъект для запроса RHS

1.3 Вложение области видимости

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

1.4 Исключения

ReferenceErrorсвязанные с неспособностью различения масштабов,TypeErrorУказывает, что определение области выполнено успешно, но операция над результатом недопустима или необоснованна.

  • Запрос RHS не может найти требуемую переменную в цепочке областей видимости, движок выдаетReferenceErrorаномальный.
  • В нестрогом режиме запрос LHS не может найти требуемую переменную в цепочке областей видимости, а переменная с таким именем создается в глобальной области видимости и возвращается в движок.
  • В строгом режиме (начиная с ES5 автоматическое или неявное создание глобальных переменных запрещено) ошибка LHS-запроса вызоветReferenceErrorаномальный
  • В случае успешного запроса RHS движок вызовет необоснованную операцию над переменной.TypeErrorаномальный. (например, вызов функции для значения нефункционального типа или ссылка на свойство для значения типа null или undefined)

1.5 Резюме

var a = 2разбивается на 2 отдельных этапа.

  • 1,var aобъявить новую переменную в своей области
  • 2,a = 2Будет ли LHS запрашивать a, а затем присваивать ему значение

Глава 2. Лексическая область действия

2.1 Лексический этап

Лексическая область — это область, определенная на лексической фазе, которая определяется тем, где записываются области видимости переменных и блоков при написании кода, поэтому область видимости остается неизменной, когда лексический анализатор обрабатывает код. (без учета обмана лексической области видимости)

2.1.1 Найти
  • Поиск области останавливается, когда найден первый соответствующий идентификатор.

  • Эффект затенения: идентификаторы с одним и тем же именем могут быть определены в нескольких вложенных областях, и внутренний идентификатор будет «затенять» внешний идентификатор.

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

    window.a
    
  • Лексическая область видимости определяется только тем, где объявлена ​​функция.

  • Поиск с лексической областью действия будет искать только идентификаторы первого уровня, такие как a, b, c. дляfoo.bar.baz, лексическая область будет искать толькоfooидентификатор, после нахождения,Правила доступа к свойствам объектавозьмет на себяbarа такжеbazдоступ к собственности.

2.2 Лексический обман

Подмена лексической области видимости может привести к снижению производительности. Следующие два методаНе рекомендуется

2.2.1 eval

eval(..)Функции могут взять строку в качестве аргумента и обрабатывать содержимое его в качестве кода, который, по-видимому, существует в этой точке в программе на момент написания.

function foo (str, a) {
    eval( str ); // 欺骗!
    console.log( a, b );
}

var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

eval('var b = 3')будет рассматриваться как если бы он был там.

  • В нестрогом режиме, еслиeval(..)Код, выполняемый в содержит одно или несколько объявлений, которые изменяют лексическую область во время записи во время выполнения. В приведенном выше кодеfoo(..)Переменная b создается внутри и скрывает переменную с тем же именем во внешней области.
  • В строгом режиме,eval(..)Имеет собственную лексическую область во время выполнения, в которой объявления не могут изменить область.
function foo (str) {
    "use strict"; 
    eval( str ); 
    console.log( a ); // ReferenceError: a is not defined
}

foo( "var a = 2;" ); 
  • setTimeout(..)а такжеsetInterval(..)Первым параметром может быть строка, которая будет интерпретироваться как динамически сгенерированный код функции.Устарело, не используйте
  • new Function(..) Последний параметр может принимать строку кода (предыдущие параметры являются формальными параметрами только что сгенерированной функции).избегать использования
2.2.2 with

withЧасто используется как ярлык для многократного обращения к нескольким свойствам одного и того же объекта.Нет необходимости многократно обращаться к самому объекту.

var obj = {
    a: 1,
    b: 2,
    c: 3
};

// 单调乏味的重复“obj”
obj.a = 2;
obj.b = 3;
obj.c = 4;

// 简单的快捷方式
with (obj) {
	a = 3;
    b = 4;
    c = 5;
}

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

Обычные объявления var внутри этого блока не ограничиваются областью действия этого блока, но добавляются в область действия функции, в которой находится with.

function foo(obj) {
    with (obj) {
        a = 2;
    }
}

var o1 = {
    a: 3
};

var o2 = {
    b : 3
}

foo( o1 );
console.log( o1.a ); // 2

foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2 -- 不好,a被泄露到全局作用域上了!

В приведенном выше примере созданo1а такжеo2два объекта. у одного из нихaсобственности, другой нет. существуетwith(obj){..}Внутри находится ссылка LHS и присваиваем ей 2.

  • o1После передачи область действия оператора witho1,a = 2Операция присваивания находитo1.aи присвойте ему 2.
  • o2После прохождения область действияo2не вaатрибут, поэтому выполняется обычный поиск идентификатора LHS, область действия o2,foo(..)Ни область, ни глобальная область не находят идентификатор a, поэтому, когдаa = 2При выполнении автоматически создает глобальную переменную (нестрогий режим), поэтомуo2.aостаются неопределенными.
2.2.3 Производительность
  • Механизм JavaScript выполняет несколько оптимизаций производительности на этапе компиляции. Некоторые из этих оптимизаций основаны на возможности выполнять статический анализ на основе лексического кода кода и заранее определять, где определены все переменные и функции, чтобы идентификаторы можно было быстро найти во время выполнения. .
  • двигатель найден в кодеeval(..)илиwith, это может быть только простоПредположениеВсе суждения о положении идентификатора неверны. Потому что это не может быть явно известно на этапе лексического анализаeval(..)Какой код будет получен, как этот код изменит область действия, и нет никакого способа узнать, что передатьwithЧто именно представляет собой содержимое объекта, используемого для создания лексической области видимости.
  • В пессимистической ситуации, еслиeval(..)или с, все оптимизациивозможноЭто бессмысленно, самый простой способ -не делай этого вообщелюбые оптимизации. Код должен работать очень медленно.

2.3 Резюме

Лексическая область видимости означает, что область видимости определяется расположением объявления функции при написании кода.

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

Есть два механизма «обмануть» лексическую область видимости:

  • eval(..): выполнить исчисление над строкой «кода», содержащей одно или несколько объявлений, тем самым изменив существующую лексическую область видимости (Время выполнения).
  • with: ссылка на объекттак какобласть, рассматривает свойства объекта как идентификаторы в области, создает новую лексическую область (Время выполнения).

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

Глава 3 Область действия функции и область действия блока

3.1 Объем функций

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

function foo(a) {
    var b = 2;
    
    // 一些代码
    
    function bar() {
        // ...
    }
    
    // 更多的代码
    
    var c = 3;
}

foo(..)Область содержит идентификаторы (переменные, функции) a, b, c и bar. Везде, где в области действия появляется объявление идентификатора, переменная или функция, представленная идентификатором, будет присоединена к области, в которой она находится.

Глобальная область содержит только один идентификатор:foo.

3.2 Скрыть внутреннюю реализацию

Принцип наименьших привилегий (принцип наименьшей авторизации или наименьшего раскрытия): при разработке программного обеспечения необходимый контент должен быть открыт до минимума, а другой контент должен быть «скрытым», например, дизайн API модуля или объекта.

function doSomething(a) {
    function doSomethingElse(a) {
        return a - 1;
    }
    
    var b;
    
    b = a + doSomethingElse( a * 2 );
    
    console.log( b * 3 );
}

doSomething( 2 ); // 15

bа такжеdoSomethingElse(..)доступ извне невозможен, а толькоdoSomething(..)Контролируемое дизайном, конкретное содержание приватизировано.

3.2.1 Избегание конфликтов

Еще одним преимуществом «скрытия» переменных и функций в области видимости является избежание конфликтов между идентификаторами с одинаковыми именами.

function foo() {
    function bar(a) {
        i = 3; // 修改for循环所属作用域中的i
        console.log( a + i );
    }
    
    for (var i = 0; i < 10; i++) {
        bar( i * 2 ); // 糟糕,无限循环了!
    }
}
foo();

bar(..)внутреннее выражение присваиванияi = 3случайно переопределенное объявление вfoo(..)я во внутреннем цикле for.

решение:

  • Объявите локальную переменную с любым именем, например.var i = 3.
  • взять совершенно другое имя идентификатора, например.var j = 3.

Типичный пример предотвращения конфликтов переменных:

  • глобальное пространство имен

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

  • Управление модулями

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

3.3 Объем функций

var a = 2;

function foo() { // <-- 添加这一行
    
    var a = 3;
    console.log( a ); // 3
    
} // <-- 以及这一行
foo(); // <-- 以及这一行

console.log( a ); // 2

Хотя указанная выше область действия функции может «скрывать» внутренние переменные и определения функций, это вызовет следующие две дополнительные проблемы.

  • Именованная функция должна быть объявленаfoo(),иметь в видуfooСамо имя «загрязняет» область, в которой оно находится.
  • должен явно передавать имя функцииfoo()Вызовите эту функцию, чтобы запустить в ней код.

решение:

var a = 2;

(function foo(){ // <-- 添加这一行
    
    var a = 3;
    console.log( a ); // 3
    
})(); // <-- 以及这一行

console.log( a ); // 2

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

  • различатьобъявление функцииа такжефункциональное выражениеСамый простой способ сделать это — посмотреть, где в объявлении появляется ключевое слово function (не только в строке кода, но и во всем объявлении).
    • Объявление функции:functionЭто первое слово в утверждении
    • Выражение функции:нетпервое слово в утверждении
  • объявление функцииа такжефункциональное выражениеСамое важное различие между ними заключается в том, где будут связаны их идентификаторы имен.
    • В первом фрагментеfooограничен в своей области и может быть непосредственно передан черезfoo()назвать это.
    • Во втором фрагментеfooсвязан в функции самого функционального выражения, а не в его области.(function foo(){ .. }серединаfooтолько в..Доступ осуществляется к представленному местоположению, а не к внешней области.fooСкрытое внутри себя имя переменной означает, что внешняя область видимости не загрязняется без необходимости.
3.3.1 Анонимность и анонимность
setTimeout( function() {
    console.log("I wait 1 second!");
}, 1000 );

Вышеупомянутоевыражение анонимной функции,потому чтоfunction()..Идентификатора имени нет.

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

Выражения анонимных функций имеют следующие недостатки:

  • Трассировка стека не показывает осмысленных имен функций, что затрудняет отладку.
  • Имя функции отсутствует, когда функция должна ссылаться на себя, она может использовать только ужеИстекшийизarguments.calleeЦитировать
    • рекурсия
    • После запуска события прослушиватель событий должен отвязать себя.
  • Анонимные функции опускают имена функций, которые важны для удобочитаемости/понятности кода.

решение:

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

setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
    console.log( "I waited 1 second!" );
}, 1000 );

3.3.2 Немедленное выполнение функциональных выражений

Выражение немедленно вызываемой функции (IIFE)

  • Выражения анонимных/именованных функций

    Первый ( ) превращает функцию в выражение, а второй ( ) выполняет функцию

    var a = 2;
    (function IIFE() {
        
        var a = 3;
        console.log( a ); // 3
        
    })();
    
    console.log( a ); // 2
    
    
  • Улучшен(function(){ .. }())

    ( ) для вызова был перемещен в ( ) для переноса.

  • Как вызов функции и передача параметров в

    var a = 2;
    (function IIFE( global ) {
        
        var a = 3;
        console.log( a ); // 3
        console.log( global.a ); // 2
        
    })( window );
    
    console.log( a ); // 2
    
    
  • решитьundefinedИсключение, вызванное ошибочной перезаписью значения идентификатора по умолчанию.

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

    undefined = true;
    
    (function IIFE( undefined ) {
        
        var a;
        if (a === undefined) {
            console.log("Undefined is safe here!");
        }
    })();
    
    
  • Инвертируйте порядок выполнения кода, поставьте функцию, которую необходимо запустить, на второе место и выполните ее в IIFE.Позжепередается как параметр

    функциональное выражениеdefОпределяется во второй части фрагмента, а затем передается как параметр (этот параметр также называется def) в первой части определения функции IIFE. Наконец, вызывается параметр def (то есть переданная функция), передавая window в качестве значения глобального параметра.

    var a = 2;
    
    (function IIFE( def ) {
        def( window );
    })(function def( global ) {
       
        var a = 3;
        console.log( a ); // 3
        console.log( global.a ); // 2
        
    });
    
    

3.4 Область действия блока

На первый взгляд кажется, что JavaScript не имеет функциональности области видимости блока, если только он не понят лучше (с, try/catch, let, const).

for (var i = 0; i < 10; i++) {
    console.log( i );
}

в приведенном выше кодеiбудет привязан во внешней области (функция или глобальная).

var foo = true;

if (foo) {
    var bar = foo * 2;
    bar = something( bar );
    console.log( bar );
}

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

3.4.1 with

Форма области блока, сwithОбласти, созданные из объектов, только в **with** действителен в объявлении, а не во внешней области.

3.4.2 try/catch

Предложение catch для try/catch в спецификации ES3 создает область блока, в которой объявленная переменная действительна только в пределах catch.

try {
    undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
    console.log( err ); // 能够正常执行!
}

console.log( err ); // ReferenceError: err not found

Многие инструменты статической проверки по-прежнему предупреждают, когда два или более предложения catch в одной и той же области видимости объявляют неправильную переменную с одним и тем же именем идентификатора.На самом деле это не дублирующее определение, потому что все переменные надежно ограничены областью блока.

3.4.3 let

Представлен ES6letключевое слово, которое связывает переменную с любой областью, в которой она находится (обычно{ .. }внутренний), т.е.letпеременные, объявленные для него неявноугонятьобласть блока, в которой он находится.

var foo = true;

if (foo) {
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
}

console.log( bar ); // ReferenceError

существующие проблемы

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

решение

Создавайте блоки явно для области блока. Явный код лучше, чем неявный или какой-то тонкий, но неясный код.

var foo = true;

if (foo) {
    { // <-- 显式的块
        let bar = foo * 2;
        bar = something( bar );
        console.log( bar );
    }
}

console.log( bar ); // ReferenceError

Блок явно создается внутри оператора if, и если его необходимо реорганизовать, весь блок можно удобно переместить без какого-либо влияния на позицию и семантику внешнего оператора if.

  • Объявления, сделанные в let, не поднимаются в область блока

    console.log( bar ); // ReferenceError
    let bar = 2;
    
    
  • 1. Сбор мусора

    function process(data) {
        // 在这里做点有趣的事情
    }
    
    var someReallyBigData = { .. };
    
    process( someReallyBigData );
    
    var btn = document.getElementById( "my_button" );
    
    btn.addEventListener( "click", function click(evt) {
        console.log("button clicked");
    }, /*capturingPhase*/false );
    
    

    clickобратный вызов функции иненужныйsomeReallyBigData. теоретически когдаprocess(..)После выполнения структуры данных, которые занимают много места в памяти, могут быть удалены сборщиком мусора. Однако из-заclickФункция формирует замыкание, охватывающее всю область видимости, и движок JS, скорее всего, сохранит эту структуру (в зависимости от реализации).

  • 2. пусть петля

    for (let i = 0; i < 10; i++) {
        console.log( i );
    }
    
    console.log( i ); // ReferenceError
    
    

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

    {
        let j;
        for (j = 0; j < 10; j++) {
            let i = j; // 每个迭代重新绑定!
            console.log( i ); 
       	} 
    }
    
    
3.4.4 const

ссылки ES6const, переменные блочной области можно создавать, но их значения фиксированы (постоянны)

var foo = true;

if(foo) {
    var a = 2;
    const b = 3; // 包含在if中的块作用域常量
    
    a = 3; // 正常!
    b = 4; // 错误!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

Глава 4 Вознесение

  • Любая переменная, объявленная в области, будет присоединена к этой области.
  • Все объявления, включая переменные и функции, обрабатываются перед выполнением любого кода.
  • var a = 2;будет рассматриваться как две декларации,var a;а такжеa = 2;, первое утверждение вэтап компиляциипродолжить, второй оператор присваивания останется ждатьэтап выполнения.
  • Все объявления (переменные и функции) будут "перемещены"на вершину соответствующих областей, процесс, называемыйпродвигать**
  • Поднимается только само объявление, а не присваивание или другая логика выполнения, включая функциональные выражения.
a = 2;

var a;

console.log( a ); // 2

---------------------------------------
// 实际按如下形式进行处理
var a; // 编译阶段

a = 2; // 执行阶段

console.log( a ); // 2

console.log( a ); // undefinde

var a = 2;

---------------------------------------
// 实际按如下形式进行处理
var a; // 编译

console.log( a ); // undefinde

a = 2; // 执行

  • Переменный подъем на область действия
function foo() {
    var a;
    
    console.log( a ); // undefinde
    
    a = 2;
}

foo();

  • объявления функций поднимаются, ноФункциональные выражения не поднимаются.
foo(); // 不是ReferenceError,而是TypeError!

var foo = function bar() {
    // ...
};

В приведенной выше программе идентификатор переменнойfoo()поднимается и назначается охватывающей области, поэтомуfoo()Не вызывает ReferenceError. В настоящее времяfooне назначает(Если это объявление функции, а не функциональное выражение, то присваивание будет),foo()из-заundefinedЗначение вызова функции приводит к недопустимой операции, поэтому выдаетTypeErrorаномальный.

  • Даже для именованного функционального выражения идентификатор имени нельзя использовать в своей собственной области видимости, пока он не будет назначен.
foo(); // TypeError
bar(); // ReferenceError

var foo = function bar() {
    // ...
};

---------------------------------------
// 实际按如下形式进行处理
var foo;

foo(); // TypeError
bar(); // ReferenceError

foo = function() {
    var bar = ...self...
    // ...
};

4.1 Функция в первую очередь

  • Однако поднимаются как объявления функций, так и объявления переменных.Сначала поднимаются функции, затем переменные
foo(); // 1

var foo;

function foo() {
    console.log( 1 ); 
};

foo = function() {
    console.log( 2 ); 
};

---------------------------------------
// 实际按如下形式进行处理

function foo() { // 函数提升是整体提升,声明 + 赋值
    console.log( 1 ); 
};

foo(); // 1

foo = function() {
    console.log( 2 ); 
};

  • var fooнесмотря на появление вfunction foo()..., но это повторяющееся объявление, а объявление функции поднимается перед обычной переменной, поэтому оно игнорируется.
  • Объявление функции, которое появляется позже, может бытьпокрытиепередний.
foo(); // 3

function foo() {
    console.log( 1 ); 
};

var foo = function() {
    console.log( 2 ); 
};

function foo() {
    console.log( 3 ); 
};

  • Объявления функций внутри обычного блока обычно поднимаются наверх своей области видимости и не контролируются условными операторами.Старайтесь избегать объявления функций внутри обычных блоков..
foo(); // "b"

var a = true;
if (a) {
    function foo() { console.log( "a" ); };
}
else {
    function foo() { console.log( "b" ); };
}

Глава 5 Закрытие области действия

5.1 Закрытие

  • Закрытие создается, когда функция может запомнить и получить доступ к лексической области, в которой она находится, даже если имя функции выполняется за пределами текущей лексической области.
function foo() {
    var a = 2;
    
    function bar() {
		console.log( a );
    }
    
    return bar;
}

var baz = foo();

baz(); // 2 ---- 这就是闭包的效果

bar()Выполняется за пределами лексического объема, определенного им самим.

bar()иметь покрытиеfoo()Закрытие внутренней области, так что область всегда может выжить дляbar()Ссылки, сделанные в любое время после этого, не будут собираться сборщиком мусора.

  • bar()Подождиfoo()Ссылки на внутренние объемы, эта ссылка называется закрытыми сумками.
// 对函数类型的值进行传递
function foo() {
    var a = 2;
    
    function baz() {
		console.log( a ); // 2
    }
    
    bar( baz );
}

function bar(fn) {
    fn(); // 这就是闭包
}

foo();

  • поставить внутреннюю функциюbazПерейти кbar, при вызове этой внутренней функции (теперь называемойfn), который охватываетfoo()Закрытие внутренней области формируется, поскольку она имеет доступ к файлу .
// 间接的传递函数
var fn;

function foo() {
    var a = 2;
    
    function baz() {
		console.log( a ); 
    }
    
    fn = baz; // 将baz分配给全局变量
}

function bar() {
    fn(); // 这就是闭包
}

foo();
bar(); // 2

  • Передача внутренней функции за пределы лексической области, в которой она находится, будет содержать ссылку на исходную область определения, и замыкание будет использоваться везде, где выполняется функция.
function wait(message) {
    
    setTimeout( function timer() {
        console.log( message );
    }, 1000 );
}

wait( "Hello, closure!" );

  • Внутри движка встроенные служебные функцииsetTimeout(..)Содержит ссылку на параметр, называемый здесь таймером, который будет вызывать механизм, в то время как лексическая область видимости остается нетронутой в процессе.это закрытие
  • Таймеры, прослушиватели событий, запросы Ajax, межоконные коммуникации, веб-воркеры или любые другие асинхронные (или синхронные) задачи, если вы используетеПерезвоните, который на самом деле используетЗакрытие!
// 典型的闭包例子:IIFE
var a = 2;

(function IIFE() {
    console.log( a );
})();

5.2 Циклы и замыкания

for (var i = 1; i <= 5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i * 1000 );
}

//输入五次6

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

Попытка Вариант 1: Используйте IIFE, чтобы добавить больше области закрытия

for (var i = 1; i <= 5; i++) {
    (function() {
        setTimeout( function timer() {
        	console.log( i );
    	}, i * 1000 );
    })();
}

//失败,因为IIFE作用域是空的,需要包含一点实质内容才可以使用

Попробуйте сценарий 2: увеличение переменной IIFE

for (var i = 1; i <= 5; i++) {
    (function() {
        var j = i;
        setTimeout( function timer() {
        	console.log( j );
    	}, j * 1000 );
    })();
}

// 正常工作

Попытка 3: Улучшено, будетiПередано как параметр функции IIFE

for (var i = 1; i <= 5; i++) {
    (function(j) {
        setTimeout( function timer() {
        	console.log( j );
    	}, j * 1000 );
    })( i );
}

// 正常工作

5.2.1 Область действия блока и замыкания
  • letМожет использоваться для захвата области блока и объявления переменной в этой области блока.
  • По сути, это превращает блок в область действия, которую можно закрыть.
for (var i = 1; i <= 5; i++) {
    let j = i; // 闭包的块作用域!
    setTimeout( function timer() {
        console.log( j );
    }, j * 1000 );
}

// 正常工作

  • forголовка петлиletОбъявления имеют особое поведение. Переменная объявляется более одного раза в цикле,каждая итерацияобъявим. Каждая последующая итерация инициализирует эту переменную со значением в конце предыдущей итерации.

Вышеприведенное предложение относится к циклу 3.4.3----2.let, то есть к следующему

{
    let j;
    for (j = 0; j < 10; j++) {
        let i = j; // 每个迭代重新绑定!
        console.log( i ); 
   	} 
}

Улучшения цикла:

for (let i = 1; i <= 5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i * 1000 );
}

// 正常工作

5.3 Модули

Модульный режим требует двух предварительных условий:

  • Должна быть внешняя объемлющая функция, которая должна по крайней мереназывается один раз(Каждый вызов создает новый экземпляр модуля, который может реализовать одноэлементный шаблон через IIFE)
  • Охватывающая функция должнавернуть хотя бы одну внутреннюю функцию, чтобы внутренняя функция могла формировать замыкание в приватной области и могла получать доступ к приватному состоянию или изменять его.
function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    
    function doSomething() {
        console.log( something );
    }
    
    function doAnother() {
        console.log( another.join( " ! ") );
    }
    
    return {
        doSomething: doSomething,
        doAnother: doAnother
    }
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

// 1、必须通过调用CoolModule()来创建一个模块实例
// 2、CoolModule()返回一个对象字面量语法{ key: value, ... }表示的对象,对象中含有对内部函数而不是内部数据变量的引用。内部数据变量保持隐藏且私有的状态。

  • Одноэлементный шаблон с использованием IIFE

немедленноВызовите эту функцию и присвойте возвращаемое значение непосредственно идентификатору модуля синглтона foo.

var foo = (function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    
    function doSomething() {
        console.log( something );
    }
    
    function doAnother() {
        console.log( another.join( " ! ") );
    }
    
    return {
        doSomething: doSomething,
        doAnother: doAnother
    }
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

5.5.1 Механизмы современных модулей

Большинство загрузчиков/менеджеров зависимостей модулей по существу инкапсулируют это определение модуля в дружественный API.

var MyModules = (function Manager() {
    var modules = {};
    
    function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++ ) {
			deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps ); // 核心,为了模块的定义引用了包装函数(可以传入任何依赖),并且将返回值(模块的API),储存在一个根据名字来管理的模块列表中。
    }
    
    function get(name) {
        return modules[name];
    }
    
    return {
        define: define,
        get: get
    };
    
})();

Используйте приведенную выше функцию для определения модуля:

MyModules.define( "bar", [], function() {
    function hello(who) {
        return "Let me introduct: " + who;
    }
    
    return {
        hello: hello
    };
} );

MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    
    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }
    
    return {
        awesome: awesome
    };
} );

var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );

console.log(
	bar.hello( "hippo" );
) // Let me introduct: hippo

foo.awesome(); // LET ME INTRODUCT: HIPPO

5.5.2 Механизмы будущих модулей

ES6 обрабатывает файлы как отдельные модули при загрузке через модульную систему. Каждый модуль может импортировать другие модули или определенные члены API, а также может экспортировать свои собственные члены API.

Модули ES6 не имеют встроенного формата и должны быть определены в отдельных файлах (один файл на модуль).

  • Модули на основе функций не распознаются статически (не распознаются компилятором), семантика API учитывается только во время выполнения, поэтому можно изменить API модуля во время выполнения.
  • API модуля ES6 является статическим (модуль API не меняется во время выполнения), который будет иметь истинное присутствие ссылки на член API модуля импорта в период компиляции.
// bar.js

function hello(who) {
    return "Let me introduct: " + who;
}

export hello;


// foo.js
// 仅从“bar”模块导入hello()
import hello from "bar";

var hungry = "hippo";

function awesome() {
    console.log(
    	hello( hungry ).toUpperCase();
    );
}

export awesome;

// baz.js
// 导入完整的“foo”和”bar“模块
module foo from "foo";
module bar from "bar";

console.log(
	bar.hello( "rhino")
); // Let me introduct: rhino

foo.awesome(); // LET ME INTRODUCT: HIPPO

  • import: Импортируйте один или несколько API в модуле в текущую область и привяжите их к переменной соответственно.
  • module: Импортируйте и привяжите API всего модуля к переменной.
  • export: Экспорт идентификатора (переменной, функции) текущего модуля в качестве общедоступного API.

Приложение А. Динамическая область

  • Лексическая область определяется при написании кода или определения с упором на функции.где объявитьЦепочка охвата зависит от вложенности кода.
  • Динамическая область определяется во время выполнения (это тоже), сосредоточьтесь на функцииоткуда звонитьцепочка областей действия основана на стеке вызовов.
  • JavaScript делаетне имеетДинамический объем, он имеет только лексический объем. ноthisМеханизм чем-то похож на динамическую область видимости.
// 词法作用域,关注函数在何处声明,a通过RHS引用到了全局作用域中的a
function foo() {
    console.log( a ); // 2
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();

-----------------------------
// 动态作用域,关注函数从何处调用,当foo()无法找到a的变量引用时,会顺着调用栈在调用foo()的地方查找a
function foo() {
    console.log( a ); // 3(不是2!)
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;
bar();

Приложение B. Альтернативы блочной области видимости

Начиная с ES3, JavaScript имеет блочную область видимости, включая предложения with и catch.

// ES6环境
{
    let a = 2;
    console.log( a ); // 2
}

console.log( a ); // ReferenceError

Приведенный выше код отлично работает в среде ES6, но как добиться этого в среде до ES6?

ответИспользуйте предложения catch, что является предпочтительным способом переноса большинства функций в ES6.

try {
    throw 2;
} catch (a) {
    console.log( a ); // 2
}

console.log( a ); // ReferenceError

B.1 Traceur

// 代码转换成如下形式
{
    try {
        throw undefined;
    } catch (a) {
        a = 2;
        console.log( a ); // 2
    }
}

console.log( a ); // ReferenceError

B.2 Неявная и явная область применения

letОбъявление создает явную область действия и привязывается к ней, а не неявно захватывает существующую область (сравните предыдущийletопределение).

let (a = 2) {
    console.log( a ); // 2
}

console.log( a ); // ReferenceError

Существующие проблемы:

letобъявления не включены в ES6, и компилятор Traceur не принимает такой код

  • Вариант первый: использовать легальную грамматику ES6 и пойти на компромисс в спецификациях кода
/*let*/ { let a = 2;
    console.log( a );
}

console.log( a ); // ReferenceError

  • Решение 2. С помощью инструмента Let-ER сгенерируйте полный стандартный код ES6, не сгенерируйте ES3-альтернативу HACK через Try/Catch
{
    let a = 2;
    console.log( a );
}

console.log( a ); // ReferenceError

Б.3 Производительность

  • Производительность try/catch действительно плохая, но нет технической причины, по которой try/catch должен быть таким медленным или что он должен быть медленным все время.
  • IIFE и try/catch не полностью эквивалентны, потому что если какую-либо часть кода вынуть и обернуть функцией, это изменит смысл этого кода, и все это, return, break и continue изменятся. IIFE не является универсальным решением и подходит только для ручного управления в определенных ситуациях.