走进this的内心世界

151 阅读11分钟

前言

在js中有很多的关键字,各种不同的关键字都有它们自己的个性,而this感觉是其中一个比较花心的,一会儿想跟小美在一起,一会儿想跟小丽在一起,我们接下来就来走入this的内心世界,来探讨下this到底指向谁。

1. 为什么要有this

在js中,this是一个关键字,而每个关键字都有它存在的意义(官方也不会吃饱了没事干),为了明白为什么要有this,下面我们来看一段代码:

function identify(context) {
  return context.name.toUpperCase()
}

function speak(context) {
  var greeting = 'Hello, I am ' +  identify(context)
  console.log(greeting);
  
}
var me = {
  name: 'Tom'
}
speak(me)

image.png

乍一看,大家可能觉得这不是很简单的两个拼接字符串的函数吗,这跟this有啥关系?

这样想的话格局就小了奥,咱们目光长远一点,如果是由很多个函数它们存在于不同的作用域中并且它们的参数都是context的话,那么你还能一眼就看出来他们呢的context是传入什么吗?在这种情况下如果不使用this的话代码就会显得非常冗杂。

为了避免这一情况,我们下面来试试用this的话代码会变成什么样子:

function identify() {
  return this.name.toUpperCase()
}

function speak() {
  var greeting = 'Hello, I am ' +  identify.call(this)
  console.log(greeting);
  
}
var me = {
  name: 'Tom'
}
speak.call(me)

在上面代码我们可以发现,当使用了this之后两个函数中就不再需要传入参数,大大的简化了代码,让代码变得更加优雅。上面的代码看不懂没关系,下面我会为大家一一讲解。

为什么要有this?

this 让函数可以自动引用合适的上下文对象

2. this 是谁的

  1. this 在全局下指向的是Wingdow
  2. this 是一个代词,在 js 中永远代指某一个域,且 this 只存在于域中才有意义,this是谁的取决于在哪个域中

下面我们来看一下this在全局中的指向:

这是this浏览器中的指向全局: image.png 这是thisnode中的指向全局:

image.png

tips:谷歌V8的全局叫window,node的V8的全局叫global

接下来我们来看两段代码,来看看下面函数中的this是谁的:

function foo() {
  console.log(this);
}//this是foo的,因为定义在foo中

function foo() {
  function fn() {
    let p = {
      name: '阿美',
      age: 18
    }
    console.log(this);
  }
}//this是fn的,因为this定义在了fn中

3. this 的指向

在上文中了解了 this 到底是谁的之后,下面我们就来看看 this 的心到底是归谁所有。(毕竟咱妈把咱生下来之后想找哪个女朋友还得听从咱的内心)

在了解 this 的指向之前,我们先来了解函数的独立调用非独立调用的概念:

独立调用:函数单独调用前面不加任何东西(例如:fn()

非独立调用:函数是被别人所调用,就是前面加了东西(例如:xxx.fn()

3.1 默认绑定

在了解完两个函数调用之后,我们来看下面两段代码来看看this指向哪里:

function foo() {
  let person = {
    name: '阿美',
    age: 18
  }
  console.log(this);
  
}
foo()



function fn() {
  let a = 1
  function bar() {
    console.log(this);
  }
  bar()
}一下一下
fn()

大家可能会猜测,在第一段代码中this是在foo中是属于foo的,在第二段代码中this是在bar中的。那么this是不是分别指向foo和bar呢?下面我们来看看二者的输出结果:

image.png

这时我们会发现this指向的是全局,并不是foo和bar,这是为什么呢?这就和前面提到的函数两种调用有关了,只要是函数的独立调用,那么函数中的this就是指向全局。而这个规则就是官方内定的一种this绑定规则——默认绑定

默认绑定:函数独立调用,this指向window

3.2 隐式绑定

在上文中我们讲到了函数的两种调用独立和非独立调用,在讲完了函数独立调用时this的指向之后,我们接下来讲解一下函数非独立调用的时候this指向哪里,下面来看一段代码:

function foo() {
  console.log(this);
}
const obj = {
  a: 1,
  foo: foo//前面是key,后面的是函数foo的引用
}
obj.foo()

image.png

我们可以看到上面代码的输出结果是obj这个对象,不是全局了。有聪明的朋友可能已经猜到了,这和函数的非独立调用有关。当我们使用非独立调用的时候,谁调用的这个函数,那么函数中的this就指向谁,这就是隐式绑定

隐式绑定:当函数的引用有上下文对象时,this指向该上下文对象 (函数被非独立调用时,调用函数的那个对象)

3.3 隐式丢失

在了解了上面两种绑定方式之后,下面我们来讲一种比较特殊的情况。

在聊这个之前,我们先来回忆一下自己在学校的生活。在学校的时候日常的作业什么通常都是老师布置下来的,而我们通常对老师比较熟悉,而对老师的上级领导比如副校长不太了解。当副校长直接给我们发布指令的时候,我们可能心里会想:这个老头是谁啊?不认识,听他说个蛋。而这个时候,就需要老师来当中间人来代副校长发布指令了。

而上面这个场景就是this的隐式丢失了,下面我们用一段代码来展示一下:

function foo() {
  console.log(this);
}
const obj = {
  a: 1,
  foo:foo
}
const obj2 = {
  a: 2,
  obj: obj
}
obj2.obj.foo()// this 的隐式丢失

image.png

根据上面的代码我们可以看到foo中的this是指向obj而不是obj2,而这个就是this隐式绑定中的一个特点——隐式丢失。

隐式丢失:当函数的引用有一连串的上下文对象,this 指向最近的那个对象。(可简单记为就近原则)

3.4 显示绑定

上面的默认绑定和隐式绑定都是this遵从自己内心自愿的一种绑定方式,那么我们可不可以霸王硬上弓自己来改变this的指向呢?答案是可以的,我们可以依靠几个Function对象自带的函数来改变:callapplybind。而这三个函数的使用方法都是用函数名来调用这几个方法,让函数中的this指向这方法中传入的第一个参数,并且会调用该函数,下面来举个例子:

function foo(x, y) {
  console.log(this.a, x + y);
}
var obj = {
  a: 1
}
foo.call(obj, 2, 3) // Function.prototype.call = func,call定义在Function上
//函数体调用call,call的源代码会将该函数体触发。call参数第一个是要绑定的对象,后面参数会传入调用call的函数体中


//下面是另外两个方法,大家可以自行尝试一下

// foo.apply(obj, [2, 3])//和call传参差不多,只不过加了个[]


// let bar = foo.bind(obj, 2, 3)//接收参数和call差不多,但是bind会返回一个函数体,要接收并且调用才会实现改变this效果
// bar()
// let bar = foo.bind(obj)//如果bind中参数不够的话会去bar中寻找,如果bind中参数够则不会
// bar(2, 3)
// let bar = foo.bind(obj, 1)
// bar(2, 3)

image.png

我们可以看到在通过call方法将this的指向给掰弯后,函数foo的this.a输出的是obj.a,并且将2和3作为参数传入了foo的形参x和y中,而这个就是call方法的作用。另外两个方法使用效果也如同call一样。

显式绑定:通过 call apply bind 显式的将函数的 this 绑定到一个对象上

3.5 new 绑定

当我们在创建一个实例对象并且要赋予具体的值给它时,我们通常会在构造函数中采用this来将值赋予实例对象,而这个过程中我们会用到new这个关键字,它就可以对this进行一个绑定,让它指向创建的实例对象。

下面来看一段代码:

function Person() {
  this.name = '阿伟'
  this.age = 18
}
let p = new Person()
console.log(p);

image.png

我们可以看到实例对象p通过this获得了name和age,那如果我们在是数中有return的话,p又会是什么呢?接下来我们试试:

function Person() {
  this.name = '阿伟'
  this.age = 18
  return 123
}
let p = new Person()
console.log(p);

image.png

我们可以发现如果有return并且返回的是基础类型,那么new就不会用返回的基础类型,而是用this。我们用引用类型来试试:

function Person() {
  this.name = '阿伟'
  this.age = 18
  return [1, 2, 3]
}
let p = new Person()
console.log(p);

image.png

根据上面的运行结果我们可以发现如果return的是引用类型,那么new的执行结果就是这个引用数据类型。从上述两个例子我们可以得到:

当函数内部存在return,且返回的是一个引用类型的数据时,new的执行结果就是这个引用类型的数据。如果返回的是基础类型则返回的是this

下面以上述代码为例来看看new的原理

function Person() {
  // let obj = {}//先创建空对象

  // Person.call(obj)//然后用call将Person中的this绑定在obj身上

  // 将值赋给obj//依次执行Person中的赋值,将值赋予obj

  //obj.__proto__ = Person.prototype//让obj的隐式原型等于构造函数的显式原型

  //return前还要判断Person()的值,如果是引用类型,则采用。否则return obj
  //return Person() instanceof 'object' ? Person() : obj //返回obj 


  this.name = '阿伟'
  this.age = 18

  // return 123//不采纳,基础类型
  // return [1,2,3]//采纳,引用类型
}
let p = new Person()
console.log(p);

new的原理

  1. 创建一个空对象obj
  2. 将构造函数里的this指向obj
  3. 正常运行构造函数里的代码
  4. obj的隐式原型等于构造函数的显示原型
  5. 返回obj(返回前还会判断返回类型,如果是引用类型则返回该引用类型,否则返回obj)

4. 箭头函数

在ES6中新增了一个箭头函数样子就是这样的:() => {},由小括号、箭头和大括号所组成,跟之前的函数比起来少了个function显得更加的简洁,而它不仅把这个function丢掉了,还把this也丢了,在箭头函数中没有this

下面来看一段代码:

let Foo = () => {
  this.name = '牛哥'
}
let foo = new Foo()

image.png

我们可以看到上面代码爆出了错误,这是因为在箭头函数中没有this,并且还得知了一点,箭头函数不能作为构造函数使用。

箭头函数注意点

  1. 箭头函数没有this,写在了箭头函数中的 this 是它外层非箭头函数的
  2. 箭头函数不能作为构造函数使用

5. call 的源码

看到这个小标题大家可能会有点惊讶,源码这么高级的东西我们才学到这里怎么会呢。其实不用害怕,在call并没有什么很神奇的魔法可以让this莫名其妙就掰弯了,它的实现是很简单的,就是基于this绑定规则的默认绑定和隐式绑定。下面我们来看看源码:

function foo(x, y) {
  console.log(this.a, x, y);
  return x + y
}
const obj = {
  a: 1
}
// foo.call(obj, 2)


//call 的实现原理
Function.prototype.myCall = function (...args) {
  //将 foo 引用到 obj 上
  //让 obj 调用 foo
  //移除 obj 上的 foo
  const context = args[0]//obj
  const arg = args.splice(1)//obj后面的参数
  context.fn = this//因为myCall被foo调用所以根据隐式绑定,myCall中的this指向foo
  const res = context.fn(...arg)//函数最后可能会有return
  delete context.fn
  return res
}

let res = foo.myCall(obj, 1, 2)
console.log(res);

用我们自己的话来说呢call它的功能有几点:

  1. 改变函数this的指向
  2. 调用call时同时会把调用call的函数也调用
  3. 可以为函数传入参数

根据上面几个特性我们可以先用剩余参数来接收传入的参数,根据传入参数的性质,第一个是函数中的this被绑定的对象,剩下的都是作为参数传入函数当中,所以我们可以将对象和参数分别用contextarg储存起来。

接下来我们要实现call函数主要功能,就是将foo中的this指向obj,而这一点我们只需要将函数挂载到obj身上即可。因为大家想想如果我们在obj中调用函数foo那么是不是这样调用的:obj.foo(),根据this的隐式绑定规则,foo中的this是指向obj的,这样我们就实现了改变foo中this的指向问题。

context.fn = this解释

当实现了改变foo中this指向问题后,我们应当想想在obj中想要挂载一个函数,总不能根据函数名字去挂载,那么我们要如何进行挂载呢?接下来是一个有点要长脑子的地方:因为call函数是被foo所调用的,那么根据隐式绑定,call中的this是指向foo的。所以要在obj中挂载函数,只需要创建一个key让它等于this即可。 这时候我们让context.fn = this即可实现这个效果。

接下来我们为函数fn传入foo需要被传入的参数即可,因为可能会有return所以我们定义一个res用来接受,最后将fn从obj中删除然后return res即可完成call的实现。

最后感谢各位大佬的观看,喜欢的话点个赞吧!

image.png