重学JavaScript【作用域、执行上下文和垃圾回收】

简介: 在JavaScript中,数据分为 原始值 和 引用值,原始值就是最简单的数据,一般也称为 值类型,引用值就是由多个值构成的对象,一般被叫做 引用类型。保存原始值的变量是按值访问的,所以操作的是存储在变量中的实际值。引用值是保存在内存中的对象,要想改变它,实际上操作的是对该对象的 引用。

原始值和引用值


在JavaScript中,数据分为 原始值引用值,原始值就是最简单的数据,一般也称为 值类型,引用值就是由多个值构成的对象,一般被叫做 引用类型。保存原始值的变量是按值访问的,所以操作的是存储在变量中的实际值。引用值是保存在内存中的对象,要想改变它,实际上操作的是对该对象的 引用

  • 原始值不能添加属性,引用值可以添加属性
  • 原始值复制给另一个变量是两个独立的栈,引用值复制给另一个变量是复制的引用地址,对象所在的堆不变。
  • 对象被传入方法并改变它的属性时,对象在外部访问的还是原来的值
function setName(obj){
  obj.name = "adc";
  obj = new Object();
  obj.name = "def"
}
let person = new Object();
setName(person);
console.log(person.name) // abc


上面可得出,方法传入的对象其实是按值传递的,内部obj被重写之后,obj会变成一个指向本地对象的指针,而这个指向本地的对象在函数结束后会被销毁。


执行上下文


在JavaScript中,上下文 的概念特别重要,因为上下文决定了它们可以访问哪些数据和行为。每个上下文都有一个关联的变量对象,这个对象里包括了上下文中定义的所有东西。

  • 全局上下文,也就是最外层的上下文,一般就是 window
  • 函数上下文,执行的时候会被推入到一个上下文栈上,函数执行完毕后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文
  • 上下文是在函数调用的时候才会生效的

现在我们来模拟一个执行上下文的行为:

首先要知道的是,JavaScript的整个执行过程分为两个阶段:编译阶段(由作用域规则确定,编译成可执行代码),执行阶段(引擎完成,该阶段创建执行上下文)。

我们定义一个执行上下文栈是一个数组:ECStack = [],当JavaScript开始解释执行代码的时候,首先会遇到全局代码,所以此时我们压入一个全局执行上下文 globalContext,当整个应用程序结束的时候,ECStack才会被清空,所以ECStack底部永远会有一个 globalContext。

ECStack = [
  globalContext
]


此时,如果碰到了一个函数:

// 要执行下面的函数
function fn(){
  function inFn(){}
  inFn()
}
fn()


那么执行上下文栈会经历以下过程:

// 压栈
ECStack.push(globalContext)
ECStack.push(fnContext)
ECStack.push(inFnContext)
//弹出
ECStack.pop(inFnContext)
ECStack.pop(fnContext)
ECStack.pop(globalCotext)


执行上下文在创建阶段,会发生三件事:

  1. 创建变量对象
  2. 创建作用域链
  3. this的指向

每个执行上下文都会分配一个 变量对象(variable object,VO) ,它的属性由 变量函数声明 构成,在函数上下文的情况下,参数列表也会被加入到变量对象中作为属性,不同作用域的变量对象也不同。

注意:只有函数声明会被加入到变量对象中,而函数表达式不会!

// 函数声明
function a(){}
typeof a //function
//函数表达式
var a - function fn(){}
typeof fn // undefined


当一个函数被激活的时候,会创建一个活动对象(activation object,AO)并分配给执行上下文,活动对象由 arguments 初始化构成,随后它会被当做 变量对象 用于变量初始化。

function a(name, age){
  var gender = "male";
  function b()
}
a("小明", 20)


a 被调用时,在a的执行上下文会创建一个活动对象AO,并且被初始化为:AO = [arguments],随后AO又被当做变量对象VO进行变量初始化,此时:VO = [arguments].concat([name.age,gender,b])

一般情况下变量对象包括:形参,函数声明,和变量声明,下面用代码来表示一下某刻的变量对象都是什么:

function fn(value){
  console.log(a);
  console.log(inFn);
  var a = 2;
  function inFn(){};
  var c = function() {};
  a = 3;
}
fn(1);


在进入执行上下文后,此时的AO是:

AO = {
  arguments: {
    0: 1,
    length: 1
  }
  value: 1,
  a: undefined,
  b: reference to function inFn(){},
  c: undefined
}


接下来代码开始执行,执行完后,此时的AO是:

AO = {
  arguments: {
    0: 1,
    length: 1
  },
  value: 1,
  a: 3,
  b: reference to function inFn(){},
  c: reference to FunctionExpression "c"
}


从上面来看,代码整体的执行顺序应该是:

function fn(value){
  var a;
  function inFn(){};
  var d;
  console.log(a);
  console.log(inFn);
  a = 2;
  function inFn(){};
  c = function(){};
  a = 3;
}

1


每个时间,只会存在一个激活的变量对象。


作用域


作用域决定了查找变量的方法,JavaScript里采用的是 静态作用域动态作用域,静态作用域是在函数定义的时候才会被决定,动态作用域是在函数被调用的时候定义,下面是一道经典面试题:

var a = 1;
function out(){
  var a = 2;
  inner();
}
function inner(){
  console.log(a)
}
out()
// 1


作用域和作用域之间是有链接关系的,在查找变量的时候,如果当前上下文没有找到,就从父级执行上下文的变量对象中找,直到全局上下文。

函数的作用域在创建的时候决定,是因为内部有个属性叫:[[scope]],它会保留所有的父变量,换句话讲,它就是所有父变量对象的层级链,我们可以从控制台找到某个函数里的 [[scope]] ,但是他不代表完整的作用域链!

function out(){
  function inner(){}
}


函数创建时,各自的 [[scope]] 为:

out.[[scope]] = [
  globalContext.VO
]
inner.[[scope]] = [
  outContext.AO,
  globalContext.VO
]


当函数被激活时,进入函数上下文,创建AO后,会将活动对象添加到作用域链的顶端,此时执行上下文的作用域链,我们叫 Scope

Scope = [AO].concat([[scope]])


到现在为止,作用域链创建完毕。

下面把执行上下文和作用域结合起来,看一下它的执行过程是怎样的:

var scope = "global scope";
function fn(){
  var a = "local scope";
  return a;
}
scope();


  1. fn函数被创建,此时fn会维护一个私有属性[[scope]],把当前环境的作用域链初始化到这个[[scope]]上
fn.[[scope]] = [
  globalContext.VO
]
  1. 执行fn函数,创建fn的执行上下文,之后fn函数的执行上下文被压入执行上下文栈
ECStack = [
  fnContext,
  globalContext
]


  1. fn函数复制内部的[[scope]]属性,从而创建作用域链
fnContext = {
  Scope: fn.[[scope]]
}


  1. 此时fn的执行上下文和作用域链构建完毕,开始用 arguments 创建并初始化活动对象,加入形参,函数声明和变量声明
fnContext = {
  AO: {
    arguments: {
      length: 0
    },
    a: undefined
  },
  Scope: fn.[[scope]]
}


  1. 此时fn内部也构建完毕,开始将自己的活动对象AO压入自己作用域链的顶端
fnContext = {
  AO: {
    arguments: {
      length: 0
    },
    a: undefined
  },
  Scope: [AO, [[Scope]]]
}

  1. 注意,此时的作用域链就包括了 自己的AO 和 前面通过复制内部[[scope]]创建好的作用域链
  2. 此时,fn的作用域链,变量,执行上下文都完毕了,开始执行fn函数,接下来的每一步就是修改 AO 的值,然后把AO压栈出栈,最终:
ECStack = [
  globalContext
]


有一个讲的比较细的例子在这里:一道JS面试题引发的思考(opens new window)


垃圾回收


在函数中,局部变量会在函数执行的时候存在,如果函数结束了,变量就不被需要了,它所占用的内存就可以释放出来。常用的两种机制为 标记清理引用计数

标记清理是最常用的,就是每用到一次,该变量就会被标记一次,依次叠加,每不用一次(即离开上下文),标记就会减少一个,依次递减。

引用计数就是对每个值的引用做一个记录,引用一次就加1,浏览器记录的是引用的次数,如果该值引用的变量被其他值覆盖了,就减1,当引用数为0时,释放内存。

如果对变量引用不当,或者执行的最终作用域没有释放掉,那么它就不会被标记和引用计数,此时就会造成 内存泄漏,又一道经典面试题:

function fn(value){
  return function(name){
    return value + name
  }
}
var fn2 = fn("123");
var name = fn2("小明")


经典闭包题,函数内返回一个匿名函数!我们再来分析一遍: fn2调用了fn,返回了一个匿名函数,该匿名函数会持有fn函数作用域的VO,包括arguments和value。当fn执行结束被销毁后,它的VO还是会一直保存在内存中,它的VO仍然在匿名函数中存在,也就是说这个VO一直被用着,所以浏览器的垃圾回收机制不会对它做处理,此刻就成了内存泄漏。

要想避免内存泄漏,常用的方法就是:赋值为null

fn2 = null


强制把fn2内部清空,这样匿名函数的引用就成了null,此时它所使用的fn的VO就可以被回收了。

目录
相关文章
|
1月前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
1月前
|
算法 JavaScript 前端开发
垃圾回收机制对 JavaScript 性能的影响有哪些?
【10月更文挑战第29天】垃圾回收机制对JavaScript性能有着重要的影响。开发者需要了解不同垃圾回收算法的特点和性能开销,通过合理的代码优化和内存管理策略,来降低垃圾回收对性能的负面影响,提高JavaScript程序的整体性能。
|
1月前
|
JavaScript 前端开发 算法
JS垃圾回收
【10月更文挑战第30天】JavaScript 的垃圾回收机制是保证程序稳定运行的重要组成部分。了解垃圾回收的原理和算法,以及注意避免内存泄漏的问题,可以帮助开发者更好地利用 JavaScript 进行高效的开发
|
1月前
|
存储 JavaScript 前端开发
JavaScript的垃圾回收机制
【10月更文挑战第29天】JavaScript的垃圾回收机制是确保程序高效运行的重要保障,了解其工作原理和相关注意事项,有助于开发者更好地编写高性能、稳定的JavaScript代码。
|
2月前
|
存储 前端开发 JavaScript
JavaScript垃圾回收机制深度解析
【10月更文挑战第21】JavaScript垃圾回收机制深度解析
124 59
|
1月前
|
自然语言处理 JavaScript 前端开发
[JS]作用域的“生产者”——词法作用域
本文介绍了JavaScript中的作用域模型与作用域,包括词法作用域和动态作用域的区别,以及全局作用域、函数作用域和块级作用域的特点。通过具体示例详细解析了变量提升、块级作用域中的暂时性死区等问题,并探讨了如何在循环中使用`var`和`let`的不同效果。最后,介绍了两种可以“欺骗”词法作用域的方法:`eval(str)`和`with(obj)`。文章结合了多位博主的总结,帮助读者更快速、便捷地掌握这些知识点。
37 2
[JS]作用域的“生产者”——词法作用域
|
2月前
|
存储 JavaScript 前端开发
深入理解 JavaScript 执行上下文与 this 绑定机制
JavaScript 代码执行时,会为每段可执行代码创建对应的执行上下文,其中包含三个重要属性:变量对象、作用域链、和 this。本文深入剖析了执行上下文的生命周期以及 this 在不同情况下的指向规则。通过解析全局上下文和函数上下文中的 this,我们详细讲解了 this 的运行期绑定特性,并展示了如何通过调用方式影响 this 的绑定对象。同时,文中对箭头函数 this 的特殊性以及四条判断 this 绑定的规则进行了总结,帮助开发者更清晰地理解 JavaScript 中的 this 行为。
89 8
深入理解 JavaScript 执行上下文与 this 绑定机制
|
1月前
|
前端开发 JavaScript 数据处理
CSS 变量的作用域和 JavaScript 变量的作用域有什么不同?
【10月更文挑战第28天】CSS变量和JavaScript变量虽然都有各自的作用域概念,但由于它们所属的语言和应用场景不同,其作用域的定义、范围、覆盖规则以及与其他语言特性的交互方式等方面都存在明显的差异。理解这些差异有助于更好地在Web开发中分别运用它们来实现预期的页面效果和功能逻辑。
|
1月前
|
JavaScript 前端开发
如何在 JavaScript 中实现块级作用域?
【10月更文挑战第29天】通过使用 `let`、`const` 关键字、立即执行函数表达式以及模块模式等方法,可以在JavaScript中有效地实现块级作用域,更好地控制变量的生命周期和访问权限,提高代码的可维护性和可读性。
|
1月前
|
JavaScript 前端开发
javascript的作用域
【10月更文挑战第19天javascript的作用域