this 指向问题

在这里插入图片描述

一、常见错误理解#

  • this是指向自身吗?

特别是在函数中使用 this 的时候,this 是指的所在的这个函数对象吗?看下面示例:

function test() {
console.log(this.name)
}
test.name='aaaa'
test() // ''

输出的是空字符串,因为此时 this 指向的是 window ,所以 this 并不是指向自身

  • this指向函数的作用域吗?
function test() {
var name = 'bbbbb'
console.log(this.name)
}
test() // ''

执行后发现输出的仍然是空字符串,原因和上面一样,this 没有指向当前函数的作用域

其实 this 是在运行时绑定的,并不是在声明时绑定,this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。确定 this 指向的步骤应该是 “ 确定调用位置—应用规则—确定this指向 ”

二、调用位置#

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

关键:分析调用栈,即为了到达当前执行位置所调用的所有函数。而我们关心的调用位置就在当前正在执行的函数的前一个调用中

function baz() {
// 当前调用位置是全局作用域
console.log("baz")
bar() // bar的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log("bar")
foo() // foo的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log("foo")
}
baz() // <-- baz的调用位置

三、绑定规则#

确定调用位置后,需要对应 this 的绑定规则,有四种绑定规则,判断条件如下:

默认绑定#

一般可以理解为无法应用其他规则时的兜底默认规则,一般适用于独立函数调用时

function test(){
console.log(this.a)
}
var a = 'aaa'
test() // aaa

上面这种方式便是默认绑定,test() 不在任何对象内的独立调用,适用于默认绑定,默认绑定 this 指向的全局对象,在浏览器里面就是 window,在 node 里面就是 global

注意:在严格模式下,全局对象无法使用默认绑定,默认绑定会绑定到 undefined 上,即 this 指向 undefined

隐式绑定#

隐式绑定存在于在调用位置有上下文对象或者说调用时被对象包含或拥有

const obj = {
name: 'oooo',
say: function(){
console.log(this.name) // this指向obj
}
}
obj.say() // oooo

看上面函数 say 的调用,不是 say 单独调用,而是被对象 obj 包含着调用,此时 this 是指向 obj 对象的

当函数被多个对象包含时,指向它的上一级

var o = {
a:10,
b:{
a:2,
fn:function(){
console.log(this.a) // 2
}
}
}
o.b.fn()

隐式丢失#

有一种情况是看似应该是隐式绑定,但实际却是默认绑定

var name = 'globallll'
var obj = {
name: 'oooo',
say: function(){
console.log(this.name)
}
}
var copy = obj.say
copy();// globallll
var name = 'globallll'
var obj = {
name: 'oooo',
say: function(){
console.log(this.name)
}
}
function b(func){
func()
}
b(obj.say)// globallll

这种情况是不满足隐式函数绑定的,因为隐式函数绑定应当是被包含在对象中调用的,而不是说只要是对象的其中一部分就可以了,重点在于调用时是否被函数包含着!

解析:

第一个是把 obj 里的函数 say 的引用赋值给 copy 变量,再通过 copy 来调用,copy 调用时并没有被 obj 包含着调用,这就适用默认绑定规则—独立函数调用,因此此时 this 是指向 window

第二个例子同理,只不过看起来是调用的 obj.say(),但实际过程是:

func = obj.say
func()

显示绑定#

方法:使用 callapplybind 直接指定 this 的绑定对象

function foo() { console.log( this.a ) }
var obj = { a:2 }
foo.call( obj ) // 2

我们通过使用 call() 方法,传入我们想绑定的对象,将 this 绑定到该对象上

有时候当你传入的是一个简单数据类型,而非一个对象,这时候 call 方法会自动对其进行转换,转换成该数值的对象形式,然后进行绑定,这个过程称之为 “ 自动装箱 ”

缺点:同样会出现隐式丢失的问题

解决方法:

  • 硬绑定

缺点:会大大降低函数的灵活性,使用绑定之后就无法使用隐式绑定或者显式绑定来修改 this

function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2

foo.call(obj) 强制把 this 绑定到了 obj ,之后调用函数 bar ,它总会在 obj 上调用 foo ,这是显式的强制绑定,叫做硬绑定

  • API调用上下文

第三方库的许多函数,以及 JS 语言和环境中许多新的内置函数,都提供了一个可选的参数,通常被称为 “ 上下文 ”,其作用和 bind() 一样,确保你的回调函数使用指定的 this

function foo(id) {
console.log( id, this.id )
}
var obj = {
id: "dmeo"
}
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj ) // 1 demo 2 demo 3 demo

new绑定#

通过构建函数 new 关键字生成一个实例对象,此时 this 指向这个实例对象

function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1

另外,new 过程遇到 return 一个对象,此时 this 指向为返回的对象

function fn()
{
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined

如果返回一个简单类型的时候,则 this 指向实例对象

function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx

四、优先级#

  • 隐式绑定与显式绑定#

function foo() {
console.log(this.a)
}
var obj1 = {
a: 2,
foo: foo
}
var obj2 = {
a: 3,
foo: foo
}
obj1.foo() // 2
obj2.foo() // 3
obj1.foo.call(obj2) // 3
obj2.foo.call(obj1) // 2

结论:显式绑定 > 隐式绑定

  • new绑定与隐式绑定#

function foo(something) {
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2
obj1.foo.call(obj2, 3) // 3
console.log(obj2.a) // 3
var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4

结论:new 绑定 > 隐式绑定

  • new绑定与显式绑定#

newcall/apply 无法一起使用,因此无法通过 new foo.call(obj1) 来直接测试,但我们可以使用硬绑定来测试

function foo(something) {
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3

这里 bar 被硬绑定在了 obj1 上,但 new bar(3) 并没有把 obj1.a 修改为 3 。相反,new 修改了 bar() 中的 this 。因为使用了 new 绑定,我们得到了一个名为 baz 的新对象,并且 baz.a 的值为 3

结论:new 绑定 > 硬绑定(显式绑定)

所以,new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

五、箭头函数#

箭头函数的this指向规则:#

箭头函数没有 prototype (原型),所以箭头函数本身没有 this#

let a = () =>{}
console.log(a.prototype) // undefined

箭头函数的 this 指向在定义的时候继承自外层第一个普通函数的 this#

let a,
barObj = { msg: 'bar' },
fooObj = { msg: 'foo' }
function foo() {
a() // 结果:{ msg: 'bar' }
}
function bar() {
a = () => {
console.log(this)
} // 在bar中定义 this继承于bar函数的this指向
}
bar.call(barObj) // 将bar的this指向barObj
foo.call(fooObj) // 将foo的this指向fooObj

可以得出两点:

  • 箭头函数的 this 指向定义时所在的外层第一个普通函数,跟使用位置没有关系
  • 被继承的普通函数的 this 指向改变,箭头函数的 this 指向会跟着改变

不能直接修改箭头函数的this指向#

上个例子中的 foo 函数修改一下,尝试直接修改箭头函数的 this 指向

let fnObj = { msg: '尝试直接修改箭头函数的this指向' }
function foo() {
a.call(fnObj) // 结果:{ msg: 'bar的this指向' }
}

很明显,call 显示绑定 this 指向失败了,包括 aaplybind 都一样,不过它们( callaaplybind )会默认忽略第一个参数,但是可以正常传参

可以通过间接的形式来修改箭头函数的指向:去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变

bar.call(barObj); // 将 bar 普通函数的 this 指向 barObj 然后内部的箭头函数也会指向 barObj

箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)#

下面来看几个例子:

const obj = {
a: function() {
console.log(this)
window.setTimeout(() => {
console.log(this)
}, 1000)
}
}
obj.a.call(obj) //第一个this是obj对象,第二个this还是obj对象
const obj = {
a: function() { console.log(this) },
b: {
c: () => {console.log(this)}
}
}
obj.a() //obj对象
obj.b.c() //window对象,因为外层没有普通函数
let a = 1
function foo(){
this.a = 2
let b = () => {
console.log(this.a) // this 指向 foo
}
b()
}
let bar = new foo() // 2