前端基础进阶(四):详细图解作用域链与闭包

2017/02/24 · 基础技术 ·
作用域链,
闭包

原文出处: 波同学   

图片 1

攻克闭包难题

初学JavaScript的时候,我在学习闭包上,走了很多弯路。而这次重新回过头来对基础知识进行梳理,要讲清楚闭包,也是一个非常大的挑战。

闭包有多重要?如果你是初入前端的朋友,我没有办法直观的告诉你闭包在实际开发中的无处不在,但是我可以告诉你,前端面试,必问闭包。面试官们常常用对闭包的了解程度来判定面试者的基础水平,保守估计,10个前端面试者,至少5个都死在闭包上。

可是为什么,闭包如此重要,还是有那么多人没有搞清楚呢?是因为大家不愿意学习吗?还真不是,而是我们通过搜索找到的大部分讲解闭包的中文文章,都没有清晰明了的把闭包讲解清楚。要么浅尝辄止,要么高深莫测,要么干脆就直接乱说一通。包括我自己曾经也写过一篇关于闭包的总结,回头一看,不忍直视[捂脸]。

因此本文的目的就在于,能够清晰明了得把闭包说清楚,让读者老爷们看了之后,就把闭包给彻底学会了,而不是似懂非懂。

理解JavaScript的作用域链

2015/10/31 · JavaScript
·
作用域链

原文出处:
田小计划   

上一篇文章中介绍了Execution Context中的三个重要部分:VO/AO,scope
chain和this,并详细的介绍了VO/AO在JavaScript代码执行中的表现。

本文就看看Execution Context中的scope chain。

JavaScript 深入之闭包

2017/05/21 · JavaScript
· 闭包

原文出处: 冴羽   

一、作用域与作用域链

在详细讲解作用域链之前,我默认你已经大概明白了JavaScript中的下面这些重要概念。这些概念将会非常有帮助。

  • 基础数据类型与引用数据类型
  • 内存空间
  • 垃圾回收机制
  • 执行上下文
  • 变量对象与活动对象

如果你暂时还没有明白,可以去看本系列的前三篇文章,本文文末有目录链接。为了讲解闭包,我已经为大家做好了基础知识的铺垫。哈哈,真是好大一出戏。

作用域

  • 在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

    这里的标识符,指的是变量名或者函数名

  • JavaScript中只有全局作用域与函数作用域(因为eval我们平时开发中几乎不会用到它,这里不讨论)。

  • 作用域与执行上下文是完全不同的两个概念。我知道很多人会混淆他们,但是一定要仔细区分。

    JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

图片 2

过程

作用域链

回顾一下上一篇文章我们分析的执行上下文的生命周期,如下图。

图片 3

执行上下文生命周期

我们发现,作用域链是在执行上下文的创建阶段生成的。这个就奇怪了。上面我们刚刚说作用域在编译阶段确定规则,可是为什么作用域链却在执行阶段确定呢?

之所有有这个疑问,是因为大家对作用域和作用域链有一个误解。我们上面说了,作用域是一套规则,那么作用域链是什么呢?是这套规则的具体实现。所以这就是作用域与作用域链的关系,相信大家都应该明白了吧。

我们知道函数在调用激活时,会开始创建对应的执行上下文,在执行上下文生成的过程中,变量对象,作用域链,以及this的值会分别被确定。之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。

为了帮助大家理解作用域链,我我们先结合一个例子,以及相应的图示来说明。

JavaScript

var a = 20; function test() { var b = a + 10; function innerTest() { var
c = 10; return b + c; } return innerTest(); } test();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = 20;
 
function test() {
    var b = a + 10;
 
    function innerTest() {
        var c = 10;
        return b + c;
    }
 
    return innerTest();
}
 
test();

在上面的例子中,全局,函数test,函数innerTest的执行上下文先后创建。我们设定他们的变量对象分别为VO(global),VO(test),
VO(innerTest)。而innerTest的作用域链,则同时包含了这三个变量对象,所以innerTest的执行上下文可如下表示。

JavaScript

innerTestEC = { VO: {…}, // 变量对象 scopeChain: [VO(innerTest),
VO(test), VO(global)], // 作用域链 this: {} }

1
2
3
4
5
innerTestEC = {
    VO: {…},  // 变量对象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
    this: {}
}

是的,你没有看错,我们可以直接用一个数组来表示作用域链,数组的第一项scopeChain[0]为作用域链的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象。

很多人会误解为当前作用域与上层作用域为包含关系,但其实并不是。以最前端为起点,最末端为终点的单方向通道我认为是更加贴切的形容。如图。

图片 4

作用域链图示

注意,因为变量对象在执行上下文进入执行阶段时,就变成了活动对象,这一点在上一篇文章中已经讲过,因此图中使用了AO来表示。Active
Object

是的,作用域链是由一系列变量对象组成,我们可以在这个单向通道中,查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。

作用域

开始介绍作用域链之前,先看看JavaScript中的作用域(scope)。在很多语言中(C++,C#,Java),作用域都是通过代码块(由{}包起来的代码)来决定的,但是,在JavaScript作用域是跟函数相关的,也可以说成是function-based。

例如,当for循环这个代码块结束后,依然可以访问变量”i”。

JavaScript

for(var i = 0; i < 3; i++){ console.log(i); } console.log(i); //3

1
2
3
4
5
for(var i = 0; i < 3; i++){
    console.log(i);
}
 
console.log(i); //3

对于作用域,又可以分为全局作用域(Global scope)和局部作用域(Local
scpoe)。

全局作用域中的对象可以在代码的任何地方访问,一般来说,下面情况的对象会在全局作用域中:

  • 最外层函数和在最外层函数外面定义的变量
  • 没有通过关键字”var”声明的变量
  • 浏览器中,window对象的属性

局部作用域又被称为函数作用域(Function
scope),所有的变量和函数只能在作用域内部使用。

JavaScript

var foo = 1; window.bar = 2; function baz(){ a = 3; var b = 4; } //
Global scope: foo, bar, baz, a // Local scope: b

1
2
3
4
5
6
7
8
9
var foo = 1;
window.bar = 2;
 
function baz(){
    a = 3;
    var b = 4;
}
// Global scope: foo, bar, baz, a
// Local scope: b

定义

MDN 对闭包的定义为:

闭包是指那些能够访问自由变量的函数。

那什么是自由变量呢?

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

由此,我们可以看出闭包共有两部分组成:

闭包 = 函数 + 函数能够访问的自由变量

举个例子:

var a = 1; function foo() { console.log(a); } foo();

1
2
3
4
5
6
7
var a = 1;
 
function foo() {
    console.log(a);
}
 
foo();

foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo
函数的参数,所以 a 就是自由变量。

那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……

还真是这样的!

所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。

咦,这怎么跟我们平时看到的讲到的闭包不一样呢!?

别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义:

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  2. 从实践角度:以下函数才算是闭包:
    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量

接下来就来讲讲实践上的闭包。

发表评论

电子邮件地址不会被公开。 必填项已用*标注