Function in JavaScript (Part 2)
Contents
闭包
闭包让函数可以访问到定义函数的动作发生时(定义函数的当时),函数所处的作用域里的全部变量,即便在外部函数返回后,外部函数中的函数仍能访问到那些变量。
- 只要还存在对那些变量的引用,那部分内存就无法被回收,这也意味着额外的内存开销。
// 全局函数访问全局变量可以看成闭包的一个特例
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 中可以用 var
,let
和 const
这三个关键字定义变量。
用 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 引入的 let
和 const
更直截了当,它们在最近的词法环境(可以是代码块环境、循环环境、函数环境、全局环境)里定义变量。
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 代码的执行包含两个阶段:
- 当一个新的词法环境被创建时,第一阶段被激活,此时代码未被执行,JavaScript 引擎访问并注册当前词法环境中定义的所有变量和函数,具体过程取决于变量类型(
let
,var
,const
,函数声明)和词法环境的的类型(全局,函数,代码块)。- 若创建的是一个函数环境,函数参数和传入的实参的值、隐式的
arguments
标识符也被创建出来。- 若创建的不是函数环境,此步骤被跳过。
- 若创建的是全局环境或函数环境,会对当前代码里的函数声明(不包括函数表达式和箭头函数)进行扫描。
- 这一步骤发生在对代码的求值(evaluated)之前。
- 对于每一个扫描得到的函数声明,会在当前环境中创建一个函数,并将函数与名称是函数声明里的函数名的标识符绑定(注册),若该标识符已经存在,它的值会被覆盖。
- 若创建的是块环境,此步骤被跳过。
- 扫描当前代码里的变量声明。
- 在函数或全局环境里,所有用
var
声明且在其它函数外(但可以在块里)的变量、所有用let
和const
声明且在其它函数和代码块外的变量都被会找出来。 - 在块环境里,只有用
let
和const
直接在当前代码块里声明的变量会被找出来。 - 对于所有找到的变量,若当前标识符在环境中尚不存在,则该标识符被注册并赋值为
undefined
,若已存在,则保留已有的值。
- 在函数或全局环境里,所有用
- 若创建的是一个函数环境,函数参数和传入的实参的值、隐式的
- 第二个阶段是代码执行阶段,在这个阶段中,函数的声明是被跳过的。
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