美文网首页纵横研究院前端基础技术专题社区
作用域相关/立即执行函数/闭包/闭包应用场景

作用域相关/立即执行函数/闭包/闭包应用场景

作者: 李幸娟 | 来源:发表于2019-11-10 21:00 被阅读0次

    这次学的有点断断续续的,排版有点乱 😃
    [TOC]

    作用域

    • 区分LHS 和 RHS

    1.LHS -> 左查询 -> 查询是为了赋值
    2.RHS -> 非左查询 -> 为了查到变量的值

    // 例1:
    var a = 1;
    
    // 例2:
    - RHS查询demo
    - LHS查询num,将2赋值num
    - RHS查询num的值
    
    function demo(num){
      console.log(num)
    }
    demo(2)
    
    // 例3:
    console.log(a)
    
    • LHS 和 RHS 异常报错
    • 若 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError异常;
    • 若引擎执行 LHS 查询时,在顶层(全局作用域)中也无法找到目标变量,“严格模式”下,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎;“严格模式下”,禁止自动或隐式地创建全局变量,LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出ReferenceError异常;
    • 若RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,
      比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出 TypeError;

    词法作用域

    词法作用域:由定义变量或者函数声明的位置决定

    • 举例
    1. b的词法作用域即函数a()的局部作用域
    2. 变量a是全局变量 => 函数aFun,bFun,cFun均可以访问
    3. 变量c是局部变量 => 函数aFun,bFun可以访问
    var a
    function aFun(){
      var c
      function bFun(){
        var d
        console.log(1)
      }
    }
    function cFun(){
    
    }
    
    • 未声明即使用的变量是全局变量,注意任何位置均可访问的问题

    函数作用域和块作用域

    var声明的变量都会成为全局变量

    let

    • let会为其声明的变量隐式绑定所在的块作用域,通常是{...}内部(eg.1)
    • 开发时最好为块作用域显式地创建块, 使变量的附属关系变得更加清晰(eg.2)
    • 块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关 (eg.3.1 3.2),
      • 原本process执行之后process(..) 执行后,someReallyBigData变量占用的内存就可以销毁,
      • 但是, click 函数形成了一个覆盖整个作用域的闭包,someReallyBigData变量虽然没有使用,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现),添加了块级作用域后,让引擎清楚地知道没有必要继续保存 someReallyBigDat变量;
    // eg.1
    for(let i = 0;i<3;i++){
      ...
    }
    console.log(i)  // ReferenceError
    
    // eg.2
    if(foo){
      { // => 显式的块
        let bar = 1;
        console.log(bar);
      }
    }
    
    // eg.3.1
    function process(data) {
      ...
    }
    var someReallyBigData = { .. };
    process( someReallyBigData );
    
    var btn = document.getElementById( "my_button" );
    btn.addEventListener( "click", function click(evt) {
      console.log("hello");
    });
    
    // eg.3.2
    function process(data) {
      ...
    }
    {
      // 在这个块中定义的内容可以销毁了!
      let someReallyBigData = { .. };
      process( someReallyBigData );
    }
    var btn = document.getElementById( "my_button" );
    btn.addEventListener( "click", function click(evt){
      console.log("hello");
    });
    
    

    函数声明和函数表达式

    区分:看function 的位置 (function 是声明中的第一个词,那么就是一个函数声明 )
    区别:名称标识符将会绑定在何处:

    1. a : 被绑定在全局作用域
    2. b : 被绑定在函数表达式自身的函数中, 即 (...)所在的位置中访问,外部作用域则不行,b 变量名被隐藏在自身中意味着不会污染外部作用域
    // 函数声明
    function a(){
      console.log(1)
    }
    
    // 函数表达式
    (function b(){
      ...
    })
    
    

    块级作用域

    立即执行函数表达式(IIFE)

    由于函数被包括在一对()括号内部,第一个()使之成为一个表达式,第二个()执行了这个函数

    优点:

    1. 不污染全局作用域
    2. 不需显式的通过函数名调用这个函数
    // 写法一:
    (function(){})()
    // 写法二:
    (function(){}())
    // 函数表达式
    var a = 2;
    (function foo() {
      var a = 3;
      console.log( a ); // 3
    })();
    console.log( a ); // 2
    foo() // 报错:Uncaught ReferenceError
    

    IIFE进阶用法

    • 将IIFE当作函数调用并传递参数进去(eg.1)
    // eg.1
    // 实参:window
    // 形参:global
    var a = 2;
    (function IIFE(global){
        var a =3;
    console.log(a); // 3
    console.log(global.a) // 2
    })(window)
    

    什么是闭包?

    当函数可以 记住并访问 其所在的 词法作用域 ,使函数在他本身词法作用域以外执行,就产生了闭包;

    1. 内部函数 showA 在被执行之前就被返回, 函数 showA 在定义的词法作用域以外被调用;
    2. 由于函数 showA 占用了函数 result 的变量a, 导致函数a执行完毕后,函数result() 的内存空间不会被垃圾回收机制清除;
    3. 函数 showA 依然持有函数result()函数作用域的引用,这个引用就叫做闭包;
    4. 闭包使得函数可以继续访问定义时的词法作用域;
    // eg.1
    function result() {
      var a = 1;
      function showA() {
        console.log(a)
      }
      return showA
    }
    result()()  // 1
    
    

    闭包应用场景举例

    无论以何种方式对函数类型的值进行传递,在函数在别处调用时都可以观察到闭包

    // 可以是在全局作用域调用局部函数
    function a() {
      var cc = 1
      function b() {
        console.log(cc);
      }
      return b
    }
    const fn = a()
    fn()  // 1
    
    // 也可以是局部作用域调用另一个局部作用域的函数
    function a() {
      var cc = 1
      function c() {
        console.log(cc)
      }
      b(c)
    }
    function b(fn) {
      fn()
    }
    a()
    
    

    开发中常见闭包应用场景举例

    1. setTimeout()

    
    function wait(message) {
      setTimeout( function timer() {
          console.log( message );
        }, 1000);
    }
    
    wait( "Hello,hello!" );
    
    

    1.timer 还持有变量message的引用 ,因此形成涵盖wait()函数作用域的闭包;
    2.wait() 执行1s后,他的内部作用域并不会消失,timer函数依然保有wait()作用域的闭包

    2. for循环和setTimeout拿到正确的值问题

    for(var i=0;i<10;i++){
        setTimeout(function(){
            console.log(i)//10个10
        },1000)
    }
    

    预想:每秒一次,每次一个,打印出1--10
    实际:1秒后一次打印出10个10

    出现原因

    • setTimeout的参数一直持有变量i,形成了闭包,
    • var 声明的i是全局变量
    • 每次for循环就是将定时器(微任务)加入微任务队列,for循环之后依次执行微任务队列中的任务,而此时的i==10;
    • 任务队列的十个setTimeout 共享同一个词法作用域,由于循环在定时任务触发之前就已经执行完毕,由于var声明变量具有变量提升的特点,此时的i===10,因此每次取出的的setTimeout任务访问到的i的值都是10

    ?setTimeout是1s后将任务加入任务队列还是立即加入任务队列,去队列中拿出来任务,等1s 再执行?

    解决方法:使用let

    let会产生局部作用域
    let 不仅将 i 绑定到了 for 循环的块中,事实上循环的每一个迭代它将重新绑定,确保使用上一个循环迭代结束时的值重新进行赋值

    for(var i=0;i<10;i++){
        setTimeout(function(){
            console.log(i)//10个10
        },1000)
    }
    

    上边的代码执行顺序相当于

    let a = 1;
    console.log(a)
    a = 2;
    console.log(a)
     a = 3;
    console.log(a)
    

    3. 闭包和for循环问题(同二)

    闭包只能取得包含函数中任何变量的最后一个值

    function a() {
      var arr = new Array()
      for (var i = 0; i < 10; i++) {
        arr[i] = function () {
          return i
        }
      }
      return arr
    }
    console.log(a()[0]())   // 10
    console.log(a()[1]())   // 10
    console.log(a()[2]())   // 10
    
    

    4. 使用闭包模拟私有方法和变量

    // 注意与单例模式进行区分
    // 区分:todo:闭包各自维护自己的内存空间
    function demo() {
      var a = 1
      return {
        add1: function () {
          a += 1;
          return a;
        },
        sub1: function () {
          a -= 1;
          return a;
        },
        aValue: function () {
          return a
        }
      }
    }
    var Demo1 = demo()
    var Demo2 = demo()
    console.log(Demo1.add1()) // 2
    console.log(Demo2.add1()) // 3
    

    模拟了面向对象编程的样子,实现数据隐藏和封装
    Demo1 和Demo2 各自维护独立的各自独立的词法作用域,同时引用的是自己词法作用域的变量a
    每次调用 demo 的时候,通过改变这个变量的值,会改变这个闭包的词法作用域,然而在一个闭包内对变量的修改,并不会影响到另一个闭包中的变量

    5. 使用闭包设置单例模式

    什么是单例模式?

    • 保证一个类只有一个实例

    常见的单例模式应用场景:

    1. windows的task manger(任务管理器)(非JS)
    2. 网站的计数器,一般也是采用单例模式实现,否则难以同步
    // todo :单例模式仅实例化一次
    var demo = (function () {
      var a = 1
      return {
        add1: function () {
          a += 1;
          return a;
        },
        sub1: function () {
          a -= 1;
          return a;
        },
        aValue: function () {
          return a
        }
      }
    }())
    var demo2 = demo
    console.log(demo.add1())  // 2
    console.log(demo2.add1()) // 3
    
    

    6. 内存泄漏问题

    什么是内存泄漏?

    • 程序运行需要内存,只要程序提出需求,操作系统就必须提供内存,对于持续运行的服务进程,必须及时释放不再使用的内存,否则,内存占用越来越高, 轻则影响系统性能,重则导致进程崩溃;
    • 不再用到的内存,没有及时释放,就叫做内存泄漏;

    闭包会导致内存泄漏?

    • 不会
    • 由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集,从而导致内存无法进行回收,所以可能导致内存泄漏;
    • 由于闭包会使函数中的变量一直保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题;

    7. 闭包实现数据问题,定时手动销毁

    8. 函数防抖

    9. 常见闭包中this的问题

    10. 闭包常见两个面试题

    // 题目1
    var name = "The Window";
    var object = {
      name : "My Object",
      getNameFunc : function(){
        return function(){
          return this.name;
        };
      }
    };
    
    alert(object.getNameFunc()());  // "The Window"
    
    // 题目2
    var name = "The Window";
    var object = {
      name : "My Object",
      getNameFunc : function(){
        var that = this;
        return function(){
          return that.name;
        };
      }
    };
    
    alert(object.getNameFunc()());  // "My Object"
    

    使用闭包注意

    性能

    • 使用完成之后,记得手动清楚 赋值=null ,以免一直占用内存 (即内存泄漏问题)
    • 闭包会携带包含他的函数作用域,因此会比其他函数占用更多的内存,过度使用可能会导致内存占用过多;

    相关参考文章

    相关文章

      网友评论

        本文标题:作用域相关/立即执行函数/闭包/闭包应用场景

        本文链接:https://www.haomeiwen.com/subject/vcaabctx.html