堆栈执行及闭包

401 阅读9分钟

主题

  1. 底层机制
  2. 闭包理解
  3. This总结

01 底层机制

var l = {x: 10}
var g = l
l.y = l = {x: 200}
console.log(l.y)
console.log(g)

1.1 相关名词

分析代码执行题时会遇到的名词

  1. JS执行平台
    • 不同的浏览器
    • Nodejs
    • webview
    • 不论在哪一种平台上执行 JS 都需要代码执行的环境
  2. 环境执行栈
    • 不论何种编程语言编写的代码,最终执行都是发生在内存中
    • 每当浏览器加载界面时就会从计算机内存中申请一片空间,称之为环境执行栈
    • ECS(Execute context stack)
    • 它就像一个大容器,将来所有代码执行都会在这个空间内完成
  3. 执行上下文
    • 一个JS文件包含多行代码,不同行又构成了分支、循环、函数、对象等代码块
    • 如果将多个代码块都直接放入到环境执行栈中去执行,那么很容易出现干扰和语法冲突
    • 每个代码块都有自己的执行上下文,在上下文中保存了当前代码块执行时所需的数据
    • 执行上下文使用EC?(Execution context) 表示
  4. 进栈执行
    • 执行环境栈是一个先进后出的栈结构
    • 代码运行时会产生不同的执行上下文
    • 不同的执行上下文进栈执行,代码执行结束后,决定是否出栈
  5. EC(G)
    • Execution context global 全局执行上下文,浏览器创建界面时默认创建
  6. VO(G)
    • variable object全局变量对象,用于存放全局上下文当中声明的变量
  7. GO
    • global object 全局对象,它和VO并不是一个东西,在浏览器平台下我们可以看做是window
    • 作为一个对象,它同样占据空间,浏览器加载界面时就会创建,在它内部保存了许多JS可以直接使用的API
    • 例如setTimeout setInterval JSON
    • 为了方便使用上面的API,在VO(G)当中创建了一个window属性指向当前的空间
  8. 声明
    • 采用具体的关键字声明一个变量 var let const function var name
  9. 定义
    • 定义就是给某一个变量执行赋值操作 name = 'tom'

1.2 堆栈中的基本值

var l = 100
var g = l
g = 101
console.log(l)
/* 
    01 浏览器会开启一个线程专门用于执行js代码,同时申请空间作为环境执行栈
    02 浏览器加载界面的时候会创建一个EC(G) 全局执行上下文,然后代码进栈执行
    03 全局执行上下文当中存在VO(G),用于保存当前执行上下文中的数据
    04 代码执行之前会存在变量提升,var声明的变量在提升阶段只声明不定义
*/

总结

  1. 浏览器加载界面的时候会默认创建执行环境栈,全局执行上下文,GO
  2. EC(G) 内部会有VO(G)专门用于存放当前上下文中的数据
  3. EC(G) 上下文会在浏览器关闭之后执行出栈,释放掉相应的内存
  4. 基本类型值保存在栈空间中
  5. 作用域链查找,代码运行时使用到了某个变量,首先会在当前上下文中查找,如果没有则继续向上,直到GO

1.3 堆栈中的引用类型

var g = l
g['y'] = 100
console.log(l.y)
-------------------------------------------------
var l = { x: 10 }
var g = l
g = { y: 100 }
console.log(l.x)

总结 01 基本数据类型(原始值)存放在栈内存当中,引用类型存放在堆内存空间中 02 每个堆内存都会有一个 16进制的内存地址 03 在栈区存放的就是能找到某个堆内存的16进制地址

1.4 堆栈中的函数

函数本身也是对象,对于他的分析一般分为函数的创建和函数的执行

var lg = [88, 100]
function foo(obj) {
  obj[0] = 100
  obj = [100]
  obj[1] = 200
  console.log(obj)
}
foo(lg)
console.log(lg)

1.4.1 函数的创建

  • 变量提升阶段对于函数来说既声明又定义
  • 函数的创建和变量的提升类似,可以将函数名看作是一个变量名,不同的就是包含了声明+定义
  • 函数也是一个对象,因此它同样在堆中存储,然后将内存地址存放在栈区
  • 对于函数来说,声明和定义都发生在提升阶段,因此代码执行时看到了function foo(){} 这种代码后一般是不执行操作
  • 函数在创建的时候就确定了作用域,也就是当前的执行上下文
  • 在创建函数的时候它在内存中存放的是字符串形式的函数体

1.4.2 函数的执行

  • 函数执行的目的就是为了将内存当中存储的字符串形式的代码真正的运行起来
  • 代码运行时需要保证当前代码段与其他上下文当中的代码段相互隔离,所以函数每次执行都会生成一个全新的执行上下文
  • 执行步骤
    • 确定作用域链
    • 确定函数体中的this
    • 初始化arguments
    • 形参赋值
    • 变量提升
    • 函数代码执行

函数执行时的形参赋值就相当于在AO(G)当中新增属性

1.5 闭包机制

1.5.1 闭包含义

闭包是一种机制,代码只是具体的表现形式,例如我们常说的大函数嵌套小函数,再返回一个小函数 函数执行时会生成一个全新的执行上下文,一般来说函数中的代码执行结束之后需要出栈从而释放当前上下文所占据的内存空间,从而释放它内部的声明和值,但是如果当前执行上下文当中的数据(一般就是堆内存引用)被当前上下文之外的变量所引用,那么这个上下文就不能被释放,此时就形成了一个闭包

闭包的好处是可以对一些数据进行保存,例如下文中的zce,函数内部的zce和全局的zce互不干扰,同时闭包可以保存数据,例如0x001 所对应的内存空间,本该在fn执行结束之后释放掉,但是由于ec(g)当中的 foo 对其有引用,所以可以让他在后续的代码中继续被使用

var zce = 100
function fn() {
  var zce = 200
  return function (a) {
    console.log(a + zce++)
  }
}

var foo = fn()
foo(10)
foo(20)

1.5.2 闭包与垃圾回收

  • 上述代码运行可以发现,代码的运行是需要内存的,无论是栈内存还是堆内存,都属于计算机内存
  • 内存空间的大小是有上限的,因此不能无限制使用,所以需要内存管理,也就是垃圾回收
  • 以chrome为例,它会在时间空间执行垃圾回收操作,完成内存空间的回收
    • 栈内存
      • 主要用于存储基本数据类型值
      • 当某一个上下文执行结束之后,如果它内部的空间没有再被其他人使用,那么它就会释放掉这部分空间完成垃圾回收
    • 堆内存
      • 用于存放引用数据类型
      • 如果A上下文中的堆内存在A中代码执行完成之后,仍然被B上下文所引用,那么这个堆内存以及A上下文所占用的空间就无法被释放掉,也就是常说的闭包,如果这样的代码多了那么对性能也是一种消耗
    • 在适合的时候主动将变量定义为 null, 释放掉某些引用
    • EC(G)全局执行上下文是在浏览器加载界面的时候创建的,因此界面如果不关闭,这部分执行上下文也是不会被回收的

1.5.3 练习题

let m = 5
function foo(m) {
  return function (n) {
    console.log(n + (++m))
  }
}

let fn = foo(8)
fn(10)
foo(11)(13)
fn(20)
console.log(m)
------------------------------------
let m = 10,
  n = 10
function foo(m) {
  foo = function (n) {
    console.log(m + n++)
  }
  console.log(m++)
}

foo(5)
foo(7)

------------------------------------

function fun(n, o) {
  console.log(o)
  return {
    fun: function (m) {
      return fun(m, n)
    }
  }
}
var c = fun(10).fun(3)
c.fun(6)
c.fun(8)

02 This规律

2.1 This规律

在浏览器平台下运行 JS ,非函数当中的 this 一般都指向 window 因此在这里讨论的是函数执行过程中的 this 需要注意在 ES6+ 的箭头函数中是没有自己this的,处理机制是使用自己上中的this

2.1.1 This是什么

  1. this 就是当前函数执行的主体(谁执行了函数),不等于当前上下文,当前作用域
  2. zce 在 拉勾教育 讲前端
    1. 讲前端是一个动作(函数)
    2. 拉勾教育(执行上下文)
    3. zce 主体,本次函数在当前执行上下文执行的this指向

2.1.2 常见This场景

  1. 事件绑定
  2. 普通函数
  3. 构造函数
  4. 箭头函数
  5. 基于call/bind/apply强制改变this指向

2.1.3 规律

  1. 事件绑定
    1. 不论是DOM2还是DOM事件绑定,事件触发时this一般都是被操作的元素
  2. 普通函数
    1. 函数执行时查看前面是否有点,如果有点,则点前面的就是执行主体,没有点就是window,严格模式下是undefined
    2. 特殊情况
      1. 匿名函数中的this是window或者undefined
      2. 回调函数中的this一般也是window或者undefined
      3. 小括号语法
        1. 如果小括号只有一项,则相当于没加
        2. 如果小括号当中有多项,则取出最后一项,此时相当于拷贝函数,所以调用时主体是window

2.1.4 this练习

(function () {
  console.log(this)
})()

let arr = [1, 3, 5, 7]
obj = {
  name: '拉勾教育'
}
arr.map(function (item, index) {
  console.log(this)
}, obj)
------------------------------------------------------
//? 普通函数调用
let obj = {
  fn: function () {
    console.log(this, 111)
  }
}
let fn = obj.fn;
fn()  // window
obj.fn();  // obj
(10, fn, obj.fn)();
------------------------------------------------------
var a = 3, 
  obj = { a: 5 }
obj.fn = (function () { 
  this.a *= ++a
  return function (b) {
    this.a *= (++a) + b
    console.log(a)
  }
})();
var fn = obj.fn  
obj.fn(6)
fn(4)
console.log(obj.a, a)

---20210729直播笔记