美文网首页高级前端进阶
【进阶2-1期】深入浅出图解作用域链和闭包

【进阶2-1期】深入浅出图解作用域链和闭包

作者: 程序员依扬 | 来源:发表于2018-11-29 15:48 被阅读2次

    (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)

    本周开始前端进阶的第二期,本周的主题是作用域闭包,今天是第6天。

    本计划一共28期,每期重点攻克一个面试重难点,如果你还不了解本进阶计划,点击查看前端进阶的破冰之旅

    如果觉得本系列不错,欢迎转发,您的支持就是我坚持的最大动力。

    本期推荐文章

    从作用域链谈闭包 ,由于微信不能访问外链,点击阅读原文就可以啦。

    推荐理由

    这是一篇译文,深入浅出图解作用域链,一步步深入介绍闭包。看完本文对于相关知识的逻辑会清晰很多。

    阅读笔记

    红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数
    关键在于下面两点:

    • 是一个函数
    • 能访问另外一个函数作用域中的变量

    对于闭包有下面三个特性:

    • 1、闭包可以访问当前函数以外的变量
    function getOuter(){
      var date = '815';
      function getDate(str){
        console.log(str + date);  //访问外部的date
      }
      return getDate('今天是:'); //"今天是:815"
    }
    getOuter();
    
    • 2、即使外部函数已经返回,闭包仍能访问外部函数定义的变量
    function getOuter(){
      var date = '815';
      function getDate(str){
        console.log(str + date);  //访问外部的date
      }
      return getDate;     //外部函数返回
    }
    var today = getOuter();
    today('今天是:');   //"今天是:815"
    today('明天不是:');   //"明天不是:815"
    
    • 3、闭包可以更新外部变量的值
    function updateCount(){
      var count = 0;
      function getCount(val){
        count = val;
        console.log(count);
      }
      return getCount;     //外部函数返回
    }
    var count = updateCount();
    count(815); //815
    count(816); //816
    

    作用域链

    Javascript中有一个执行上下文(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

    详情查看 【进阶1-2期】JavaScript深入之执行上下文栈和变量对象

    作用域链:当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链。

    作用域链和原型继承查找时的区别:如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError

    作用域链的顶端是全局对象,在全局环境中定义的变量就会绑定到全局对象中。

    全局环境

    无嵌套的函数
    // my_script.js
    "use strict";
    
    var foo = 1;
    var bar = 2;
    
    function myFunc() {
      
      var a = 1;
      var b = 2;
      var foo = 3;
      console.log("inside myFunc");
      
    }
    
    console.log("outside");
    myFunc();
    

    定义时:当myFunc被定义的时候,myFunc的标识符(identifier)就被加到了全局对象中,这个标识符所引用的是一个函数对象(myFunc function object)。

    内部属性[[scope]]指向当前的作用域对象,也就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象(即全局对象)。

    image

    myFunc所引用的函数对象,其本身不仅仅含有函数的代码,并且还含有指向其被创建的时候的作用域对象。

    调用时:当myFunc函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包含myFunc函数所定义的本地变量,以及其参数(arguments)。这个新的作用域对象的父作用域对象就是在运行myFunc时能直接访问的那个作用域对象(即全局对象)。

    image
    有嵌套的函数

    当函数返回没有被引用的时候,就会被垃圾回收器回收。但是对于闭包,即使外部函数返回了,函数对象仍会引用它被创建时的作用域对象。

    "use strict";
    function createCounter(initial) {
      var counter = initial;
      
      function increment(value) {
        counter += value;
      }
      
      function get() {
        return counter;
      }
      
      return {
        increment: increment,
        get: get
      };
    }
    
    var myCounter = createCounter(100);
    console.log(myCounter.get());   // 返回 100
    
    myCounter.increment(5);
    console.log(myCounter.get());   // 返回 105
    

    当调用 createCounter(100) 时,内嵌函数increment和get都有指向createCounter(100) scope的引用。假设createCounter(100)没有任何返回值,那么createCounter(100) scope不再被引用,于是就可以被垃圾回收。

    image

    但是createCounter(100)实际上是有返回值的,并且返回值被存储在了myCounter中,所以对象之间的引用关系如下图:

    image

    即使createCounter(100)已经返回,但是其作用域仍在,并且只能被内联函数访问。可以通过调用myCounter.increment() 或 myCounter.get()来直接访问createCounter(100)的作用域。

    当myCounter.increment() 或 myCounter.get()被调用时,新的作用域对象会被创建,并且该作用域对象的父作用域对象会是当前可以直接访问的作用域对象。

    调用get()时,当执行到return counter时,在get()所在的作用域并没有找到对应的标示符,就会沿着作用域链往上找,直到找到变量counter,然后返回该变量。

    image

    单独调用increment(5)时,参数value保存在当前的作用域对象。当函数要访问counter时,没有找到,于是沿着作用域链向上查找,在createCounter(100)的作用域找到了对应的标示符,increment()就会修改counter的值。除此之外,没有其他方式来修改这个变量。闭包的强大也在于此,能够存贮私有数据。

    image

    创建两个函数:myCounter1myCounter2

    //my_script.js
    "use strict";
    function createCounter(initial) {
      /* ... see the code from previous example ... */
    }
    
    //-- create counter objects
    var myCounter1 = createCounter(100);
    var myCounter2 = createCounter(200);
    

    关系图如下


    image

    myCounter1.increment和myCounter2.increment的函数对象拥有着一样的代码以及一样的属性值(name,length等等),但是它们的[[scope]]指向的是不一样的作用域对象。

    参考

    从作用域链谈闭包

    往期文章查看

    每周计划安排

    每周面试重难点计划如下,如有修改会通知大家。每周一期,为期半年,准备明年跳槽的小伙伴们可以把本公众号[置顶]了。

    • 【进阶1期】 调用堆栈
    • 【进阶2期】 作用域闭包
    • 【进阶3期】 this全面解析
    • 【进阶4期】 深浅拷贝原理
    • 【进阶5期】 原型Prototype
    • 【进阶6期】 高阶函数
    • 【进阶7期】 事件机制
    • 【进阶8期】 Event Loop原理
    • 【进阶9期】 Promise原理
    • 【进阶10期】Async/Await原理
    • 【进阶11期】防抖/节流原理
    • 【进阶12期】模块化详解
    • 【进阶13期】ES6重难点
    • 【进阶14期】计算机网络概述
    • 【进阶15期】浏览器渲染原理
    • 【进阶16期】webpack配置
    • 【进阶17期】webpack原理
    • 【进阶18期】前端监控
    • 【进阶19期】跨域和安全
    • 【进阶20期】性能优化
    • 【进阶21期】VirtualDom原理
    • 【进阶22期】Diff算法
    • 【进阶23期】MVVM双向绑定
    • 【进阶24期】Vuex原理
    • 【进阶25期】Redux原理
    • 【进阶26期】路由原理
    • 【进阶27期】VueRouter源码解析
    • 【进阶28期】ReactRouter源码解析

    交流

    本人Github链接如下,欢迎各位Star

    http://github.com/yygmind/blog

    我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

    如果你想加群讨论每期面试知识点,公众号回复[加群]即可


    相关文章

      网友评论

        本文标题:【进阶2-1期】深入浅出图解作用域链和闭包

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