讲清楚之 javascript 作用域

# 作用域

## 什么是作用域(Scope)?

作用域产生于程序源代码中定义变量的区域,在程序编码阶段就确定了。javascript 中分为全局作用域(Global context: `window`/`global` )和局部作用域(Local Scope ,  又称为函数作用域 Function context)。简单讲作用域就是当前函数的`生成环境`或者`上下文`,包含了当前函数内定义的变量以及对外层作用域的引用。 

作用域:

| 作用域(Scope)    | -              |

|:--------------------|:----------------|

| window/global Scope | 全局作用域      |

| function Scope      | 函数作用域      |

| Block Scope        | 块作用域(ES6) |

| eval Scope          | eval作用域      |

作用域定义了一套规则,这套规则定义了引擎如何在当前作用域或嵌套作用域根,据标识符来查询变量。反过来说N个作用域组成的`作用域链`决定了函数作用域内标识符查找后返回的值。

所以作用域确定了当前上下文内定义的变量的可见性,即子作用域可以访问到当前作用域内属性、函数。并且作用域链(Scope Chain)也确定了在当前上下文中查找标识符后返回的值。

![用](作用域.png)

> Scope分为Lexical Scope和Dynamic Scope。Lexical Scope正如字面意思,即词法阶段定义的Scope。换种说法,作用域是根据源代码中变量和块的位置,在词法分析器(lexer)处理源代码时设置。javascript 采用的就是词法作用域。

## 作用域规则

作用域限制了函数内变量、函数的可访问性。在函数内部申明的属性、函数属于该函数的私有属性,不对函数外部代码暴露,同时函数内部申明的`嵌套函数`继承了对当前函数内属性、函数的访问权。具体规则如下:

- 如果变量 a 在函数内部定义, 则函数内部其他变量具有访问变量 a 的权限,但是函数外部代码没有访问变量 a 的权限。所以同一作用域内变量可以相互访问,即 a、b、c 在同一个作用域他们就可以相互访问。这就像鸡妈妈有宝宝,鸡宝宝可以相互打闹,其他鸡就不能跟他们打闹了,为什么? 因为鸡妈妈不容许~ o(^∀^)o 。

```javascript

let a = 1

function foo () {

    let b = 1 + a

    let c = 2

    console.log(b) // 2

}

console.log(c) // error 全局作用无法访问到 c

foo()

```

- 如果变量 a 在全局作用域下定义(window/global),则全局作用域下的局部作用域内的执行代码或者说是表达式都可以访问到变量 a 的值。局部变量里的同名变量(a)会截断对全局变量 a 的访问。(这里的变量 a 就相当于是饲养员,候饲养员会在合适的时候给鸡儿们投食。但是农场主为了节约成本,规定饲养员要就近给鸡投食,当饲养员1离鸡宝宝更近时其他饲养员就不能千里迢迢跨过鸭绿江去喂鸡了。)

```javascript

let a = 1

let b = 2

function foo () {

    let b = 3

    function too () {

        console.log(a) // 1

        console.log(b) // 3

    }

    too()

}

foo()

```

再次强调 javascript 作用域会严格限制变量的可访问范围: 即根据源代码中代码和块的位置,嵌套作用域拥有对被嵌套作用域(外层作用域)的访问权限。(这一条规则说明整个农场是有规则的,不能反向的投食。)

## 作用域链(Scope Chain)

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

上面解释的稍微有些晦涩,对于我这样大脑不好使的就需要在大脑里重复的'读'几次才能明白。那么作用域链是干嘛的? 简单的说作用域链就是管理函数申明是形成的作用域嵌套(依赖)关系,并在函数运行阶段解析函数访问标识符的`值`。

再简单点解释作用域链是干嘛的:作用域链就是用来查找变量的,作用域链是由一系列作用域串联起来的。

### 作用域链的访问

在函数执行过程中,每遇到一个变量,都会经历一次标识符解析过程,以决定从哪里获取和存储数据。该过程从作用域链头部,也就是当前执行函数的作用域开始(下图中从左向右),查找同名的标识符,如果找到了就返回这个标识符对应的值,如果没找到继续搜索作用域链中的下一个作用域,如果搜索完所有作用域都未找到,则认为该标识符未定义。函数执行过程中,每个标识符值得解析都要经历这样的搜索过程。

![](D:\work\Plist\javascriptSchool\ts.png)

为了具象化分析问题,我们可以假设作用域链是一个数组(Scope Array),数组成员有一系列变量对象组成。我们可以在数组这个单向通道中,也就是上图模拟从左向右查询变量对象中的标识符,这样就可以访问到上一层作用域中的变量了。直到最顶层(全局作用域),并且一旦找到,即停止查找。所以内层的变量可以屏蔽外层的同名变量。想象一下如果变量不是按从内向外的查找,那整个语言设计会变得N复杂了(我们需要设计一套复杂的鸡宝宝找食物的规则)

还是上面的栗子:

```javascript

let a = 1

let b = 2

function foo () {

    let b = 3

    function too () {

        console.log(a) // 1

        console.log(b) // 3

    }

    too()

}

foo()

```

作用域嵌套结构是这样的:

![用域 (1](作用域2.png)

栗子中,当 javascript 引擎执行到函数 too 时, 全局、函数 foo、函数 too 的上下文分别会被创建。上下文内包含它们各自的变量对象和作用域链(注意: 作用域链包含可访问到的上层作用域的变量对象,在上下文创建阶段根据作用域规则被收集起来形成一个可访问链),我们设定他们的变量对象分别为VO(global),VO(foo), VO(too)。而 too 的作用域链,则同时包含了这三个变量对象,所以 too 的执行上下文可如下表示:

```javascript

too = {

    VO: {...},  // 变量对象

    scopeChain: [VO(too), VO(foo), VO(global)], // 作用域链

}

```

我们直接用`scopeChain`来表示作用域链数组,数组的第一项scopeChain[0]为作用域链的最前端(当前函数的变量对象),而数组的最后一项,为作用域链的最末端(全局变量对象 window )。注意,所有作用域链的最末端都为全局变量对象。

再举个栗子:

```javascript

let a = 1

function foo() {

    console.log(a)

}

function too() {

    let a = 2

    foo()

}

too() // 1

```

这个栗子如果对作用域的特点理解不透彻很容易以为输出是2。但其实最终输出的是 1。 foo() 在执行的时候先在当前作用域内查找变量 a 。然后根据函数定义时的作用域关系会在当前作用域的上层作用域里查找变量标识符 a,所以最后查到的是全局作用域的 a 而不是 foo函数里面的 a 。

> 变量对象、执行上下文会在后面介绍。

## 闭包

在`JavaScript`中,函数和函数声明时的词法作用域形成闭包。或者更通俗的理解为闭包就是能够读取其他函数内部变量的函数,这里把`闭包`理解为函数内部定义的函数。

我们来看个闭包的例子

```javascript

let a = 1

function foo() {

  let a = 2

  function too() {

    console.log(a)

  }

  return too

}

foo()() // 2

```

这是一个闭包的栗子,一个函数执行后返回另一个可执行函数,被返回的函数保留有对它定义时外层函数作用域的访问权。`foo()()` 调用时依次执行了 foo、too 函数。too 虽然是在全局作用域里执行的,但是too定义在 foo 作用域里面,根据作用域链规则取最近的嵌套作用域的属性 a = 2。

再拿农场的故事做比如。农场主发现还有一种方法会更节约成本,就是让每个鸡妈妈作为家庭成员的‘饲养员’, 从而改变了之前的‘饲养结构’。

从作用域链的结构可以发现,`javascript`引擎在查找变量标识符时是依据作用域链依次向上查找的。当标识符所在的作用域位于作用域链的更深的位置,读写的时候相对就慢一些。所以在编写代码的时候应尽量少使用全局代码,尽可能的将全局的变量缓存在局部作用域中。

不加强记忆很容记错作用域与执行上下文的区别。代码的执行过程分为编译阶段和解释执行阶段。始终应该记住`javascript`作用域在源代码的编码阶段就确定了,而作用域链是在编译阶段被收集到执行上下文的变量对象里的。所以作用域、作用域链都是在当前运行环境内代码执行前就确定了。这里暂且不过多的展开执行上下文的概念,可以关注后续文章。

闭包的一些优缺点

闭包的用处:

- 用于保存私有属性:将不需要对外暴露的属性、函数保存在`闭包函数`的`父函数`里,避免外部操作对值的`干扰`

- 避免局部属性污染全局变量空间导致的`命名空间`混乱

- 模块化封装,将对立的功能模块通过闭包进去封装,只暴露较少的 API 供外部应用使用

闭包的缺点:

- 内存消耗:由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。

- 导致内存泄露:由于IE的 js 对象和 DOM 对象使用不同的垃圾收集方法,因此闭包在IE中会导致内存泄露问题,也就是无法销毁驻留在内存中的元素。解决方法是,在退出函数之前,将不使用的局部变量全部删除)。

> 编译阶段和解释执行阶段会在变量对象一节详细介绍。

关于闭包会的一些其他知识点在后面的章节里也会有提及,尽请关注。

## 思考

最后,再来看一个面试题:

```javascript

for (var i = 0; i < 5; i++) {

    setTimeout(function() {

        console.log(i);

    }, 1000);

}

// 5 5 5 5 5

```

要求对上面的代码进行修改,使其输出'0 1 2 3 4'

这里也涉及到作用域链的概念,当然跟 javascript 的执行机制也有关。修改方式有很多种,下面给出一种:

```javascript

for (var i = 0; i < 5; i++) {

    setTimeout(function() {

        console.log(i);

    }(i), 1000);

}

// 0 1 2 3 4

```

详细原理分析会在`javascript 执行机制`一节详细介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容