为什么要把作用域与闭包联系起来,因为闭包是作用域息息相关的,作用域就是产生闭包的一部分。
一、作用域
1、什么是作用域
作用域:作用域是在程序运行时代码中的某些特定部分中变量、函数和对象的可访问性。简单来说就是一块独立的存放空间。外层作用域无法访问内层作用域变量(除开闭包)。我们再来看看那有些作用域:
- 全局作用域:不在大括号内或者函数内声明的变量就是全局作用域下的,也就叫做全局变量。全局作用域下声明的变量再程序任何位置都能访问。
- 函数作用域:在函数内部的范围就是函数作用域,函数作用域内只能函数内部进行访问,无法在外部进行访问(闭包除外)。
- 块级作用域:在大括号内的范围就是块级作用域,比如 for 循环的大括号内,if 大括号内都是块级作用域。在大括号外无法对内部进行访问(var 声明的变量除外)
- 静态作用域:静态作用域又叫词法作用域,当前变量所在作用域被创建时就确定好了,而非执行阶段确定的,js遵循的就是静态作用域。
- 动态作用域:动态作用域就是与代码执行顺序有关,变量所在的作用域是在代码执行的时候确定的。
2、作用域链
作用域链其实就是作用域相互嵌套,作用域之间形成引用关系,这样就生成了作用域链。作用域链我们也可以叫做静态作用域链,因为作用域链的查找规则就是遵循的静态作用域。
我们来看下面这段代码
const age = 1
function foo() {
console.log(age) // 1
}
function bar() {
const age = 2
foo()
}
为何打印输出的是1,而不是2。因为静态作用域在变量、函数在定义的时候就确定好了,而不是执行时确定好的,所以上面 foo 所在的作用域直接就是全局作用域,在查找 age 变量的时候,自己函数作用域查找不到,就会向上级作用域查找,就找到了全局作用域下的 age,而不是 bar 函数作用域下的 age。
二、闭包
1、闭包的概念
什么是闭包?
维基百科对闭包的解释:
1、闭包又称词法闭包或函数闭包;
2、是在支持头等函数的编程语言中,是实现词法绑定的一种技术;
3、闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境;
4、闭包跟函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行。
MDN对闭包的解释:
1、一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。
2、也就是说,闭包让你可以在一个内层函数中访问到期外层函数的作用域。
3、在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
好像还是不太明白,那我们继续往下看。
2、闭包是如何形成的
闭包简单来说就是:函数访问了外层作用域变量就会产生闭包。为什么说函数访问了外层作用域变量就会产生闭包呢?我们来看下面例子:
function foo() {
const age = 1
const name = 'cj';
return function bar() {
const sum = '2'
console.log(name);
}
}
const func = foo()
func() // 'cj'
这不就是一个闭包吗,对的。我们先忘记它是一个,来思考一个问题:当 foo 函数调用完毕赋值给 func 后,foo 是不是立刻就会被销毁?如果被销毁了,那么 func 为什么还能获取到 return 出来的 bar 函数,和打印 name 变量。那难道 foo 函数没有被销毁?是要被销毁的,假如 foo 函数内部还有其它无关的变量、函数,那不是也会被保存在内存中,这样会带来性能问题。那内部是怎样实现及销毁 foo 函数,又能保存需要访问的其内部变量和函数呢?
这就是闭包所要解决的问题,有了闭包我们就能够保存要被销毁的作用域中还需要用到的变量、函数不被销毁。那闭包到底是怎么实现的?闭包其实就是将函数引用的父作用域中的变量给函数打包带走。
那怎样让函数打包带走?这个时候就用到了函数的一个属性:[[Scopes]],它就是用来放函数打包带走用到的环境。这个属性得是一个栈,因为函数有子函数、子函数可能还有子函数,每次打包都要放在这里一个包,所以就要设计成一个栈结构。我们来看看 [[Scopes]] 到底是什么样子的:
我们看见了 [[Scopes]] 属性上有Closure、Script、Global。
- Closure:这就是打包外部引用变量的 Closure包,相当于像拥有了需要的外部作用域。
- Script:调用函数本身作用域,也就是 return 的子函数作用域。
- Global:全局作用域。
这时我们也大致知道了,js 引擎会将这三个串联起来,形成新的作用域链。而 Global 与 Closure 就形成函数所需要的外部环境,自然就能够正常的运行了。使得不管函数走到哪里,随时随地可以访问带外部环境。
1、js 引擎如何知道那些引用会被打包带走呢?
答:js 引擎在解析代码时,会进行 parse 解析步骤,这个时候就知道需要用到那些外部引用了。
2、那么又是在什么时候添加到函数的 [[Scopes]] 属性上的?
答:在创建函数的时候保存到函数属性上的,创建的函数返回的时候会打包给函数。
现在我们回过头再看维基百科和MDN对闭包的解释是不是就明白说的什么了。
3、闭包的好处
闭包有什么好处呢?我们知道闭包就是解决父作用域销毁,子函数与引用到的外部变量依然会进行保存。在我们的实际场景中就有很多例子:
- 防抖:需要保存上一次的定时器,这样才能知道在上一次规定的定时器时间内是否再次触发,如果触发了就重新定义一个新的定时器。
function debounce(fn) {
// 创建一个标记用来存放定时器的返回值
let timeout = null;
return function() {
// 每次当用户点击/输入的时候,把前一个定时器清除
clearTimeout(timeout);
// 然后创建一个新的 setTimeout,
// 这样就能保证点击按钮后的 interval 间隔内
// 如果用户还点击了的话,就不会执行 fn 函数
timeout = setTimeout(() => {
fn.call(this, arguments);
}, 1000);
};
}
- 节流:保存上一次的变量开关,不然无法确定在规定的时间内有没有再次触发
function throttle(fn) {
// 通过闭包保存一个标记
let canRun = true;
return function() {
// 在函数开头判断标志是否为 true,不为 true 则中断函数
if(!canRun) {
return;
}
// 将 canRun 设置为 false,防止执行之前再被执行
canRun = false;
setTimeout( () => {
fn.call(this, arguments);
// 执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
canRun = true;
}, 1000);
};
}
- 柯里化:让函数记住一部分的参数
function foo(x){
return function(y){
return function(z){
return x+y+z
}
}
}
const sum = foo(1)(2)
sum(3)
4、闭包的坏处
闭包的特点就是保存,不被销毁。但是这样可能会造成内存泄漏,GC(垃圾回收机制)无法回收的情况。