# 搞定this

# 一些关于this的错误理解:

  • 人们很容易把this理解成指向函数自身,事实是,如果要从函数对象内部引用它自身,那只使用this是不够的。一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它
    • 具名函数,在它内部可以使用函数名来引用自身
    • 匿名函数对象内部引用自身的方法,是使用arguments.callee来引用当前正在运行的函数对象,然而,更好的方式是避免使用匿名函数,至少在需要自引用时使用具名函数(表达式)。arguments.callee已经被弃用,不应该再使用它。
    function foo(){
      console.log('foo: ', foo)
      foo.count = 4
    }
    foo()
    
    for(let key in foo){
      console.log(key+ ":" +foo[key])
    }
    
    let bar = function (){
      console.log(arguments.callee === bar)
    }
    bar()
    
    
  • 需要明确的是,this在任何情况下都不指向函数的词法作用域,每当你想要把this和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的

# this的绑定

  • 每个函数的this是在调用时被绑定的,完全取决于函数的调用位置

  • 函数的执行上下文: 当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到

  • 调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)

  • 调用栈:调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中,你可以把调用栈想象成一个函数调用链

# this的绑定规则

  • 几种绑定的优先级: new绑定 > 显式绑定 >隐式绑定 >默认绑定

# 默认绑定

  • 绑定规则: 独立函数调用,直接使用不带任何修饰的函数引用进行调用
  • this绑定到window/undefined

# 隐式绑定

  • 绑定规则: 调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含

# 例1

  • 对象属性引用链中只有上一层或者说最后一层在调用位置中起作用
function foo(){
  console.log(this.a)
}
var obj2 = {
  a: 42,
  foo: foo
}
var obj1 = {
  a: 2,
  obj2: obj2
}

console.log(obj1.obj2.foo()) //42

# 例2

  • 隐式丢失: 一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式
  • 函数别名,将函数重新赋值
function foo(){
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo
}
var bar = obj.foo //函数别名
var a = 'oops, global' //a是全局对象的属性
bar() //'oops, global'
//虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,
//因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定
  • 传入回调函数
function foo(){
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo
}
var a = 'oops, global' //a是全局对象的属性

function doFoo(fn){
  //这里有一个隐式赋值,fn引用的其实是foo
  //fn = obj.foo
  fn() //<--调用位置
}
doFoo(obj.foo) //'oops, global'
// 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值
  • 如果把函数传入语言内置的函数而不是传入你自己声明的函数,结果是一样的
function foo(){
  console.log(this.a)
}
var obj = {
  a: 2,
  foo: foo
}
var a = 'oops, global' //a是全局对象的属性

setTimeout(obj.foo, 100) //'oops, global'
  • 回调函数丢失this绑定是非常常见的,除此之外,调用回调函数的函数可能会修改this。在一些流行的JavaScript库中事件处理器常会把回调函数的this强制绑定到触发事件的DOM元素上

# 显式绑定

  • 利用call,apply,bind直接指定this的绑定对象,因此我们称之为显式绑定。

# new绑定

  • 绑定到新创建的对象

# 透彻认识函数的this在不同调用环境下的指向

  • 1.被事件调用的函数中的this,指向触发事件的对象,(监听函数内部的this指向触发事件的那个元素节点)
<div class="box"></div>
<div class="lili"></div>
  
<script>
  let box = document.querySelector('.box');
  let lili = document.querySelector('.lili');
  box.onclick = move;
  lili.onclick = move;
  function move(){
    console.log(this)
    this.style.left = '100px';
  }
</script>
  • 全局环境 window/undefined
function move(){
  'use strict'
  console.log(this)
}
move() //window/undefined
window.move() //window

# 2.多层对象中的函数的this指向

  • 函数被多层对象所包含,如果函数被最外层对象调用,this的指向也只是它上一级的对象
  • 多层对象中的函数被赋值给一个全局变量,再去执行,this指向全局
var obj = {
  a: 10,
  b: {
    fn: function(){
      console.log(this)
    }
  }
}
obj.b.fn() // { fn: f}
let f = obj.b.fn;
f() //window

# 3.构造函数中的this

  • 构造函数会隐式返回一个this对象,即实例化生成的对象
  • 如果构造函数中有显式return,return的值是对象,则this指向return的对象,如果return不是对象,或者return null,则this保持原来的隐式返回
function fn(){
  this.num = 10;
  console.log(this)
}
// fn作为一函数,函数体内容如上
fn.num = 20;
// fn作为一对象,有一属性num

fn.prototype.num = 30;
fn.prototype.method = function(){
  console.log(this.num)
}

let prototype = fn.prototype;
let method = prototype.method;

let obj = new fn()

new fn().method() //10
prototype.method() //30
method() //undefined


let obj = {};
obj.num =10;
obj.fn = function(){
  console.log(this.num)
}
obj.fn() //10
var abc = obj.fn;
abc() //undefined

# 4.箭头函数中this的指向

  • 在手写某些原理时,慎用箭头函数,箭头函数会是全局的匿名函数中this变为window
  • 箭头函数本身是没有this和arguments的
  • 箭头函数中的this其实是调用定义在上一层作用域中的this,这里强调是上一层的独立的作用域,因为对象是不能形成独立的作用域的
  • 默认的,匿名的回调函数中的this绑定window
  box.onclick = move;
  lili.onclick = move;
  function move(){
    console.log(this) 
    setTimeout(function(){
      console.log(this)
    },1000)
    setTimeout(() => {
      console.log(this)
    },1000)
  }

  let obj = {
    fn:function(){
      console.log(this)
    },
    fn1:()=>{
      console.log(this)
    }
  }
  obj.fn() //{fn: f}
  obj.fn1() //window

# 5.如何改变this的指向

  • call,apply,bind
  • 箭头函数中的this,在定义时就已经确定,无法使用上述方法修改
  • 箭头函数即使多层对象包含,this仍是如此
let obj = {
    fn:function(){
      console.log(this)
    },
    fn1:()=>{
      console.log(this)
    }
  }
  obj.fn.call(box) // <div class="box"></box>
  obj.fn1.call(box) //window

function A() {
  var obj = {}
  console.log('000', this) //window
  function B(){
      console.log('111',this); //window
  }

  return B();
}
var obj = A();

let a = {
  b: {
      c: function(){
          console.log('111',this)
      },
      d: () => {
          console.log('222',this)
      }
  }
}
let tmp0 = a.b.c() //{ c: f, d: f}
let tmp1 = a.b.c
tmp1() //window
let tmp2 = a.b.d() //window
let tmp3 = a.b.d
tmp3() //window