参考链接:https://www.cnblogs.com/penghuwan/p/7404372.html
在《你不知道的JavaScript》中这样写道“
对于那些有一点 JavaScript 使用经验但从未真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生”。
理解闭包之前先巩固一下作用域和词法作用域的知识
作用域
作用域是一套规则,用于确定再何处以及和如何查找变量的规则
//函数作用域
function foo() {
var a = 'iceman';
console.log(a); // 输出"iceman"
}
foo();
// 全局作用域
// 函数内部如果没有找见就会像上级作用域查找,找到就停止
var b = 'programmer';
function foo() {
console.log(b); // 输出"programmer"
}
foo();
-
在查找b变量的时候,先在函数作用域中查找,没有找到,再去全局作用域中查找,有一个往外层查找的过程。很像是顺着一条链条从下往上查找变量,这条链条,我们就称之为
作用域链
。
词法作用域
词法作用域是作用域的一种工作模型,作用域有两种工作模型,在JavaScript中的词法作用域是比较主流的一种,另一种动态作用域(比较少的语言在用)
- 词法作用域就是静态作用域
- JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了
var value = 1;
// 此时的foo函数定义在全局,
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 结果 1
// 执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,
// 就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。
在此强调,词法作用域就是作用域是由书写代码时函数声明的位置来决定的。编译阶段就能够知道全部标识符在哪里以及是如何声明的,所以词法作用域是静态的作用域,也就是词法作用域能够预测在执行代码的过程中如何查找标识符。
动态作用域
事实上JavaScript并不具有动态作用域,它只有词法作用域,简单明了
动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用,但是this机制某种程度上很像动态作用域
闭包
闭包就是能访问另一个函数作用域中变量的函数(通常是指包含闭包函数的外部函数)
MDN中的定义: 闭包是指能访问那些自由变量的函数
自由变量:是指在函数中使用,既不是函数参数也不是局部局部变量的变量
function test(){
let a = 11
return function () {
console.log(a)
}
}
let fun = test()
fun() // 11
// 打印a的匿名函数被包裹在外部函数test中,并且访问了外部函数作用域的变量,因此从定义上说它就是一个闭包
A)函数执行环境、作用域链和变量对象
- 当某个函数被调用时,会相应的创建一个执行环境及相应的作用域链
- 每个执行环境都有一个与之关联的变量对象,用来存放环境中定义的所有变量和函数
在函数调用的时候,会创建一个函数的执行环境,这个执行环境有一个与之对应的变量对象和作用域链。
function test () {
// 一段代码静静躺在这里,不会产生执行环境
// 简单的说一个运行中的环境即为执行环境
}
变量对象包括:
每个函数的变量对象保存了它所拥有的的数据,以供函数内部访问和调用(
位于执行环境
)
- 1.声明变量
- 2.声明函数
- 3.接收参数
function test(data) {
let name = '变量'
function testFun () {
console.log('函数')
}
}
test('参数')
// 此时函数运行,产生了执行环境,所对应的变量对象如下:
ExecutionContext = {
variableObject: {
name:'变量'
testFun: [对函数声明testFun的引用]
arg: '参数'
},
this: thisValue,
Scope: [ // Scope chain
// 所有变量对象的列表
]
};
作用域链
通过作用域链,函数能够访问来自上层作用域(执行环境)中的变量
function test () {
var a = 11
function fun () {
console.log(a)
}
fun()
}
test() // 11
- 作用域链其实就是个从当前函数的变量对象开始,从里到外取出所有变量对象,组成的一个列表。通过这个作用域链列表,就可以实现对上层作用域的访问。
B) 闭包和函数柯里化
在定义一个函数的时候,可能会使用到多层嵌套的闭包,这种用法,叫做“柯里化”。 而闭包柯里化有两大作用:参数累加和延迟调用
function foo (a) {
return function (b) {
return function (c) {
console.log(a + b + c);
}
}
}
// 调用方式一
foo('我')('是')('sun'); // 打印 我是sun
// 调用方式二
let foo1 = foo('我');
let foo2 = foo1('是');
foo2('sun'); // 打印 我是sun
最内层的闭包在外层函数foo和foo1调用的时候都没有调用,直到最后得到foo2并调用foo2()的时候,这个最内层的闭包才得到执行, 这也是闭包的一大特性——延迟执行
闭包造成额外的内存占用
函数的变量对象一般在安徽省农户调用结束是被销毁,但是闭包的情况不同
function foo (a) {
return function () {
console.log(a)
}
}
let foo1 = foo(1);
let foo2 = foo(2);
let foo3 = foo(3);
foo1(); // 输出1
foo2(); // 输出2
foo3(); // 输出3
foo函数调用结束后, foo函数的变量对象并不会被立即销毁,而是只有当取得foo函数闭包的值的foo1, foo2, foo3调用结束, 这三个函数的变量对象和作用域链被销毁后, foo函数才算“完成任务”,这时,它才能被销毁。
- 所以说,闭包会造成额外的内存占用
闭包的优点:
- 方便调用上下文中声明的局部变量
- 逻辑紧密,可以在一个函数中再创建个函数,避免了传参的问题
闭包的缺点:
因为使用闭包,可以使函数在执行完后不被销毁,保留在内存中,如果大量使用闭包就会造成内存泄露,内存消耗很大
总结:
闭包能够访问外部函数的变量,即使变量已经离开它所创建的环境,是因为外部变量会被闭包的作用域对象所持有。闭包这种特性实现了嵌套函数之间数据的隐式传递。
闭包的应用
function addFn(a,b){
return(function(){
console.log(a+"+"+b);
})
}
let test =addFn(1,2);
setTimeout(test,3000);
一般setTimeout的第一个参数是个函数,但是不能传值。如果想传值进去,可以调用一个函数返回一个内部函数的调用,将内部函数的调用传给setTimeout。内部函数执行所需的参数,外部函数传给他,在setTimeout函数中也可以访问到外部函数。
this的原理
文章链接:http://www.ruanyifeng.com/blog/2018/06/javascript-this.html
理解堆栈溢出和内存泄漏的原理,如何防止
1、
内存泄露
:是指申请的内存执行完后没有及时的清理或者销毁,占用空闲内存,内存泄露过多的话,就会导致后面的程序申请不到内存。因此内存泄露会导致内部内存溢出
2、
堆栈溢出
:是指内存空间已经被申请完,没有足够的内存提供了
3、
常见的内存泄露的原因:
- 全局变量引起的内存泄露
- 闭包
- 没有被清除的计时器
5、
解决方法
:
- 减少不必要的全局变量
- 减少闭包的使用(因为闭包会导致内存泄露)
- 避免死循环的发生
如何处理循环的异步操作
先看一下在循环内部出现异步操作引发的问题
fun = () => {
let work = ['周一移动审批', '周二对接oa', '周三uat']
work.forEach(item => {
// 这里的service.getList()是引入的一个外部接口文件里面的一个方法,返回一个Promise对象(Es6新特性)
// 是一个异步操作,
service.getList()
.then(()=>{
console.log(item)
// //这时候打印出来的很可能就是周三 uat,周三uat,周三uat (因为异步循环还没有等异步操作返回Promise对象过来item值已经改变)
})
.catch()
})
}
正确处理:
使用关键字async/await
fun = async () => {
let work = ['周一移动审批', '周二对接oa', '周三uat']
work.forEach(item => {
await service.getList()
.then(()=>{
console.log(item)
})
.catch()
})
}
async告诉fun方法里面存在异步的操作,await放在具体异步操作(方法)前面,意思是等待该异步返回Promise才会继续后面的操作
另一种递归处理
fun = (i) => {
let work = ['周一移动审批', '周二对接oa', '周三uat']
service.getList()
.then(()=>{
if(i < work.length){
console.log(work[i])
i++
fun(i)
}
})
.catch()
}
fun(0)
用递归来实现自我循环(具体循环在then里面,可以确保前面的compute.exec()的异步操作完成).then()是返回了Promise对象为resolve后才进行的(可以了解一下Promise对象)
模块化方案
1、CommonJS
阮一峰:Javascript模块化编程(一):模块的写法
2、AMD
阮一峰:Javascript模块化编程(二):AMD规范
阮一峰:Javascript模块化编程(三):require.js的用法
3、CMD
4、Es6模块化