JavaScript 执行机制

279 阅读6分钟

JavaScript 执行机制

JavaScript 代码执行先编译,后执行。首先在编译阶段,会生成执行上下文和可执行代码两部分,然后是执行阶段,按照顺序一行一行执行代码, 并且查找变量或者函数。

JavaScript执行机制.png

执行上下文

执行上下文(Execution context)是 JavaScript 执行一段代码时的运行环境。确定执行时的变量、函数、this等。

执行上下文为三种情况:

  1. 全局执行上下文,在页面生存周期内唯一;
  2. 函数执行上下文,在调用函数是创建;
  3. eval 函数执行上下文。

调用栈

调用栈是用来管理执行上下文的栈。栈的特点是后进先出。调用时有大小的,入栈的执行上下文超过一定的数目会导致栈溢出

调用栈的入栈出栈过程:

1. 创建全局执行上下文,压入调用栈的栈底,然后执行全局代码;
2. 调用函数创建函数执行上下文,压入调用栈,然后执行函数内代码;
3. 若函数内还存在函数调用,则重复步骤2,若函数执行结束,该函数的执行上下文栈顶弹出;
4. 重复步骤2、步骤3。

变量环境

在执行上下文中存在一个变量环境的对象。用于存放变量提升的内容。

变量提升

函数声明和var关键字创建的变量声明会在编译阶段提升,且变量会设置默认值undefined。这些内容存放在变量环境对象中。

var test = 'test';

上面test变量分为声明和赋值两部分(函数只有声明过程)。在编译阶段,声明且设置默认值undefined存放在变量环境中;在执行阶段,进行赋值操作。

1588691775654

当提升过程中,存在同名的变量/函数处理原则:

  1. 如果都是变量提升,始终都会设置默认值undefined;
  2. 如果都是函数提升,后声明覆盖先声明的;
  3. 两者皆有,则会忽略变量声明。
// 同名提升,忽略变量声明
console.log(test)// ƒ test () {}
var test = 'test';
function test () {}
console.log(test)// test

词法环境

作用域

作用域指的是代码中定义变量的区域,控制着变量和函数的可见性和生命周期。作用域分为:

  1. 全局作用域,在代码中任何地方都可以访问;
  2. 函数作用域,只能在函数内部访问,函数执行结束,内部声明变量和函数会被销毁;
  3. 块级作用域(let/const),在大括号中的变量拥有单独的作用域(ES6 新增)。

变量提升存在的问题

变量提升存在的问题主要是:

  1. 变量覆盖。变量容易被不经意覆盖,导致程序不按预期执行。

    var name = '变量1';
    function func(){
    	console.log(name);
    	if (false) {
    		var name = '变量2';
    	}
    }
    func();// undefined
    

    上面的代码中,若需要使用外部的name值则需要修改if代码块中的变量名。

  2. 变量污染。在代码块中变量,会一直存在直到函数结束。

    var i = 1;
    for(var i = 0; i < 10; i++){}
    console.log(i);// 10
    

let/const

在 ES6 中引入了let/const关键字实现了块级作用域letconst声明的变量区别在于前者声明的变量值可修改,后者声明的变量值不可修改(若变量为对象,可改变属性)。

let test1 = 'test1';
const test2 = 'test2';
test1 = 'test11';
test2 = 'test22'; //报错 Assignment to constant variable.

块级作用域解决了变量提升带来的问题。具体见如下代码:

let name = '变量1';
function func(){
	console.log(name);
	if (false) {
		let name = '变量2';
	}
}
func();// 变量1

let i = 1;
for(let i = 0; i < 10; i++){}
console.log(i);// 1

let/const使用的一些特殊之处:

  1. 存在暂时性死区,变量不会提升;

  2. 在浏览器中,顶层对象是window。在 ES6 之前,顶层对象的属性和全局变量是等价的,ES6 做了改变,letconstclass关键字声明的全局变量不属于顶层对象的属性了;

    var a = 'a';
    window.a // a
    
    let b = 'b';
    window.b // undefined
    
  3. 在函数顶层的词法环境变量,在编译阶段变量声明会存放在词法环境对象中;

  4. 在函数内部的块级作用域,会在执行时追加到词法环境对象中;

  5. 词法环境内部是一个栈结构,栈底是函数最外层/全局的变量;当进入一个块级作用域后,会将该块级作用域下变量压入栈顶,块级作用域执行完成从栈顶弹出。

outer

每个执行上下文的变量环境中,包含了一个外部引用指向外部的执行上下文,这个外部引用就是 outer。在变量查找的过程中,通过outer进行外部作用域查找。

this

全局执行上下文中的 this

全局执行上下文中的 this 指向 window 对象。

函数执行上下文中的 this

  • 默认绑定。在非严格模式下,this指向 window 对象;严格模式下,this绑定为this

    // 非严格模式
    function aa(){
    	console.log(this)
    }
    aa();// Window
    
    // 严格模式
    function bb(){
    	'use strict'
    	console.log(this)
    }
    bb();// undefined
    
  • 隐式绑定this指向最后调用它的对象。

    var name = 'test1';
    let obj = {
    	name: 'test2',
    	func(){
    		console.log(this.name);
    	}
    }
    
    let obj1 = obj.func;
    obj1(); // tes1
    obj.func(); // test2
    
  • 显示绑定。通过call()apply()bind()方法指定this的绑定对象。

    • callapply直接执行函数;
    • bind创建一个新函数,需要手动调用执行;
    • 接受的第一个参数是空或者null、undefined时: 非严格模式下,this指向window对象; 严格模式下,若参数为空或者undefined时,this绑定到undefined;参数为null时,this绑定到null
    • callbind接受多个参数,apply接受一个数组参数。
  • new 绑定this指向实例对象。

  • 箭头函数this由外层作用域决定,且指向函数定义时的this而非执行时。

可执行代码

JavaScript 引擎将声明以外的代码编译成子节点。

变量查找

作用域链

变量查找不是按照调用栈的顺序,从栈顶进行查找;而是在当前执行上下文中未找到时,在outer所指的执行上下文中查找,形成了作用域链。

作用域链有由词法作用域决定。词法作用域是静态作用域,由代码中函数的声明的位置决定,和函数调用无关。

闭包

通过chrome DevTools可以看出,当内部函数使用了外部函数中的变量时,就产生了闭包。(也可以理解成,在调用外部函数时,返回了一个内部函数。当内部函数中使用了外部函数的变量时,就产生了闭包。)

1588867799222

闭包回收:当闭包作为全局变量时,会一直存在;作为局部变量时,函数销毁后,会被垃圾回收器回收。

查找规则

  • 在当前作用域中查找:
    1. 先从词法环境中进行查找:按照栈顶至栈底顺序查找;
    2. 然后在变量环境中查找。
  • 执行上下文之间查找:
    1. 在当前作用域未查到,通过作用域链找到外部的作用域,进行查找;
    2. 重复步骤1,直到查找到全局作用域。

总结

主要是对 JavaScript 中执行上下文相关内容进行了一些总结,执行机制的其他的部分介绍比较粗略,后续再多多了解。

参考资料

  1. 浏览器工作原理与实践
  2. ECMAScript 6 入门