做用域、执行上下文、闭包


title: JavaScript中的做用域、执行上下文与闭包 date: 2019-03-18 08:34:44 tags:javascript


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

做用域 (Scope)

什么是做用域

做用域是定义变量的区域。java

它规定了执行代码时查找变量的范围,也就是变量的做用范围。git

JavaScript 采用词法做用域(lexical scoping),也就是静态做用域。github

由于 JavaScript 采用的是词法做用域,函数的做用域在函数定义的时候就决定了面试

var a = 1
function foo1() {
    console.log(a)
}
function foo2() {
    var a = 2
    foo1()
}

foo2()  // 1
复制代码

在 JavaScript 中无块级做用域(一对儿 { } 包含的区域 ),只有全局做用域函数做用域segmentfault

if (true) {
    var name = 'abc'
}
console.log(name)  // 'abc'
复制代码

同一个页面中全部的 <script> 标签中的没在函数体内的变量,都在同一个全局做用域中。浏览器

执行上下文 (Execution context)

什么是执行上下文

执行上下文能够抽象成一个对象,当一个函数执行时就会建立一个执行上下文bash

每一个执行上下文,都有三个属性:闭包

  • 变量对象(Variable object, VO)
  • 做用域链(Scope chain)
  • this

既然每执行一个函数就会建立一个执行上下文,咱们写的函数可不止一个,那么如何管理众多的上下文呢?

JavaScript 引擎建立了执行上下文栈(Execution context stack, ECS) 来管理执行上下文

JavaScript 引擎在执行代码时,最早遇到的是全局代码,会建立一个全局上下文(globalContext)并将之压入执行上下文栈(ECS)中,当执行一个函数时,建立一个函数上下文并压入 ECS 中。函数执行完毕时,这个函数上下文会出栈并被清理。当整个程序执行完毕时,globalContext 也会出栈并被清理。

看一个例子:

function fun1() {
    fun2()
}

function fun2() {
    console.log('fun2')
}

fun1()
复制代码

当 JavaScript 引擎执行代码时,先建立一个全局上下文(globalContext) 压入执行上下文栈(ECS)中。

当执行到 fun1() 时,建立一个函数上下文(EC_fun1)并压入栈中。

而后执行 fun1 函数体里的 fun2(),建立 EC_fun2并压入栈中。

执行 fun2 函数里的 console.log() 函数,建立 EC_consolelog 压入栈中。

而后 console.log 执行完毕,EC_consolelog 出栈并销毁。

而后 fun2 执行完毕,EC_fun2 出栈并销毁。

fun1 执行完毕,EC_fun1 出栈并销毁。

变量对象 (Variable object, VO)

变量对象是与执行上下文相关的数据做用域,它存储了在该上下文定义的变量和函数声明。变量对象是在建立函数上下文时建立的,它经过函数的 arguments 属性初识化。

变量对象包括:

  1. 函数的全部形参(若是是函数上下文)
    • 由名称和对应值组成一个变量对象(VO)的属性
  2. 函数声明
    • 由名称和对应值(函数对象)组成一个变量对象(VO)的属性
    • 若是 VO 已存在相同的名称属性,则替换这个属性
  3. 变量声明
    • 由名称和 undefined 组成一个 VO 的属性
    • 若是名称和已经声明的形参或函数相同,变量声明不会干扰已存在的这类属性

举个例子:

function foo(a) {
    var b = 2
    var a = 10
    function c() {}
    var d = function() {}
}

foo(11)
复制代码

在执行 foo(11)时,建立一个执行上下文,此时执行上下文的变量对象时:

VO = {
    arguments: {
        0: 11,
        length: 1
    },
    a: 11,
    b: undefined,
    c: reference to function c() {},
    d: undefined
}
复制代码

全局对象

  • 全局上下文的变量对象就是全局对象。
  • 全局上下文的 this 指向的是全局对象。
  • 在浏览器环境下全局对象是 window ,Node.js 环境下全局对象是 global

活动对象 (Activation object,AO)

活动对象与变量对象实际上是同一个东西,只有当进入一个执行上下文中(这个执行上下文处在执行上下文栈的栈顶),这个执行上下文的变量对象(VO)才会被激活,此时这个变量对象叫作活动对象(AO)。只有活动对象上的各类属性才能被访问。

做用域链 (Scope chain)

当查找变量时,会在当前执行上下文的变量对象(也是活动对象)中查找,若是没找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象(也是全局对象)。这样由多个执行上下文的变量对象构成的链表就叫作做用域链

函数定义时,有一个内部属性 [[scope]] 保存了全部父变量对象。当函数执行时会建立一个做用域链,这个做用域链包含了函数的 [[scope]] 属性和执行上下文的活动对象(AO)。

var a = 1
var b = 1
function foo() {
    var b = 2
    return a + b
}
foo()  // 3
复制代码

执行过程以下:

  1. foo 函数被建立,foo 会将父变量对象(实际上是当前活动对象也是全局对象)保存在 [[scope]] 属性中。
foo.[[scope]] = [globalContext.VO]
复制代码
  1. 执行 foo 函数,建立 foo 函数执行上下文并压入执行上下文栈(ECS)中。
ECS = [ fooContext,
        globalContext]
复制代码
  1. foo 函数并不马上执行,须要一些准备工做,第一步:复制函数的 [[scope]] 属性到建立的做用域链(Scope)中
fooContext = {
    Scope: foo.[[scope]]
}
复制代码
  1. 第二步:建立活动对象并用函数的 arguments 属性初始化
fooContext = {
    AO: {
        arguments: {
            length: 0
        },
        b: undefined
    },
    Scope: [
            foo.[[scope]]
                         ]
}
复制代码
  1. 第三步:将活动对象压入 做用域链顶端
fooContext = {
    AO: {
        arguments: {
            length: 0
        },
        b: undefined
    },
    Scope: [
            AO,
            foo.[[scope]]
                         ]
}
复制代码
  1. 准备工做作完,开始执行 foo 函数里的代码,更新 AO 的属性值
AO:{
    arguments: {
        length: 0
    },
    b: 2
}
复制代码
  1. foo 执行完毕,foo 函数上下文出栈并销毁
ECS = [
       globalContext
                     ]
复制代码

闭包

什么是闭包

MDN 对闭包的定义为:

闭包是指那些可以访问自由变量的函数

那什么是自由变量呢?

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

自由变量实际上是指函数执行上下文的做用域链中非活动对象的那部分属性(也就是外层做用域的变量 函数.[[scope]])

因此在《JavaScript高级程序设计中》是这样描述的:

闭包是指有权访问另外一个函数做用域中的变量的函数。

var a = 1
function foo() {
    console.log(a)
}
foo()
复制代码

foo 函数能够访问变量a,但 a 既不是函数参数也不是函数的局部变量,a 就是自由变量。
那么,foo 函数就是一个闭包。

到这里,你或许会有疑问

这怎么和咱们平时知道的闭包不是同一个,不是什么函数中嵌套一个函数,里面的函数才是一个闭包,这里怎么没有嵌套函数了?

实际上是站的角度不一样:

  1. 从理论角度:全部函数都是闭包。由于他们都在建立时就将上层执行上下文的数据保存起来了(函数的 [[scope]] 属性)。因此在函数中能够做用域链访问到外层做用域的变量,也就是能够访问自由变量,它就是一个闭包。
  2. 从实际角度:如下才是闭包:
    • 即便建立它的上下文已经销毁,但它依然存在(好比,内部函数从父函数中返回)
    • 在代码中引用了自由变量

举个例子:

function foo() {
    var a = 0
    return function add() {
        console.log(a++)
    }
}
var add1 = foo()
var add2 = foo()

add1()  // 0
add1()  // 1
add1()  // 2
add2()  // 0
add2()  // 1
复制代码

foo() 函数执行完毕,foo函数执行上下文已经被销毁,那么上下文的变量对象中的 a 变量应该也被销毁了啊,问啥还能访问到?

由于 add 函数在定义时就存在一个 [[scope]] 属性,它保存了 foo 函数执行上下文的变量对象,在执行 add 函数时,会建立一个执行上下文链并将 add.[[scope]] 复制到该执行上下文的做用域链中,因此在 add 函数中能够经过做用域链访问到 a 属性。

你可能还有一个问题,为何 add1()add2() 访问的不是同一个 a
由于每执行一次函数就会建立一个函数执行上下文,因此执行 add1 = foo()add2 = foo() 产生的是不一样的执行上下文(对象),他们的 a 属性固然不一样了。

闭包的面试题

经典面试题,使用闭包解决 for 循环中 var 异步打印 i 值的问题

for (var i = 0; i < 5 ; i++) {
    setTimeout(function foo() {
        console.log(i)
    },1000 * i)
}
复制代码

以上代码执行结果是:5 5 5 5 5

这里面涉及到事件循环机制,这里就很少赘述,简单的说就是等 for 循环结束后,才开始依次执行这几个 setTimeout() 里面的 foo 函数。

JS 没有块级做用域,此时的 i 值为 5 ,console.log(i) 访问的就是全局变量 i ,因此打印 5。

咱们要作就是使用闭包的特性,让 console.log(i) 访问的不是全局变量 i

for (var i = 0; i < 5 ; i++) {
    ;(function(i) {
        setTimeout(function foo() {
        console.log(i)
        },1000 * i)
    })(i)
}
复制代码

或者这样:

for (var i = 0; i < 5 ; i++) {
    setTimeout((function foo(i) {
        return function() {
             console.log(i)
        } 
    })(i),1000 * i)
}
复制代码

也可使用 ES6 中的 let 换掉 var,使得 for 循环中 i 成为一个块级做用域的本地变量。

for (let i = 0; i < 5 ; i++) {
    setTimeout(function foo() {
        console.log(i)
    },1000 * i)
}
复制代码

闭包的优缺点

闭包能够建立私有属性和方法。

var singel = (function () {
    // 私有属性,外部访问不到
    var age = 20
    function foo() {
        console.log('foo')
    }
    return {
        // 公有属性
        name: 'Tom',
        getAge: function() {
            return age
        },
        setAge: function(n) {
            age = n
        }
    }
})()

console.log(singel.age)  // undefined
singel.foo()  // Uncaught TypeError: singel.foo is not a function
console.log(singel.getAge())  // 20
singel.setAge(10) 
console.log(singel.getAge())  // 10
复制代码

单例:指的是只有一个实例的对象。JavaScript 通常以字面量的方式来建立单例。

匿名函数最大的用途就是建立闭包。而且还能够构建命名空间,减小全局变量的污染。

经过匿名函数实现一个闭包计数器:

var numberCounter = (function() {
    var num = 0
    return function() {
        return ++num
    }
})()
复制代码

闭包的缺陷:

  • 闭包所访问的自由变量会常驻内存增大内存的使用量,所以闭包滥用会形成网页性能问题。在老版本浏览器中因为垃圾回收有问题致使内存泄漏。正常使用闭包不会致使内存泄漏。

总结

  • 做用域是定义变量的区域,规定了变量的访问范围,它在函数定义时肯定。
  • 执行上下文是一个对象,在函数执行时建立,它有变量对象、做用域链、this 三个属性。
  • 函数执行时,变量对象经过函数的 arguments 属性初始化,它包含函数的参数、函数体里声明的变量。
  • 函数执行时,做用域链是由函数的 [[scope]] 属性中的变量 + 活动对象中的变量组成的。
  • 闭包是访问外部做用域的变量的函数。
  • 闭包能够建立私有属性和方法。
  • 闭包滥用会影响页面性能。

阅读原文

参考资料:

github.com/mqyqingfeng…

segmentfault.com/a/119000000…

www.kancloud.cn/kancloud/ja…

blog.csdn.net/qq_27626333…

相关文章
相关标签/搜索