系列文章:
在看这篇闭包之前,请一定要先看‘作用域链【JS深入知识汇点2】’这篇文章,不然很难把知识点串联在一起。
闭包
什么是闭包?
闭包就是那些引用了外部作用域中变量的函数,当函数可以记住并且访问所在的词法作用域时,并且函数是在当前词法作用域之外执行,此时该函数和声明该函数的词法环境的组合。
通过背包的类比, 无论何时声明新函数并将其赋值给变量,都会携带一个背包,背包里是函数创建时作用域中的所有变量。
闭包解决了什么问题?
由于闭包可以缓存上级作用域,就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。
闭包有哪些应用场景?
只要有函数类型的值进行传递,此值被调用时,都有闭包的身影。比如一个 ajax 请求的成果回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调。
闭包和纯函数区别:
区分闭包和纯函数:
- 引用了外部作用域中变量的函数是闭包
- 没有引用外部作用域中变量的函数,它们通常返回一个值并且没有副作用
经典试题
直接看代码吧,用语言来描述过于空洞。
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
这是一个高频率会看到的题,我们期望的结果是:分别输出数字1 - 5,每秒一个,每次一个。但实际上,会以每秒一次的频率输出五次6。
那代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?缺陷是我们试图假设循环中每个迭代在运行时,都会为自己"捕获"一个 i 的副本。但是实际上,尽管这五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此只有一个i。
如果想要返回的预期结果,可以通过以下方法:
立即执行函数表达式
在迭代内,使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新作用域封闭在每个迭代内部。
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, i * 1000)
})(i);
}
let 语法
let语法本质上是将一个块转换成一个可以被关闭的作用域,let声明的变量在每次迭代都会声明。
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000)
}
var、let 和 const 区别的实现原理是什么
| 声明方式 | 变量提升 | 重复声明 | 作用域 |
|---|---|---|---|
| var | 提升 | 允许 | 非块级 |
| let | 不提升 | 不允许 | 块级 |
| const | 不提升 | 不允许 | 块级 |
var、let 和 const 内存分配原理
- var:会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量。
- let:不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果有相同变量名存在就报错。
- const: 不会在栈空间预分配内存空间,也会查重报错。不过 const 存储的是基本类型的话,无法修改,如果是引用类型,无法修改栈内存里分配的指针,但是可以修改指针指向的对象里面的属性。
难题解析
Q1:改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
// 方法 1
for (var i = 0; i< 10; i++){
setTimeout((i) => {
console.log(i);
}, 1000, i)
}
// 方法 2
for (var i = 0; i< 10; i++){
((i) => setTimeout(() => {
console.log(i);
}, 1000))(i)
}
// 方法 3
for (var i = 0; i< 10; i++){
setTimeout(console.log, 1000, i)
}
// 方法 4
for (var i = 0; i< 10; i++){
setTimeout(console.log.bind({}, i), 1000, i)
}
// 方法 5
for (let i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
// 方法6
for (var i = 0; i< 10; i++){
let
setTimeout(() => {
console.log(i);
}, 1000)
}
Q2:
"use strict";
var myClosure = (function outerFunction() {
var hidden = 1;
return {
inc: function innerFunction() {
return hidden++;
}
};
}());
myClosure.inc(); // 返回 1
myClosure.inc(); // 返回 2
myClosure.inc(); // 返回 3
Q3:
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
//问:三行a,b,c的输出分别是什么?
var a = fun(0); //undefined
a.fun(1); // 0
a.fun(2); // 0
a.fun(3); // 0
var b = fun(0).fun(1).fun(2).fun(3); // undefined 0 1 2
var c = fun(0).fun(1); // undefined 0
c.fun(2); // 1
c.fun(3); // 1
Q4:
var a = 10
function fn() {
var b = 20
function bar() {
console.log(a + b) //30
}
return bar
}
var x = fn(),
b = 200
x() // 30
Q5: const 和 let 声明的全局变量是否在 window 上?
ES5中,var 声明的全局变量会提升,并且直接挂载到全局对象的属性上,
所以在 window 上能看到 var 声明的变量。
在 ES6 中,let、const 声明的变量不会提升(所以会出现暂时性死区),
并且会在一个块作用域中,如下所示,所以在window里访问不到
let a = 10;
const b = 20;
相当于:
(function() {
var a = 10;
var b = 20
})()