想学好JavaScript,这些基础知识才是最重要的

368 阅读6分钟

写下这个标题我一度怀疑自己是不是标题党。我把答案留到最后,也给看到这篇文章的同学们一个自我判断的空间。

我的上一篇文章从宏观的角度描述了浏览器的工作流程。其中说到了渲染的过程中在渲染进程中会使用JavaScript引擎来进行JavaScript代码的运行。今天,我们的视角仍然相对宏观一些,来看一下JavaScript引擎的运行机制。

什么是运行机制呢?就是代码能够正常运行的保障体系。

首先回答我一个问题,你知道执行上下文、调用栈、作用域、作用域链、闭包和this在JavaScript中的概念以及相互之间的关系吗?如果你有一些犹豫,那么这篇文章将会帮助到你。

闲话不多说,我们先思考一个问题:变量提升是什么我们都知道,但是为什么要提升,知道的大聪明们就先忍一忍,容我给大伙普及一下。

执行上下文

我们的代码被执行之前,首先会被编译成可执行的代码。在编译阶段,为了代码后续的顺利执行,会为代码创建执行环境,这个环境中包含了当前环境中可以访问到的变量,对象,函数等,在后续的执行过程中,就可以到执行环境中获取这些值。这个执行环境就叫做执行上下文。为了提高效率,执行上下文被分成了几个主要的部分:

  1. 变量环境。
  2. 词法环境。
  3. 可执行代码块。
  4. this。 这些环境不是一开始就完整的,是随着语言的发展逐渐完善的,我们会在接下来的文章中理清楚。

现在我们也知道了变量提升的本质:代码被编译的时候变量被统一存放到执行上下文的变量环境中。

执行上下文我们主要需要了解两种:全局上下文,这个是在代码整体被编译之前生成的;函数执行上下文,这是在函数被编译前生成的,函数是在执行的时候才被编译的。

其实,还有eval上下文,但是我们平时接触这个方法也比较少。

调用栈

我们现在已经知道了什么是执行上下文了,我在前面提到了一下,函数执行上下文是在函数执行时才被生成的。也就是说,每次有函数被执行,就会有函数上下文生成,这么多的上下文,要怎么管理呢? JavaScript设计了一种栈结构来保存这些临时生成的上下文,因为函数的调用是顺序的,嵌套的。每个函数返回的时候,上下文需要被销毁。这样,才能保证内存不会满。

栈结构刚好满足这个需求,于是这个存储执行上下文的栈,就叫做执行上下文栈,因为和函数的调用紧密相关,又叫作调用栈。 调用栈是有大小的,如果超出了就会报错,这就是嵌套函数容易造成栈溢出的原因。

作用域

刚才我们说了,执行上下文在一个代码中可能会有很多,这些执行上下文中都保留了很多的变量,那这些变量在任何地方都能访问吗?如果是的,那重名的变量怎么办呢?

答案是,不可能所有变量都是在任何地方都能访问。控制着这些变量访问性的机制就是作用域。

在ES6之前,作用域只有全局作用域和函数作用域。这和全局上下文和函数上下文是一一对应的。

在ES6之后,为了解决变量提升带来的各种糟糕的体验,重新设计了词法作用域。然而需要兼容老版本的代码,就只能另外创建let和const关键字来满足支持新的语法。所以,要说起来,词法作用域和变量作用域的功能一样,是存储let和const两个关键字声明的变量的。既然机制类似,let和const声明的变量,其实,也是提升的,只不过JavaScript引擎限制了赋值前的访问。

但是,不管是全局作用域,函数作用域还是块级作用域,都是词法作用域。也就是说,上下文中变量的访问方式,是在编译阶段就确定了的。编译的时候,你属于哪个上下文环境,你就只能首先访问你所在的上下文。

作用域链

刚才说到编译的时候变量属于哪个上下文,执行的时候也只能优先访问哪个上下文环境。

如果找不到怎么办呢?如果找不到,在当前上下文中会有一个outer的特殊内置变量,它指向了当前上下文创建的时候的外部上下文。

所以,接下来就会通过这个outer跳到外部上下文中寻找。

这样,通过一个又一个outer,形成了一个链条,这就是作用域链。

我们总结一下,作用域不是一个空间概念,而是一个机制,它规定了我们如何访问上下文中的变量。作用域链是这个机制将各个作用域串通的一个机制。

闭包

如果在一个上下文中访问了它外部上下文中的变量,但是外部上下文需要销毁,那怎么办呢?

这就是闭包的由来。

这种情况下,为了保证被销毁的上下文环境中的变量能够保留下来,JavaScript会在保存复杂变量的堆内存中复制一份需要保留的变量,这个保留变量的空间就叫做闭包环境。

this

this是一个特殊的语法,他和上面说的所有的概念都没有直接关系,但是他又常常被搞混在上面的那些概念中。

现在,我们就为它正名一下:this提供了一种绑定对象方法和属性的简单方式。他之所以令人容易混淆,就是因为它被定义在了上下文中,然而他和上下文中的其他概念又都没有什么直接关系。

所以,这个this就指向调用这个方法的对象。如果方法是一个普通函数怎么办呢?那this就是undefined或者全局对象,就看你是否处于严格模式下。

有几种显示改变this的方式:通过call,apply和bind来改变this,通过new关键字创建实例对象,那么this总是指向这个实例。

总结

这篇文章描述了JavaScript运行机制中的关键知识以及他们之间的联系,回到开头的问题,我觉得不算标题党。