作用域闭包

关于闭包的定义

  1. 闭包是指有权限访问另一个函数作用域中变量的函数
    • 函数
    • 能访问另一个函数作用域中变量
  2. 执行完的执行上下文被弹出栈,其词法环境处于失联状态,外部不能直接访问,在这种情况下还保留了其词法环境的引用,通过引用能访问其词法环境,这个引用就是闭包
    • 引用
    • 失联词法环境通过其引用能访问到
  3. 一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包.也就是说,闭包可以让你在一个内层函数访问其外层函数的作用域.(出自MDN)
    • 函数
    • 引用
    • 访问外层函数作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
var a = 2;

function bar() {
console.log(a);
}

return bar;
}

var baz = foo();

baz();
// ------------注意----------------
foo()() // 如果直接调用,没用变量接收内部函数,将不会形成实际意义上的闭包(也是闭包,不过马上就被销毁了),因为执行完就被销毁了。

正常情况下,foo执行结束,出栈,其词法环境失联,不能访问.
每个函数创建执行上下文,都会创建新的词法环境,其中包括一个scope(对外层词法环境的引用)会保存对上层词法环境的引用。bar创建时,词法环境中就会有scope指向foo词法环境.因此baz能通过引用访问foo词法环境,拿到变量.

闭包应用

结合工厂函数完成一些业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function makeAdder (x) {
return function (y) {
return x + y
}
}

let add5 = makeAdder(5)
let add10 = makeAdder(10)

add5(1) // 6
add10(1) // 11

// -----------------

function changeSize (size) {
return function () {
document.body.style.fontSize = size + 'px'
}
}

let size12 = changeSize(12)
let size14 = changeSize(14)
let size18 = changeSize(18)

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

模拟实现私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var count = (function () {
var num = 0
function changeNum (val) {
num += val
}
return {
add: function (v) {
changeNum(v)
},
dec: function (v) {
changeNum(-v)
},
val: function () {
return num
}
}
})()
count.add(2)
count.val()
count.dec(1)
count.val()

循环中的闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var fn = []
for(var i=0; i<3; i++) {
fn[i] = function () {
console.log(i)
}
}
fn[0]() // 3
fn[1]() // 3
fn[2]() // 3
/*
循环产生三个闭包,但其词法环境的引用是同一个,环境中的i即同一个
*/

// ---------------

for(var i=0; i<3; i++) {
fn[i] = (function (num) {
return function () {
console.log(num)
}
})(i)
}
fn[0]() // 0
fn[1]() // 1
fn[2]() // 2
/*
循环产生三个闭包,每个闭包有单独的词法环境引用,num在不同词法环境中,值不一样
*/

作用域&链

作用域

决定了一段代码能够访问到哪些数据,这些数据存放在词法环境对象内。

全局,函数,块级作用域

es6之前,作用域只有两个,不存在块级作用域

  • 全局
  • 函数

es6之前,只有两种声明变量的方法

  • var
  • function

var,function存在变量提升并且能覆盖同名变量,造成很多不合理场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var a = 1
function fn () {
a = 2
console.log(a) // 2
}
fn()
console.log(a) // 2
/*
函数内未用var声明,默认var,覆盖全局a,导致污染
*/

var a = 1
function fn () {
console.log(a) // undefined
if (false) {
var a = 2
}
}
fn()
console.log(a) // 1
/*
fn内有var a声明,会被提升,因此输出a未undefined
fn内var a属于函数作用域,此时不会影响外部的a
*/

es6出现块级作用域
声明方式增加了let,const,class,import,这些声明方式都会形成块级作用域,不会绑定到全局,同时支持{}形式,形成块级作用域(函数{}不算)
现代浏览器var,function声明不受{}块级限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let a = 1
{
let a = 2
console.log(a) // 2
}
console.log(a) // 1
console.log(window.a) // undefined
/*
{}和let形成块级作用域,与外层互不影响,let,const声明不会绑到全局
*/
// ----------------------------
{
var a = 1
let b = 1
}
console.log(a) // 1
console.log(b) // 报错
/*
var声明不受{}限制,不会形成块级作用域
b定义在{}块级作用域中,外部访问不到,报错
*/
// ----------------------------
function test(){
var foo = 33;
{
let foo = (foo + 55);
}
}
test(); // 报错
/*
函数内{}和let形成块级作用域,{}中let foo = (foo + 55)的执行过程是:词法环境定义foo,未初始化,等式右侧优先,foo + 55报错foo未初始化.
*/

作用域链

当执行一个函数时,引擎会将函数的执行环境推入执行栈并生成作用域链。

作用域链的顶部永远是当前环境的词法环境对象,下一个词法环境对象来自于包含环境,下下个词法环境对象来自于包含环境的包含环境。作用域链的尾部是全局词法环境对象。

作用域链是一个链表的形式。当我们在一个函数内部调用某一个变量时,会从作用域链顶部开始查找。如果在当前执行环境内存在定义则返回,否则会在当前环境的词法环境组件中引用的外部环境引用中查找。然后重复前面的逻辑,直到找到全局词法环境对象(外部引用环境为null)如果仍然未找到会抛出异常。