Глубокое понимание контекста выполнения JavaScript и стека выполнения

JavaScript

предисловие

Если вы разработчик JavaScript или хотите им стать, то вы должны знать внутренний механизм выполнения программ JavaScript. Контекст выполнения и стек выполнения — одна из ключевых концепций JavaScript и одна из трудностей в JavaScript. Понимание контекста выполнения и стеков выполнения также помогает понять другие концепции JavaScript, такие как подъем, область действия и замыкания. В этой статье мы попытаемся представить эти концепции максимально простым для понимания образом.

Чтобы прочитать больше качественных статей, нажмитеБлог GitHub

1. Контекст выполнения

1. Что такое контекст выполнения

Короче говоря, контекст выполнения — это абстрактное понятие среды, в которой анализируется и выполняется текущий код JavaScript.Любой код, работающий в JavaScript, выполняется в контексте выполнения.

2. Тип контекста выполнения

Всего существует три типа контекстов выполнения:

  • Глобальный контекст выполнения: это контекст выполнения по умолчанию и самый простой. Код, которого нет ни в одной функции, находится в глобальном контексте выполнения. Он делает две вещи: 1. Создает глобальный объект, который в браузере является объектом окна. 2. Наведите указатель this на глобальный объект. В программе может существовать только один глобальный контекст выполнения.
  • Контекст выполнения функции: каждый раз, когда вызывается функция, для этой функции создается новый контекст выполнения. Каждая функция имеет свой собственный контекст выполнения, но он создается только при вызове функции. В программе может существовать любое количество контекстов выполнения функций. Всякий раз, когда создается новый контекст выполнения, он выполняет серию шагов в определенном порядке, обсуждаемом далее в этой статье.
  • Контекст выполнения функции Eval: код, работающий в функции eval, также получает свой собственный контекст выполнения, но поскольку функция eval обычно не используется разработчиками Javascript, она не будет здесь обсуждаться.

Во-вторых, жизненный цикл контекста выполнения.

Жизненный цикл контекста выполнения состоит из трех фаз:Фаза создания → Фаза выполнения → Фаза повторного использования, эта статья посвящена этапу создания.

1. Этап создания

Когда функция вызывается, но до выполнения любого из ее внутреннего кода, она делает три вещи:

  • Создайте объект переменной: сначала инициализируйте аргументы аргумента функции, поднимите объявление функции и объявление переменной. Это будет подробно объяснено ниже.
  • Создайте цепочку областей действия (цепочка областей действия): на этапе создания контекста времени выполнения цепочка областей действия создается после объекта переменных. Сама цепочка областей видимости содержит переменные объекты. Цепочка областей видимости используется для разрешения переменных. При запросе разрешения переменной JavaScript всегда начинает с самого внутреннего уровня вложенности кода.Если переменная не найдена на самом внутреннем уровне, он перейдет к родительской области, чтобы искать ее, пока переменная не будет найдена.
  • Определите эту точку: включая различные ситуации, которые будут подробно объяснены ниже

Перед выполнением JS-скрипта код должен быть проанализирован (поэтому JS — это скриптовый язык, который интерпретирует выполнение).При синтаксическом анализе сначала будет создан глобальный контекст выполнения, а переменные и объявления функций, которые будут выполняться в коде, будут быть вывезены в первую очередь. Переменным временно присваивается значение undefined, а функции объявляются готовыми к использованию. Этот шаг выполнен, а затем начинается формальный процесс выполнения.

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

2. Этап исполнения

Выполнение присвоения переменных, выполнение кода

3. Стадия переработки

Контекст выполнения извлекается из стека и ожидает, пока виртуальная машина перезапустит контекст выполнения.

3. Переменное продвижение и детали этого указания

1. Продвижение объявлений переменных

Большинство языков программирования объявляют переменные перед их использованием, но в JS все немного иначе:

console.log(a)// undefined
var a = 10

Приведенный выше код выводится нормальноundefinedа не ошибкаUncaught ReferenceError: a is not defined, это происходит из-за поднятия объявлений, что эквивалентно следующему коду:

var a; //声明 默认值是undefined “准备工作”
console.log(a);
a=10; //赋值

2. Продвижение объявления функции

Все мы знаем, что есть два способа создать функцию, один из них — через объявление функции.function foo(){}Другой - через функциональное выражениеvar foo = function(){}, в чем разница между этими двумя функциями продвижения?

console.log(f1) // function f1(){}
function f1() {} // 函数声明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式

Далее мы проиллюстрируем эту проблему на примере:

function test() {
    foo(); // Uncaught TypeError "foo is not a function"
    bar(); // "this will run!"
    var foo = function () { // function expression assigned to local variable 'foo'
        alert("this won't run!");
    }
    function bar() { // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();

В приведенном выше примере foo() вызывается с ошибкой, но bar может вызываться нормально.

Ранее мы говорили, что и переменные, и функции будут повышаться, и когда мы сталкиваемся с функциональными выражениями,var foo = function(){}, сначалаvar fooПоднимитесь наверх тела функции, но значение foo в данный момент не определено, поэтому выполнитеfoo()сообщить об ошибке.

А для функцииbar(), Это для повышения общей функции, поэтомуbar()чтобы иметь возможность успешно выполнять.

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

alert(a);//输出:function a(){ alert('我是函数') }
function a(){ alert('我是函数') }//
var a = '我是变量';
alert(a);   //输出:'我是变量'

Объявление функции имеет более высокий приоритет, чем объявление var, что означает, что когда две переменные с одинаковыми именами объявляются функциями function и var одновременно, объявление функции переопределяет объявление var.

Этот код эквивалентен:

function a(){alert('我是函数')} 
var a;    //hoisting
alert(a);    //输出:function a(){ alert('我是函数') }
a = '我是变量';//赋值
alert(a);   //输出:'我是变量'

Наконец, давайте рассмотрим более сложный пример:

function test(arg){
    // 1. 形参 arg 是 "hi"
    // 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
    console.log(arg);  
    var arg = 'hello'; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
    function arg(){
	console.log('hello world') 
    }
    console.log(arg);  
}
test('hi');
/* 输出:
function arg(){
    console.log('hello world') 
    }
hello 
*/

Это связано с тем, что при выполнении функции сначала формируется новая частная область, а затем выполняются следующие шаги:

  • Если есть формальный параметр, сначала присвойте значение формальному параметру
  • Выполните предварительную интерпретацию в приватной области, объявление функции имеет более высокий приоритет, чем объявление переменной, и, наконец, последнее будет перезаписано первым,но можно переназначить
  • Код в приватной области выполняется сверху вниз

3. Определите точку этого

Сначала поймите очень важную концепцию -Значение this может быть подтверждено только при его выполнении, но не может быть подтверждено при его определении!Почему — потому что это часть контекста выполнения, а контекст выполнения нужно определять до выполнения кода, а не когда он определен. См. следующий пример:

// 情况1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()

// 情况2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj

// 情况3
function CreateJsPerson(name,age){
//this是当前类的一个实例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);

// 情况4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">   
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

Далее мы объясним вышеперечисленные ситуации одну за другой.

  • Для прямого вызова foo, независимо от того, где находится функция foo, это должно быть окно
  • Для obj.foo() нам просто нужно помнить, что кто бы ни вызывал функцию, это это, поэтому в этом сценарии это в функции foo является объектом obj
  • В режиме конструктора это в this.xxx=xxx, которое появляется в классе (в теле функции), является экземпляром текущего класса.
  • call, apply и bind: это первый параметр
  • Стрелочная функция this указывает на: стрелочная функция не имеет собственного this, чтобы увидеть, есть ли функция во внешнем слое, если есть, то this внешней функции - это this внутренней стрелочной функции, если нет , то это окно.

В-четвертых, стек контекста выполнения (Execution Context Stack)

Существует много функций и несколько контекстов выполнения функций.Каждый раз при вызове функции создается новый контекст выполнения.Как управлять таким количеством созданных контекстов выполнения?

Движок JavaScript создает стек контекстов выполнения для управления контекстами выполнения.Стек контекста выполнения можно рассматривать как структуру стека для хранения вызовов функций, следуя принципу «первым пришел — последним вышел»..

Из приведенной выше блок-схемы нам нужно запомнить несколько ключевых моментов:

  • JavaScript выполняется в одном потоке, и весь код ставится в очередь на выполнение.
  • Когда браузер начинает выполнять глобальный код, он сначала создает глобальный контекст выполнения и помещает его на вершину стека выполнения.
  • Всякий раз, когда начинается выполнение функции, контекст выполнения функции создается и помещается на вершину стека выполнения. После завершения выполнения текущей функции контекст выполнения текущей функции извлекается из стека и ожидает сборки мусора.
  • Механизм выполнения JS браузера всегда обращается к контексту выполнения в верхней части стека.
  • Существует только один глобальный контекст, и он извлекается из стека при закрытии браузера.

Давайте посмотрим на другой пример:

var color = 'blue';
function changeColor() {
    var anotherColor = 'red';
    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }
    swapColors();
}
changeColor();

Приведенный выше код работает следующим образом:

  • Когда приведенный выше код загружается в браузер, механизм JavaScript создает глобальный контекст выполнения и помещает его в текущий стек выполнения.
  • При вызове функции changeColor внутренний код функции changeColor еще не выполнен, движок выполнения js сразу создает контекст выполнения для changeColor (сокращенно EC), а затем помещает контекст выполнения в стек выполнения (ECStack для короткая).
  • Во время выполнения функции changeColor вызывается функция swapColors Аналогично, перед выполнением функции swapColors создается контекст выполнения swapColors, который помещается в стек выполнения.
  • Выполнение функции swapColors завершается, а контекст выполнения функции swapColors извлекается из стека и уничтожается.
  • Выполнение функции changeColor завершается, а контекст выполнения функции changeColor извлекается из стека и уничтожается.

Порекомендуйте полезный инструмент мониторинга ошибок для всехFundebug, добро пожаловать, чтобы попробовать это бесплатно!

Добро пожаловать в публичный аккаунт:Мастер по фронтенду, мы будем свидетелями вашего роста вместе! Если вы чувствуете, что что-то приобрели, пожалуйста, дайте мне вознаграждение, чтобы мотивировать меня выпускать больше высококачественного контента с открытым исходным кодом.

Справочная статья