讲清楚之javascript做用域

什么是做用域(Scope)?

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

做用域:前端

做用域(Scope) -
window/global Scope 全局做用域
function Scope 函数做用域
Block Scope 块做用域(ES6)
eval Scope eval做用域

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

因此做用域肯定了当前上下文内定义的变量的可见性,即子做用域能够访问到当前做用域内属性、函数。而且做用域链(Scope Chain)也肯定了在当前上下文中查找标识符后返回的值。面试

图片描述

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

做用域规则

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

  • 若是变量 a 在函数内部定义, 则函数内部其余变量具备访问变量 a 的权限,可是函数外部代码没有访问变量 a 的权限。因此同一做用域内变量能够相互访问,即 a、b、c 在同一个做用域他们就能够相互访问。这就像鸡妈妈有宝宝,鸡宝宝能够相互打闹,其余鸡就不能跟他们打闹了,为何? 由于鸡妈妈不允许~ o(^∀^)o 。
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离鸡宝宝更近时其余饲养员就不能千里迢迢跨过鸭绿江去喂鸡了。)
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)

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

上面解释的稍微有些晦涩,对于我这样大脑很差使的就须要在大脑里重复的'读'几回才能明白。那么做用域链是干吗的? 简单的说做用域链就是管理函数申明是造成的做用域嵌套(依赖)关系,并在函数运行阶段解析函数访问标识符的模块化

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

做用域链的访问

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

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

仍是上面的栗子:

let a = 1
let b = 2
function foo () {
    let b = 3
    function too () {
        console.log(a) // 1
        console.log(b) // 3
    }
    too()
}
foo()

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

图片描述

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

too = {
    VO: {...},  // 变量对象
    scopeChain: [VO(too), VO(foo), VO(global)], // 做用域链
}

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

再举个栗子:

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中,函数和函数声明时的词法做用域造成闭包。或者更通俗的理解为闭包就是可以读取其余函数内部变量的函数,这里把闭包理解为函数内部定义的函数。

咱们来看个闭包的例子

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中会致使内存泄露问题,也就是没法销毁驻留在内存中的元素。解决方法是,在退出函数以前,将不使用的局部变量所有删除)。
编译阶段和解释执行阶段会在变量对象一节详细介绍。

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

思考

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

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

// 5 5 5 5 5

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

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

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

// 0 1 2 3 4

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

相关文章
相关标签/搜索