几道题弄明白js的执行机制与this、闭包

125 阅读9分钟

闭包

js在开始运行一段代码时,会在虚拟内存(运行内存)中开辟一个栈内存(stack)和一个堆内存(heap),堆内存默认开辟一个空间用来存放js的全局对象window。而在栈内存中默认开辟一个空间---全局执行上下文栈 [EC(G)] .用来存放全局变量[VO(G)]和运行代码。(使用let、const声明的变量会放在VO(G)中,而使用var定义的会放在存放window的堆空间中)。

在定义一个引用内存时,会先在堆空间中开辟一个空间,这个空间主要用来存放代码和值。如果是函数,还会自动添加一个变量[scope]:xxx,用于存放作用域。(在哪里定义的,作用域就是谁,比如在全局执行上下文栈中定义的,那么它的[scope]就是[EC(G)])...

然后每执行一个函数或者执行“块级上下文”的代码时会在栈内存中开辟一个函数执行上下文[EC(fn)]和块级执行上下文[EC(block)]。然后在这个执行上下文中会首先定义开辟一个地方用来存放变量[VO(Fn)],然后是存放作用域链、参数赋值、变量提升、执行代码、return、释放空间。但是如果存在闭包了,这个空间就不会被释放。

而闭包就是:

  • 函数执行产生一个私有上下文,首先这里的私有变量会被保护起来,不受外界的干扰!!
  • 而且如果上下文不被释放,则里面的私有变量也会被"保存"下来! 这样其下级上下文就可以操作(访问或修改)这些值了!我们吧这种“保护” + “保存” 的机制称之为闭包。

垃圾回收

堆内存:看当前堆内存空间的地址,是否被别的东西占用

  • 如果没有被占用:浏览器会在空闲的时候,释放这个堆
  • 如果被占用,则不能被释放

fn = null 清除之前的引用(占用),手动释放无用的内存

两个方法:引用计数、标记清除

栈内存:

  1. EC(G)打开页面的时候形成,也只有页面关闭才会释放

  2. 函数/块级私有上下文:

  3. 正常情况下,代码执行完,产生的私有上下文会被释放掉

  4. 特殊情况:如果上下文中创建的某个东西(一般指的是函数),被上下文以外的事物占用了,则不仅这个东西不能被释放,而且当前私有上下文也不能被释放!!

this

我们研究的this,都是研究函数私有上下文中的THIS:

  • 因为全局上下文中的this -> window
  • 块级上下文中没有自己的this,在此上下文中遇到的this,都是其所处环境(上级上下文)中的this
  • ES中的箭头函数和块级上下文类似,也是没有自己的this,遇到的this也是其上级上下文中的

THIS是执行主体:通俗来讲,谁把它执行的,而不是在哪执行,也不是在哪定义的,所以this是谁和在哪执行以及在哪定义都没有直接的关系;想搞定THIS,我们可以按照以下总结的规律去分析!

  • @1 给DOM元素进行事件绑定(不论是DOM0还是DOM2),当事件行为触发,绑定的方法执行,方法中的THIS是当前DOM元素本身。
  • @2 当方法执行,我们看函数前面是否有“点”
    • 有:“点”之前是谁,THIS就是谁
    • 没有:THIS就是Window(非严格模式)或者undefined(严格模式 “use strict”)
    • 匿名函数(自执行函数或者回调函数等)中的THIS一般都是window/undefined
const fn = function(){
	console.log(this)
};
let obj = {
	name:'zhufeng',
	//fn:fn
	fn
}
fn() //window
obj.fn()  //obj
// 自执行函数:创建完立即执行
// ~function(){}()
// !function(){}()
// +function(){}()

(function(x){
    console.log(this)  //window / undefined
})(10);

// 回调函数:把一个函数作为实参值,传递给另一个函数  【在另外一个函数中,把其执行】、
const fn = function(callback){callback()};
fn(function(){})

setTimeout(function(){
	console.log(this)  // window    
},1000)


let arr = [10,20];
let obj = { name:'ly' };
arr.forEach(function(item,index){
    //console.log(item,index)
	console.log(this); // window/undefined
})

arr.forEach(function(item,index) {
    console.log(this)  //obj
},obj)  //forEach([回调函数],[修改回调函数中的THIS])

题目1

var x = 3 ,
    obj = { x:5};
obj.fn = (function(){
    this.x *= ++x;
    return function(y) {
        this.x *= (++x) + y;
        console.log(x);
    }
})();
var fn = obj.fn;
obj.fn(6);
fn(4);
console.log(obj.x,x)

// 13

// 234

//95 234

解题步骤:

创建全局上下文栈EC(G):

  • EC(G)中有VO(G)用于存放变量

  • 存放代码

  • 执行代码

    • 在VO(G)中创建值3,再创建变量x,赋值

    • 创建对象堆区 ,地址为0x001,其中有键值对:x -> 5

    • 立即执行函数

      • 创建堆区0x002,作用域[[scope]]:EC(G)

      • 指向函数0x002,

        • 创建函数上下文 EC(AN1)

        • 创建AO(AN1),用于存放该函数上下文中的变量以及值

        • 创建作用域链:<EC(AN1),EC(G)>

        • 参数变量赋值、变量提升

        • 执行代码

              this.x *= ++x;
              return function(y) {
                  this.x *= (++x) + y;
                  console.log(x);
              }
          
        • 查找this,由于是匿名函数,this指向window,this.x就是window. x --> 3

        • this.x *= ++x -> this.x = this.x * (++x) -> this.x = 3 * 4 -> this.x = 12 -> window.x = 12

        • 所以EC(G)中VO(G)中的x -> 12

        • return 的是函数

          • 开辟函数堆0x003

            • 作用域[[scope]]:EC(AN1)
            • 存放代码
            • this.x *= (++x) + y;
              console.log(x);
              ​
              ​
              
        • return 0x003

      • 代码执行完毕,给0x001对象堆中添加键值对:fn -> 0x003

    • 执行代码 var fn = obj.fn(省略了变量提升)

    • 给EC(G)中的VO(G)添加了键值对 fn -> 0x003

    • 执行obj.fn(6)

      • obj.fn --- > 0x003

      • 开辟函数上下文 EC(AN2)

        • 开辟AO(AN2)

        • 作用域链 <EC(AN2),EC(AN1)> (上一级的查看0x003的[[scope]])

        • 参数赋值 : 存放在AO(AN2) y -> 6

        • 执行代码(代码存放在0x003中): this.x *= (++x) + y;

          • this.x指的是obj,查看对象obj堆中是否有x,发现有x,x = 5
          • this.x = 5 * ((++x) + y);
          • 查看AO(AN2)中有没有x,没有,沿着作用域链去上一级上下文(EC(AN1))找。
          • 在AO(AN1)中也没找到,继续沿着EC(AN1)函数上下文的作用域链往上找(EC(G))
          • 发现在VO(G)中有x,x=12
          • 执行 ++x,EC(G)的VO(G)中的x被修改,x=13
          • 查看AO(AN2)中有没有y,发现有y,值为6
          • this.x = 5 * (13 + 6) -> this.x = 5*19 -> this.x = 95 -> obj.x = 95
        • 执行consoel.log(x)

          • 查看AO(AN2)中有没有x,没有,沿着作用域链去上一级上下文(EC(AN1))找。
          • 在AO(AN1)中也没找到,继续沿着EC(AN1)函数上下文的作用域链往上找(EC(G))
          • 发现在VO(G)中有x,x=13
          • 输出 13
        • 函数执行完毕,该函数上下文被释放

    • 执行fn(4);

      • fn --> 0x003

      • 开辟函数上下文EC(AN3)

        • 开辟AO(AN3)

        • 作用域链 <EC(AN3),EC(AN1)> (上一级的查看0x003的[[scope]])

        • 参数赋值 : 存放在AO(AN3) y -> 4

        • 执行代码(代码存放在0x003中): this.x *= (++x) + y;

          • this.x指的是window,查看window的VO(G)/GO中是否有x,发现有x,x = 13
          • this.x = 13 * ((++x) + y);
          • 查看AO(AN3)中有没有x,没有,沿着作用域链去上一级上下文(EC(AN1))找。
          • 在AO(AN1)中也没找到,继续沿着EC(AN1)函数上下文的作用域链往上找(EC(G))
          • 发现在VO(G)中有x,x=13
          • 执行 ++x,EC(G)的VO(G)中的x被修改,x=14
          • 查看AO(AN3)中有没有y,发现有y,值为4
          • this.x = 13 * (14 + 4) -> this.x = 13*18 -> this.x = 234 -> window.x = 234
        • 执行consoel.log(x)

          • 查看AO(AN3)中有没有x,没有,沿着作用域链去上一级上下文(EC(AN1))找。
          • 在AO(AN1)中也没找到,继续沿着EC(AN1)函数上下文的作用域链往上找(EC(G))
          • 发现在VO(G)中有x,x=234
          • 输出 234
        • 函数执行完毕,该函数上下文被释放

    • 执行console.log(obj.x,x)

      • obj堆0x001中发现x为95,
      • EC(G)的VO(G)中发现x为234
      • 输出95 234

image.png

题2:

let a = 0,
	b = 0;
let A = function(a) {
	A = function(b) {
		alert(a + b++);
	};
	alert(a++);
}
A(1); // 1
A(2); // 4

执行顺序:

  1. 开辟全局上下文执行栈EC(G),将代码(字符串)存放找栈中

  2. 执行代码,发现let声明,ab值为原始值,先创建值0,再创建变量a和b,再进行地址的引用(赋值)

  3. 发现let 声明A,值为引用类型

    • 堆区开辟空间 0x001

      • 堆区第一部分存放作用域[[scope]]:EC(G) -> 在哪声明的
      • 第二部分存放代码字符串: A = function(b) {alert(a + b++);};alert(a++);
      • 第三部分存放键值对:name:'' , length:1 ptototype:xxx [[prototype]]:xxx、
    • 再EC(G)中创建变量A

    • 将堆区空间地址0x001赋值给A

  4. 执行到A(1)代码,执行函数

    要开辟函数上下文EC(A1)

    • 第一部分:AO(A1) 存放变量以及值

    • 第二部分:作用域链: <EC(A1),EC(G)>

      • <1,2> 中1值的是当前作用域,2指的是上级作用域
    • 形参赋值:a = 1

      • 存放在AO(A1)中
    • 变量提升

    • 代码执行:A = function(b) {alert(a + b++);};alert(a++);

    • 发现函数声明

      • 堆区开辟空间0x002

        • 堆区第一部分存放作用域[[scope]]:EC(A1)
        • 第二部分存放代码字符串: alert(a + b++);
        • 第三部分存放键值对:name:'' , length:1 ptototype:xxx [[prototype]]:xxx
      • 但是发现A不是自己私有的,就得沿着作用域链向上查找,去EC(G)中查找,找到了A

      • 将EC(G)中的A的值改为了0x002

    • 执行下一句代码 alert(a++)

      • 输出 “1”
      • a -> 2

    A(1)执行完毕,由于0x001没有被使用了,将会被释放掉。但是0x002函数堆被EC(G)中的A占用了,所以0x002会保留,那么0x002的作用域是EC(A1),则EC(A1)也会被保留下来。

  5. 执行代码A(2)

    要开辟函数上下文EC(A2)

    • 第一部分:AO(A2),存放变量和值

    • 第二部分,作用域链<EC(A2) , EC(A1)>

    • 形参赋值:b -> 2

      • 存放在AO(A2)中
    • 变量提升

    • 代码执行:alert(a + b++);

      • a本上下文没有,沿着作用域链去上级查找,找到a -> 2

      • b是私有的

      • 输出 "4"

        • b++,
        • AO(A2)中的 b -> 3
    • 释放掉

  6. 手动释放 A = null

    • 函数堆0x002没有被引用了,0x002被释放
    • 0x002被释放之后,EC(A1)作用域也就不会被使用了
    • EC(A1)释放掉

害,不知道你们看不看得懂,反正我是看得懂,哈哈哈哈哈~