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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值