执行上下文/作用域链/闭包
1. 对闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数。创建闭包最常见的方法就是在一个函数中创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途:
- 在函数外部能够访问到函数内部的变量。可以通过外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 是已经运行结束的函数中的上下文变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
在JS中,闭包存在的意义就是让我们可以在函数外部访问函数内部的变量。
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
当我们将B作为返回值,就可以得到A中的变量的值了。
下面有一个在面试中关于必报的典型例子:
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
因为setTimeout()
是一个异步函数,所以会先执行完for
,此时i已经=6,所以会打印5个6。可以使用三种方式解决上述问题:
- 使用let定义i
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
- 使用闭包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
在上面的代码中,使用立即执行函数将i传入函数内部,这个时候值就被固定在参数j上面不会改变,当执行timer时,就可以使用外部函数的变量j。
- 使用setTimeout的回调函数
for (var i = 1; i <= 5; i++) {
setTimeout(function timer(j) {
console.log(j)
}, i * 1000, i)
}
使用闭包要注意的点:
- 因为使用闭包函数中的变量会一直保存到内存中,内存消耗很大,容易浪费资源,解决办法是在退出函数之前,将不用的变量全部删除。
- 闭包会在父函数外部,改变父函数内部的值。
闭包的优点和缺点
优点:可以在全局重复使用变量,便不会造成变量污染。可以用来定义私有属性和私有方法。
缺点:比普通函数更消耗内存,会导致网页性能变差。
闭包的应用
- 防抖与节流
防抖
function debounce(fn, delay) {
let timer = null // 借助闭包
return function () {
if (timer) {
clearTimeout(timer) // 取消由 setTimeout() 方法设置的定时操作。
}
timer = setTimeout(fn, delay)
}
}
节流
// 定时器版
function throttleTime(fn, delay) {
let timer = null // 表示当前函数是否在执行
return function () {
if (!timer) {
timer = setTimeout(() => {
fn()
timer = null
}, delay)
}
}
}
// 时间戳版
function throttleTimeStamp(fn, delay) {
let preTime = Date.now()
return function () {
let nowTime = Date.now()
if (nowTime - preTime >= delay) {
// 保存函数的执行时间
preTime = Date.now()
return fn()
}
}
}
- 函数柯里化
function add() {
// 将参数赋值给args
// 因为arguments是对象,要将它转化为数组
let args = Array.prototype.slice.call(arguments)
let inner = function () {
// 将inner接收到的参数加到args中
args.push(...arguments)
// 因为不知道有多少次add调用,所以要一直递归
return inner
}
// 因为在返回的inner函数之前被调用了toString()
// 所以返回的其实是一个字符串
// 这里重写toString()方法,进行累加求和
inner.toString = function () {
return args.reduce(function (pre, cur) {
return pre += cur
})
}
return inner
}
const res = add(1, 5, 6)(2)(3)(4).toString()
console.log(res)
2.作用域、作用域链
全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 全局作用域容易引起命名冲突问题,过多的全局作用域变量会污染全局命名空间
函数作用域 - 函数作用域声明在函数内部
- 内层作用域可以访问到外部,反之不行
块级作用域
- 使用let和const指令可以声明块级作用域
- let和const声明的变量不会有变量提升,但不可重复声明
作用域链
在当前作用域中查找所需变量,但该作用于没有这个变量,那么这个变量就是自由变量,如果在自己的作用域找不到该变量就去父级作用域查找,知道访问到window的作用域终止,这一层层的关系就是作用域链。
3.执行上下文
- 全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局上下文。 - 函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
执行上下文栈
- JS使用执行上下文栈来管理执行上下文
- 当JS执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。