Skip to content

执行上下文 #6

@coconilu

Description

@coconilu

前言

前面的一篇博客《JS运行原理》,已经大致讲解过“执行上下文”。这篇博客计划详细说说这个概念。

定义

我们一般把JS引擎运行JS代码分为两步,

  1. 创建执行上下文
  2. 逐行执行代码

函数声明和函数表达式都属于函数定义阶段,只有在函数被执行阶段,新的执行上下文才会被创建。

执行上下文确定了第二步需要用的变量(或者说是寻找变量的途径),如果在执行代码的时候无法确定变量将会报错ReferenceError,不包括变量为undefined和null的情况。
执行上下文主要包括三个重要属性:

  1. 变量对象
  2. 作用域链
  3. this

提醒一下,for或if语句不会创建新的执行上下文,函数才会创建新的执行上下文

这篇博客涉及到的概念

  1. 执行上下文栈
  2. 提升
  3. 变量对象
  4. 作用域链
  5. 计算this的指向
  6. 暂时性死区

执行上下文栈

JS引擎刚开始读取JS文件的时候,会创建一个全局上下文,并把它放到执行上下文栈里,之后如果遇到需要新创建执行上下文,会把它压到栈顶;直到相关的代码执行完毕并返回结果,该执行上下文会被出栈。执行上下文栈是创建作用域链的重要参考。

提升(Hoisting)

提升是相对于var声明的变量和函数声明的,函数表达式并不会被提升。

// 函数声明形如:
// name是函数名,类似变量名可以被调用
function name([param,[, param,[..., param]]]) {
   [statements]
}

// 函数表达式形如:
// 其中的[name]是函数代名词,只是作为函数体的一个本地变量(意味着外部是不可以调用的),主要用于调用自身
var function_expression = function [name]([param1[, param2[, ..., paramN]]]) {
   statements
};

提升的伪逻辑如下:

  1. 提升var变量,如果存在同名变量(包括同名形参),不进行操作;若不存在,则初始化为undefined
  2. 提升函数声明,提升并覆盖已有的同名变量
(function (a, b) {
    console.log(a, b)
    var a;
    function b() {};
    a = 1;
    console.log(a, b)
})(3, 4)

output:
3 [Function: b]
1 [Function: b]

伪代码转换:
(function (a, b) {
    var a=3,b=4;
    b=function () {}

    console.log(a, b)
    a = 1;
    console.log(a, b)
})(3, 4)

变量对象

全局执行上下文的变量对象有:

  1. 全局对象(浏览器环境中是window,nodejs中是global),允许直接访问属性(比如经常使用的new Date,本身就是Window.Date)
  2. 使用或不使用var定义的变量,差别是不使用var定义的变量可以用delete操作符删除
  3. 函数声明

函数执行上下文的变量对象有:

  1. arguments
  2. 实参
  3. 使用var定义的变量
  4. 函数声明

作用域链

当在本执行上下文中的变量对象中没有找到目标变量的话,就会借助作用域来查找变量。
比如:在函数执行上下文中未使用var定义的变量被调用的时候,因为在变量对象中找不到,所以会顺着作用域链查找下一个执行上下文中的变量对象。

作用域链是根据执行上下文栈来确定的。

特殊作用域:

  1. try-catch语句块,临时在作用域链中添加error对象
  2. with语句块,也会临时在作用域链中添加with的对象
  3. eval语句,如果你间接的使用 eval(),比如通过一个引用来调用它,而不是直接的调用 eval ,从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中

计算this的指向

一旦确定了this的指向,当访问this的某个成员变量或函数,如果不存在不会根据执行上下文栈找到上一个this并查询。

1. 全局执行上下文

this指向全局对象,在浏览器环境下是window,在nodejs下是global

2. 函数执行上下文

  1. 作为对象的方法,即使方法是来自该对象的原型链上的方法,this也指向该对象
  2. 作为构造函数,指向正在构建的新对象
  3. 如果是简单调用,就是没有形如fun()而不是obj.fun(),this指向全局对象,但是在严格模式下,会指向undefined
  4. 箭头函数,与封闭词法上下文的this保持一致

这里的封闭词法上下文没有那么高大上,说白了,箭头函数的执行上下文没有定义this属性,根据执行上下文栈找到上一个执行上下文的this,就是箭头函数中的this。下面看一段代码。

var x = 'windows';
function fun1() {
    var fun2 = () => {
        console.log(this.x)
    }
    fun2()
}
fun1();

var obj = {
    x: 'obj',
    showX: fun1
}
obj.showX()

浏览器非严格模式的output:
windows
obj

nodejs,默认就是严格模式:
undefined
obj

fun2的上一个执行上下文就是fun1,所以fun2的this就是fun1的this。第一次调用是简单调用,所以fun1的this指向全局变量(window);第二次调用是作为对象的方法被调用,所以this指向新对象,而新对象里刚好有x变量。

3. 特殊情况

  1. 使用apply和call传递this指向
  2. 使用bind方法绑定this的指向,只对第一次的绑定生效
  3. DOM事件构造函数里的this指向触发事件的DOM元素
  4. 内联事件处理函数里的this指向监听器所绑定的DOM元素

暂时性死区

只有在es6中的let和const才会发生这种情况。

进入一个执行上下文的时候,
let、const声明的变量和形参定义的变量,都会屏蔽掉前一个执行上下文中的同名变量,
且在声明语句之前是不可以使用该变量,
声明语句之后,若该变量没有被赋值,则默认为undefined。

var x = 0;
function f(x = x) { // A行
    console.log(x)
    console.log(y) // B行
    let y = 1;
}
f()

output:
ReferenceError: x is not defined
ReferenceError: y is not defined

上面的代码里A行中的第一个x是形参定义的变量,它会屏蔽掉全局中的x,所以会报错;B行中的y由于是let定义的变量,所以在声明语句前调用将会报ReferenceError错误。

参考链接

http://yanhaijing.com/javascript/2014/04/29/what-is-the-execution-context-in-javascript/
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
https://www.cnblogs.com/snandy/archive/2011/03/19/1988284.html
http://blog.csdn.net/yangbingbinga/article/details/61424363

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions