一、前言

目前从事前端开发的人员的人数一直是只增不减,培训班也是层出不穷,很多人是半路出家,也有的人像我一样,虽然是专业出身但是大学心思根本没在学习上?️,这导致很多人虽然能改能写但是很多原理性的东西并不了解,间接导致代码质量,和思考深度有所欠缺,所以需要学习一些基础原理性的只是进行补充,今天的笔记记录一下学习栈的内容。

二、栈

栈 是一种遵循 后进先出(LIFO) 原则的有序集合。新添加和待删除的数据都保存在栈的同一端栈顶,另一端就是栈底。新元素靠近栈顶,旧元素靠近栈底。 栈由编译器自动分配释放。栈使用一级缓存。调用时处于存储空间,调用完毕自动释放。(我比较懒所以摘抄了一段);
网上好多栈的原理图我这里就不放了,我用我自己的理解来放上一张图解释
Img226136660.jpg
弹夹,栈的原理就像弹夹,先进去的最后出来,后进去的先出来,谁也不能插队。既然弹夹容量有限,那所装的子弹数也是固定的,我们当然不希望空蛋壳占据子弹的位置。这种现象称之为内存泄漏。

三、执行栈

程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈)。
程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。
执行上下文可以通俗的理解为,执行这一段代码所依赖的环境和方法,数据等。也就是说如果当前的一个变量或方法被其他地方所使用,那当被引用的代码执行时,它原本的变量或方法也就成来执行上下文的一部分。
这里引发来我对作用域的思考,为什么里层作用域可以访问外部变量,但是外部足作用域无法访问里层作用域的变量。这样是不是使得执行上下文在堆栈里存放的顺序更加合理。因为因为JS执行中最先进入全局环境,所以处于"栈底的永远是全局环境的执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文"。

四、实现栈方法

理解来栈的原理,可以用代码来实现一下栈的功能,这样细化思维锻炼能力,能理解的更加透彻,说不定还有意外收获。我从网上截取来一段实现栈的代码。

let Stack = (function() {
    const items = new WeakMap();
    class Stack {
        constructor() {
            items.set(this, []);
        }
        getItems() {
            let s = items.get(this);
            return s;
        }
        push(element) {
            this.getItems().push(element);
        }
        pop() {
            return this.getItems().pop();
        }
        peek() {
            return this.getItems()[this.getItems.length - 1];
        }
        isEmpty() {
            return this.getItems().length == 0;
        }
        size() {
            return this.getItems().length;
        }
        clear() {
            this.getItems() = [];
        }
    }
    return Stack;})();

这段代码采用了ES6的写法,首先在自执行函数里实例化了 WeakMap ,先不去管创建了一个什么实例继续往下看,用es6写法class创建了一个类后
constructor初始化这个类items.set(this, []);将当前作用域创建为空数组。后边的每个方法都是在操作当前作用域,都使用了getItems这个方法,这个方法实际上引用了WeakMap的方法。这时候来解释一下WeakMap,这是ES6中用于创建弱引用类型变量的方法。在上文中提到,创建的变量如果被使用,就会被引用,就不会从栈中释放。但是WeakMap为我们提供来一种特性,就是以WeakMap创建的变量,在被使用时不被计入引用个数。同时WeakMap创建值需要以键值对的方式创建,(key,vaue)同时
key需要为对象的形式例如items.set(this, []);this是当前作用域对象,当前作用域为键值,也就是说如果当前作用域解除来引用那对应的数组也会被释放。的需要举个栗子?:
var a={b:1}; var c={d:a.b}现在c中引用了a中的值,有一个引用,即便a=null;也不会被释放因为c还在引用。
但是如果我const items = new WeakMap();
items.set(a, '{b:1}');var c={d:a.b};
只要a=null;那a就会被释放即便c还在引用a,但是因为若引用,a在c中的引用在垃圾回收时不算做引用次数。

五、内存泄漏

内存泄漏是通过本次学习栈引发的思考,在经常使用的全局变量,闭包,计时器,等一系列持续引用的方法时,会导致栈中的位置长期被占用,得不到释放,导致内存泄漏,最后导致栈溢出。
那导致内存溢出最常见的方法是什么,是递归,多次重复的递归但是没有返回,JS引擎会认为你还没有执行完,会保留你的调用帧。所以栈内会保留你所有的递归调用帧。举个栗子?:
通过上面的说法是不是在每次递归的时候返回结果再去递归就会释放之前的递归调用帧呢。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};

return Fibonacci(n - 1) + Fibonacci(n - 2);
};
这样其实并不会去释放之前的调用帧,因为返回的并不是一个调用函数,实际上返回值指向的是两个递归的和,这时候还会继续保留当前调用帧,继续递归下去。
如何优化:
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};

return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
在函数调用结束时返回一个函数,这时候引擎直到你要执行下一个函数了,会释放之前的调用帧创建新的调用。


印第安老鹌鹑
24 声望3 粉丝

杜昕宇不让我写简介