【无路可退】—— 向this指向发起冲锋

215 阅读11分钟

学习this的意义何在?

有时候在被一些稀奇古怪的考this的面试题折磨时,我不禁会想,我学这玩意干嘛,更具体的说是,我学点大概的不就行了,反正不影响平时写JS代码。直到我在各大技术平台看到很多大佬写的有关于this的博客以及他们学习的方法,我才意识到自己真的只是井底之蛙。深入了解this指向真的很重要!!!

重要在哪里?

以下几点:

  1. 如果你是还没工作的学生或者正在求职的小伙伴,this知识点必问,重不重要?
  2. this的使用频率超级高,学习JS的很多知识点都需要用到this,比如new关键字,call,apply方法,这些基本的知识不学怎么掌握好JS?
  3. 合理的使用this,可以让我们写出简洁且复用性高的代码。是不是很有用?

看到这里,相信你已经知道this的重要性了,当然我的文章主要还是写给前端小白看的,因为自己本身也是一个小白。所以会尽可能的将这篇博客讲的通俗易懂。let's go。

this究竟是什么?

之前我在学习this的过程中一直没有考虑这个问题,很多博客也只是讲一下this的指向以及绑定规则。而在我看来,对与小白而言,必须得从0开始。首先必须得知道要学习的是个什么东西。

重点来了

this其实就是一个指针,指向最后调用它所在的函数的对象

this其实就是一个指针,指向最后调用它所在的函数的对象

this其实就是一个指针,指向最后调用它所在的函数的对象

这是彻底明白this指向的关键,下面我可能会谈到很多中this的绑定规则,但是万变不离其宗,不管规则再多,也就是都不开上面这三句重复的话。

this的绑定规则

为了能够一眼看出this指向的是什么,我们首先需要知道this的绑定规则有哪些?

  1. 默认绑定
  2. 隐式绑定
  3. 硬绑定
  4. new绑定
  5. 箭头函数绑定

上面这些绑定有可能你见过,但是你不知到它的名字,但是今天之后,请牢牢记住。我们将依次来进行解析。

默认绑定

默认绑定也就是在没有应用其他任何绑定规则时的绑定。

小白专用区:默认绑定指的就是普通函数调用。

文字解释太苍白?举个栗子康康吧。

非严格模式下默认绑定的指向

非严格模式下this指向的是全局对象window

var a = 10;
function foo () {
  console.log(this.a)
}
foo();

foo就是一个普通函数,直接调用,没有应用其他任何绑定规则,普普通通。

严格模式下默认绑定的指向

在严格模式下会绑定到undefined

"use strict";
var a = 10;
function foo () {
  console.log('this1', this)
  console.log(window.a)
  console.log(this.a)
}
console.log(window.foo)
console.log('this2', this)
foo();
  • 开启了严格模式,只是说使得函数内的this指向undefined
  • 开启了严格模式,并不会改变全局中this的指向。因此this1中打印的是undefined,而this2还是window对象。
  • 开启了严格模式,不会阻止a被绑定到window对象上。

不了解严格模式的同学可以去MDN上的严格模式

浏览器与node环境下this的指向还有些区别,本文是面向小白编(xia)程(che)的,就不给新同学增加负担啦。

默认绑定需要注意的地方

let或const声明的变量不会绑定到window上。

let a = 10
const b = 20

function foo () {
  console.log(this.a)
  console.log(this.b)
}
foo();
console.log(window.a)

所以上面的结果是三个undefined

隐式绑定

函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun().

隐式绑定时,this永远指向最后调用它所在函数的对象。

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

foo被obj调用,所以this指向obj。所以最后的结果是1。

隐式绑定的隐式丢失问题

看标题是不是觉得肯定很复杂,其实不是的,还是记住那句话this其实就是一个指针,指向最后调用它所在的函数的对象

问题1

使用另一个变量来给函数取别名会发生隐式丢失。

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;

obj.foo();
foo2();

obj.foo()肯定会打印出1,那么foo2()会打印出什么呢?我知道这里肯定有些同学是这样想的。

foo2 就是 obj.foo 啊,所以肯定打印的结果和上面一样是1。

如果这样想,你就掉进了陷阱,答案不然。

这是因为虽然foo2指向的是obj.foo函数,不过调用它的却是window对象,所以它里面this的指向是为window

你如果还是有点不理解,两种选择。

第一种:别钻牛角尖,事实就是如此,说服自己接收这个现实。 第二种:多做类似的题目,像你之前无数次在的试卷那样,从题海中寻找解决疑惑。

我更倾向于接收第一种,那样会让你省掉很多不必要的时间去学习其他知识。但我知道,这其实这挺难的,所以这里我也为你准备了第二种。

function foo () {
  console.log(this.a)
};
var obj = { a: 1, foo };
var a = 2;
var foo2 = obj.foo;
var obj2 = { a: 3, foo2: obj.foo }

obj.foo();
foo2();
obj2.foo2();

这三种不同的foo()打印出来的分别是什么呢?

1
2
3
  • bj.foo()中的this指向调用者obj
  • foo2()发生了隐式丢失,调用者是window,使得foo()中的this指向window
  • foo3()发生了隐式丢失,调用者是obj2,使得foo()中的this指向obj2
问题2

把一个函数当成参数传递时,也有可能导致隐式丢失。

看这道题

function foo () {
  console.log(this.a)
}
function doFoo (fn) {
  console.log(this)
  fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)

这里我们将obj.foo当成参数传递到doFoo函数中,在传递的过程中,obj.foo()函数内的this发生了改变,指向了window

因此结果为:

Window{...}
2
总结一下
  • 当函数是在某个对象下被调用时,也就是隐式绑定时,this会指向这个对象。
  • 隐式绑定存在这么两个问题有可能导致隐式
    • 重新为函数命名,然后将重新命名后的变量在全局对象中调用
    • 将函数作为参数传入给另一个函数,并在另一个函数中直接调用。
  • 如果你记不住,还是记住那句话,this其实就是一个指针,指向最后调用它所在的函数的对象

显示绑定

显示绑定就是强制改变函数内this的指向,将它指向你想指向的对象。

常见的改变this指向的方法有call()、apply()或者bind(),new和箭头函数因为比较特殊会放在下面讲。

使用call()、apply()或者bind()需要特别注意的地方

  • 使用.call()或者.apply()的函数是会直接执行的
  • bind()是创建一个新的函数,需要手动调用才会执行
  • .call().apply()用法基本类似,不过call接收若干个参数,而apply接收的是一个数组

call,apply,bind的第一个参数不为空

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

foo() // 2
foo.call(obj) // 1
foo.apply(obj) //1 
foo.bind(obj) // 不执行

foo() 用什么绑定规则判断this指向? 对的默认绑定,复习一下。

foo.call(obj)foo.apply(obj)都是强行将foo中的this指向了obj,并且立即执行。

foo.bind(obj) 也是强行将foo中的this指向了obj,但不会立即执行,而是返回一个改变了this执行的原函数。

call,apply,bind的第一个参数为空

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

如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数。

用显示绑定来解决绑定丢失的问题

var obj1 = {
  a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }, 0)
  }
}
var a = 3

obj2.foo1()
obj2.foo2()

最终结果

2
Window{...}
3

这里最不让人理解的地方是定时器中的this.a为什么是全局下的3,首先我们得明确的是setTimeout中的this指向的是window。

而且

setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }, 0)

这段代码其实也可以看做是将一个函数传入另一个函数。发送绑定丢失的原理和上面的隐式绑定丢失第二条是一样的。

那么我们如果我们想让定时器中的this指向obj1,我们就可以通过显示绑定的方法来解决啦。

var obj1 = {
a: 1
}
var obj2 = {
  a: 2,
  foo1: function () {
    console.log(this.a)
  },
  foo2: function () {
    setTimeout(function () {
      console.log(this)
      console.log(this.a)
    }.call(obj1), 0)
  }
}
var a = 3
obj2.foo1()
obj2.foo2()

现在的执行结果就是:

2
{ a: 1 }
1

new绑定

使用new来调用一个函数,会构造一个新对象并把这个新对象绑定到调用函数中的this

new做的事情

1.创建一个新对象(实例)

2.将此对象的隐式原型指向构造函数的显示原型

3.将传入的构造函数的this指向实例并向内传递参数

4.判断构造函数返回的是对象还是基本类型,如果是对象就返回对象,如果不是则返回实例

function _new(fn, ...args) {
const obj = {}  // 创建一个空对象
obj.__proto__ = fn.prototype //让这个空对象的原型指向构造函数的原型对象
let res = fn.call(obj, ...args) //将构造函数中的this指向这个空对象
return typeof res === object ?  res : obj  //判断执行后的返回值是否为对象,如果是,返回此对象,如果不是,返回这个空对象
}

因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。

function Person (name) {
  this.name = name
}
var name = 'window'
var person1 = new Person('xiaohei')
console.log(person1.name)

输出结果为 xiaohei, 原因是因为在var person1 = new Person('xiaohei');这一步,会将Person 中的this绑定到person1对象上。

箭头函数绑定

箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,箭头函数里面的this是由外层作用域来决定的,且指向函数定义时的this而非执行时。箭头函数在使用时,需要注意以下几点:

(1)函数体内的this对象,继承的是外层代码块的this。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)箭头函数没有自己的this,所以不能用call()apply()bind()这些方法去改变this的指向.

举个栗子

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

分析

  • 对于obj.foo1()函数的调用,它的外层作用域是window,对象obj当然不属于作用域了(我们知道作用域只有全局作用域window和局部作用域函数)。所以会打印出window
  • obj.foo2()(),首先会执行obj.foo2(),这不是个箭头函数,所以它里面的this是调用它的obj对象,因此打印出obj,而返回的匿名函数是一个箭头函数,它的this由外层作用域决定,那也就是函数foo2咯,那也就是它的this会和foo2函数里的this一样,就也打印出了obj

绑定优先级

上面那么多种绑定规则,难免会出现几种绑定同时出现的情况,那么它们也当然存在一个优先级的关系。

这个规则就是

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

再次总结

判断this指向时的脑回路

  • 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
  • 函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。
  • 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()
  • 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
  • 如果把null或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  • 如果是箭头函数,箭头函数的this继承的是外层代码块的this。

忘记上面学的,记住这一点

说了这么多规则,你可能已经失去耐心了,那不是你的原因,是笔者文笔不够精彩以及知识面是在有限。在文末,我会给你推荐几篇我看多的并且觉得十分有用的博客来帮助你深入学习this指向

结语

感谢你看到这里。

小白入门1嗨,你真的懂this吗

小白入门2面试官问:JS的this指向

巩固刷题【建议👍】再来40道this面试题酸爽继续(1.2w字用手整理)

总结升华this、apply、call、bind