JS内存空间与闭包

2020-03-19  本文已影响0人  昫嵐

一、JS内存空间

JS有7种数据类型:基本数据类型(Boolean、String、Number、undefined、null、Symbol)和引用数据类型(Object)。其中,基本数据类型都是按值访问的,它们被保存在栈数据结构中,每一个变量都对应一个具体的变量值,我们可以直接操作保存在变量中的实际的值;而对于引用数据结构,在栈中保存的值是指向堆中对象的指针,JavaScript不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间,而只能通过栈中的变量地址来访问到堆内存中的对象。

借一张图来理解一下:


上图中,a1,a2,a3都是基本数据类型,在栈内存空间中保存着具体的值;而c,d是引用数据类型,分别保存的是指向堆内存空间中数组[1,2,3]和对象{m:20}的地址指针。因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从变量对象中获取了该对象的地址引用(或者地址指针),然后再从堆内存中取得我们需要的数据。

深、浅拷贝

当我们在拷贝基本数据类型的时候,就相当于在栈内存中copy了一份变量-值,此时修改拷贝后的变量是不会影响到原变量的;由于对象保存在堆内存中,而栈内存保存的是指向这个对象的指针,所以当我们拷贝引用数据类型的时候,只是拷贝了一份这个对象的地址,实际拷贝变量引用的还是原变量所指向的对象,当我们操作拷贝变量的时候,就会修改原变量的内容,这就是浅拷贝。



浅拷贝的方法有:

深拷贝的方法有:

二、JS的内存空间管理

JS内存管理主要针对的是局部变量,例如函数中声明和使用的变量。其内存生命周期为:

JS有自动的垃圾回收机制,垃圾收集器需要跟踪每个变量,找出那些不再继续使用的值,然后释放其占用的内存。垃圾收集器会每隔固定的时间段就执行一次释放操作。JS的垃圾回收机制主要有两种:标记清除法引用计数法

三、执行上下文(Execution Context)

每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。JavaScript中的运行环境大概包括三种情况。

因此在一个JavaScript程序中,必定会产生多个执行上下文,JavaScript引擎会以栈的方式来处理它们,这个栈,称为函数调用栈(call stack)。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

当代码在执行过程中,遇到以上三种情况,都会生成一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈。

例如:

var color = 'blue';

function changeColor() {
    var anotherColor = 'red';

    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }

    swapColors();
}

changeColor();

那么在这段程序的执行过程中,函数执行栈中的上下文变化情况应该为:



全局上下文在浏览器窗口关闭后出栈。

对于执行上下文的总结:

四、变量对象与活动对象

js是单线程语言,在浏览器中一个页面永远只有一个线程在执行js脚本代码。js引擎执行过程主要分为三个阶段,分别是语法分析,预编译和执行阶段:

当调用一个函数时,首先对函数进行语法分析,如果没有语法错误,就进入预编译阶段,一个新的执行上下文就会被创建,后被压入函数调用栈。而一个执行上下文的生命周期可以分为两个阶段。

什么是变量对象(VO, Variable Object)

变量对象的创建,依次经历了以下几个过程。

所以所谓的变量提升,就是在变量对象创建的过程中,首先把检查varfunction声明的变量,在变量对象中分别创建以变量名、函数名为属性名的属性,属性值为undefined。并且根据变量对象创建的过程来看,函数声明的检查是处在变量声明的检查之前的,所以function的提升会优先于var的提升。

var、function 与 let、const
都知道var,function存在变量提升,而存在let、const声明的是块级作用域,不存在变量提升,所以let和const会存在暂时性死区的情况,即在变量声明之前使用会抛出 "x is not defined" 的错误。如下

let x = 'global'
{
  console.log(x) // Uncaught ReferenceError: x is not defined
  let x = 1
}

但是根据变量对象创建的过程,在创建了argument对象后,就会检查函数声明和变量声明,也就是说let是会变量提升的。那么怎么理解块级作用域里暂时性死区的存在呢?知乎专栏:我用了两个月的时间才理解 let 给出了对于这个问题的思考。

对于let的变量创建、初始化和赋值过程,可以理解为以下几步:

{
  let x = 1
  x = 2
}

所以说在初始化之前,变量是不能被访问的,所以一旦被访问,就会抛出错误。而对于var声明的变量,创建过程中自动初始化为undefined,所以是可以访问的,function声明的函数,在创建过程中自动初始化为为指向函数的指针。对于const来说,由于它是不能被赋值的,所以只存在创建和初始化两个过程,与let相同的是在环境中创建了变量后没有立即初始化,而是等到函数执行了以后进行初始化。

什么是活动对象(AO, Active Object)
变量对象是在执行上下文创建过程中创建的一个保存变量声明的对象,未进入执行阶段之前,变量对象中的属性都不能访问,但是进入执行阶段之后,变量对象转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。

VO和AO的区别来看,它们其实都是同一个对象,只是处于执行上下文的不同生命周期。不过只有处于函数调用栈栈顶的执行上下文中的变量对象,才会变成活动对象。全局上下文中的变量对象,就是window对象,this也指向window。

五、作用域

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

但是相比于上述的编译语言来说,JS引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

什么是作用域

在JavaScript中,我们可以将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

JS所采用的的作用域模型是词法作用域。在编译步骤中,首先进行的是词法分析,词法作用域就是定义在词法阶段的作用域,也就是说,词法作用域是由在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

作用域与执行上下文的区别

作用域链

作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止,此时作用域查找停止。这种作用域查找方式以链式呈现,所以称为作用域链。

六、闭包

闭包是一种特殊的对象。它由两部分组成。执行上下文(代号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

如果闭包不存在的话,也就是 innerFoo() 的引用没有赋值给fn,在 foo() 执行后,foo()的执行上下文弹出函数调用栈,因为其中的变量和函数没有被引用,就被垃圾回收器认定为垃圾进行销毁。但是在 foo() 中,全局执行上下文中的fn获取了 innerFoo() 的引用,也就是说,innerFoo() 仍然在执行环境中,那么根据浏览器的垃圾回收机制,它就不会被当做垃圾回收。而 innerFoo() 中能够访问的变量(包括它自己的作用域中的变量和它作用域链上的变量)同样会被引用,所以也不会被回收。所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量

虽然上述例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() {
        console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
    var c = 100;
    fn(); // 此处的保留的innerFoo的引用
}
foo();
bar();

innerFoo() 的作用域链,仍然是innerFoo -> foo -> window,这是在词法分析的时候就已经确定了,词法作用域是静态作用域。

七、this

在执行上下文的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。其中变量对象与作用域链我们都已经仔细总结过了,而这里的关键,就是确定this指向。

this的指向,是在函数被调用的时候确定的。并且在函数执行过程中,this一旦被确定,就不可更改了。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象

this有四种绑定方式:

'use strict'
var a = 20;
var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
        return this.a;
    }
}

console.log(obj.c);   // 40
console.log(obj.fn());  //10

因为单独的{}是不会形成新的作用域的,因此c: this.a + 20这里的this.a,由于并没有作用域的限制,所以它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。而fn中的this由于存在调用函数的作用域,this绑定的是obj对象,所以console.log(obj.fn());的结果是obj中的a。

另一个例子:

'use strict';
var a = 20;
function foo () {
    var a = 1;
    var obj = {
        a: 10,
        c: this.a + 20,
        fn: function () {
            return this.a;
        }
    }
    return obj.c;
}
console.log(foo());    // 报错
console.log(window.foo());  // 40

在严格模式下,函数独立调用,那么该函数内部的this,则指向undefined。所以console.log(foo()) 会报错;而console.log(window.foo()) 中,foo被window调用,所以this绑定的是全局变量 a=20 。如果把 "use strict" 去掉,则两个log打印的都是40。
另外需要注意的是,this只会绑定离它最近的对象,也就是调用该方法的对象。

var name = "freedom";
var obj = {
  name = "father",
  getName: function() {
      return this.name;
  },
  child = {
      name = "child",
      getName: function() {
          return this.name;
      }
  }
}
console.log(obj.getName());  //father
console.log(obj.child.getName());  //child

call,apply,bind区别

new运算符做了些什么

var mynew = function(){
        // 获取参数上下文的第一个参数
        var func = Array.prototype.shift.call(arguments);

        // 判断第一个参数是否是函数,不是函数则抛出错误
        if (typeof func !== 'function') {
            throw 'the first argument must be function';
        }

        // 创建一个对象,绑定func原型
        var obj = Object.create(func.prototype);
        
        // 执行构造函数,绑定this
        var ret = func.bind(obj,...arguments)();

        // 返回对象
        return (ret instanceof Object)? ret:obj;
    }

参考文献

https://yangbo5207.github.io/wutongluo/
《你不知道的JS(上)》

上一篇下一篇

猜你喜欢

热点阅读