bind,call,apply以及执行上下文

304 阅读4分钟

this

JS中的this,老生常谈了,经常做经常错。这边做一个this的总结: this,指向使用它的函数。如果没有函数调用,默认就是windows。 一下是this会被使用的各种情况,进行总结:

  • fn() this => window/global
  • obj.fn() this => obj
  • fn.call(xx) this => xx
  • fn.apply(xx) this => xx
  • fn.bind(xx) this => xx
  • new Fn() this => 形成的实例
  • ()=>{} this => 这个函数外面的this

bind,call,apply区别

正如上面的式子所表现的,这三个方法们都可以用来绑定this,事实上在大量的源码中也能看见用call,apply绑定this来调用函数。接下来分别说一下这三个方法的用途

  • bind: 这个方法不会立即调用这个函数,而是会生成一个新的函数,并将传入的第一个参数作为this传入函数
name="znz"
function demo01(){
  console.log(this.name);
}
demo01() // znz
const demo02 = demo01.bind({name:"xiao"})
demo02() // xiao 
  • call:这个方法在函数调用的时候,将传入的第一个参数作为this,第二个参数作为函数中真正的第一个参数调用
function demo1(a){
    console.log("this.name",this.name)  // this.name znz 
    console.log("name",a.name)  // name xiao
}
demo1.call({'name':"znz"}, {"name":"xiao"}) 
  • apply:这个方法跟call一样,在函数调用的时候传入第一个参数作为this调用,不同点在于第二参数需要传入一个数组作为参数。数组中元素按照顺序当做函数的参数顺序执行
function demo1(){
       console.log("this.name",this.name)   // this.name znz
    for (const key in arguments) {
        console.log("name",arguments[key].name)  // name xiao name nan
    }
}
demo1.apply({'name':"znz"}, [{"name":"xiao"},{"name":"nan"}])  

执行上下文

对于this的多变,js中有一个抽象概念专门用来阐述————执行上下文:

  1. 全局执行上下文: 一般来说,代码如果不在函数中时,就处在全局执行上下文中,此时this就绑定为window。一般来说,js每次执行,只会产生一个执行上下文
  2. 函数执行上下文:
    每当一个调用一个函数的时候,js就会进入一个新的执行上下文,此时this就看给它绑定的是什么,如果没有绑定那就是window,如果是函数本身调用的就是函数本身。
  3. eval执行上下文:
    效果同函数执行上下文,此时它会创建属于自身的执行上下文
执行栈

执行栈是一种按照LIFO数据结构编写的栈,它的基本特征是后进先出。 如下代码:

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

image.png 此时代码的执行顺序为:

  1. 首先将全局执行上下文入栈
  2. 发现first函数,将first执行上下文入栈
  3. 发现second函数,将second执行上下文入栈
  4. second函数执行完毕,second执行上下文弹栈
  5. first函数执行完毕,first执行上下文弹栈
  6. 全局执行上下文弹栈,代码执行完毕

执行顺序

执行栈的执行过程分为创建阶段和执行阶段

创建阶段

在创建阶段中,以下三个量会被定义完成

this,激活变量,词法环境

  • this:
    之前说执行上下文中已经提到,在全局执行上下文中,是window,在函数执行上下文中,是看谁调用的它
  • 激活变量:
    如果是window,那么就是全局的变量。而对于函数,当函数被调用但是还没有执行的时候,它会创建一个叫active object的对象来进行存储诸如实参,形参,局部变量等各种类型数据,然后将它推到这个函数执行上下文的顶部
  • 词法环境: 也就是作用域链,每个函数在执行创建时,它会将它所在的父变量对象传给一个叫[scope]的变量,当函数执行时,就会查找当前的激活变量对象是否有这个值,如果没有,它就会顺着[scope]中的值找到父变量对象,在父变量对象中寻找需要的值,一直找到全局对象(全局对象存储在[scope][1][global])为止。这种一层层寻找变量的方法被称为作用域链。
执行阶段

函数根据代码执行,在执行阶段,函数会不断改变激活变量的里面存储的值,直到函数执行结束,整个执行上下文会被全部摧毁

对于let/const/var的解释

现在,可以完整的解释为什么let/const对比var直接的不同点了。
在es6中,js执行的时候,新加了一个[scope][1][script]变量。在var在全局执行上下文声明时,我们会在全局变量中设置变量,但是执行let/const时,我们并不会在全局变量中声明,这就很有效的解决了全局变量这个问题。而在函数中声明变量时,var会将变量进行一个初始化并赋值一个undefined,而let/const并不会赋值,他们会一直等到定义被执行的时候才会进行初始化,所以这就是let/const会出现暂时性的死区的原因。