从上下文,到做用域(彩蛋:理解闭包)

前言

近几天在编程群中的聊天,让我发现了不少人并不清楚什么是上下文(context)、什么是做用域(scope),并且纠结在其中。我当初对这两个概念也只有粗浅的理解,不过我从一开始就不怎么困惑,由于我清楚本身对这一问题的认识边界。如今,我对它们的认识也只加深了一点点。不过,群聊中小伙伴的热情鼓舞了我——不少最最初学的小伙伴,想到和思考的是不少我从没考虑过的问题,小伙伴们真是达到了“进一寸有一寸的欢喜”这一境界。见贤思齐,我决定把这一点点进步记录下来。javascript

上下文与做用域的关系

不少人弄不清除,缘由固然是既不了解上下文,也不了解做用域——我是说,几乎没有人明白上下文是什么而不明白做用域是什么,反之亦然。上下文(context)做用域(scope)都是编译原理的知识,具体编程语言有具体的实现规则,本文关注 JavaScript 语言的实现。首先须要关注的是,这两个概念的关系很是密切,因此先了解它们的关系,有助于理解它们究竟是什么。java

上下文(context)做用域(scope)的关系:git

上下文是一段程序运行所须要的最小数据集合;做用域是当前上下文中,按照具体规则可以访问到的标识符(变量)的范围。github

后文是对上下文和做用域更详细的解释,知道了上面指出的关系,往下阅读时就能够加深对这一关系的理解了。编程

上下文

上下文(context)是一段程序运行所须要的最小数据集合。咱们能够从上下文交换(context switch)来理解上下文,在多进程或多线程环境中,任务切换时首先要中断当前的任务,将计算资源交给下一个任务。由于稍后还要恢复以前的任务,因此中断的时候要保存现场,即当前任务的上下文,也能够叫作环境。即上下文就是恢复现场所需的最小数据集合。容易把人弄晕的一点是,咱们这里说的上下文环境有时候也称做做用域(scope),即这两个概念有时候是混用的。不过,它们有不一样的侧重点,下一节将会说明。多线程

另外,JavaScript 中常见的情形是一个方法/函数的执行。从一段程序的角度看,这段程序运行所需的全部变量,就是它的上下文。闭包

做用域

做用域(scope)是标识符(变量)在程序中的可见性范围。做用域规则是按照具体规则维护标识符的可见性,以肯定当前执行的代码对这些标识符的访问权限。做用域(scope)是在具体的做用域规则之下肯定的。app

前面说过,有时候上下文、环境、做用域是同义词;不过,上下文(context)指代的是总体环境,做用域关注的是标识符(变量)的可访问性(可见性)。上下文肯定了,根据具体编程语言的做用域规则,做用域也就肯定了。这就是上下文与做用域的关系编程语言

写 JavaScript 代码时,若是 Function 做为参数,能够指定它在具体对象上调用时,这个对象经常叫作 context:函数

function callWithContext(fn, context) {
  return fn.call(context);
}

const apple = {
  name: "Apple"
};

const orange = {
  name: "Orange"
};

function echo() {
  console.log(this.name);
}

callWithContext(echo, apple);  // Apple
callWithContext(echo, orange); // Orange复制代码

为何将这个参数叫作 context?由于它关系到调用环境,指定了它,就指定了函数的调用上下文。再加上具体的做用域规则,做用域也肯定了。

在 JavaScript 中,这个具体的做用域规则就是词法做用域(lexical scope),也就是 JavaScript 中的做用域链的规则。词法做用域是的变量在编译时(词法阶段)就是肯定的,因此词法做用域又叫静态做用域(static scope),与之相对的是动态做用域(dynamic scope)

You Don't Know JS: Scope & Closures 用简单例子解释过动态做用域,下面用一个相似的例子说明一下:

function foo() {
  console.log(a);
}

function bar() {
  let a = 3;
  foo();
}

let a = 2;

bar(); // 2复制代码

有必定 JavaScript 编程经验的人都能看出,这段程序会输出 2,但若是在动态做用域的规则下,应该输出 3,即 a 的引用再也不是编译时肯定,而是调用时肯定的。这有点像 JavaScript 中的 this,因此 MDN 中,function.bind 的方法签名中第一个形参名称用的是 thisArg 这一更科学的名字:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

一样状况的还可见于 Lodash 的文档:

_.bind(func, thisArg, [partials])

彩蛋:理解闭包

上一节中的代码中,之因此输出 2,是由于 foo 是一个闭包函数。若是从本文中理解了上下文和做用域的概念,对于闭包是什么这一问题是否是感到豁然开朗?

前面说过,词法做用域也叫静态做用域,变量在词法阶段肯定,也就是定义时肯定。虽然在 bar 内调用,但因为 foo 是闭包函数,即便它在本身定义的词法做用域之外的地方执行,它也一直保持着本身的做用域。所谓闭包函数,即这个函数封闭了它本身的定义时的环境,造成了一个闭包,因此 foo 并不会从 bar 中寻找变量,这就是静态做用域的特色。

一个更加典型的例子是:

function fn() {
  let a = 0;
  function func() {
    console.log(a);
  }
  return func;
}

let a = 1;
let sub = fn();

sub(); // 0;复制代码

sub 就是 func 这一返回值,func 定义在 fn 内部而且被传递出来了,因此 fn 执行以后垃圾回收器依然没有回收它的内部做用域,由于 func/sub 在使用。sub 依然持有 func 定义时的做用域的引用,而这个引用就叫做闭包。调用 sub 时,它能够访问 func 定义时的词法做用域,所以找到的 a 是 fn 内部的变量 a,它的值是 0。

参考资料

You Don't Know JS: Scope & Closures
Context (computing)
Scope (computer science)
Function.prototype.bind()
Function _.bind()

相关文章
相关标签/搜索