闭包

闭包让函数可以访问到定义函数的动作发生时(定义函数的当时),函数所处的作用域里的全部变量,即便在外部函数返回后,外部函数中的函数仍能访问到那些变量。

  • 只要还存在对那些变量的引用,那部分内存就无法被回收,这也意味着额外的内存开销。
// 全局函数访问全局变量可以看成闭包的一个特例
var outer = "ho"
function foo() {
    console.log(`I can see ${outer}`)
}
foo()

用闭包模拟“私有”变量:

function Person() {
    var age = 0
    this.getAge = function() {
        return age
    }
    this.grow = function() {
        age++
    }
}
var person = new Person()
console.log(person.age, person.getAge()) // undefined 0
person.grow()
console.log(person.getAge()) // 1

回调函数中使用闭包:

<div id="box1" style="position:absolute">first box</div>
<script>
    function animateIt(eleId) {
        // 好处在于多个动画元素的闭包变量互不干扰
        var ele = document.getElementById(eleId)
        var tick = 0
        var timer = setInterval(function() {
            if (tick < 100) {
                ele.style.left = ele.style.top = tick + "px"
                tick++
            } else {
                clearInterval(timer) // [ ]
            }
        }, 10)
    }
    animateIt("box1")
</script>

执行上下文

代码被 JavaScript 引擎执行时,每条语句都在特定的执行上下文(execution context)里。

  • 执行上下文是 JavaScript 的内部概念,和函数上下文 this 完全不同。

  • 执行上下文分为全局执行上下文函数执行上下文

    • 全局上下文只有一个,它在 JavaScript 程序开始运行时被创建。
    • 每次函数调用都会创建一个新的函数执行上下文。
  • 最简单的记录执行上下文的方式使用一个栈(执行上下文栈、调用栈)。

  • 除了记录程序的执行位置外,执行上下文还负责使用词法环境(lexical environment)进行标识符解析(identifier identifier),即确定标识符指向哪个变量。

    • 词法环境是 JavaScript 引擎内部的一个结构,用于记录标识符和变量之间的映射关系,它是 JavaScript 作用域的内部实现机制,因此也被通俗地称为作用域(scope)。
    • 一个词法环境可以是函数、代码块、try-catch 语句的 catch 部分。
      • ES6 以前 JavaScript 的词法环境只能是一个函数。
    • 除了记录局部变量、函数声明、函数参数每个词法环境还必须记住它的外层(父层)词法环境;如果在当前词法环境没有找到标识符,就会转向外层词法环境继续找,若在最外的全局词法环境仍没要到,则报引用错误。
    • JavaScript 利用函数作为一等对象的特点来记录外层此法环境。
      • 当一个函数被创建(定义)时,指向它的外层词法环境的引用被存储到该函数的名为 [[Environment]] 的内部属性中。
        • [[]] 是内部属性的标志;内部属性不能直接访问、操作。
        • 当一个函数被调用时,一个新的函数执行上下文被创建被放至调用栈栈顶,此外,一个与之关联的词法环境也被创建出来
        • JavaScript 引擎将被调用函数的 [[Environment]] 属性引用的词法环境作为新创建的词法环境的外层词法环境。
    • 不直接遍历整个调用栈来解析标识符的原因是 JavaScript 函数作为一等对象,可以被任意传递,定义函数的位置和调用函数的位置通常不相关
    var a = 1
    function foo() {
        console.log(a)
    }
    function bar() {
        var a = 0
        var obj = {
            func: foo
        }
        foo()       // * 1
        obj.func()  // * 1
    }
    bar()
    

变量类型

JavaScript 中可以用 varletconst 这三个关键字定义变量。

const 定义的变量声明时必须初始化,初始化后值便不能对它赋一个全新的值,即不能覆盖,但可以修改原来的值(对引用类型而言)。

const str = "js"
try {
    str = "go"
} catch(e) {
    console.log(e)      // TypeError: Assignment to constant variable.
}

const constObj = {}
constObj.name = "awesome"   // ok
try {
    constObj = {}
} catch(e) {
    console.log(e)       // TypeError: Assignment to constant variable.
}

const constArr = []
constArr.push(1)            // ok

var 定义的变量的作用域是最近的(closest)包含该变量的函数或全局作用域,没有块级作用域(无视代码块)。

function foo() {
    var fooLocal = 123
    for (var i = 0; i < 3; i++) {}
    console.log(i) // 3
}
foo()
console.log(typeof fooLocal) // undefined

ES6 引入的 letconst 更直截了当,它们在最近的词法环境(可以是代码块环境、循环环境、函数环境、全局环境)里定义变量。

function foo() {
    let fooLocal = 123
    for (let i = 0; i < 3; i++) { console.log(fooLocal + i) }
    console.log(typeof i) // undefined
}
foo()
console.log(typeof fooLocal) // undefined

JavaScript 代码逐行解释执行。

JavaScript 代码的执行包含两个阶段:

  1. 当一个新的词法环境被创建时,第一阶段被激活,此时代码未被执行,JavaScript 引擎访问并注册当前词法环境中定义的所有变量和函数,具体过程取决于变量类型(letvarconst,函数声明)和词法环境的的类型(全局,函数,代码块)。
    1. 若创建的是一个函数环境,函数参数和传入的实参的值、隐式的 arguments 标识符也被创建出来。
      • 若创建的不是函数环境,此步骤被跳过。
    2. 若创建的是全局环境或函数环境,会对当前代码里的函数声明(不包括函数表达式和箭头函数)进行扫描。
      • 这一步骤发生在对代码的求值(evaluated)之前
      • 对于每一个扫描得到的函数声明,会在当前环境中创建一个函数,并将函数与名称是函数声明里的函数名的标识符绑定(注册),若该标识符已经存在,它的值会被覆盖
      • 若创建的是块环境,此步骤被跳过。
    3. 扫描当前代码里的变量声明。
      • 在函数或全局环境里,所有用 var 声明且在其它函数外(但可以在块里)的变量、所有用 letconst 声明且在其它函数和代码块外的变量都被会找出来。
      • 在块环境里,只有用 letconst 直接在当前代码块里声明的变量会被找出来。
      • 对于所有找到的变量,若当前标识符在环境中尚不存在,则该标识符被注册并赋值为 undefined,若已存在,则保留已有的值。
  2. 第二个阶段是代码执行阶段,在这个阶段中,函数的声明是被跳过

JavaScript 中函数声明的顺序不重要,可以在函数声明之前就调用函数;JavaScript 这样实现的目的是减轻开发者的心智负担。

此过程不适用于函数表达式和箭头函数,它们要在程序执行到定义时才被创建。

console.log(typeof foo, typeof fooExpr, typeof fooArrow) // function undefined undefined
function foo() {}
var fooExpr = function() {}
var fooArrow = () => {}


console.log(typeof foo, foo()) // function foo
var foo = 3
console.log(typeof foo) // number
function foo() { return "foo" }
console.log(typeof foo) // number
foo() // Uncaught TypeError: foo is not a function

澄清

函数上下文 this 表示函数在哪个对象上被调用

执行上下文记录程序的执行位置,使用词法环境进行标识符解析。

  • 全局上下文在程序执行时创建;
  • 函数执行上下文在每次调用函数时都会被创建并置于调用栈栈顶,同时一个与该函数执行上下文关联的词法环境也被创建出来
    • 函数的外层词法环境在函数定义时即确定下来了,指向该函数的外层词法环境的引用存储在函数的 [[Environment]] 属性中。
    • JavaScript 引擎将被调用函数的 [[Environment]] 属性引用的词法环境作为新创建的词法环境的外层词法环境。
    • 词法环境记录标识符和变量之间的映射关系,是作用域的内部实现机制,也被称为作用域
      • 词法环境记录局部变量、函数声明、函数参数和它的外层词法环境。
    • 一个词法环境可以是函数、代码块、try-catch 语句的 catch 部分。

闭包模拟的“私有”变量并不真正私有:

function Person() {
    var age = 0
    this.getAge = function() {
        return age
    }
    this.grow = function() {
        age++
    }
}
var person = new Person()
console.log(person.age, person.getAge()) // undefined 0
person.grow()

var badPerson = {}
badPerson.getAge = person.getAge
console.log(badPerson.getAge()) // 1