前端面试之路开篇——JS基础(一)

391 阅读8分钟

前言

为什么要写这一系列文章呢?算下来,做前端满打满算刚好4年,虽然大学学的是计算机,但是懂的都懂,18年毕业后算是从零开始学的编程,没错,不是从零开始学前端,是从零开始学编程。刚入行期间经历的种种困难和心酸,依旧仿佛昨日。所谓不同的阶段,会有不同的焦虑,在20年第一次换了工作后,进入了一家不错的中型公司,便开始了温水煮青蛙的咸鱼生活,直到今年互联网一度跌落神坛,各种裁员风波层出不穷,真正给我的生活敲响了警钟。决心做出一些改变,系统性的学习一下前端。所谓能进大厂的终归是少数,要付出的努力可想而知,在这里,我想强行代表大多数的前端小伙伴——学历一般,公司一般,水平一般的普通人,尝试通过自己的努力,看看能不能完善自己的技术,冲进大厂。恰好国庆期间看到了这篇文章面试大厂的避坑指南,你还在盲目刷题吗?(附学习路线),按大佬文章中的学习时间来算,大概花了将近6个月的时间,那么从今天开始到明年4月底,我要用将近7个月的时间,跟随大佬的脚印,一步一步的去走,看看能达到什么样的效果。(好家伙,里边的文章链接真是套娃,实属深度遍历,确实够看几个月的)并且,将这些基础知识,梳理成自己的文章,让语句通俗易懂,如果看过之前我写过的小白都能听懂的最新Babel7配置探索这篇文章,那么一定会深信不疑,话不多说,我们正式开始吧~

学习总规划(目录)

既然是前端体系温习,那么就应该有一个学习的路线图,我罗列下我的规划,放链接的就是已经写完的文章,相当于目录,方便跳转查看:

前端面试之路——JS基础(一)执行上下文/执行栈/作用域链/闭包:本篇文章
前端面试之路——JS基础(二)this/call/apply/bind
前端面试之路——JS基础(三)原型/继承
前端面试之路——JS基础(四)Promise
前端面试之路——JS基础(五)事件循环
前端面试之路——JS基础(六)深浅拷贝
todo......
前端面试之路——工程化(Webpack)
前端面试之路——工程化(Vite)
todo......
从零开始学习Vue3源码 todo......

执行上下文

什么是执行上下文?

一句话:执行上下文就是JavaScript在执行时环境的抽象概念。

执行上下文的类型

  • 全局执行上下文: 浏览器环境下,任何不在函数内部的代码,都在全局上下文中,会创建window对象,并设置this指向这个对象。
  • 函数执行上下文: 当函数被调用的时候,都会为当前函数生成一个新的上下文,可以有任意多个,每当一个新的函数执行上下文创建的时候,会按照一定顺序执行(后文会提及)。
  • Eval函数执行上下文: 执行在eval函数中的代码,也有属于单独自己的执行上下文。

执行栈

也叫调用栈,是一种先进后出(LIFO:last in first out)的数据结构,用来存储代码运行时的所有执行上下文。

  • JS引擎第一次遇到js脚本时,会创建一个全局执行上下文,并压入栈中;
  • JS引擎每当遇到一个函数调用,都会创建一个新的函数执行上下文,并压入栈中;
  • 当该函数执行完毕,函数执行上下文会在栈中弹出,继续执行下一个函数;
  • 所有代码执行完毕,JS引擎将全局执行上下文从栈中移出;

可以通过简单的代码示例和执行栈的变化,来进行理解,其实就是整个入栈出栈的过程:

let a = '开始啦'; // 全局上执行下文
function first() { 
  console.log('第一个函数开始');
  second(); // second函数执行上下文
  console.log('第一个函数结束');
}
function second() { 
  console.log('第二个函数');
}
first(); // first函数执行上下文

image.png

到此,我们便理解了JavaScript引擎是如何管理执行上下文的了。

作用域

作用域也是一个抽象概念,可以理解为在固定的环境范围内,所有的可访问变量

作用域类型

  • 全局作用域: 声明在全局的变量;
  • 函数作用域: 声明在函数内部的变量;
  • Es6中新增的块级作用域: 被大括号{ }所包裹,iffor中的{ }也属于块级作用域,可以用letconst进行变量声明,声明的变量在外部的作用域无法被访问;

作用域链

当查找变量的时候,首先会先从当前的作用域中进行查找,如果没有找到,就会从父级的作用域中查找,如果还没有找到,一直找到全局作用域,也就是全局对象。这样由多个作用域链接在一起,形成类似于链表的结构,就叫做作用域链;类似于查找张三这个人,要先在张村中找,找不到的话,就去县里边找,再找不到就去市里边找,一层一层向外寻找。

闭包

什么是闭包?

看了好多各式各样的定义,虽然专业,但是都比较拗口和晦涩难懂,而且不好记忆;个人理解,闭包就是引用了外部作用域变量的函数,再简单点说,一个函数,如果访问了外部作用域的变量,它就是个闭包。常见的闭包一般有两种形式,一种是函数作为返回值,另一种是函数作为参数进行传递;作用都是为了让局部变量的值始终保持在内存中,对内部变量进行保护,使外部访问不到。因为闭包的概念一直以来都是比较模糊和难以理解,所以我们可以结合几个实际例子,来进行理解:

let a = 1
function add (x) {
  return function (y) {
    return x + y
  }
}
const sum = add(2)
const result = sum(c)
console.log('result:', result)

上边这个代码中我们可以看到,add函数的返回值是一个函数,那么便形成了一个闭包,闭包中包含变量x,所以当sum被调用的时候,可以访问到闭包中的x变量,并且进行相加操作。有一句话说的非常不错,我们可以用背包进行闭包的类比:当一个函数被创建传递,或从另一个函数返回的时候,它会携带一个背包,背包中是函数声明时,作用域内全部的变量。

关于闭包导致内存泄漏的江湖传说

我们先说下啥是内存泄漏:用动态存储分配函数内存空间,在使用完毕后未释放,导致一直占据该内存单元,直到程序结束。 简单来说,就是当你不再需要使用一个对象的时候,它依旧存在并且占用着内存。

在 Javascript 中,局部变量会随着函数的执行完毕而被销毁,除非还有指向他们的引用。当闭包本身也被垃圾回收之后,这些闭包中的私有状态随后也会被垃圾回收。通常我们可以通过切断闭包的引用来达到这一目的,比如主动给其赋值为null,说了这么一大堆干的,我们看几个小例子来加深理解:

function person () {
    let name = 'foo'
    let age = 10
    function getInfo () {
        console.log(name)
        console.log(age)
    }
    return bar
}
let fn = person()
fn()

从上边的代码中可以看到,fn函数调用完毕后,person函数会自动销毁,但是person中的name和age不会被销毁,因为在getInfo中被引用了,这时便造成了内存泄漏,解决的方法也很简单,fn = null一行代码,进行释放即可,之后就会被进行垃圾回收。

说了半天,我们来辟下谣,顺便小结一下,闭包会不会造成内存泄漏呢?

  • 根据上边所说的闭包定义,如果闭包的引用没有被及时清除,那么就是会造成内存泄漏,并不是说闭包造成的内存泄漏,而是闭包中引用的变量,没有被垃圾回收清除掉,才造成了内存泄漏,即使是1byte的内存,也叫内存泄漏,并不是页面卡了,浏览器崩了才叫内存泄漏,不过这种少量的内存泄漏,也是无关痛痒的,只不过为了规范和防范与未然,最好手动释放闭包,尤其是定时器,用完最好及时清理掉;
  • 基本类型的值存在内存中,被保存在栈内存中,引用类型的值是对象,保存在堆内存中。所以对象、数组之类的,才会发生内存泄漏。
  • 早期传言的闭包造成内存泄漏,是指IE早期浏览器的bug,当闭包销毁后,其中的变量依旧无法被销毁还有循环引用的问题;

结尾

今天是第一天,希望有志同道合的小伙伴和我一起努力,因水平有限,不足之处,请在评论区指正,我们一起交流进步~