五.JS第一座大山:堆栈内存和闭包作用域

606 阅读12分钟

一.词法解析和变量提升

1.1 创建变量

ES6之前创建变量用的是var
ES之后创建变量用的是let/const
三者区别
第一:

var定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
let定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
const用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。(对于const声 明的引用类型,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。)
第二:var可以先使用,后声明;let必须先声明后使用。
第三:var是允许在相同作用域内重复声明同一个变量的,而let与const不允许这一现象。
第四:在全局上下文中,基于let声明的全局变量和全局对象GO(window)没有任何关系 ; var声明的变量会和GO有映射关系;
第五:解决暂时性死区: 暂时性死区是浏览器的bug:检测一个未被声明的变量类型时,不会报错,会返回undefined
如:console.log(typeof a) //undefined
而:console.log(typeof a)//未声明之前不能使用
let a
第六:let /const/function会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于let的块作用特征即可解决

1.2 变量提升

当浏览器开辟出供代码执行的栈内存后,代码并没有自上而下立即执行,而是继续做了一些事情:把当前作用域中所有带var、function关键字的进行提前的声明和定义 =>变量提升机制 【预解析】

  • 带var的只是提前声明,没有赋值,默认值是undefined。
  • 带function的声明加赋值。
  • 不带var的a=3表示给全局设置window.a=3和在全局作用域下var a=3是一样的;
//因为有变量提升机制,所以这段代码不会报错。
      console.log(foo);//undefined
      bar();//2
      var foo = 1;
      function bar() {
        if (!foo) {
          var foo = 2;
        }
        console.log(foo);
      }

在变量提升阶段,遇到大括号判断体等,不论条件是否成立,都要进行变量提升,而在高版本浏览器中,函数只声明、不赋值。

      console.log(foo);//foo() {}
      function foo() {}
      console.log(foo);//undefined
      {
        function foo() {}
      }

ES6语法中,存在块级作用域,就是let /const/function会把当前所在的大括号(除函数之外)作为一个全新的块级上下文。
因为要兼容ES3和ES6,带function a(){}的在全局下声明过,也在私有下处理过,遇到此行代码,私有下不会再处理,但是浏览器会把当前代码之前所有对a的操作,映射给全局一份,以此兼容ES3,但他后面的代码和全局没有任何关系了。梨子如下:

      var a = 0;
      if (true) {
        a = 1;
        function a() {}//这前面的对a的操作映射一份给全局,后面的留给自己
        a = 11;
        console.log(a);//11
      }
      console.log(a);//1
      var a = 0;
      if (true) {
        a = 1;
        function a() {}
        a = 11;
        function a() {}//这前面的对a的操作映射一份给全局,后面的留给自己
        console.log(a); //11
      }
      console.log(a); //11
      {
        function foo() {
          alert("1");
        }
        foo = 1;
        function foo() {
          alert("2");
        } //把之前的对a的操作映射给全局一份(包括他自己也弄没了),后面的全是自己的
        foo = 2;
        console.log(foo); // => 2
      }
      console.log(foo); // => 1

二.堆栈内存及垃圾回收机制

栈内存:浏览器在计算机内存中分配出一块内存供代码执行的环境栈(ECStack),也称栈内存 ;

基本数据类型都是存到栈里面的。
引用数据类型指针存到栈内存 堆内存:浏览器会把内置的属性和方法放到一个单独的内存中, 引用数据类型是先开辟一个堆内存,把东西存进去,最后把地址放到栈中供代码关联使用; js 中存在多种作用域(全局,函数私有的,块级私有的),代码执行前首先会形成自己的执行上下文,然后把上下文进栈,进栈后,在当前上下文再依次执行代码; 全局执行器上下文(EC(G))进栈(ECStack)执行,执行完代码就会把形成的上下文释放(出栈),当页面关闭全局上下文出栈; VO 变量对象:每一个执行上下文都会有自己的一个VO变量对象,用来存放在当前上下文中创建的变量和函数。(函数私有上下文叫 AO 活跃对象,但也是变量对象)。
GO 全局对象:他是一个堆内存(存储的都是浏览器内置的 api 属性方法),在浏览器端,让 window 指向它
VO(G)全局变量对象:全局上下文中用来存储全局变量的空间,他不是 GO=》只不过某些情况下 VO(G)中的东西会和 GO 中的东西有所关联而已;
函数执行:

  • 函数执行的时候,形成一个全新的私有上下文EC(FN),共字符串代码执行

  • 进栈执行,从上面进去,把全局往下压

  • 私有上下文有私有变量对象 AO(FN),在私有上下文中创建的对象会放到这里来;

  • 代码执行之前还需要:

    • 1.初始化作用域链(scopeChain):<EC(FN),EC(G)>
    • 2.初始化 this 指向:window
    • 3.初始化实参集合:arguments
    • 4.形参赋值
    • 5.变量提升
    • 6.代码执行

垃圾回收机制

1、项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成 前端内存泄漏【高程3】。我们尽可能减少使用闭包,因为它会消耗内存。

2、浏览器垃圾回收机制/内存收机制:

谷歌:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。
IE:“引用计数法”,当前内存被占用一次,计数累加1次,移除占用就减1,减到0时,浏览器就回收它。 3、优化手段:内存优化 ; 手动释放:取消内存的占用即可。 (1)堆内存:fn = null 【null:空指针对象】
(2)栈内存:把上下文中,被外部占用的堆的占用取消即可。

三.作用域和作用域链

  • 创建函数的时候,已经声明了当前函数的作用域==>当前创建函数所处的上下文。若果是在全局下创建的函数就是[[scope]]:EC(G)

  • 函数执行的时候,形成一个全新的私有上下文EC(FN),共字符串代码执行(进栈执行)

  • 初始化作用域链(scopeChain):<EC(FN),EC(G)>从自己所在的上下文(函数执行形成的私有上下文),到当前函数创建的时候所在的上下文(是当前函数的作用域)。日后在私有上下文代码执行的时候,遇到一个变量,我们首先看是否是自己的私有变量,是则操作自己的,不是就按照作用域链找上级上下文的...直到找到全局为止。

四.闭包及其两大作用:保护、保存

函数执行时形成的私有上下文EC(FN),正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。 函数执行函数执行过程中,会形成一个全新的私有上下文,可能会被释放,可能不会被释放,不论释放与否,他的作用是:

(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);

(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包

五.高阶编程:多型函数、柯理化函数、compose组合函数

六.BAT经典面试题

  • 题1:
      {
        function foo() {}
        foo = 1;

        console.log(foo);//1
      }

      console.log(foo);//function

/*
      EC(G):{
            大括号中出现let/const/function...都会被认为是块级作用域;
            function的只提前声明,不会提前赋值;
         1. 变量提升:foo
         2. 代码执行:EC(block)块级作用域{
                        AO:      foo-----AF0  改foo=1
                       1 变量提升  foo
                       2 代码执行: function foo() { }  ==》AF0
                                       //foo在全局声明过,为了兼容es3,浏览器会把这行代                                         码之前对foo操作映射到全局  :把全局的foo---AF0
                                   foo = 1
          }
          代码执行2:console.log(foo);//function foo(){}
      }
      */
  • 题2:
      let x = 1;
      function A(y) {
        let x = 2;
        function B(z) {
          console.log(x + y + z);
        }
        return B;
      }
      let c = A(2);
      c(3); //==>7
  • 题3:
      let x = 5;
      function fn(x) {
        return function (y) {
          console.log(y + ++x);
        };
      }
      let f = fn(6);
      f(7); //14
      fn(8)(9); //18
      f(10); //  18
      console.log(x); //5

无eeee111ee题.png

  • 题4:
      let a = 0,
        b = 0;
      function A(a) {
        A = function (b) {
          console.log(a + b++);
        };
        console.log(a++);
      }
      A(1); //1
      A(2); //4

无标1111111122222222题.png

  • 题5:
var a = 10,
  b = 11,
  c = 12; //c=3
function test(a) {
  a = 1;
  var b = 2;
  c = 3;
}
test(10);
console.log(a, b, c);
/*解析:
       EC(G)
       变量提升:a=10  ,b=11,c=12   ,
                function test(){}   scope:EC(G)  形参:a  存放到AF0中
                 
       代码执行1:test(10)---AF0(10)
                {
                    形参赋值:a=10  改a=1
                    变量提升:a=1(找到自己形参改), b=2,c=3(找到上级修改)
                    代码执行:
                }
        代码执行2:console.log(a, b, c);//10,11,3         

      */
  • 题6:
var a = 4;
function b(x, y, a) {
  console.log(a);
  arguments[2] = 10;
  console.log(a);
}
a = b(1, 2, 3);
console.log(a);
//  3 10 undefined
/*
      EC(G):{
          变量提升:a=4,
                   function b(){...}  scope:EC(G)  形参:x,y,a  存到AF0中
          代码执行:a=b(1,2,3)  -->a=AF0(1,2,3)  形成EC(A)
                   console.log(a)==>undefined
        }
            EC(A)私有上下文  进栈执行{
                形参赋值:x=1,y=2,a=3,a=10
                变量提升
                代码执行: console.log(a);   ==>3
                          arguments[2] = 10;  a=10
                          console.log(a);   ==>10
                函数执行完毕后返回给a=undefined          
            }    
      */
  • 题7:初始化实参集合
function func(x, y, z) {
  x = 100;
  console.log(arguments[0]);
  y = 200;
  console.log(arguments[1]);
  z = 300;
  console.log(arguments[2]);
}
func(1, 2); //100 200 undefined
/*
      执行函数时要初始化实参集合arguments:{0:10,1:20,length:2}
      形参赋值:x=10 ,y=20 ,z=undefined
      映射关系:x---argunments[0]
               y---argunments[1]
               z---argunments[2]
*/
  • 题8:
var a = 9;
function fn() {
  a = 0;
  return function (b) {
    return b + a++;
  };
}
var f = fn();
console.log(f(5));
console.log(fn()(5));
console.log(f(5));
console.log(a);
/*
       EC(G)全局上下文:{
          VO:   a=9   fn=AF0   f=AF0()   a=0   f=BF0  a=1,a=0 ,a=1,a+1=2

          变量提升:a
          代码执行:f=fn() 
          形成EC(Fn)上下文:{
                        AO:
                        变量提升:
                        代码执行:a=0  //把全局的a改为0
                                  return function (b) {   return b + a++;   };把这个函数作为结果赋值给f=BF0
                      }
          } 
       AF0函数堆:{`
                 a = 0;
                 return function (b) {
                  return b + a++;
                 };
        `} 
        BF0函数堆:{`
                function (b) {   return b + a++;   };
        `} 
     
        全局中代码执行1: console.log(f(5));
         {
             console.log(f(5));相当于打印console.log(5+a++);//5+0=5
             私有:b=5
             全局:a=0 a++=0  然后把全局的a+1
            
         }
      
        全局中代码执行2:console.log(fn()(5));//5
        {
            执行fn(),又把a=0了,返回function (b) { return b + a++;};
            再把5传进去执行这个函数  5+0=5   全局下a+1
        }
        全局中代码执行3:console.log(f(5))// 5+1=6  全局a+1
        全局中代码执行4:console.log(a);//2
      */
  • 题9:
var test = (function (i) {
  return function () {
    alert(i * 2);
  };
})(2);
test(5); //4
  • 题10:
var x = 5,
  y = 6;
function func() {
  x += y;
  func = function (y) {
    console.log(y + --x);
  };
  console.log(x, y);
}
func(4);
func(3);
console.log(x, y);
/*
       EC(G)全局上下文:{
          VO:  x=5,y=6,func=BF0,x=11,x=10
          变量提升:x,y,func,
          代码执行:func(4)---AF0(4)
          形成EC(Fn)上下文:{
                        AO:
                        变量提升:
                        代码执行1:x+=y ==》x=11赋值给全局
                        代码执行2:func=BF0  形参:y
                        代码执行3:console.log(x, y);//输出11,6
                      }
           代码执行func(3)---BF0(3)
           形成EC(Fn1)上下文:{
                         AO:y=3
                        变量提升:
                        代码执行1:y+--x =3+10=13  全局x=10  //输出13
           }
           代码执行:console.log(x, y);//10,6
          } 
      AF0函数堆:{
           x += y;
           func = function (y) { console.log(y + --x);  };
           console.log(x, y);
      }
      BF0函数堆:{
          console.log(y + --x);
      }
      */
  • 题11:
function fun(n, o) {
  console.log(o);
  return {
    fun: function (m) {
      return fun(m, n);
    },
  };
}
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);
/*
       EC(G)全局上下文:{
          VO: fun=AF0 ,c=fun(0).fun(1)[此刻要执行函数],c=BF0
          变量提升:fun,c,
          代码执行:c=fun(0).fun(1)--》AF(0).AF0(1)
          形成EC(Fn1)上下文:{
                        AO:n=1,o=0
                        变量提升:
                        代码执行1:console.log(o);//undefined
                        代码执行2:return {  fun: function (m) {return fun(m, n);  },返回了一个对象,形成一个对象堆存储BF0
                        代码执行3:BF0.fun(1)执行了BF0对象的fun方法,传参数1===》执行BF1(1)里面的函数
                        形成BF1(1)上下文:{
                            AO:m=1
                            变量提升:
                            代码执行:return fun(m, n) //m=1自己的,n=0上级的,==》return fun(1,0)===>0
                                     并且还返回了BF0对象给c
                        }
                    };
                        
                      }
          代码执行:c.fun(2);==>m=2,n=1==>fun(2,1)//1
          代码执行:c.fun(3);==>m=3,n=1==>fun(3,1)//1

                    } 
        AF0函数堆  形参为:n,o {
            console.log(o);
            return {  
                fun: function (m) {return fun(m, n); },
            };
        } 
        BF0对象堆{
        `   fun: BF1    `
        }
        BF1函数堆{
            function (m) {return fun(m, n) 
        }
      */
  • 题12:
      let a = 1;
      function fn1() {
        let a1 = 2;
        function fn2() {
          let a2 = 3;
          function fn3() {
            let a3 = 4;
            a = a1 + a2 + a3;
            console.log(a);
          }
          fn3();
        }
        fn2();
      }
      fn1();//9

闭包的典型情况:

闭包是作用域应用的特殊情况: 闭包自由变量的查找,是从函数定义的地方,向上级作用域查找

  • 题13:1.函数作为参数被传递
 let a = 100;
      function f1() {
        console.log(a);
      }
      function f2(f) {
        let a = 200;
        f();
      }
      f2(f1);//100
  • 题14:2.函数作为返回值被返回

function create(){
    //函数时在这里面创建的,会优先使用这里面的数据
let a=100
return function(){
    console.log(a)
}
}
let fn=create()
let a=200
fn();//100
  • 题15:3.闭包应用:隐藏数据,只提供api
 function createCache() {
        const data = {}; //隐藏起来了
        return {
          set: function (key, value) {
            data[key] = value;
          },
          get: function (key) {
            return data[key];
          },
        };
      }
      const c = createCache();
      c.set("a", 100);
      console.log(c.get("a"));
  • 题16:
let a;
      for (let i = 0; i < 10; i++) {
        a = document.createElement("a");
        a.innerHTML = i + "<br>";
        a.addEventListener("click", function (e) {
          e.preventDefault();
          alert(i);
        });
        document.body.appendChild(a);
      }