作用域、作用域链及闭包

1,041 阅读5分钟

更多文章

前言

最近看了些文章,感觉自己之前对知识的了解还是比较浅的,在此聊一下对作用域作用域链闭包新的理解

作用域

用一句话来描述:作用域就是标识符(变量)在程序中的可见范围

MDN是这么解释的:当前的执行上下文。值和表达式在其中 "可见" 或可被访问到的上下文

防止把作用域执行上下文搞混了,我们还是解释一下它们的关系:上下文作用域和环境是同义词,不过上下文指代的是整体的执行环境,作用域关心的则是标识符的可见性,上下文包含了this变量环境组件词法环境组件变量环境组件词法环境组件均是词法环境对象,这里作用域可以理解为词法环境作用域链也可以理解为词法环境链,这里引出了词法环境,我们稍后解释

  • js作用域静态作用域,也就是词法作用域,就是说作用域在你书写代码的过程中就已经确定了,用下边的代码来解释一下:
// demo-1
var sex = "男";
function A() {
    console.log(sex) // 男
}
function B() {
    var sex = "女"
    A()
    console.log(A.prototype)
}
B()
// A.prototype打印结果
// [[Scopes]]: Scopes[1]
// 0: Global {window: Window, self: Window, document: document, name: "xw", location: Location, …}
// 可以看出B不在A作用域链上

// demo-2
var sex = "男";
function B() {
    var sex = "女"
    function A() {
        console.log(sex) // 女
    }
    A()
    console.log(A.prototype)
}
B()
// A.prototype打印结果
// [[Scopes]]: Scopes[2]
// 0: Closure (B) {sex: "女"}
// 1: Global {window: Window, self: Window, document: document, name: "xw", location: Location, …}

// 上边连个demo就能够确定,作用域在定义阶段就已经确定了

词法环境

词法环境是一种规范类型,基于ECMAScript代码的词法嵌套结构来定义变量和函数的关联关系,执行上下文创建阶段词法环境组件变量环境组件最初是同一个值,在执行环境相关联的代码的执行过程中,变量环境组件永远不变,而词法环境组件有可能改变(对于这句话的解释请看连接

  • 组成
  1. 环境记录(EnvironmentRecord):储存变量和函数声明的实际位置

词法环境组件(LexicalEnvironment)中保存着let、const、函数声明的变量以及外部环境引用

变量环境组件(VariableEnvironment)中保存着var声明的变量

  1. 对外部环境的引用(Outer):当前可以访问的外部词法环境,上下文所有本地绑定存储在环境记录表中,在当前环境记录中无法查询到某个变量将会到外部(父级)环境记录表中查询,直到找到为止,如果没有找到将会抛出错误: ReferenceError
  • 类型
  1. 全局环境:外部引用为null,环境记录保存着window、关联的方法属性和定义的全局变量
  2. 函数环境:外部引用可以是全局环境也可以是外部函数环境,环境记录中保存着函数内定义的变量、函数及arguments
// 全局环境
GlobalExectionContent = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      // 剩余标识符
    },
    Outer: null,
  }
}
// 函数环境
FunctionExectionContent = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 剩余标识符
    },
    Outer: [Global or outer function environment reference],
  }
}

当在一个函数内查询一个变量,会现在当前环境查询,所谓当前环境查询就是在当前执行上下文保存的词法环境的环境记录中查询,若查不到则到引用的外部环境中查,这也就是我们说的作用域链(词法环境链)

demo为例分析一下:

// demo
var sex = "男";
const height = "170cm";
let age = "18"
function B(name) {
    var sex = "女"
    function A() {
      console.log(sex) // 女
    }
    A()
}
B('老王')
// 词法环境(预编译阶段)
// 全局词法环境
GlobalExectionContent = {
  // 全局词法环境组件
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      height: <uninitialied>,
      age: <uninitialied>,
      B: <func>
      // 剩余标识符
    },
    Outer: null // 外部引用为null
  },
  // 全局变量环境组件
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Object",
      sex: undefined
      // 剩余标识符
    },
    Outer: null,
  }
}
// 函数B词法环境
FunctionExectionContentB = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      arguments: { 0: '老王', length: 1 },
      A: <func>
      // 剩余标识符
    },
    Outer: GlobalExectionContent,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      sex: undefined
      // 剩余标识符
    },
    Outer: GlobalExectionContent // // 外部引用环境为B词法环境
  }
}
// 函数A词法环境
FunctionExectionContentA = {
  LexicalEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      arguments: { length: 0 }
      // 剩余标识符
    },
    Outer: FunctionExectionContentB,
  },
  VariableEnvironment: {
    EnvironmentRecord: {
      Type: "Declarative",
      // 剩余标识符
    },
    Outer: FunctionExectionContentB // 外部引用环境为B词法环境
  }
}

查阅资料时看到了两张图片系统的介绍了执行上下文和作用域,建议看一下

闭包

MDN定义:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

《你不知道的JavaScript》: 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行

个人总结一下认为关键点有两个:

  1. 函数
  2. 外部词法环境的访问

即闭包发生的对象是函数,而产生闭包的条件是对外部词法环境的访问,看下边两个demo:

// demo-1  
function test() {
  var name = "老王"
  function test1() {
    var sex = "18"
    return Number(sex)
  }
  test1()
}
// demo-2 
function test() {
  var name = "老王"
  function test1() {
    console.log(name)
  }
  test1()
}
// demo-1中的test1与全局和test词法环境没有发生访问,所以这里没有产生闭包
// demo-2中的test2与test1词法环境产生了访问,这个访问的就是name,所以这里产生了闭包
// 不在举例更多的demo,网上可参考案例非常多,这里只搞明白什么是闭包

按这个推测,所有和全局变量产生访问关系的函数都产生了闭包

闭包可以保存私有属性、避免全局变量污染、缓存变量,不被垃圾回收机制回收等特点,但同时内存消耗大,滥用会导致内存泄漏的问题

闭包在柯里化使用也很广泛,这里说一个经典的面试题

// 实现一个 add 方法:add(1)(2)(3) = 6;
function add() {
  const arr = [...arguments];
  const fn = function () {
    arr.push(...arguments);
    return fn;
  }
  fn.toString = function () {
    return arr.reduce((a, b) => a + b, 0);
  }
  return fn;
}
console.log(add(1)(2)(3)) // 6

结语

有些知识回头看一下,会有不一样的收获