logo头像
Snippet 博客主题

js执行上下文、词法作用域、块级作用域相关学习

本文于989天之前发表,文中内容可能已经过时。

JavaScript引擎

JavaScript引擎的一个流行示例是Google的V8引擎。例如,在Chrome和Node.js中使用V8引擎,下面是一个非常简化的视图:

浏览器中的JavaScript解释器是作为一个单线程实现的,这实际上意味着,在浏览器中,一次只能发生一件事,其他操作或事件将排队在所谓的执行堆栈中。

词法作用域

函数作用域和块级作用域

块级作用域与函数声明

  • ES5 只有全局作用域和函数作用域,没有块级作用域。
  • ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数。
  • ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
  • ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
  • 为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
    • 允许在块级作用域内声明函数。
    • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
    • 同时,函数声明还会提升到所在的块级作用域的头部。

letconst必须在{}大括号包裹下产生块级作用域,来感受下letvar的区别吧

1
2
3
4
5
6
7
8
var aa = 'bb';
function fn() {
console.log(aa);
{ // 在块级作用域内var声明变量
var aa = 'aa';
}
}
fn(); // undefined
1
2
3
4
5
6
7
8
var aa = 'bb';
function fn() {
console.log(aa);
{ // 在块级作用域内let声明变量
let aa = 'aa';
}
}
fn(); // 'bb'
1
2
3
4
5
6
7
8
9
10
11
12
function bs() {
console.log(1);
}
function fn() {
bs();
{ // 在块级作用域内声明函数
function bs() {
console.log(2);
}
}
}
fn(); // TypeError: bs is not a function
1
2
3
4
5
6
7
8
9
10
11
12
function fn() {
let a = 1;
if (true) { // 在块级作用域内声明变量
a = 5;
var b = 2;
const c = 3;
}
console.log(a); // 5
console.log(b); // 2
console.log(c); // ReferenceError: c is not defined
}
fn();
1
2
3
4
5
6
7
8
9
10
11
12
function fn() {
let a = 1;
if (false) { // 在块级作用域内声明变量
a = 5;
var b = 2;
const c = 3;
}
console.log(a); // 1
console.log(b); // undefined
console.log(c); // ReferenceError: c is not defined
}
fn();

变量提升

暂时性死区

使用let/const声明的变量,从一开始就形成了封闭作用域,在声明变量之前是无法使用这个变量的,这个特点也是为了弥补var的缺陷(var声明的变量有变量提升)

1
2
console.log(aa); // ReferenceError: Cannot access 'aa' before initialization)
let aa;

剖析暂时性死区的原理,其实let/const同样也有提升的作用,但是和var的区别在于:

  • var在创建时就被初始化,并且赋值为undefined
  • let/const在进入块级作用域后,会因为提升的原因先创建,但不会被初始化,直到声明语句执行的时候才被初始化,初始化的时候如果使用let声明的变量没有赋值,则会默认赋值为undefined,而const必须在初始化的时候赋值。而创建到初始化之间的代码片段就形成了暂时性死区

ES6 Class 不存在变量提升

1
2
3
4
console.log(Test); // ReferenceError: Cannot access 'Test' before initialization
class Test {
// ...
}

解释器如何评估JS代码(evaluate)

  1. 扫描被调用函数中的代码
  2. 在代码执行前,创建执行上文
  3. 进入创建阶段
    • 初始化作用域链
    • 创建变量对象
    • 创建arguments对象,检查参数上下文,初始化名称和值,并创建引用副本
    • 扫描上下文中函数的声明
      • 对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名,该函数在内存中有一个指向该函数的引用指针
      • 如果函数名已经存在,指针将会被覆盖
    • 扫描变量的声明
      • 对于找到的每个变量,在变量对象中创建一个属性,该属性是确切的变量名,该变量的值是undefined
      • 如果变量名已经存在,将不会做任何处理继续执行
    • 决定this的值
  4. 代码执行阶段
    • 变量赋值,按顺序执行代码

结合自身的感悟如下:

  • 知道了js代码执行前有预编译这么一回事,也就能掌握变量提升,解释器根据var, function等关键字进行预编译,function声明整体前置,var声明变量并赋值为undefined,体现了function是一等公民

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    (function() {
    console.log(foo); // foo() { return 'hello'; }
    console.log(bar); // undefined

    var bar = function() {
    return 'world';
    };

    var foo = 'hello';

    function foo() {
    return 'hello';
    }

    console.log(foo); // 'hello'
    }());​
  • 词法作用域决定了函数执行上下文作用域,跟函数在哪调用没有关系(this在任何情况下都不指向函数的词法作用域,this是在函数运行时,创建上下文的时候确定的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function a() {
    var myOtherVar = "inside A";
    b();
    }

    function b() {
    var myVar = "inside B";
    console.log("myOtherVar:", myOtherVar); // 'global otherVar'

    function c() {
    console.log("myVar:", myVar); // 'inside B'
    }

    c();
    }

    var myOtherVar = "global otherVar";
    var myVar = "global myVar";
    a();

执行上下文

执行上下文:当前代码正在执行的环境(作用域),一般有以下三种情况:

  • 全局代码 – 代码首次开始执行的默认环境
  • 函数代码 – 每当进入一个函数内部
  • Eval代码 – eval内部代码执行时
  • Nothing special is going on here, we have 1 global context represented by the purple border and 3 different function contexts represented by the green, blue and orange borders. There can only ever be 1 global context, which can be accessed from any other context in your program.

  • You can have any number of function contexts, and each function call creates a new context, which creates a private scope where anything declared inside of the function can not be directly accessed from outside the current function scope. In the example above, a function can access a variable declared outside of its current context, but an outside context can not access the variables / functions declared inside.(理解为作用域链)

现在我们知道每当有函数被调用时,都会创建一个新的执行上下文。在js内部,每个执行上文创建都要经历下面2个阶段:

  1. 创建阶段(函数被调用,但还没有执行内部代码)
    • 创建作用域链
    • 创建变量和参数
    • 决定this指向
  2. 代码执行阶段
    • 变量赋值,执行代码

this 全面解析

推荐阅读:刘小夕 - 嗨,你真的懂this吗?

闭包

参考文章:

微信打赏

赞赏是不耍流氓的鼓励