Стек — это структура данных, представляющая собой список элементов, организованных по принципу LIFO (от англ. last in — first out, «последним пришёл — первым вышел»).
Стек можно представить в виде детской пирамидки: чтобы извлечь второе сверху кольцо, нужно сначала снять самое верхнее.
Уже знакомые Вам методы push
и pop
позволяют работать с массивом как со стеком:
var stack = [];
stack.push('Tom');
stack.push('Jane');
stack.push('Bill');
stack.pop(); // Bill
stack.pop(); // Jane
stack.pop(); // Tom
Какое отношение стек имеет к функциям?
function a() { b() }
function b() { c() }
function c() { d() }
function d() { console.log('D') }
Поскольку функции могут вызывать друг друга, им необходима информация о том, в какую часть программы следует вернуться после завершения.
Пусть некоторый вызов из предыдущего примера привел нас в функцию d
.
function d() {
console.log('D');
// Выпонение завершено, куда возвращаемся?
}
Что следует выполнять после завершения функции? Для ответа на этот вопрос существует стек вызовов.
Стек вызовов — это специальная область памяти, которая содержит информацию, необходимую для возврата управления из подпрограммы в место вызова.
Стек является «клубком ниток», который помогает среде исполнения найти дорогу в «лабиринте вызовов».
Записи в стеке вызовов называют фреймами (от англ. frame — кадр).
Вызываем a()
.
/*1*/ function a() { b() }
/*2*/ function b() { c() }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ →a();
В стек записывается <main>:6
(адрес, откуда сделан вызов).
Находимся в a:1
, вызываем b()
.
/*1*/ function a() { →b() }
/*2*/ function b() { c() }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1
Находимся в b:2
, вызываем c()
.
/*1*/ function a() { b() }
/*2*/ function b() { →c() }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1, b:2
Находимся в c:3
, вызываем d()
.
/*1*/ function a() { b() }
/*2*/ function b() { c() }
/*3*/ function c() { →d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1, b:2, c:3
Находимся в d:4
, вызываем console.log()
.
/*1*/ function a() { b() }
/*2*/ function b() { c() }
/*3*/ function c() { d() }
/*4*/ function d() { →console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1, b:2, c:3, d:4
Вернулись из console.log
в d:4
.
/*1*/ function a() { b() }
/*2*/ function b() { c() }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') → }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1, b:2, c:3
Вернулись в c:3
.
/*1*/ function a() { b() }
/*2*/ function b() { c() }
/*3*/ function c() { d() → }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1, b:2
Вернулись в b:2
.
/*1*/ function a() { b() }
/*2*/ function b() { c() → }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6, a:1
Вернулись в a:1
.
/*1*/ function a() { b() → }
/*2*/ function b() { c() }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a();
Стек: <main>:6
Вернулись в главную программу.
/*1*/ function a() { b() }
/*2*/ function b() { c() }
/*3*/ function c() { d() }
/*4*/ function d() { console.log('D') }
/*5*/
/*6*/ a(); →
Стек пуст.
Используйте console.trace()
, чтобы увидеть стек вызовов в определенный момент работы программы.
function a() { b() }
function b() { c() }
function c() { d() }
function d() { console.trace() }
Используйте трассировку для отладки Ваших программ, но с осторожностью.
Стек вызовов может содержать избыточную информацию, которая существенно усложняет отладку.