js高级-this 指向问题

130 阅读10分钟
    this 究竟指向谁是一个让人很迷惑的问题,它在全局中指向window这是大家都知道的,但是它在函数中指向谁?在嵌套函数中
    指向谁?箭头函数中有没有this?函数的调用方式的不同会不会对this的指向产生影响?这些都是我们经常掌握的问题。废话不多
    说,今天就js中this 指向的问题做一个全方位的梳理总结。
 提纲:
      1.this 赋值的原理。
      2.this在全局作用域中的指向
      3.this绑定的理解核心
          跟函数所处的位置是没有关系的,跟函数被调用的方式存在关系。
      4.this 绑定的四个原则
      5.this 绑定原则的优先级
      6.一些js函数的this绑定
      7.this绑定规则之外之忽略显示绑定
      8.this绑定规则之外之间接函数引用
      9.this绑定规则之外之箭头函数中的this获取
      10.面试题解析

this 赋值的原理

   js的执行流程是执行上下文在函数调用栈中的入栈出栈过程中完成的,在js中存在两种上下文,全局执行上下文GEC 和函数执行上
下文FEC。执行上下文中,保存着这个执行上下文的VO(变量环境,可以为GO或AO)属性,作用域相关属性,还保留着这个执行上下文
的this属性。我们知道v8引擎是用c++写,那么这里的执行上下文对应的就是一个结构体(为啥不是类,就算是类,类的本质也是结构
体,嘿嘿),那么this的本质其实就是执行上下文这个结构体的一个成员变量,v8引擎会在运行过程中动态的给这个this进行赋值,
要真正探究this的赋值原理,就需要去v8引擎中一探究竟,但是这个探究说实话,我还不具备能力,这里只能通过对使用过程中,this
的值的反向推理做梳理总结,来梳理this的指向问题。

this在全局作用域中的指向

    this在全局作用域中的指向,没有什么好总结的,因为我们一般也不怎么在全局代码中使用this,这里直接给出答案。
       (1)在浏览器中,全局作用域的this,执行window。
       (2)在node 环境中,全局作用的this,指向{}.
        有兴趣的可以去打印一下。

this绑定的理解核心

大家要记住一点,this的指向跟函数所处的位置是没有关系的,跟函数被调用的方式存在关系。这是理解this指向四个原则的核心点,请大家牢记。

函数所处的位置跟编译有关系,但是this的绑定是在代码的执行过程中绑定的,所以跟它没关系。

函数被调用时,会有涉及到一些js引擎内部处理this指向的操作,所以跟函数的调用方式存在关系

this 绑定的四个原则

1.默认绑定原则

通过独立函数调用的方式的函数内部的this,统统默认指向window

什么情况下使用默认绑定呢?

独立函数的调用会使用默认绑定原则。

独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用;

     无论是发生在全局作用域中的独立函数调用,还是发生在函数中的独立函数调用,亦或是通过赋值方式将一个函数赋值给了一个变
量,只要这个函数是独立调用的,即没有通过对象的方法的形式或者通过apply,bind,call这三种形式调用的函数,他内部的this
都是指向window(浏览器中)。

在全局作用域中通过独立函数调用的默认绑定,this指向window(浏览器中)。 案例一

function foo() {
  console.log(this)
}

foo() //window

案例二

function foo3() {
  console.log(this)
  foo2()
}

foo3()//window

在函数中调用其他函数,但是是独立函数调用的,使用默认绑定原则,this指向window(浏览器中)。 案例一

function foo1() {
  console.log(this) // window
}

function foo2() {
  console.log(this) 
  foo1()//window
}

通过变量获取对象中的函数,但最终仍是独立函数调用的,使用默认绑定原则,this指向window(浏览器中)。

案例一

var obj = {
  name: "why",
  foo: function() {
    console.log(this) 
  }
}

var bar = obj.foo
bar() // window

案例二

function foo() {
  console.log(this) //window
}
var obj = {
  name: "why",
  foo: foo
}

var bar = obj.foo
bar() // window

通过函数返回的函数中的this,只要是独立函数调用,this指向window(浏览器中)。

function foo() {
  function bar() {
    console.log(this)
  }
  return bar
}

var fn = foo()
fn() // window

2.隐式绑定原则

函数的调用位置处在某个对象的发起的函数调用的位置时,采用隐式绑定原则,此时this 指向发起函数调用的那个对象。需要注意的是此时的this是指向发起函数调用的那个对象的,具体可看案例三。

    此时由于函数是被对象调用的,在函数被调用时,js引擎会帮我们把这个对象作为参数,将函数内部的this隐式绑定为这个对象。
因为函数是对象的一个属性,所以js引擎是可以获取这种对应关系的,通过这种关系,进行了隐式绑定。

案例一

function foo() {
  console.log(this) //obj
}
var obj = {
  name: "why",
  foo: foo
}

obj.foo() // obj对象

案例二

var obj = {
  name: "why",
  eating: function() {
    console.log(this)
  },
  running: function() {
    console.log(this)
  }
}

obj.eating() //window (浏览器中)
obj.running() //window(浏览器中)

案例三: 虽然foo是obj1的一个属性,但是obj2也通过引用并通过属性绑定了这种关系,且最后是obj2发起的函数调用,所以此时this指向obj2。

var obj1 = {
  name: "obj1",
  foo: function() {
    console.log(this)
  }
}

var obj2 = {
  name: "obj2",
  bar: obj1.foo
}

obj2.bar() //obj2

3.显示绑定

**通过函数的bind(),apply(),call()方法进行调用或绑定的函数,都会进行显示的this的绑定,此时this指向这三个函数给函数传递的第一个参数。 **

案例一

function foo() {
  console.log("函数被调用了", this)
}
var obj = {
  name: "obj"
}
foo.call(obj)//函数被调用了 { name: 'obj' }
foo.apply(obj) //调用了 { name: 'obj' }
foo.apply("aaaa") // [String: 'aaaa']

案例二 这里foo的调用既是独立函数调用,foo函数也通过bind进行了显示绑定,具体执行结果由两者的优先级高低决定,这里可以看出显示绑定的优先级高于默认绑定

function foo() {
  console.log(this)
}
var newFoo = foo.bind("aaa")

newFoo() //[String: 'aaaa']

4.new绑定

    JavaScript中的函数可以当做一个类的构造函数来使用,也就是使用new关键字。
    使用new关键字来调用函数是,会执行如下的操作:
        1.创建一个全新的对象;
        2.这个新对象会被执行prototype连接;
        3.这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成);
        4.如果函数没有返回其他对象,表达式会返回这个新对象;

我们通过一个new关键字调用一个函数时(构造器), 这个时候this指向调用这个构造器时创建出来的对象。 即this = 创建出来的对象。这个绑定过程就是new 绑定。

此时this.name =why,this.age = 18,所以this 指向 被创建出来的对象。

function Person(name, age) {
  this.name = name
  this.age = age
}

var p1 = new Person("why", 18)
console.log(p1.name, p1.age) // why,18

5.this 绑定原则的优先级

new绑定 > 显示绑定(apply/call/bind) > 隐式绑定(obj.foo()) > 默认绑定(独立函数调用)

1.默认规则的优先级最低
2.显示绑定优先级高于隐式绑定
var obj = {
  name: "obj",
  foo: function() {
    console.log(this)
  }
}

obj.foo()
// 1.call/apply的显示绑定高于隐式绑定
obj.foo.apply('abc') //[String: 'abc']
obj.foo.call('abc') //[String: 'abc']


// 2.bind的优先级高于隐式绑定
var bar = obj.foo.bind("abc")
bar() // [String: 'abc']
3.new绑定优先级高于隐式绑定
var obj = {
  name: "obj",
  foo: function() {
    console.log(this.name) //undeifiend,说明this不是obj对象
    console.log(typeof this) // object,
    console.log(this) // foo {} 指foo这个函数对象,new优先级更高
  }
}

// new的优先级高于隐式绑定
var f = new obj.foo()

4.new绑定优先级高于bind
    (1new绑定和call、apply是不允许同时使用的,所以不存在谁的优先级更高
    (2new绑定可以和bind一起使用,new绑定优先级更高
    
// new的优先级高于bind
function foo() {
  console.log(this) // foo {} 指foo这个函数对象,new优先级更高
}

var bar = foo.bind("aaa")

var obj = new bar()

6.一些js函数的this绑定

setTimeout 的this 绑定为 window

setTimeout(function() {
  console.log(this) // window
}, 2000)

dom操作函数中的this,指向绑定的dom元素

const boxDiv = document.querySelector(".box");
boxDiv.onclick = function () {
  console.log(this);
};

boxDiv.addEventListener("click", function () {
  console.log(this);
});

数组中的操作函数,this为函数调用时传入的第二个参数

 // 3.数组.forEach/map/filter/find
var names = ["abc", "cba", "nba"]
names.forEach(function(item) {
  console.log(item, this) //abc [String: 'abc']
}, "abc")
names.map(function(item) {
  console.log(item, this) //bc [String: 'cba']
}, "cba")

7.this绑定规则之外之忽略显示绑定

apply/call/bind: 当传入null/undefined时, 自动将this绑定成全局对象

function foo() {
  console.log(this)
}


foo.apply(null)
foo.apply(undefined)

var bar = foo.bind(null)
bar()

8.this绑定规则之外之间接函数引用

创建一个函数的 间接引用,这种情况使用默认绑定规则,foo函数被直接调用,所以是默认绑定。

var obj1 = {
  name: "obj1",
  foo: function() {
    console.log(this)
  }
}

var obj2 = {
  name: "obj2"
};

(obj2.bar = obj1.foo)() //window对象(浏览器中)

9.this绑定规则之外之箭头函数中的this获取

箭头函数不使用this的四种标准规则(也就是不绑定this),而是根据外层作用域来决定this。

var name = "why"

var foo = () => {
  console.log(this)
}

foo() // 外层作用域为全局作用域,浏览器中window即为全局作用域,window
var obj = {foo: foo}
obj.foo() // 对象中没有作用域,所以外层作用域也为全局作用域,window
foo.call("abc")// 跟绑定规则无关,显示绑定靠边站,只看外层作用域=全局作用域,window

10.面试题解析

案例一

var name = "window";

var person = {
  name: "person",
  sayName: function () {
    console.log(this.name);
  }
};

function sayName() {
  var sss = person.sayName;
  sss(); // window: 独立函数调用
  person.sayName(); // person: 隐式调用
  (person.sayName)(); // person: 隐式调用
  (b = person.sayName)(); // window: 赋值表达式(独立函数调用)
}

sayName();

案例二

var name = 'window'

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = { name: 'person2' }

person1.foo1(); // person1(隐式绑定)
person1.foo1.call(person2); // person2(显示绑定优先级大于隐式绑定)

person1.foo2(); // window(不绑定作用域,上层作用域是全局)
person1.foo2.call(person2); // window

person1.foo3()(); // window(独立函数调用)
person1.foo3.call(person2)(); // window(独立函数调用)
person1.foo3().call(person2); // person2(最终调用返回函数式, 使用的是显示绑定)

person1.foo4()(); // person1(箭头函数不绑定this, 上层作用域this是person1,foo4是函数有函数作用域,所以this的当前作用域是foo4的函数作用域,this的上层作用域是foo4的this指向。)
person1.foo4.call(person2)(); // person2(上层作用域被显示的绑定了一个person2)
person1.foo4().call(person2); // person1(上层找到person1)

案例三

var name = 'window'

function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1() // person1
person1.foo1.call(person2) // person2(显示高于隐式绑定)

person1.foo2() // person1 (上层作用域中的this是person1)
person1.foo2.call(person2) // person1 (上层作用域中的this是person1)

person1.foo3()() // window(独立函数调用)
person1.foo3.call(person2)() // window
person1.foo3().call(person2) // person2

person1.foo4()() // person1
person1.foo4.call(person2)() // person2
person1.foo4().call(person2) // person1


案例四

var name = 'window'

function Person (name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function () {
      return function () {
        console.log(this.name)
      }
    },
    foo2: function () {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()() // window
person1.obj.foo1.call(person2)() // window
person1.obj.foo1().call(person2) // person2

person1.obj.foo2()() // obj
person1.obj.foo2.call(person2)() // person2
person1.obj.foo2().call(person2) // obj


// 

// 上层作用域的理解
// var obj = {
//   name: "obj",
//   foo: function() {
//     // 上层作用域是全局
//   }
// }

// function Student() {
//   this.foo = function() {

//   }
// }