关于js中作用域、作用域链、执行上下文、this、闭包的解惑

325 阅读5分钟

作用域

背景:

我们都知道编程语言中,能够存储变量中的值,并且能在之后对这个值进行访问或者修改。

而变量引入程序,这些变量存储在哪?程序需要如何找到这些变量

这些问题需要设计一套良好的规则来存储变量,之后可以方便的找到这些变量,这套规则被称为作用域

定义:

简单来说,作用域是根据名称查找变量的一套规则

作用:

js的作用域遵循词法作用域原则。这意味着js的作用域是由书写代码时变量和块作用域写在哪决定的。

作用域链

作用域链,其实也理解为作用域嵌套。

当一个块或者函数嵌套到另外一个块或函数时,就发生了作用域嵌套。引擎会在当前作用域查找变量,如果找不到,就向上一级(外层嵌套的作用域)继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找的过程都会停止。

执行上下文

执行上下文的概念

引用ES6语言规范的解释:

一个执行上下文是用于由ECMAScript实现跟踪代码的运行时计算的规范装置。在任何时候,最多只有一个执行上下文在实际执行代码。这称为运行执行上下文。

简单来说,当 JS 引擎解析到可执行代码片段(通常是函数调用阶段)的时候,就会先做一些执行前的准备工作,这个 “准备工作” ,就叫做 "执行上下文(execution context 简称 EC)" 或者也可以叫做执行环境

当执行 JS 代码时,会产生三种执行上下文类型:

  • 全局执行上下文;
  • 函数执行上下文;
  • eval 执行上下文;

每个执行上下文中都有如下的重要的属性:

  • 作用域链;
  • 调用者信息(this);
  • 词法环境组件(LexicalEnvironment component
  • 变量环境组件(VariableEnvironment component

词法环境 是一种持有 标识符—变量映射 的结构。具体解释见ES6语言规范

变量环境 它也是一个 词法环境 ,所以它有着词法环境的所有特性。

之所以在 ES5 的规范力要单独分出一个变量环境的概念是为 ES6 服务的: 在 ES6 中,词法环境组件和 变量环境 的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 var 变量绑定。

执行上下文的创建

创建执行上下文有明确的几个步骤:

  1. 确定 this,即我们所熟知的 this 绑定。
  2. 创建 词法环境(LexicalEnvironment) 组件。
  3. 创建 变量环境组件(VariableEnvironment) 组件。

用伪代码这样定义一个执行上下文:

ExecutionContext = {
    // this
    ThisBinding = <this value>
    //词法环境
    LexicalEnvironment = { ... },
    //变量环境
    VariableEnvironment = { ... },
}

执行上下文堆栈

引用ES6语言规范解释:

堆栈用于跟踪执行上下文。正在运行的执行上下文始终是此堆栈的顶部元素。每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文无关的可执行代码时,就会创建新的执行上下文。新创建的执行上下文被压入堆栈,成为正在运行的执行上下文。

执行栈存储着所有执行上下文,并遵循着后进先出的原则。看如下代码:

var say = function(){
    hello();
}

var hello = function(){
    console.log("Hello,world!");
}

say();

执行栈过程:

  1. 创建全局执行上下文,压入执行栈中;
  2. 调用了 say函数,创建 say函数的执行上下文,并压入执行栈中;
  3. 进入 say函数 内部,调用了 hello函数,创建 hello函数的执行上下文,并压入执行栈中;
  4. hello函数执行完了,将 hello 移出执行栈;
  5. say函数执行完了,将 say 移出执行栈;

image.png

this

this 是在函数运行时进行绑定的,它的指向是什么,完全取决于函数在哪里被调用。这里要关注的是,this的绑定方式,以及绑定的优先级。

this的绑定优先级遵循:

new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

关于this 绑定的详细信息,参考畅谈this的四种绑定方式

闭包

关于闭包,引用《你不知道的JavaScript》对应闭包的定义:

当函数能够记住并访问所在的词法作用域时,就产生了闭包。

简单来说,函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。闭包是对作用域链的完美诠释。

闭包其实是一种特殊的函数,它可以访问函数内部的变量,还可以让这些变量的值始终保持在内存中,不会在函数调用后被垃圾回收机制清除。

如下代码:

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
const b = A()
b() // 1

经典面试题,循环中使用闭包解决 var 定义函数的问题:

for ( var i=1; i<=5; i++) {
  setTimeout( function timer() {
    console.log( i );
  }, i*1000 );
}

因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。

用闭包解决:

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

闭包的优缺点

优点:

  1. 变量的值始终保持在内存中;
  2. 避免全局变量的污染;
  3. 私有成员的存在;

缺点:

  1. 容易造成内存泄漏;

参考

最后

image.png