this指向

115 阅读9分钟

在全局函数中, this 等于 window;当函数作为某个对象的方法调用时, this 等于那个对象; 匿名函数的执行环境具有全局性, 所以 this 等于 window

在不同的情况下,this的指向也不同,那么为什会出现这种情况呢? 这就要说到执行上下文了.

函数在运行时,会创建一个执行环境,这个执行环境就叫做执行上下文,JavaScript会以栈的方式处理它们,这个栈就是函数调用栈.函数调用栈规定了JavaScript代码的执行顺序,栈底永远是全局上下文,栈顶则是当前正在执行的上下文.当代码在执行过程中,遇到全局环境,函数环境或者eval环境时,就会生成一个执行上下文并放入函数调用栈中,处于栈顶的上下文执行完毕之后,会自动出栈.

JS代码在引擎中是以“一段一段”的方式来分析执行的,而并非一行一行来分析执行的.而这“一段一段”可执行的代码无非为三种:Global CodeFucntion CodeEval Code,当这些可执行的代码在执行的时候又会创建出一个一个的执行上下文,这些上下文分别为: 全局执行上下文、函数执行上下文、eval执行上下文

  • 全局执行上下文:默认执行环境,任何不在函数内部的代码都在全局执行上下文中.它会执行两件事:创建一个全局的window对象;设置this的值等于这个对象.一个程序中只有一个全局执行上下文.
  • 函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的执行环境.函数执行上下文可以有多个.
  • eval执行上下文:在执行eval函数内部的代码时也会创建属于它自己的执行环境.

当函数被调用执行时,创建一个执行环境,同时生成变量对象,创建作用域链,指定this的值,因此 当前函数的this是在函数被调用执行时才确定的.如果当前的执行上下文处于函数调用栈的顶端,那么它的变量对象会变为活动对象,同时将this指向活动对象.变量对象和活动对象是一个东西只是处于不同的状态和阶段.变量对象内部的属性不能直接被访问.

  • 变量对象/活动对象:变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明.不同的函数上下文对应不同的变量对象: 全局上下文中的变量对象、函数执行上下文中的变量对象.

    • 全局上下文中的变量对象:其实就是全局对象,在浏览器环境中其实就是window对象,在node环境其实就是global对象,我们可以通过this来访问.
    • 函数执行上下文中的变量对象:活动对象,当进入这个执行上下文时,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问.在函数执行时,会为当前函数创建执行上下文,同时创建当前函数执行上下文的变量对象:根据函数的参数初始化arguments对象;根据函数声明生成对应的属性,其值为一个指向内存中函数的引用指针,如果函数名称存在则覆盖;根据变量对象生成对应属性,此时初始值为undefined,如果变量名存在则忽略该变量声明.
  • 作用域链: 规定了如何查找变量,确定了当前执行上下文对变量对象的访问权限.当查找变量的时候,会先从当前上下文的变量对象查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象查找,一直到全局上下文的变量对象,这样由多个执行上下文的变量对象构成的链表就叫做作用域链.

    • 词法环境由环境记录(存储变量--let/const和函数声明的实际位置)和对外部环境的引用(可以访问其他词法外部环境)组成.词法环境分为:全局环境和函数环境.全局环境是一个没有外部环境的词法环境,其外部环境引用为null,拥有一个全局对象及其关联的方法和属性以及用户自定义的全局变量;函数环境:用户在函数中定义的变量被存储在环境记录,包含arguments对象,对外部环境的引用可以是全局环境也可以是包含函数的外部函数环境.
    • 变量环境也是一个词法环境,具有词法环境的所有属性,只不过存储的变量时通过var绑定的.
  • this的绑定(this永远指向最后调用它的那个对象):

    • 在全局运行,函数内部的this直接绑定到全局.
    • 通过对象调用,函数内部的this直接绑定到调用它的对象上.如果独立调用,那么该函数内部的this指向undefined,非严格模式下当this指向undefined时,它会自动指向全局对象.(如果函数没有对象调用,那么默认它的调用对象为undefined)
    • 从一个环境传入另一个环境,通过callapply,改变this指向
    • 箭头函数不绑定this,永远指向定义函数时this而不是执行时,如果箭头函数被非箭头函数包含,那么this指向的时最近一层非箭头函数的this,否则为undefined
    • 匿名函数的this永远指向window
    • new绑定,new的过程: 创建一个空对象;将这个空对象的隐士原型指向构造函数的显示原型;使用call改变this指向;如果没有返回值或者返回一个非对象,那么就返回一个obj,如果返回值是一个新对象,那么直接返回该对象.
    const obj = {}
    obj.__proto__ = myFuc.prototype
    const res = myFunc.call(this,args)
    return typeof res === 'Object' ? res : obj
    

    示例1

    var name = 'windowName';
    function a () {
      var name = 'cherry';
      console.log(this.name);
      console.log('inner ====' + this)
    }
    a()
    console.log('outer======' + this)
    
    1. 创建全局执行上下文,推入执行栈
    2. 创建全局变量对象,定义函数a和变量name,this指向全局window
    3. 顺序执行,变量对象变为活动对象,为name赋值为windowName;执行函数a
    4. 创建a函数执行上下文,推入执行栈,创建函数变量对象,定义变量name,this指向调用它的对象undefined -> 严格模式下报错,非严格模式下window
    5. 函数变量对象转为活动对象,为name赋值为cherry,执行log输出this.name=>windowName,执行log输出this=> window
    6. 函数执行完毕,函数执行上下文出栈,全局变量对象转为活动对象
    7. 全局执行上下文,执行log输出this

示例2

var name = 'windoName';
a()
function a () {
  console.log('name-----' + name);
  console.log('this name ----' + this.name)
  console.log('this ----' + this)
}
var  obj = {
  fn:a
}
obj.fn()
var b = obj.fn
b()
  1. 创建全局执行上下文,推入执行栈
  2. 创建全局变量对象,定义变量name、obj、b、函数a,this指向全局对象window
  3. 顺序执行,变量对象转为活动对象,为name赋值为windowName,执行函数a
  4. 创建a函数执行上下文,创建函数变量对象,没有要定义的属性,this指向全局对象window
  5. 变量对象转为活动对象,执行log(name ==> windowName),因变量对象没有name,向父级执行上下文(这里是全局执行上下文)查找name,执行log(this.name ==> windowName),执行log(this => window)
  6. 函数a执行完成,出栈,全局变量对象转为活动对象,为obj赋值
  7. 执行obj.fn(),创建fn的函数执行上下文,推入执行栈,创建函数变量对象,this指向调用它的对象obj
  8. 顺序执行,log(name ==> windowName)同上,执行log(this.name ==> undefined),执行log(this => obj)
  9. 执行完毕,出栈
  10. 进入全局执行上下文,为b赋值为obj.fn
  11. 执行b,创建b函数执行上下文,同上4、5

示例3

var name = 'windowName'

var obj = {
  name: 'cherry',
  func1:function(){
    console.log(this.name,'----this.name')
  },
  fun2:function(){
    setTimeout(function(){
      this.func1()
    },100)
  },
  fun3:function(){
    setTimeout(() => {
      this.func1()
    },100)
  },
  fun4:function(){
    setTimeout(function() {
      this.func1()
    }.call(obj),100)
  }
}
obj.fun2()
obj.fun3()
obj.fun4()
  1. 创建全局执行上下文,推入执行栈,创建全局变量对象,定义变量name、obj,this指向window
  2. 顺序执行,为name、obj赋值,执行obj.fun2()
  3. 创建函数执行上下文,推入执行栈,创建函数变量对象,this指向obj
  4. 执行setTimeout,执行匿名函数,创建函数执行上下文,推入执行栈,创建函数变量对象,this指向window,执行this.func1(),因查找不到报错,出栈
  5. obj.fun2执行结束,函数执行上下文出栈,执行obj.fun3,创建函数执行上下文推入执行栈,创建函数变量对象,this指向obj
  6. 执行setTimeout,匿名箭头函数,创建函数执行上下文,推入执行栈,创建函数变量对象,this指向obj
  7. 执行this.func1,创建func1函数指向上下文,推入执行栈,log(this.name => cherry),执行完毕,依次出栈
  8. 执行obj.fun4,步骤同上,因指向call操作this指向obj,执行this.func1()

callapplythis绑定到它的第一个参数上,如果第一个参数不是对象,那么JS会将其强制转化为对象,如果第一个参数为null或者undefined,非严格模式下自动指向全局对象.callapply不同的传递给它的执行参数一个是字符串一个是数组,但是都会得到一个立即执行的函数. bind则会生成一个新的函数且这个函数的this永远绑定到bind的第一个参数上.由于箭头函数没有自己的this指向,使用callapplybind时只能传递参数不能改变this指向,第一个参数会被忽略.