执行上下文
JS 是通过执行上下文来运行的。一般来说,从启动程序开始,我们从 全局执行上下文 开始。在 全局执行上下文 中定义的变量,被称之为 全局变量。当程序调用一个函数时,将经历如下几个步骤:
1. JS 创建一个新的执行上下文,可以将其命名为 `XX函数执行上下文`,或 `本地执行上下文`。
2. 在这个 `本地执行上下文` 中,可能定义了一些变量,我们把它们叫做执行这个执行上下文的 `本地变量`。
3. 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。当函数遇到 return 或 } 时,将会结束:
1. 这个本地执行上下文从执行堆栈中弹出。
2. (Important) 函数将返回值返回给 `调用执行上下文` (也就是调用目前本地执行上下文的执行上下文),它可以是 `全局执行上下文`,也可以是另外一个函数的 `本地执行上下文`。返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有 return 语句,则返回 undefined 。
3. 这个本地执行上下文被销毁,这个本地执行上下文中声明的所有变量都将被删除。词法作用域
一个函数可以访问在它的调用执行上下文中定义的变量。来个简单的例子:
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)在这里先说一下 JS 寻找变量的方法:先在 本地执行上下文 中找,如果找不到就往 调用执行上下文 中找,直到 全局执行上下文,如果还是找不到,那就为 undefined。
接下来详细说一下上面这个例子的执行过程:
- 在全局执行上下文中声明一个新的变量
val1,并将其赋值为2。 - 第
2-5行,声明一个新的变量multiplyThis,并给它分配一个函数定义。 - 第
6行,声明一个在全局执行上下文multiplied新变量。 - 从全局执行上下文内存中查找变量
multiplyThis,并将其作为函数执行,传递数字6作为参数。 - 新函数调用(创建新执行上下文),创建一个新的
multiplyThis函数执行上下文。 - 在
multiplyThis执行上下文中,声明一个变量n并将其赋值为6。 - 第
3行,在multiplyThis执行上下文中,声明一个变量ret。 - 继续第
3行。对两个操作数n和val1进行乘法运算.在multiplyThis执行上下文中查找变量n。我们在步骤6中声明了它,它的内容是数字6。在multiplyThis执行上下文中查找变量val1。multiplyThis执行上下文没有一个标记为val1的变量。我们向调用上下文查找,调用上下文是全局执行上下文,在全局执行上下文中寻找val1。它在步骤1中定义,数值是2。 - 继续第
3行。将两个操作数相乘并将其赋值给ret变量,6 \* 2 = 12,ret现在值为12。 - 返回
ret变量,销毁multiplyThis执行上下文及其变量ret和n。变量val1没有被销毁,因为它是全局执行上下文的一部分。 - 回到第
6行。在调用上下文中,数字12赋值给multiplied的变量。 - 最后在第
7行,我们在控制台中打印multiplied变量的值。
返回函数的函数
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)- 第
2-8行。我们在全局执行上下文中声明了一个名为createAdder的变量,并为其分配了一个函数定义。第3-7行描述了上述函数定义,和以前一样,在这一点上,我们没有直接讨论这个函数。我们只是将函数定义存储到那个变量 (createAdder) 中。 - 第
9行。我们在全局执行上下文中声明了一个名为adder的新变量,暂时,值为undefined。 - 第
9行。我们看到括号(),我们需要执行或调用一个函数,查找全局执行上下文的内存并查找名为createAdder的变量,它是在步骤2中创建的。好吧,我们调用它。 - 调用函数时,执行到第
2行。创建一个新的createAdder执行上下文。我们可以在createAdder的执行上下文中创建自有变量。js引擎将createAdder的上下文添加到调用堆栈。这个函数没有参数,让我们直接跳到它的主体部分。 - 第
3-6行。我们有一个新的函数声明,我们在 createAdder 执行上下文中创建一个变量addNumbers。这很重要,addnumber只存在于createAdder执行上下文中。我们将函数定义存储在名为addNumbers的自有变量中。 - 第
7行,我们返回变量addNumbers的内容。js 引擎查找一个名为addNumbers的变量并找到它,这是一个函数定义。好的,函数可以返回任何东西,包括函数定义。我们返addNumbers的定义。第4行和第5行括号之间的内容构成该函数定义。 - 返回时,
createAdder执行上下文将被销毁。addNumbers变量不再存在。但addNumbers函数定义仍然存在,因为它返回并赋值给了adder变量。 - 第
10行。我们在全局执行上下文中定义了一个新的变量sum,先赋值为undefined; - 第
1行。我们在全局执行上下文中声明一个变量val并赋值为7。 - 接下来我们需要执行一个函数。哪个函数? 是名为
adder变量中定义的函数。我们在全局执行上下文中查找它,果然找到了它,这个函数有两个参数。 - 让我们查找这两个参数,第一个是我们在步骤
1中定义的变量val,它表示数字7,第二个是数字8。 - 现在我们要执行这个函数,函数定义概述在第
3-5行,因为这个函数是匿名,为了方便理解,我们暂且叫它adder吧。这时创建一个adder函数执行上下文,在adder执行上下文中创建了两个新变量a和b。它们分别被赋值为7和8,因为这些是我们在上一步传递给函数的参数。 - 第
4行。在adder执行上下文中声明了一个名为ret的新变量。 - 第
4行。将变量a的内容和变量b的内容相加得15并赋给ret变量。 ret变量从该函数返回。这个匿名函数执行上下文被销毁,从调用堆栈中删除,变量a、b和ret不再存在。- 返回值被分配给我们在步骤
9中定义的sum变量。 - 我们将
sum的值打印到控制台。 - 如预期,控制台将打印 15。我们在这里确实经历了很多困难,我想在这里说明几点。首先,函数定义可以存储在变量中,函数定义在程序调用之前是不可见的。其次,每次调用函数时,都会 (临时) 创建一个本地执行上下文。当函数完成时,执行上下文将消失。函数在遇到
return或右括号}时执行完成。
一个闭包
下面这段代码会输出什么?
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment: ', c1, c2, c3)答案:example increment: 123
为什么不是 example increment: 111 呢? 在第 9 行 increment 变量不是只是一个 myFunction 的函数定义吗?那么当 c1 c2 c3 调用它的时候, counter 不是会从 undefined 开始加 1,然后被销毁吗?不知怎么滴,increment 函数记住了那个 cunter 的值。这是怎么回事?
==> 闭包机制
无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域(函数内部)中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。闭包本质就是:上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。
「函数」和「函数内部能访问到的变量」的总和,就是一个闭包。另一个闭包
下面这段代码会输出什么?
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
// const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application: ', d)答案:example partial application: 7
简单来说,addThree 调用了函数 addX,并且得到一个 function 的定义 + 一个闭包(这个闭包中有变量 X = 3)。
紧接着,变量 d 调用了 addThree,相当于调用了一个带有闭包的 function,最终得到了结果 7。
闭包的应用
1. 模拟 Java Class 中的 私有变量
下面这个程序会输出什么?
function foo() {
const secret = Math.trunc(Math.random()*100)
return function inner() {
console.log(`The secret number is ${secret}.`)
}
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`答案:The secret number is 70. (一个 0-100 之间的随机整数)
上面的例子告诉我们,secret 是一个 “私有变量”。因为 f 调用了 foo(),得到的是另一个 function 叫做 inner,并且带有一个闭包。而实际上这个闭包中,存在变量 secret。因此,只有当调用 f() 时,secrect 变量才能够显示出来。
2. 函数柯里化
function curry(fn) {
const args = []
return function inner(arg) {
if(args.length === fn.length) return fn(...args)
args.push(arg)
return inner
}
}
function add(a, b) {
return a + b
}
const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5在上面的例子中,curry 函数接收一个 fn 参数(是一个函数定义)。curriedAdd 调用 curry 并将函数 add 的定义传入 curry,返回 [ 函数 inner的定义,并带有一个闭包,这个闭包中存在变量 args = [], fn = add ]1⃣️。
此时,先看 curriedAdd(2),相当于调用了一次 1⃣️,此时 args.length = 0, fn.length = 2。所以,args = [2]。
再看 curriedAdd(2)(3),那么同理,此时 args.length = 1, fn.length = 2,所以 args = [2, 3]。
让我们再调用一次,curriedAdd(2)(3)(),此时 args.length = fn.length = 2,因此返回的不再是 1⃣️ 了,而是 fn,也就是 add 函数,...args 代表对 args 中的内容进行解构,所以我们可以认为,它返回的就是 add(2,3) 的结果,也就是 5。
3. 事件驱动式编程
const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'
function onClick() {
$('body').style.background = BACKGROUND_COLOR
}
$('button').addEventListener('click', onClick)<button>Set background color</button>BACKGROUND_COLOR 作为 onClick 函数的闭包变量。
4. 函数模块化
let namespace = {};
(function foo(n) {
let numbers = []
function format(n) {
return Math.trunc(n)
}
function tick() {
numbers.push(Math.random() * 100)
}
function toString() {
return numbers.map(format)
}
n.counter = {
tick,
toString
}
}(namespace))
const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())变量 numbers 被放在闭包中被更新。
更多例子
function foo() {
let x = 42
let inner = function() { console.log(x) }
x = x+1
return inner
}
var f = foo()
f() // logs 43function createObject() {
let x = 42;
return {
log() { console.log(x) },
increment() { x++ },
update(value) { x = value }
}
}
const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42function foo() {
var result = []
for (var i = 0; i < 3; i++) {
result.push(function inner() { console.log(i) } )
}
return result
}
const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
result[i]()
}