js堆栈内存以及作用域链

372 阅读11分钟

js底层存储机制,堆(Heap)、栈Stack内存

编程应从堆栈内存分析开始:
堆 HeapStack
--都是从电脑的内存条中分配的内存[优化手段:内存优化处理,内存回收/垃圾回收]
上下文 EC [Execution Context]
全局对象GO
变量对象 VO/AO

分析下面代码的执行过程

var a = 12;
var b = a;
console.log(a);
  • 1.先创建一个原始值12,存放在栈内存中,原始值直接存放到栈内存中,栈内存适合访问方便占用空间小特点:访问速度快
    1. 声明一个变量 declare存储起来 ,存储到当前上文的VO变量对象中
    1. 让变量与值关联起来,就是常说的定义

如果是引用数据类型:

var a = {
    n:12
}
var b = a;
b['n'] = 13;
console.log(a.n);
    1. 如果是对象类型的数据,不能直接存放在栈内存中,而是先在栈内存中存放一份地址指针,然后开辟一份堆内存空间
  • 2.获取以及修改数据实际上是改变的堆内存的数据,并不更改地址的引用。

1.1数据结构中的栈
  • 栈是一种数据存放的方式,特点是先进后出,后进先出 进栈演示

image.png

出栈演示:

image.png

方法名操作
push()添加新元素到栈顶
pop()移除栈顶的元素,同时返回被移除的元素
class Stack {
    items=[];
    //添加元素到栈顶,也就是栈的末尾
    push(element){
        this.items.push(element);
    }
    // 栈的后进先出原则,从栈顶出栈
    pop(element) {
        return this.items.pop(element);
    }
}
let stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.pop());
1.2代码的运行方式
  • 代表函数一层一层的调用
var obj = {
    a:1,
    b:2,
    c:3
}
function three(a){    
    function two(b){
     	function one(c){
    		console.log(this.a + b + c);
    		debugger;
        }
        one.call(obj,1)
    }
    two(2);
}
three(3);

代码执行方式.png

1.3内存区域
  • 栈是存放数据的内存区域
  • 程序运行的时候,需要内存空间存放数据。一般来说,系统会划分出2种不同的内存空间; 种是stack(栈),另一种是堆(heap)
    • stack是有结构的,每个区安装一定的秩序存放,可以明确的知道每个区块的大小
    • heap是没有结构的,数据可以任意存放,因此stack的寻址速度要快于heap
  • 只要是局部的、占用空间确定的数据,一般都存放在stack里面,否则就放在heap里面,所有的对象都存放在heap。(一个对象引用的是同一片地址,修改当前的对象,并不修改当前对象的引用地址)

队列

  • 队列是一种操作受限制的线性表
  • 特殊之处在与,只能从后面插入,从全面删除
  • 特点是:先进先出,后进后厨 进队演示:

image.png

出队演示:

image.png

class Queue {
    items =  [];
    // 进入队列,放入队尾
    enqueue(element){
        this.items.push(element);
    }
    // 走出队列,取出第一个
    dequeue() {
        return this.items.shift();
    }    
}

事件队列

用一段伪代码来简要了解是事件循环

var eventLoop = []; //eventLoop是一个用作队列的数组
var event;
//"永远"执行
while(true){
    // 一次tick
    if(eventLoop.length > 0){
        event = eventLoop.shift();
    }
    //执行事件
    try {
        event();
    }catch(e) {
        reportError(e)
    }
}

执行上下文

image.png

变量对象VO

变量对象:记录 arguments对象,函数声明以及调用方式,var变量声明等信息。

作用域链

作用域链是对保证对执行环境的有权访问的所有变量和函数的有序访问

this

隐式的传递一个对象的引用,可以达到自动引用合适的上下文对象 juejin.cn/post/696658…

如何存储

  • 当函数运行时,会创建一个执行环境,这个执行环境就叫做执行上下文(Execution Context)
  • 执行上下文会创建一个对象叫做变量对象(Variable Object)基础数据类型都保存在VO中
  • 引用数据类型保存在堆里,通过操作对象的引用地址来操作对象
function task(){
    var a = 1;
    var b = {
         name: "jiajia"
    };
    var c = [1, 2, 3];
}
task();

上下信息:

let ExecutionContext = {
    VO: {
        a:1,
        b:"XO1",
        C:"XA1"
    },
    scopeChain:[VO(task),VO(global)],
    this:window
}

image.png

如何复制

  • 基本数据类型的复制
var a = 1;
var b = a;
b = 2;
console.log(a);

image.png

复制过程

var ExecuteContext = {
    VO: {
        a:1
    }
}

ExecuteContext.VO.b = ExecuteContext.VO.a;
ExecuteContext.VO.b = 2;
console.log(ExecuteContext.VO.a);

引用数据类型复制

  • 引用数据类型的复制的是引用的地址指针
var m = {a:1, b:2};
var n = m;
n.a = 10;
console.log(m.a);

image.png

argments传递引用类型的值

在向参数传递应用类型的值时,会把这个内存中的地址复制一份给局部变量

function setName(obj){
    obj.name = "Jack";
    obj = new Object();
    obj.name = "Rose";
}
var person = new Object();
setName(person);
console.log(person.name)

多个执行上下文栈

JS执行上下文分类

  • JS代码在执行的时候会进入一个执行上下文,可以理解为当前代码运行环境
  • 在JS运行环境主要分为全局执行上下文环境和函数执行上下文环境
    • 全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是window对象,可以通过this直接访问
    • window对象还是var声明全局对象的载体。通过var创建的全局对象,都是可以通过window直接访问的。

多个执行上下文

  • 在JS执行过程中会产出多个执行上下文,JS引擎会有栈来管理这些执行上下文
  • 执行上下文栈,也叫调用栈call stack,具有先进后出的特点
  • 栈底永远是全局上下文window,栈顶为当前执行上下文(,也叫激活对象)
  • 当开启一个函数执行就会生成一个新的执行上下文调用栈,执行完毕自动出栈

执行上下文的生命周期

image.png

执行上下文生命周期分为2个阶段

一个新的执行上下文的生命周期分为2个阶段

  • 创建阶段
    • 确定作用域链
    • 创建变量对象
    • 确定this指向
  • 执行阶段
    • 变量赋值
    • 函数赋值
    • 代码执行

变量对象

变量对象的创建
  • 函数参数(arguments)、变量对象会保存声明(var)、函数定义(function)

    • 变量对象会首先获得函数的参数变量和值,建立argument对象

    • 获取所有用function声明的函数,函数名为变量对象的属性名,值为函数对象,如果已经存在,值会覆盖

    • 在所有var关键字声明的变量,每找到一个变量声明,就会在变量对象上建一个属性,置为undefined,如果变量名已经存在,则会跳过,并不会改值,此阶段为变量声明提升,let声明的变量不会在此阶段执行

  • 函数声明优先级更高,同名的函数会覆盖函数和变量,但是同名的var变量并不会覆盖函数,执行阶段重新赋值可以改变原有的值。

这里的覆盖指的是创建的过程eg:
function foo() { console.log('function foo')};
var foo = 20;
console.log(foo)  // 20 为什么会覆盖,而不是跳过? 上面的跳过指的是变量对象的创建阶段

实际执行过程

// 首先将所有的函数声明放入变量对象中
function foo() { console.log('function foo')};
// 其次将变量声明放入变量对象中,但是foo已经存在同名的函数,因此此时会跳过undefined的赋值
// var foo = undefined;
// 然后是代码的执行阶段
console.log(foo) ; //function foo
foo = 20;

变量对象的创建过程.png

为了更好的理解我们来2个栗子: 栗子1:

function test() {
    console.log(a);
    console.log(foo());
    var a = 1;
    function foo() {
        return 2;
    }
}
test();
  1. 创建过程
var testEC = {
    VO: {
        arguments: {...},
        foo:<foo reference>,
        a:undefiend
    },
    scopeChain:[VO(test),VO(global)],
    this:window,
}

在进入执行阶段之前,变量对象中的属性不能访问!但是进入执行阶段后,变量对象变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作

因此栗子1的真正的执行顺序为:

function test() {
    function foo() {
        return 2;
    }
    var a;
    console.log(a);
    console.log(foo());
    a = 1;   
}
test();

栗子2:

function test() {
    console.log(foo);
    console.log(bar);

    var foo = 'Hello';
    console.log(foo);
    var bar = function () {
        return 'world';
    }

    function foo() {
        return 'hello';
    }
}
test();
  1. 创建阶段
VO = {
    arguments: {...},
    foo: <foo reference>,
    bar:undefined,  
    scopeChain:[test,global]
    this:Window
}
  1. 执行阶段 (变量赋值 + 函数赋值 + 代码执行) Active Object 在执行阶段当前执行上下文变为激活对象
AO = {
    arguments: {...},
    foo:"Hello",
    bar: <bar reference>,
    this:Window,
}

全局上下文变量对象

windowEC = {
    VO: Window,
    scopeChain: {},
    this:Window,
}

作用域:

  • 在js中,可以通过作用域定义的一套规则,用来管理引擎如何在当前作用域以及桥头的子作用域中根据标识符进行变量查找

  • RHS查询 LHS查询

  • 作用域与执行上下文是完全2个不同的概。

代码执行过程.png

作用域链:

本质上是一个执行变量对象的指针列表,它这是应用但不实际包含变量对象 回顾执行上文的生命周期

执行上下文的声明周期.png

在函数调用时,会开始创建对于的执行上下文,在执行上下文的过程中,变量对象,作用域链,以及this值会被确定。 作用域链,是有当前的环境与上层环境的一系列变量对象组成,它保证了当前执行环境负荷访问权限的变量和函数有序的访问

栗子1:

var a = 20;
function test() {
    var b = a + 10;
    function innerTest() {
        var c = 10;
        return b + c;
    }
    return innerTest();
}
test();

上面栗子的执行上下文创建阶段做了什么:

innerTestEC = {
    VO: {...} ,//变量对象
    scopeChain: [VO(innerText),VO(test),VO(global)],//作用域链
    this:Window,
}

innerText的作用域链.png

闭包

  • 闭包由2个部分组成,一个是当前执行上文A,一个是在该执行上次文创建的函数B
  • B执行的时候引用了当前执行上下文中A中的变量就会产出闭包
  • 当一个值失去引用的时候就会被标记,被垃圾回收机制回收并释放空间
  • 闭包的本质就是在函数外部保持内部变量的应用,阻止垃圾回收(保存数据)
  • 调用栈不会影响作用域链,调用栈在执行的时候才确定,作用域链在编译阶段就确定了

函数的执行上下文,在执行完毕之后,生命周期就结束了,那么该函数的执行上文就失去引用,该占用的内存很快就会被释放,可是闭包的存在阻止这一过程的发生。 来个栗子:

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() {
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    fn(); // 此处的保留的innerFoo的引用
}
foo();
bar(); // 2

闭包

  • 闭包有两部分组成,一个是当前的执行上下文A,一个是在该执行上下文中创建的函数B

  • 当B执行的时候引用了当前执行上下文A中的变量就会产出闭包

  • 当一个值失去引用的时候就会会标记,被垃圾收集回收机回收并释放空间

  • 闭包的本质就是在函数外部保持内部变量的引用,从而阻止垃圾回收

  • 调用栈的并不会影响作用域链,函数调用栈是在执行时才确定,而作用域规则是在代码编译阶段就已经确定了

  • MDN定义:闭包是指这样的作用域foo,它包含了一个函数fn,这个函数fn1可以调用被这个作用域所封闭的变量a、函数等内容

    • Call Stack为当前的函数调用栈
    • Scope为当前正在被执行函数的作用域链
    • Local为当前的活动对象
闭包典型应用场景:柯里化,模块化

说明闭包,for 循环是最常见的例子

for (var i=1; i<=5; i++) { 
    setTimeout( function timer() { 
        console.log( i ); 
    }, i*1000 ); 
}

自调用作用域是空的,那么仅仅将它们进行封闭是不够的

实现函数的柯里化:

const curring = function (fn, arr = []) {
  let len = fn.length;
  return function (...args) {
    let newArgs = [...args, ...arr];
    if (len) {
      return fn(...newArgs);
    } else {
      return curring(fn, newArgs);
    }
  };
};

实现模块化:

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() { 
        console.log( something ); 
    }
    function doAnother() { 
        console.log( another.join( " ! " ) ); 
    }
    return { 
        doSomething: doSomething, 
        doAnother: doAnother 
    };    
}
var foo = CoolModule(); 
foo.doSomething(); // cool 
foo.doAnother(); // 1 ! 2 ! 3

this

  • 当前函数的this是在被调用的时候确定的
  • 如果当前执行上下文处于调用栈的栈顶,这个时候变量对象变成了活动对象,THIS指针才能确定
全局对象
  • 全局this指向本身
var a = 1; //声明绑定变量对象,但在全局环境中,变量对象就是全局对象
this.b=2;  //this绑定全局对象
  • 用点调用
    • 在一个函数上下文中,this由函数的调用者提供,由调用函数的方式来决定指向
    • 如果是函数执行,且前面由点,则点前面的调用者就是当前的this
let obj = {
    getName(){
      console.log(this);
  }
}
obj.getName() //指向当前的obj
  • 直接调用

如果没有,this就是window(严格模式下是undefined),自执行函数中的this一般都是window

let obj = {
    getName(){
        console.log(this);
    }
};
let getName = obj.getName; //赋值操作,会给getName此变量开辟一块空间
getName(); //指向全局window
  • 绑定事件

给元素绑定事件的时候,绑定的方法的this一般指向当前元素

container.addEventListener("click",() =>{
    console.log(this);
})
  • 箭头函数
    • 箭头函数没有自己的this
    • 也没有prototype
    • 也没有arguments
    • 无法创建箭头函数的实例
let fn = () => {
console.log(this);
console.log(arguments);//Uncaught ReferenceError: arguments is not defined
}
console.log(fn.prototype);//undefined
fn();
new fn();//VM4416:8 Uncaught TypeError: fn is not a constructor
  • 构造函数

构造函数中的THIS是当前类的实例

function fn(){
}
let obj = new fn();
  • call/apply/bind
    • call做了哪些事情?
    • 如何实现call、apply、bind
function call(obj,...arguments) {
    // 1. 让当前对象变为上下文对象
    let context = obj;
    let fn = Symbol();
    // 2.给当前的context上添加唯一的函数对象fn并执行
    let result = context[fn](...arguments);
    // 3. 删除该唯一的函数对象
    delete context[fn];
    return result;
}
!(function(proto){
    function getContext(context){
        
    }
})(Function.prototype)

参考阅读: segmentfault.com/a/119000001…