前端知识自检——作用域和闭包

参考链接: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 © {
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模块化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值