js的执行过程

js的执行

开篇:不管是什么语言,通过运行题的考察都可以了解到一个开发者对于语言的基本掌握程度,对于js的这方面的知识不管是看书还是看博客都可以轻松获取,以下将讲到执行程序时候涉及到的语法知识,看不懂就直接看里面的代码解析再看概念。我先推荐一本《你不知道的javascript》

1.什么时候变量提升和函数提升

​ 首先了解js正常执行代码的过程, 浏览器先按照js的顺序加载script标签分隔的代码块,js代码块加载完毕之后,立刻进入到下面的三个阶段(语法,预编译,执行),然后再按照顺序找下一个代码块,再继续执行这三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的,并且都在同一个全局作用域中。

1.1语法分析

​ 分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误(syntaxError),正确就进入预编译阶段。

1.2预编译

预编译阶段,代码每进入一个运行环境就构造对应的“执行上下文“(execution context)。js引擎在创建执行上下文后把上下文推入函数调用栈(call stack),栈底永远是全局执行上下文(global execution context),栈顶则永远时当前的执行上下文。

代码

function inner(b){
 let a=3;
 return a+b;
}
function outer(x){
    let y=3;
    return inner(y+x);
}
console.log(outer(5));
/*
1.进入全局环境,创建全局执行上下文(用main表示),推入栈
2.调用console.log(),进入console.log函数运行环境,创建console.log函数执行上下文,推入栈
3.在console.log()中调用outer()函数,进入outer函数运行环境, 创建outer函数执行上下文,推入栈
4.在outer函数中调用inner函数,进入inner函数运行环境,创建inner函数执行上下文,推入栈
5.inner函数中没有调用其它函数,inner函数执行完毕,inner执行上下文出栈。
6.outer函数执行完毕,outer执行上下文出栈
7.全局执行上下文(global Execution Cntext)在浏览器或者该标签关闭的时候出栈。

*/

1.2.1 js的三种运行环境

  • 全局环境(js代码加载完毕后,进入预编译阶段即进入了全局环境)
  • 函数环境(函数执行时进入该函数环境,不同函数的函数环境不同)
  • eval环境(不建议使用,会有安全,性能等问题)

1.2.2 函数调用栈(call stack)

也就是执行栈,一个存储函数调用的栈结构,遵循先进后出的原则。

1.2.3创建执行上下文(做了以下三件事)

1、创建变量对象(variable object)

创建变量对象做了以下三件事情,创建arguments对象-->函数声明提升-->变量声明提升

创建arguments对象

检查当前上下文的参数,建立该对象的属性与属性值,仅在函数环境(非箭头函数)中进行的,全局环境没有此过程

函数声明提升

检查当前上下文的函数声明,按照代码顺序查找,将找到的函数提前声明,(注意函数的声明有两种方式,一种函数表达式,一种直接声明,这里的指直接声明)如果当前上下文的变量对象没有该函数名属性,则在该变量对象以函数名建立一个属性,属性值则指向该函数所在堆内存地址引用,如果存在,则会被新的引用覆盖掉。

即函数在当前执行上下文的任意地方都可以被调用

//直接声明
function global(){//会提升
    
  
}
//函数表达式
var indirect=function(){
    
}
function global(){//会提升
    a();
    function a(){
        console.log("dd");
    }
    a();
}
global();
a();//报错
针对a这个函数来讲,a函数存在于global的执行上下文。所以注意声明的范围是当前上下文
变量声明提升

检查 var 变量,声明创建属性:按代码顺序查找,将找到的变量提前声明,如果变量不存在,则赋值为:undefined。若存在,则忽略该声明。

如果是let变量,有创建对应属性,但是不会初始化为undefined.

参考:https://zhuanlan.zhihu.com/p/28140450

注意:函数声明提前和变量声明提升是在创建变量对象中进行的,且函数声明优先级高于变量声明。

//当前是全局环境,在全局环境中,window 对象就是全局执行上下文的变量对象,所有的变量和函数都是 window 对象的属性方法。var声明后的变量都可以在window的属性中找到。
console.log(global);//函数声明优先级高于变量声明,所以代表函数不是undefinded
global();
var global=function(){
    console.log("函数表达式")
}
function global(){
    console.log("直接声明")
}
global();
/*
预编译阶段,函数声明提升,所以function global(){
    console.log("直接声明")
}这个函数提升了,之后变量提升,所以var global=undefined;
前面我们可以认为js引擎对代码预处理。
然后开始的代码执行,global(),执行console.log("直接声明"),
 在执行global=function(){
    console.log("函数表达式")
}的赋值操作,所以最后的global是执行 console.log("函数表达式")
两步走:预编译,执行阶段
*/

如果是这样呢

console.log(global);
var global=function(){
    console.log("函数表达式")
}
global();
function global(){
    console.log("直接声明")
}
global();

一定要分两步走:预编译--》执行阶段

2、创建作用域链(scope chain)

作用域链由当前执行环境的变量对象(VO)和上层的一系列活动对象(AO)组成,保证了当前执行环境对符合访问权限的变量和函数的有序访问。

代码

function test() {
    var a = 10;

    function innerTest() {
        
        var b = 20;

        return a + b
    }

   console.log(innerTest()) ;
}
test();

在上面的例子中,当执行到调用 innerTest 函数,进入 innerTest 函数环境。全局执行上下文和 test 函数执行上下文已进入执行阶段,innerTest 函数执行上下文在预编译阶段创建变量对象,所以他们的活动对象和变量对象分别是 AO(global)AO(test)VO(innerTest) ,而 innerTest 的作用域链由当前执行环境的变量对象(未进入执行阶段前)与上层环境的一系列活动对象组成,如下:

innerTestEC = {

    //变量对象
    VO: {b: undefined}, 

    //作用域链
    scopeChain: [VO(innerTest), AO(test), AO(global)],  

    //this指向
    this: window
}
  • 作用域链的第一项永远是当前作用域(当前上下文的变量对象或活动对象);
  • 最后一项永远是全局作用域(全局执行上下文的活动对象);
  • 作用域链保证了变量和函数的有序访问,查找方式是沿着作用域链从左至右查找变量或函数,找到则会停止查找,找不到则一直查找到全局作用域,再找不到则会抛出引用错误。

再加入变量提升

function test() {
    var a = 10;

    function innerTest() {
        //innerTestEC执行上下文中a=undefinded,因为预编译时候var a=undefinded,记住分两步走:预编译--》执行阶段
        console.log(a);
        var a=0;
        var b = 20;

        return a + b
    }

   console.log(innerTest()) ;
}
test();
3、确定this的指向

分到下面单独讲。

预编译和同步执行的例子(没有涉及异步代码)

function Foo(){
   getName=function(){//变量提升到外部的全局作用域
       console.log(1);
   }
  return this;

}
Foo.getName=function(){
    console.log(2);
}
Foo.prototype.getName=function(){

    console.log(3);
}
var getName=function(){
    console.log(4);
}
function getName(){//函数的声明提升
    console.log(5);
}
console.log(Foo);
// console.log(Foo());
Foo.getName();//创建了一个对象,2
getName();//通过变量重新定义了函数,4
Foo().getName();//重新定义函数,1,this是window

getName();//1
new Foo.getName();//2,先执行Foo.getName()

new Foo().getName();//3
new new Foo().getName();//3 new Foo() 再调用getName();再new

1.3执行阶段

涉及到event-loop,单独放下面讲,其实可以看到,平时一些js的概念就被包含在Js的执行的三个阶段中。

2.异步和同步

这一部分得多找代码看看,然后自己对照概念看。

同步:做一件事的时候严格按照先后顺序(排队)。比如煮水喝茶,首先一直等待煮水,然后水开才泡茶叶

异步:如果一件事消耗时间过长,可以暂时去做其他事。比如点击煮水的按钮,然后我们不理它去干其他事,等到水滚的声音(异步任务返回有结果了)告诉我们水好了,才去继续泡茶。

总结:同步任务可以说不耗时,异步任务耗时

事件循环event-loop:由于js是单线程的语言,所以执行脚本就只有一个主线程,主线程通过函数调用栈(call stack)去执行同步任务,但是有一些异步任务(setTimeout等)需要处理,所以又需要任务队列进行辅助。针对js对不同任务执行的顺序取名叫event-loop。

任务队列:不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task

microtaskprocess.nextTickpromiseMutationObserver

macrotaskscriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

event-loop的顺序:

  1. 首先执行宏任务中的同步代码,遇到异步任务,注册到event table,当异步事件有结果后,如果是微任务就推入microtask,宏任务就推入macrotask,等待主线程读取执行。
  2. 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  3. 主线程会去微任务队列读取里面的所有微任务(队列的特点是先进先出,所以第一个进入的微任务先出队列)
  4. 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数,这时候又从顺序1开始,执行里面的同步代码,遇到异步任务就根据情况推入不同队列。。。

代码

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);
//Promise.resolve()等同于new Promise((resolve)=>resolve());
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');        
           

1.首先都是执行宏任务script的同步代码,输出script start,遇到异步任务setTimeout,setTimeout到指定时间后进入宏任务队列,执行Promise.resolve(),第一个then中的函数进入微任务队列,再执行console.log('script end');

2.主线程检查微任务队列,接着执行第一个then的回调函数,输出"promise1",这里只要把第一个then的回调函数中同步代码运行完,就会把下一个then中的回调函数推入微任务队列,再进入下一个宏任务前微任务队列必须空,所以执行推入的下一个then,输出promise2.

3.主线程调用宏任务中的setTimeout的回调函数,输出“setTimeout”。

总结:宏任务script的同步代码--》所有微任务--》宏任务队列的异步任务

其实主线程调用推入到宏任务队列的异步任务,执行的时候也是以同步任务的顺序执行,比如上面步骤3 中执行的是setTimeout的回调函数中的同步代码,遇到异步任务又推入任务队列中。。。这样就符合事件循环的顺序。

https://blog.csdn.net/lqyygyss/article/details/102662606(对promise运行更深入理解)

3.this的指向

怎么明确this的指向,js的this的指向无非是这么几种.

this是代码运行时确定指向的,不是定义的时候,要与箭头函数的this区分;

js中的调用模式

普通函数调用// 就是在全局环境下,在不指定this时,指向window,在严格模式下指向的是undefined
作为方法来调用,对象的函数
作为构造函数来调用,this绑定构造函数创建的新对象
使用apply/call方法来调用,A.fn.call(B,"dance");
Function.prototype.bind方法
es6箭头函数 由外层函数作用域决定(参考:https://www.cnblogs.com/listenMao/p/13184938.html)

代码

//普通函数调用
function global(){
    console.log("全局环境下")
    console.log(this);
}
global();
function g(){//g函数对应的执行上下文的this是window
    function a(){//a函数对应的执行上下文的this是window
        console.log(this);
    }
    a();
}
g();
var a="qiang";
function fn1(){
    console.log(this.a);
}
fn1();
let b="qiang2";//let 声明的变量不会作为window的属性
function fn2(){
    console.log(this.b)
}
fn2();


//对象的方法
let obj={
    name:"a";
    fn:function(){
        console.log(this);
    }
}
obj.fn;//this指向obj
//构造函数中的this
function fn3(){
    this.name="qiang";
}
console.log(fn3);
var fn3=new fn3();//使用Let不行,会有暂时性死区,具体看阮一峰的e6
console.log(fn3);
console.log(fn3.name);

//call,apply,bind改变this.
let obj={
    t:"a",
    fn:function(){
        console.log(this);
    }
}
function call(){
    console.log(this.t)
}
call.apply(obj);
//箭头看对应的参考网站

参考

https://zhuanlan.zhihu.com/p/99269362(js执行过程)

https://www.cnblogs.com/chengxs/p/10240163.html(js执行过程)

https://www.cnblogs.com/hyns/p/12392249.html(执行过程)

https://www.jianshu.com/p/f398ed121500(执行过程)

原文地址:https://www.cnblogs.com/listenMao/p/13329620.html