前言

本篇章是偏了解性的博客,次要讲述在js环境中变量、办法执行形式。了解执行程序,可能更好地帮忙你在开发中解决奇奇怪怪的问题。

面试答复

1.执行上下文:执行上下文能够简略了解成一个对象,这个对象蕴含变量对象、作用域链、this指向,个别就全局执行上下文和函数执行上下文。

2.变量晋升:变量晋升就是在赋值操作之前,就应用对应的变量,导致变量变成undefined。起因在于执行过程中,首先会建设流动对象,而后构建作用域链,再确定this指向,最初才是代码执行。创立变量或者函数的步骤都在建设流动对象阶段,而赋值操作是在代码执行阶段,所以才会找不到。

3.this:this永远指向函数运行时所在的对象,而不是函数被创立时所在的对象。扭转this指向通常有三种办法,bind、call、apply,bind会返回一个新函数 ,call在扭转this指向后还执行了函数,且可能接管多个参数,而apply与call的区别在于apply接管数组作为传入参数。

4.手写apply:首先判断传入的参数是否为值类型,如果是值类型,则间接返回该值类型,如果是援用类型,则给该参数增加fn属性用来保留以后this,这个this指向以后的调用函数。下一步判断是否存在其余参数,如果有就将它开展,并将它作为参数传入到下面的this函数中,并把执行后果保留到result里,而后删除fn属性,并返回result。至于call与apply的区别在于传入的参数不一样,bind与apply的区别在于bind返回一个新函数并不执行。

5.事件循环(Event Loop):事件循环是浏览器的一种解决JS单线程运行时不阻塞的机制,具体流程是这样的:1、首先所有同步工作都在主线程上执行,造成一个执行栈。2、如果遇到了异步工作,就丢到主线程外的工作队列,等异步工作有后果后,就会转移到调用栈中。3、再而后执行栈中所有同步工作执行结束,就会读取调用栈,如果有工作就丢到执行栈,开始执行这个工作中同步工作。4、最初主线程一直反复下面的几个步骤,这就是事件循环的一个机制。从执行程序上来看,就是一个主线程 > 微工作 > 宏工作,有后果 > 宏工作,无后果的程序。

知识点

javascript函数执行过程次要由创立执行环境、进入函数调用栈、执行、销毁这四个阶段形成,上面咱们来一一了解每一个阶段所做的事件。

1.创立执行环境

执行环境,也就是执行上下文,分为全局环境、函数环境、Eval 函数执行环境。

全局环境指的是JS默认的代码执行环境,是最外围的一个执行环境,在web浏览器中,全局执行环境被认为是window对象。一旦代码被载入,引擎最先进入的是这个环境,全局环境不会被主动回收,只有在敞开浏览器窗口的时候才会被销毁,所以在定义全局变量肯定要分外小心。

函数环境是一个绝对于全局环境的概念,因为在执行代码时,线程就是在全局环境和函数环境之间来回穿梭的,能够简略了解为函数环境即任何一个函数被调用都会创立一个新的执行环境,执行完结后返回全局环境,而创立的函数环境期待垃圾回收。

Eval 函数执行环境不常常用,尽量避免,这里不做探讨。

2.函数调用栈

在创立执行环境后,函数/代码下一个阶段会被放入一个栈中,这个栈被称为函数调用栈。js依据函数的调用(执行)来决定执行程序。函数调用栈的栈底永远都是全局环境,而栈顶就是以后正在执行函数的环境。当栈顶的执行环境执行完之后,就会出栈,并把执行权交给之前的执行环境。举例:

function A(){   console.log("this is A");   function B(){       console.log("this is B");   }   B();}A();

1.首先 A() ;A 函数执行了,A执行环境入栈。
2.A函数执行时,又调用了 B(),B又执行了,B入栈。
3.B中没有可执行的函数了,B执行完出栈。
4.继续执行A, A中没有可执行的函数了,A执行完 出栈。

上述例子只是为了说明函数调用栈的作用,具体的执行过程,包含执行上下文、作用域链、this指向等操作是在执行过程产生的。

3.执行过程

当函数被调用时,会创立一个新的函数执行环境,该创立过程次要由两个阶段组成:建设阶段 、代码执行阶段

A. 建设阶段(产生在调用/执行一个函数时,然而在执行函数外部的具体代码之前)

   1.建设流动对象   2.构建作用域链   3.确定this的指向

B. 代码执行阶段

   1.执行函数外部的具体代码

接下来,咱们一一了解其中的步骤:

A.1. 建设流动对象

这里咱们首先了解两个概念:变量对象(Variable object,VO) 、流动对象(Activation object)

变量对象(Variable object,VO) 是一个与执行上下文相干的非凡对象,在函数上下文中,VO是不能间接拜访的。变量对象用来存储上下文的函数申明、 函数形参、变量申明。优先级:函数申明>函数的形参>变量。

函数申明:每找到一个函数申明,就在流动对象上面用函数名建设一个属性,属性值就是指向该函数在内存中的地址的一个援用,如果上述函数名曾经存在于流动对象下,那么则会被新的函数援用所笼罩。

函数形参:建设arguments对象,查看以后上下文中的参数,建设该对象下的属性以及属性值 。没有实参的话,属性值为undefined。

变量申明:每找到一个变量申明,就在流动对象上面用变量名建设一个属性,该属性值为undefined。如果变量名称跟曾经申明的形式参数函数雷同,则变量申明不会烦扰曾经存在的这类属性。

流动对象(Activation object,AO):因为变量对象不能拜访,在函数执行阶段,由变量对象转化而来的可拜访对象。

举例:

var a = 10;function b () {    console.log('全局的b函数')};function bar(a, b) {    console.log('1', a, b)     var a = 1    function b() {        console.log('bar下的b函数')    }    console.log('2', a, b) }bar(2, 3)console.log('3', a, b)

解析:我这边的了解跟参考资料有所不同,望斧正。

// 创立阶段:// 第一步,遇到了全局代码,进入全局上下文,此时的执行上下文栈是这样ECStack = [    globalContext: {        VO: {            // 依据优先级程序会优先解决全局下的b函数申明,值为该函数所在内存地址的援用            b: <reference to function>,            // 紧接着,按程序再解决bar函数申明            bar: <refernce to function>,            // 依据1.3,再解决变量,并赋值为undefined            a: undefined        }    }];// 第二步,调用bar函数,就又新建了一个函数上下文,此时的执行上下文栈是这样ECStack = [    globalContext: {        VO: {            b: <reference to function b() {}>,             bar: <refernce to function bar() {}>,            a: undefined        }    },    <bar>functionContext: {        VO: {            //依据优先级,先申明函数b,并且赋值为b函数所在内存地址的援用;            //紧接着,形参b反复,优先级低于函数b,因而,疏忽新的申明,而a为2;            //最初是变量申明a,因为a=2曾经存在,因而新的变量申明,a=undefined会被疏忽,最初变量状况如下:            b: <refernce to function b() {}>,            a: 2        }    }]console.log('1', a, b):    '1' , 2 , function b(){console.log('bar下的b函数')}console.log('2', a, b):    '2' , 1 , function b(){console.log('bar下的b函数')} ,这里a为1,是因为执行了代码,把1赋值给了aconsole.log('3', a, b):    '3' , 10 , function b(){console.log('全局的b函数')} ,a为10,function b(){console.log('全局的b函数')}是因为bar函数曾经执行完了,就从调用执行栈中弹出。

块级作用域,简略来说就是函数外部和{}之间的局部。变量晋升也是在建设阶段产生的问题,即在赋值操作之前(赋值操作在代码执行阶段),就应用对应的变量,从而使变量为undefined,举例:

console.log(a)var a = 100function b(){    console.log(a)    var a = 200    console.log(a)}b()console.log(a)// undefined    undefined    200    100
var foo = function (){    console.log(1)}function foo(){    console.log(2)}foo()    //1转化一下-->function foo(){console.log(2)}var foo//因为下述代码曾经属于代码执行阶段,所以上述规定不能利用于此处//function foo 与 var foo 属于同一个内存地址,所以可能产生笼罩,下述的赋值,即起到笼罩的作用,所以输入的为1foo = function (){console.log(1)}foo()

参考资料:https://article.itxueyuan.com/O0mA6

A.2.构建作用域链

作用域链的最前端,始终都是以后执行的代码所在函数的流动对象。下一个流动对象(AO)为蕴含本函数的内部函数的AO,以此类推。最末端,为全局环境的变量对象。

留神:

1.尽管作用域链是在函数调用时构建的,然而它跟调用程序(进入调用栈的程序)无关,因为它只跟蕴含关系(函数、蕴含函数的嵌套关系)无关。

2.作用域链是创立函数的时候就创立了,此时的链只有全局变量对象,保留在函数的[[Scope]]属性中,而后函数执行时的,只是通过复制该属性中的对象来构建作用域链。

举例:

function a(){    var va =1    function b(){        var vb = 2        console.log(va)        console.log(vb)    }    b()}a()

b函数被a函数蕴含,a函数被window全局环境蕴含。

参考资料:https://blog.csdn.net/weixin_33919950/article/details/89625339

A.3.确定this的指向

this的指向在函数定义的时候是确定不了的,只有函数被调用的时候能力确定,并且this的最终指向的是那个调用它的对象 。

状况1:匿名函数

匿名函数this的默认指向为windows

var age =1var obj = {    age:2,    getAge:function(){        console.log('getAge',this.age)        return function(){            console.log('unkonw',this.age)        }    }}obj.getAge()()//getAge 2//unkonw 1
状况2:函数调用

1.如果一个函数中有this,然而它没有被上一级的对象所调用,那么this指向的就是window。

var va = 'abc'function a(){    var va = 123    console.log(this)        //window    console.log(this.va)    //'abc'}a()

2.如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

var obj={    a:'abc',    b:function(){        console.log(this.a)        //'abc'    }}obj.b()

3.如果一个函数中有this,这个函数中蕴含多个对象,只管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,也就是说this指向的是间接调用它的对象。

var obj={    a:123,    b:{        a:'abc',        b:function(){            console.log(this.a)        //'abc'        }    }}obj.b.b()
var  a =1;var obj = {    a:2,    b:{        a:3,        getValue:function(){            console.log(this.a)        }    }}console.log(obj.b.getValue());//obj.b.getValue()这里this指向obj.b,输入3var test = obj.b.getValue;//此时仅做赋值,并没有调用函数,因为是.getValue而不是getValue()console.log(test()); // 此时调用函数合乎第一种状况,所以this指向window
状况3:构造函数中的this指向

首先new关键字会创立一个空的对象,而后会主动调用一个函数apply办法,将this指向这个空对象,这样的话函数外部的this就会被这个空的对象代替,能够参考后续对于new操作符的知识点。

function a(){    console.log(this)        //a {}}var va = new a()

当构造函数的this碰到return时:如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

function a(){    this.a = 1    return {a:'abc'}}var va = new a()console.log(va.a) //'abc'
状况4:箭头函数的this指向

箭头函数的this指向,是指向箭头函数被创立时内部作用域(要么是window,要么是最近一层的部分函数)的this指向的对象,而不是调用时指定this指向。举例:

var name = 'window'; var A = {   name: 'A',   sayHello: () => {      console.log(this.name)   }}A.sayHello();//此时箭头函数内部作用域为window,所以指向的是window
var name = 'window'; var A = {   name: 'A',   sayHello: function(){      var s = () => console.log(this.name)      return s//返回箭头函数s   }}var sayHello = A.sayHello();    //这里拿到的s,因为return了sayHello();//function()的this指向为对象A(因为A.sayHello,function被调用了,参考状况1),而箭头函数内部的this指向即为function()的this指向这个的this指向 对象A。这跟上述定义并不抵触,箭头函数的this指向依然指向function的this,只不过function的this指向是在调用的时候才确定。
状况5:call、bind、apply的this指向

因为js中this的指向受函数运行环境、调用的影响,指向常常扭转,使得开发变得艰难和含糊,所以在写一些简单函数的时候常常会用到this指向绑定,以避免出现不必要的问题,call、apply、bind根本都能实现这一性能。

1.bind:bind用于将函数体内的this绑定到某个对象,而后返回一个新函数

var counter = {  count: 0,  inc: function () {    this.count++;  }}var obj = {  count: 100};var func = counter.inc.bind(obj);func();obj.count // 101

2.call:call办法能够指定this 的指向,而后再指定的作用域中,执行函数。call能够承受多个参数,第一个参数是this指向的对象,之后的是函数回调所需的入参

var newObj = {a:1,b:2}var a = 11var b = 22var obj = {    a:111,    b:222,    fn:function test(val){        return this.a + this.b + val    }}var func = obj.fnfunc.call(newObj,4)     //7

3.apply:apply 和call作用相似,也是扭转this指向,而后调用该函数,惟一区别是apply接管数组作为函数执行时的参数

var newObj = {a:1,b:2}var a = 11var b = 22var obj = {    a:111,    b:222,    fn:function test(a,b){        return this.a + this.b + a + b    }}var func = obj.fnfunc.apply(newObj,[3,4])    // 10 

PS:call、bind、apply能实现以下根底性能,

  • 间接调用函数,扭转this,劫持其余对象的办法
  • 两个函数实现继承
  • 为类数组(arguments和nodeList)增加数组办法,如push、pop

    (function(){  Array.prototype.push.call(arguments,'王五');  console.log(arguments);//['张三','李四','王五']})('张三','李四')
  • 合并数组、求数组内最大值

4.面试题:手动实现bind、call、apply

实现call(obj,arg,arg....)
1.扭转this的指向。
2.传入参数。
3.返回函数执行后果

// 函数原型上增加myCall办法来模仿call,这样就能够间接在函数后进行调用Function.prototype.myCall = function (context) {    let currentObj = typeof context ==='object' ? context : {} // context就是传入的第一个参数    currentObj.fn = this // 设置一个长期属性,把this的值给这个长期属性,这里的this指向的是调用myCall办法的上一级对象(函数),将parent函数存起来,parent调用的myCall,此时this指向的就是该办法。    let arg = [...arguments].slice(1) // 将参数中除了第一个参数之后的全副存起来    currentObj.fn(...arg) // 将参数传入,此时应用第一个参数调用父级函数,使父级函数的this指向指为第一个参数(参考A.1.状况1)    delete currentObj.fn // 将函数删除,防止长期属性的烦扰。}var a = {value: 1}function test(a,b) {    console.log(this.value + a + b) }test.myCall(a, 2, 3)    //6

实现apply(obj, [params1, params2, ....])
apply与call的区别在于传参,所以把 let arg = [...arguments].slice(1) 替换一下,如下:

Function.prototype.myApply = function (context) {    let currentObj = typeof context ==='object' ? context : {} // 这里就是传入的第一个参数    currentObj.fn = this // 将parent函数存起来,parent调用的myCall,此时this指向的就是该办法    if(arguments[1]) {        currentObj.fn(...arguments[1])    } else {        currentObj.fn()    }    delete currentObj.fn}var a = {value: 1}function test(a,b) {    console.log(this.value + a + b) }test.myApply(a, [2,3])

实现bind(obj, ...arg)
bind函数与call、apply的区别有两点:一是返回一个新函数;二是柯里化,柯里化举例如下:

function add(x) {  return function(y) {    return x + y;  };};add(1)(2);    // 3

实现bind代码:

// 实现bind()办法,调用bind()必须是一个函数,因为要返回一个新函数;// 能够通过new批改this,new的优先级最高bind()能够将参数分两次传递进来Function.prototype.myBind = function (context) {    if(typeof this !== "function") { // 如果不是函数则间接抛出        throw new TypeError('Error')    }    let self = this // 保留this,即为parent    let arg = [...arguments].slice(1) // 将参数中除了第一个之后的全副存起来    // bind()返回的是一个函数,所以能够应用new,并且会批改this的指向    return function F() {        if(this instanceof F) { // 如果new执行此时即为true            return new self(...arg, ...arguments) // 返回new parent(第一次传递的参数, 第二次传递的参数)-->arguments是执行返回的函数时的参数        }else{            return self.apply(context, [...arg, ...arguments]) // 如果没有执行new,那么间接执行parent,通过apply会将this执行最后传进来的对象a          }    }}

B.1.执行函数外部的具体代码

执行代码阶段最次要有两个事件:变量赋值、JS事件机制。其中变量赋值比拟好了解,这里不做过多解释,接下来着重了解JS事件机制。

JavaScript是单线程指的是同一时间只能干一件事件,只有后面的事件执行完,能力执行前面的事件。导致遇到耗时的工作时前面的代码无奈执行,因而有了同步、异步工作。

同步工作:for循环、new Promise等除了异步工作外的其余工作

异步工作:微工作、宏工作,其中微工作有Promise.then、process.nextTick、queueMicrotask,宏工作有setTimeout、setInterval、IO、UI渲染等

async/await只是一个语法糖,只是帮忙咱们返回一个Promise而已,如果办法被调用的话,async相当于new Promise ,await前面的代码相当于.then里的代码,且.then代码必须得在promise中resolve后才会执行,否则.then代码不会执行,await在async代码执行结束后,进入微工作队列。

理解工作的概念后,当初整顿一下工作执行的流程:

1、所有同步工作都在主线程上执行,造成一个执行栈(execution context stack)。

2、主线程之外,还存在一个工作队列(task queue)。只有是异步工作,就丢到这个工作队列,有了运行后果,就在工作队列之中搁置一个事件。

3、当工作队列中的异步工作有了后果之后,就会将工作挪动到调用栈中。

4、一旦执行栈中的所有同步工作执行结束,零碎就会读取调用栈,开始执行该异步工作中同步工作。

5、主线程一直反复下面的第三步,称为事件循环(Event Loop)。

总结执行程序:主线程(同步工作,new Promise)> Promise.then(微工作)> setTimeout(宏工作,有后果) > setTimeout(宏工作,无后果),同类型工作依照先进先出的程序执行。

面试题:

async function async1() {    console.log(2);    await async2();    console.log(7);}async function async2() {    new Promise(function (resolve) {        console.log(3);        resolve();    }).then(function () {        console.log(6);    });}console.log(1);setTimeout(function() {    console.log(8);}, 0)async1();new Promise(function(resolve, reject){    console.log(4)    setTimeout(function(){        console.log(9)        resolve(13)          console.log(10)          setTimeout(function(){            console.log(12)          })    }, 0)}).then(res =>{         // flag    console.log(11)    setTimeout(() =>{        console.log(13)    }, 0)})console.log(5);//1-13按程序执行,参考上述执行程序及规定。

4.销毁执行环境和流动对象

某个执行环境所有代码执行完之后,该环境被销毁,保留在其中的所有变量和函数定义也随之销毁,这边闭包有所不同,将会在下篇博客中,进一步了解。全局执行环境只会在关了浏览器或者程序的时候才被销毁。

最初

走过路过,不要错过,点赞、珍藏、评论三连~