几分钟搞懂JavaScript中的this关键字

253 阅读7分钟

一. 引言

在js中,this存在的意义就是为了让对象中的函数有能力访问对象自己的属性,同时this可以减少上下文参数的传递,显著提升代码质量。

观察以下两段代码;

image.png image.png

通过运行输出俩段代码的值都是一样的输出Hello,I amTOM,但是下面相比上面在代码上在相对简洁,减少了函数之间的参数传递,比较优雅。虽然this使用起来很优雅,但是学起来也很简单。

首先需知道this 是一个代词,在js 中永远代指某一个域,且 this 只存在于域中才有意义(写在对象中无意义),this 在全局下指向的是 window。 image.png

二. this的指向

搞清楚this的指向只需要理解四个绑定原则:默认绑定,隐式绑定,显示绑定和new绑定

2.1 默认绑定

默认绑定也就是当函数独立调用时,this 指向 windwow(也就是全局)。

独立调用也就是不作为对象的方法被调用,非独立调用就是通过对象.函数名()调用。

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

如上代码,this是在foo函数作用域里的,那么this会指向什么呢?结果如下:指向widow。 image.png

function foo() {
  let a = 1
  function bar() {
    console.log(this);
  }
  bar()
}
foo()

再看这段代码,在foo函数里面调用bar函数,this是在bar函数作用域里面,结果如下:还是指向window。 image.png 这就说明this的指向与它在哪个函数作用域里无关,与它是怎么被调用的有关

2.2 隐式绑定

隐式绑定就是当函数的引用有上下文对象(当函数被某一个对象所拥有且调用),this 指向该上下文对象。

function foo() {
  console.log(this);  //{ a: 1, fun: [Function: foo] }

}
const obj = {
  a: 1,
  fun: foo
}
obj.fun()   //非独立调用,是由对象 obj 调用

在对象obj里面的一个属性为fun,值为foo函数体(没有调用,foo()才是调用)。所以当前foo函数就被obj这个对象所拥有且调用(如果不调用的话这个函数就跟没有一样),this就指向这个obj对象。

image.png

当这种情况下呢?obj拥有并调用foo,而obj又被obj2拥有,那么这个this指向谁呢?通过编译输出结果为 1,也就是 obj 中的 a 值,则说明this指向的是obj对象,即 obj2 不拥有foo函数(也称隐式丢失),也就是当函数的引用有一连串的上下文对象,this 指向最近的那个对象。

2.3 显示绑定

显示绑定:call apply bind显示地将函数的 this 绑定到一个对象上。

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

var obj = {
  a: 1
}

foo.call(obj)   //将 foo函数强行绑定到 obj对象上

使用call内置方法可以将 foo函数强行绑定到 obj对象上,与此还有apply bind也可以。分别为“foo.apply(obj)”, 先 let bar = foo.bind(obj); 再 bar();bind有点不一样,它则需要用一个变量接收然后再用该变量调用才能绑定。

当函数有参数应该如何调用呢?如下; image.png image.png image.png

2.4 new 绑定

new 绑定:this 指向实例对象

function Person() {
  this.name = '阿炜'
  this.age = 18
}

let p = new Person()

console.log(p);   // Person { name: '阿炜', age: 18 }

分析以上代码,容易得出:当使用 new调用构造函数创建一个实例对象时,构造中的函数会自动绑定在该实例对象上。

所以 new的执行过程究竟是怎么样的呢?

  1. 创建一个空对象
  2. 将构造函数里的this指向该对象
  3. 正常运行构造函数里的代码
  4. 该对象的隐式原型等于构造函数的显示原型(obj.__proto__ = Person.prototype
  5. 返回该对象

注意::在返回该对象前,会判断原构造函数本身有没有return返回语句,如果本身有return且返回的是引用类型的数据(数组,对象等),则返回原构造函数本身的值,否则返回创建的对象。

image.png image.png

所以new的执行过程应该是: image.png

三. 箭头函数

为什么要讲箭头函数呢?

因为箭头函数里面没有this,它的this与它最近一层非箭头函数环境中的this保持一致。

const word = 'window hello'
const obj = {
  word: 'obj hello',

  fn: function () {
    setTimeout(() => {
      console.log(this.word);
    })
  }
}

const globalFn = obj.fn
globalFn()     //输出  undefined

const obj2 = {
  word: 'obj2 hello',
  fn: obj.fn
}
obj2.fn()   //输出  obj2 hello

为什么这样输出呢?

因为箭头函数中的this是与它最近一层非箭头函数环境中的this保持一致,也就是与对象的obj.fn中this保持一致,而obj.fn又赋值给了globalFn(obj.fn=globalFn),而对于globalFn来说,是由window调用,所以obj.Fn的this指向window,箭头函数中this又与它保持一致,所以也指向了window。

但是既然指向window,为啥不是输出window hello呢?

这是因为 word 是一个在全局作用域中定义的变量,而不是全局对象的属性。在全局作用域中,this 通常不指向变量,而是指向全局对象(如 window)。由于 word 是一个变量,而不是 this 的属性,所以 this.word 是 undefined。

下面obj2.fn()同理,调用fn时fn中的this指向obj2,内部函数中this与它最近一层非箭头函数环境中的this保持一致,所以也指向了obj2,此时输出obj2 hello。

因为箭头函数中没有this,构造函数时由 new调用,而new执行需要将构造函数里的this指向obj,所以箭头函数不能当作构造函数来使用。 image.png

四. 手敲一个call源码

call函数可以将一个函数强行绑定到一个对象上,如下;

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

call的原理其实就是前面提到的隐式绑定,当要将一个函数的this指向一个对象时,就让该对象拥有且调用该函数就可以了。因为call是在Function.prototype上的,所以我们在Function.prototype新增一个手敲的源码。因为call可以传很多参数,所以我们用...args来作为形参。如下;

Function.prototype.myCall = function (...args) {
  const context = args[0]
  const arg = args.slice(1)  
  context.fn = this
  const res = context.fn(...arg)
  delete context.fn
  return res
}

总的来就是将foo引用到 obj 上,让 obj 调用foo,移除 obj 上的 foo。

详细如下:

image.png