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

来源:互联网 发布:辐射4n卡优化补丁 编辑:程序博客网 时间:2024/06/06 00:58

前言

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

上下文与作用域的关系

很多人弄不清楚,原因当然是既不了解上下文,也不了解作用域——我是说,几乎没有人明白上下文是什么而不明白作用域是什么,反之亦然。上下文(context)和作用域(scope)都是编译原理的知识,具体编程语言有具体的实现规则,本文关注JavaScript语言的实现。首先需要关注的是,这两个概念的关系是非常密切,所以先了解它们的关系,有助于理解它们到底是什么。

上下文(context)和作用域(scope)的关系:

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

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

上下文

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

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

作用域

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

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

写JavaScript代码时,如果Function作为参数,可以指定它在具体对象上调用时,这个对象常常叫做context:

function callWidthContext(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);

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

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

You Don’t Know JS:Scope & Closure用简单例子解释过动态作用域,下面用一个类似的例子说明一下:

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,[,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()
阅读全文
0 0
原创粉丝点击