day03 JS作用域链、闭包、变量提升

221 阅读6分钟
  • 作用域链 前提:当 JavaScript执行一段可执行的代码时,会创建对应的上下文关系(主要是函数执行的时候)。执行完毕之后,上下文关系会自动消失。

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

下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。

函数的作用域在函数定义的时候就决定了。这是因为函数内部有一个属性 [[scoped]], 当函数创建的时候,就会保存所有父变量到其中,可以理解为 [[scoped]] 就是所有父变量对象的层级链,但是 [[scoped]] 并不是完整的作用域链。

举个例子:

function foo() {
    function bar() {
        ...
    }
}

函数创建时,各自的 [[scoped]]

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

函数激活时,也就是开始运行时候,进入函数上下文,就会将活动对象添加到作用域的前端。(可以粗略理解为函数内部的变量、参数、)

至此,作用域链创建完毕。

  • 闭包

MDN对闭包的定义为

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

那么什么是自由变量呢

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

由此我们得出

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

举个例子:

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

所以红宝书里说: 从技术角度来说,所有的JavaScript函数都是闭包。

当然这是理论上的闭包

为了更好的理解以下内容,我们先看一下什么叫 js执行上下文,以及执行上下文与作用域是什么关系

  1. 一段代码块对应一个执行上下文,被封装成函数的代码被视作一段代码块,或者“全局作用域”也被视作一段代码块。
  2. 当程序运行,进入到某段代码块时,一个新的执行上下文被创建,并被放入一个 stack 中。当程序运行到这段代码块结尾后,对应的执行上下文被弹出 stack。
  3. 当程序在某段代码块中运行到某个点需要转到了另一个代码块时(调用了另一个函数),那么当前的可执行上下文的状态会被置为挂起,然后生成一个新的可执行上下文放入 stack 的顶部。
  4. stack 最顶部的可执行上下文被称为 running execution context。当顶部的可执行上下文被弹出后,上一个挂起的可执行上下文继续执行。
  5. 在一个函数被执行时,创建的执行上下文对象除了保存了些代码执行的信息,还会把当前的作用域保存在执行上下文中。所以它们的关系只是存储关系。

从实践角度来说,以下函数才算是闭包:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如内部函数从父函数中返回)
  2. 在代码中引用了自由变量

不说了,贴代码 (代码来源于红宝书)

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope();
foo();

这段代码简要的执行过程

  1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
  2. 全局执行上下文初始化
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  7. f 执行上下文初始化,创建变量对象、作用域链、this等
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

了解到这个过程,我们应该思考一个问题,那就是:

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:也就是上述国程的第7步

fContext = { Scope: [AO, checkscopeContext.AO, globalContext.VO], }

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

来个代码加深一下理解

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();
// 输出都是 3 

让我们分析一下原因 当执行到 data[0]函数之前,此时的全局上下文 VO为

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行data[0]的时候,data[0]函数的作用域链为

data[0]Context = {
    Scope: [AO, gloabalContext.VO]
}

里面并没有i值,所以去globalContext.VO中查找, i为3,所以打印的结果都是3

··································分割线·············································

改成闭包


var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0](); // 0
data[1](); // 1
data[2](); // 2

这个时候 执行到 data[0]函数的时候,作用域链发生了变化

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

多了一个匿名函数的AO,发现匿名函数Context.AO中 有 i, 找到就停止了,即使globalContext.VO中也有i为3,打印的就是 0 1 2。

  • 变量提升

不多说,直接看代码

console.log(v1);
var v1 = 100;
function foo() {
    console.log(v1);
    var v1 = 200;
    console.log(v1);
}
foo();
console.log(v1);
// undefined  undefined 200  100
  1. 所有的声明都会提升到作用域的最顶上去。
  2. 同一个变量只会声明一次,其他的会被忽略掉或者覆盖掉。

再看函数提升:

bar()

var bar = function() {
  console.log(1);
}
// 报错:TypeError: bar is not a function
bar()
function bar() {
  console.log(1);
}
  1. 函数 变量形式声明 和普通变量一样 提升的 只是一个没有值的变量。
  2. 函数声明式的提升现象和变量提升略有不同,函数声明式会提升到作用域最前边,并且将声明内容一起提升到最上边
  3. 函数声明的优先级高于变量声明的优先级,并且函数声明和函数定义的部分一起被提升。