Js执行机制

js执行机制

全文联系执行上下文,深入解释js执行机制

执行流程和变量提升

变量提升

先给出一段代码:

show()
console.log(me)
var me=0;
function show(){
    console.log(1)
}

打印结果为:

Js执行机制

可以看见,1,2行还没有定义变量与函数,但是没有抛出错误。

原因就在于js变量提升机制。

变量提升

变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分函数的声明部分提升到代码开头的“行为”。

变量被提升后,会给变量设置默认值,undefined。

声明赋值:

var me=0;
//将以上代码分为声明阶段和赋值阶段

var me;//声明
me=0;//赋值

执行流程

变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,正如我们所模拟的那样。但,这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中

编译->执行

js代码通过js引擎,先编译,后执行。

Js执行机制

对上图所提及的概念稍作解释:

执行上下文

js执行一段代码时的运行环境,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

一段代码

1.当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份

2.当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。

3.当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。(本文暂不表)

变量环境

一个对象,JavaScript 引擎会把声明代码中变量储存其中,把声明以外的代码编译为字节码

var me=undefined;
function show(){
    console.log(1)
}

可执行代码

执行阶段使用的代码,其中的变量会被js引擎在变量环境中寻找。

如果编译阶段变量(函数)被声明两次,变量环境中前一个会被后一个覆盖,执行阶段也只会调用唯一一个变量。

show()
console.log(me)
me=0;

调用栈

函数调用

var me=0;
function show(){
    console.log(me)
    return me;
}
show()

这里show()就是函数调用,这一段js代码中会含有全局上下文show函数的执行上下文

js会通过名为调用栈的数据结构管理执行上下文。

调用栈call stack

栈是一种先进后出的数据结构。

调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系

js会先将全局执行上下文压入栈底,

每当调用一个函数(执行而不是声明),就会压入该函数执行上下文,

并在函数执行结束之后令其出栈

稍微举个例子,

var a=2;
function add(m,n){
    return m+n
}
function addAll(m,n){
    var b=3
    c=add(m,n)
    return a+b+c
}
addAll(3,6)

扫一眼大概知道这段代码有3段上下文:全局,addAll函数,add函数

就这段代码的上下文入栈和出栈为线索,有下:

1. 全局执行上下文压入栈底

调用栈底部->顶部为:

全局执行上下文

全局执行上下文,变量环境

a=undifined
add=(引用,指向堆内存中add函数)
addAll=(引用,指向堆内存中addAll函数)

2. 赋值a=2,调用addAll函数时,函数上下文压栈

调用栈底部->顶部为:

全局执行上下文->addAll函数执行上下文

addAll函数执行上下文,变量环境

参数列表
b=3
c=undefined

全局执行上下文,变量环境

a=2
add=(引用,指向堆内存中add函数)
addAll=(引用,指向堆内存中addAll函数)

3.调用add函数时,函数上下文压栈

调用栈底部->顶部为:

全局执行上下文->addAll函数执行上下文->add函数执行上下文

add函数执行上下文,变量环境

参数列表

addAll函数执行上下文,变量环境

参数列表
b=3
c=undefined

全局执行上下文,变量环境

a=2
add=(引用,指向堆内存中add函数)
addAll=(引用,指向堆内存中addAll函数)

4. add函数return,add函数出栈

调用栈底部->顶部为:

全局执行上下文->addAll函数执行上下文

addAll函数执行上下文,变量环境

参数列表
b=3
c=9

全局执行上下文,变量环境

a=2
add=(引用,指向堆内存中add函数)
addAll=(引用,指向堆内存中addAll函数)

5. addAll函数return,addAll函数出栈

调用栈底部->顶部为:

全局执行上下文

全局执行上下文,变量环境

a=2
add=(引用,指向堆内存中add函数)
addAll=(引用,指向堆内存中addAll函数)

示例结束。

栈溢出

调用栈有大小,当执行上下文数量过多(比如进入递归死循环),会导致栈溢出

块级作用域和作用域链

学习前端过程里,学长告诉我要避免使用var关键字声明变量,多用letconst

上述对于上下文执行已经解释了与var息息相关的变量环境,其实var使用是反直觉的,
全局变量无疑易被覆盖,污染作用域,穿透函数,难以回收(也就是内存泄漏),体现高耦合。这对于抽象函数模块化思想,对于大型项目的公共开发,都很明显不合适。

解决缺陷的方式,不得不讲讲es6引入的块级作用域

作用域

es6之前

作用域分为全局作用域函数作用域,与执行上下文同步。

  • 全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
  • 函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

es6带来的

新增块级作用域

学c类语言知道,一对{}包括的都是块级作用域

//if
if(){}

//while
while(){}

//函数
function foo(){}

//for循环
for(;;){}

//单独
{}

代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁.

JavaScript 引擎并不会把块中通过 let声明的变量存放到变量环境中,这也就意味着在块中通过 let 声明的关键字,并不会提升到全函数可见。

letconst声明的变量去哪里了?

与变量环境独立的,词法环境

词法环境

通过const,let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment) 中。

在词法环境内部,维护了一个小型结构,栈底是函数最外层的变量。
进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。

作用域层数映射了词法环境中的栈内变量数

也可以理解为,从上到下,代码执行到{压栈,代码执行到}出栈

Js执行机制

再聊聊let声明过后的变量的查找顺序,一图可解:

Js执行机制

如图,先从栈顶从上向下(即映射为,作用域从内向外)查找词法环境,再找变量环境。

作用域链

执行上下文->outer

每个执行上下文的变量环境中,都包含了一个外部引用,用来指向直接外部的执行上下文,我们把这个外部引用称为outer

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系

Js执行机制

闭包

是什么?

给一段示例代码

function foo(){
    var myName="moon"
    let a=1
    const b=2
    var innerBar={
        getName:function(){
            console.log(a)
            return myName
        },
        setName:function(newName){
            myName=newName
        }
    }
    return innerBar
}
var bar=foo()
bar.setName("ice")
console.log(bar.getName())

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象 return 给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 myNamea

Js执行机制

foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中,。这像极了 setName 和 getName 方法背的一个专属背包,无论在哪里调用了 setName 和 getName 方法,它们都会背着这个 foo 函数的专属背包。

是因为除了 setName 和 getName 函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为 foo 函数的闭包

闭包就是:当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

怎么回收?

通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。

执行上下文角度下的this

在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套this 机制。(这是两套不同的系统)

this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。

全局执行上下文中的this

在控制台中输入console.log(this)来打印出来全局执行上下文中的 this,最终输出的是 window 对象。

即全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象。

函数执行上下文中的this

  • 调用指向谁, 谁构造指向谁
  • call,bind等方法来设置函数执行上下文的 this 指向
  • 普通函数中this默认指向window,与声明无关
  • t嵌套函数中his不会从外层函数继承(箭头函数可以解决)

原文链接:https://juejin.cn/post/7342705422530478115 作者:桉陈

(0)
上一篇 2024年3月6日 上午10:47
下一篇 2024年3月6日 上午10:57

相关推荐

发表回复

登录后才能评论