Javascript 执行机制

变量提升与执行上下文

JS 代码运行会分为编译执行两个阶段

所谓的变量提升,是指在 JavaScript 代码编译过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值并存到内存中,这个默认值就是我们熟悉的 undefined。

最终在编译阶段会生成两部分内容:执行上下文(Execution context)和 可执行代码

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

  • 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
  • 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
  • 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。

调用栈

先进先出,执行上下文通过调用栈来管理

全局执行是上下文最先被压入栈中, 接下来如果有函数执行,当为函数创建好执行上下文后,也会被压入栈中,当函数返回时,执行上下文会从栈顶弹出.

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

使用 console.trace() 可以查看当前的调用栈信息.

作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

ES6 总共有三种作用于, 全局作用域,函数作用域,块级作用域,如果没有块级作用域会存在两个问题

  • 变量容易在不被察觉的情况下被覆盖掉
  • 本应销毁的变量没有被销毁

JS 通过 const, let 实现块级作用域,在创建执行上下文的时候, 这两个关键字会单独存放在词法环境中,而 var 声明的变量于函数会存放在变量环境中.

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
}
}
foo();

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

查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

作用域链于词法作用域

作用域链是由词法作用域决定的.

下面的代码当 barfoo 内部调用的时候, 会先查找自己的词法环境中有没有 name, 然后查找自己的环境变量.

但是当发现都没有的时候并不会在 foo 的词法环境和变量环境中查找,而是直接查找全局执行上下文中的词法环境和变量环境

1
2
3
4
5
6
7
8
9
function bar() {
console.log(name);
}
function foo() {
var name = "one";
bar();
}
var name = "tow";
foo();

控制变量查找顺序的就是作用域链,其实在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外 部的执行上下文,这个外部引用称为 outer. 但是决定作用域链的不是执行上下文,而是词法作用域

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

foobar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

闭包

根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
let name = "one";
const obj = {
getName() {
return name;
},
setName(_name) {
name = _name;
},
};
return obj;
}

var bar = foo();
bar.setName("two");
bar.getName();

当 obj 对象返回给全局变量 bar 时,虽然 foo 函数已经执行结束,但是 getName 和 setName 函数依然可以使用 foo 函数中的变量 name.

虽然 foo 函数执行结束后执行上下文已经从调用栈中弹出,但是由于 obj 对象的方法使用了内部 name 变量,所以 name 变量还是保存在内存中,而保存 name 变量称作闭包. 无论在那里调用 obj 对象的方法,都可以访问到 name 变量.

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

而调用 obj 方法的时候,作用域链的顺序就是:当前执行上下文–>foo 函数闭包–> 全局执行上下文

闭包回收

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

如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

this

实现 this 的一个初衷就是 在对象内部的方法中使用对象内部的属性

因为作用域链由词法作用域决定,所以调用 getName 并不会获取对象属性而是在全局的执行上下文中查找

1
2
3
4
5
6
const obj = {
name: "one",
getName() {
console.log(name);
},
};
全局执行上下文中的 this

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

这种设计很容易误操作,所以严格模式下默认执行一个函数 this 为 undefined

函数执行上下文中的 this

直接调用一个函数,其执行上下文中的 this 也是指向 window 对象的. 但是可以通过 call apply bind 修改 this 的指向

通过对象调用方法 this

this 是指向对象本身的。 也可以理解为在调用的时候转化为了这样的形式 myObj.showThis.call(myObj)

构造函数中的 this

this 指向创建的实例. new 操作符实际上做了一下几件事.

  • 创建一个空的简单 JavaScript 对象(即{});
  • 为步骤 1 新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  • 将步骤 1 新创建的对象作为 this 的上下文 ;
  • 如果该函数没有返回对象,则返回 this。
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信