前端面试必会 | 一文读懂 JavaScript 中的 this 关键字

this 是一个令无数 JavaScript 编程者又爱又恨的知识点。它的重要性毋庸置疑,然而真正想掌握它却并不是易事。但愿本文能够帮助你们理解 thisjavascript

JavaScript 中的 this

JavaScript 引擎在查找 this 时不会经过原型链一层一层的查找,由于 this 彻底是在函数调用时才能够肯定的,让咱们来看下面几种函数调用的形式。前端

Function Invocation Pattern

普通的函数调用,这是咱们使用较多的一种, foo 是以单独的变量出现而不是属性。其中的 this 指向全局对象。java

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

foo() // Window
复制代码

Method Invocation Pattern

函数做为对象的方法调用,会经过 obj.func 或者 obj[func] 的形式调用。其中的 this 指向调用它的对象。node

const obj = {
  name: 'lxfriday',
  getName(){
    console.log(this.name)
  }
}

obj.getName() // lxfriday
复制代码

Constructor Pattern

经过 new Constructor() 的形式调用,其 this 会指向新生成的对象。面试

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

const person = new Person('lxfriday')
console.log(person.name) // lxfriday
复制代码

Apply Pattern

经过 foo.apply(thisObj) 或者 foo.call(thisObj) 的形式调用,其中的 this 指向 thisObj。若是 thisObjnull 或者 undefined ,其中的 this 会指向全局上下文 Window(在浏览器中)。编程


掌握以上的几种函数调用形式就基本能够覆盖开发中遇到的常见问题了,下面我翻译了一篇文章,帮助你更深刻的理解 this数组

本文接下来的内容翻译自 blog.bitsrc.io/what-is-thi…,做者 Rajat S,内容有删改,标题有改动。浏览器

若是你已经使用过一些 JavaScript 库,你必定会注意到一个特殊的关键字 thisbash

this 在 JavaScript 中很常见,可是有不少开发人员花了不少时间来彻底理解 this 关键字的确切功能以及在代码中何处使用。闭包

在这篇文章中,我将帮助您深刻了解 this 其机制。

在深刻了解以前,请确保已在系统上安装了 Node 。而后,打开命令终端并运行 node 命令。

全局环境中的 this

this 的工做机制并不容易理解。为了理解 this 是如何工做的,咱们将探索不一样环境中的 this。首先咱们从全局上下文开始。

在全局层面中,this 等同于全局对象,在 Node repl(交互式命令行) 环境中叫 global

$ node
> this === global
true
复制代码

但上述状况只出如今 Node repl 环境中,若是咱们在 JS 文件中跑相同的代码,咱们将会获得不一样的答案。

为了测试,咱们建立一个 index.js 的文件,并添加下面的代码:

console.log(this === global);
复制代码

而后经过 node 命令运行:

$ node index.js
false 
复制代码

出现上面状况的缘由是在 JS 文件中, this 指向 module.exports,并非指向 global

函数中的 this

Function Invocation Pattern

在函数中 this 的指向取决于函数的调用形式。因此,函数每次执行的时候,可能拥有不一样的 this 指向。

index.js 文件中,编写一个很是简单的函数来检查 this 是否指向全局对象:

function fat() {
  console.log(this === global)
}
fat()
复制代码

若是咱们在 Node repl 环境执行上面的代码,将会获得 true,可是若是添加 use strict 到首行,将会获得 false,由于这个时候 this 的值为 undefined

为了进一步说明这一点,让咱们建立一个定义超级英雄的真实姓名和英雄姓名的简单函数。

function Hero(heroName, realName) {
  this.realName = realName;
  this.heroName = heroName;
}
const superman= Hero("Superman", "Clark Kent");
console.log(superman);
复制代码

请注意,这个函数不是在严格模式下执行的。代码在 node 中运行将不会出现咱们预期的 SupermanClark Kent ,咱们将获得 undefined

这背后的缘由是因为该函数不是以严格模式编写的,因此 this 引用了全局对象。

若是咱们在严格模式下运行这段代码,会由于 JavaScript 不容许给 undefined 增长属性而出现错误。这其实是一件好事,由于它阻止咱们建立全局变量。

最后,以大写形式编写函数的名称意味着咱们须要使用 new 运算符将其做为构造函数来调用。将上面的代码片断的最后两行替换为:

const superman = new Hero("Superman", "Clark Kent");
console.log(superman);
复制代码

再次运行 node index.js 命令,您如今将得到预期的输出。

构造函数中的 this

Constructor Pattern

JavaScript 没有任何特殊的构造函数。咱们所能作的就是使用 new 运算符将函数调用转换为构造函数调用,如上一节所示。

进行构造函数调用时,将建立一个新对象并将其设置为函数的 this 参数。而后,从函数隐式返回该对象,除非咱们有另外一个要显式返回的对象。

hero 函数内部编写如下 return 语句:

return {
  heroName: "Batman",
  realName: "Bruce Wayne",
};
复制代码

若是如今运行 node 命令,咱们将看到 return 语句将覆盖构造函数调用。

return 语句尝试返回不是对象的任何东西时,将隐式返回 this

方法中的 this

Method Invocation Pattern

当将函数做为对象的方法调用时,this 指向该对象,而后将该对象称为该函数调用的接收者。

在下面代码中,有一个 dialogue 方法在 hero 对象内。经过 hero.dialogue() 形式调用时,dialogue 中的 this 就会指向 hero 自己。这里,hero 就是 dialogue 方法调用的接收者。

const hero = {
  heroName: "Batman",
  dialogue() {
    console.log(`I am ${this.heroName}!`);
  }
};
hero.dialogue();
复制代码

上面的代码很是简单,可是实际开发时有可能方法调用的接收者并非原对象。看下面的代码:

const saying = hero.dialogue();
saying();
复制代码

这里,咱们把方法赋值给一个变量,而后执行这个变量指向的函数,你会发现 this 的值是 undefined。这是由于 dialogue 方法已经没法跟踪原来的接收者对象,函数如今指向的是全局对象。

当咱们将一个方法做为回调传递给另外一个方法时,一般会发生接收器的丢失。咱们能够经过添加包装函数或使用 bind 方法将 this 绑定到特定对象来解决此问题。

call、apply

Apply Pattern

尽管函数的 this 值是隐式设置的,但咱们也能够经过 call()apply() 显式地绑定 this

让咱们像这样重组前面的代码片断:

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
复制代码

咱们须要将hero 对象做为接收器与 dialogue 函数链接。为此,咱们可使用 call()apply() 来实现链接:

dialogue.call(hero)
// or
dialogue.apply(hero)
复制代码

须要注意的是,在非严格模式下,若是传递 null 或者 undefinedcallapply 做为上下文,将会致使 this 指向全局对象。

function dialogue() {
  console.log('this', this)
}
const hero = {
  heroName: 'Batman',
}
console.log(dialogue.call(null))
复制代码

上述代码,在严格模式下输出 null,非严格模式下输出全局对象。

bind

当咱们将一个方法做为回调传递给另外一个函数时,始终存在丢失该方法的预期接收者的风险,致使将 this 参数设置为全局对象。

bind() 方法容许咱们将 this 参数永久绑定到函数。所以,在下面的代码片断中,bind 将建立一个新 dialogue 函数并将其 this 值设置为 hero

const hero = {
  heroName: "Batman",
  dialogue() {
    console.log(`I am ${this.heroName}`);
  }
};
// 1s 后打印:I am Batman
setTimeout(hero.dialogue.bind(hero), 1000);
复制代码

注意:对于用 bind 绑定 this 以后新生成的函数,使用 call 或者 apply 方法没法更改这个新函数的 this

箭头函数中的 this

箭头函数和普通函数有很大的不一样,引用阮一峰 ES6入门第六章中的介绍:

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象
  2. 不能够看成构造函数,也就是说,不可使用 new 命令,不然会抛出一个错误
  3. 不可使用 arguments 对象,该对象在函数体内不存在。若是要用,能够用 rest 参数代替
  4. 不可使用 yield 命令,所以箭头函数不能用做 Generator 函数

上面四点中,第一点尤为值得注意。this 对象的指向是可变的,可是在箭头函数中,它是固定的,它只指向箭头函数定义时的外层 this箭头函数没有本身的 this,全部绑定 this 的操做,如 call apply bind 等,对箭头函数中的 this 绑定都是无效的

让们看下面的代码:

const batman = this;
const bruce = () => {
  console.log(this === batman);
};
bruce();
复制代码

在这里,咱们将 this 的值存储在变量中,而后将该值与箭头函数内部的 this 值进行比较。node index.js 执行时将会输出 true

那箭头函数中的 this 能够作哪些事情呢?

箭头函数能够帮助咱们在回调中访问 this。看一下我在下面写的 counter 对象:

const counter = {
  count: 0,
  increase() {
    setInterval(function() {
      console.log(++this.count);
    }, 1000);
  }
}
counter.increase();
复制代码

运行上面的代码,会打印 NaN。这是由于 this.count 没有指向 counter 对象。它实际上指向全局对象。

要使此计数器工做,能够用箭头函数重写,下面代码将会正常运行:

const counter = {
  count: 0,
  increase () {
    setInterval (() => {
      console.log (++this.count);
    }, 1000);
  },
};
counter.increase ();
复制代码

类中的 this

类是全部 JavaScript 应用程序中最重要的部分之一。让咱们看看类内部 this 的行为。

一个类一般包含一个 constructor,其中 this 将指向新建立的对象。

可是,在使用方法的状况下,若是该方法以普通函数的形式调用,则 this 也能够指向任何其余值。就像一个方法同样,类也可能没法跟踪接收者。

咱们用类重写上面的 Hero 函数。此类将包含构造函数和 dialogue() 方法。最后,咱们建立此类的实例并调用该 dialogue 方法。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();
复制代码

constructor 中的 this 指向新建立的类实例。batman.dialogue() 调用时,咱们将 dialogue() 做为 batman 接收器的方法调用。

可是,若是咱们存储对 dialogue() 方法的引用,而后将其做为函数调用,则咱们将再次失去方法的接收者,而 this 如今指向 undefined

为何是指向 undefined 呢?这是由于 JavaScript 类内部隐式以严格模式运行。咱们将 say() 做为一个函数调用而没有进行绑定。因此咱们要手动的绑定。

const say = batman.dialogue.bind(batman);
say();
复制代码

固然,咱们也能够在构造函数内部绑定:

class Hero {
  constructor(heroName) {
    this.heroName = heroName
    this.dialogue = this.dialogue.bind(this)
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
复制代码

加餐:手写 call、apply、bind

callapply 的模拟实现大同小异,注意 apply 的参数是一个数组,绑定 this 都采用的是对象调用方法的形式。

Function.prototype.call = function(thisObj) {
  thisObj = thisObj || window
  const funcName = Symbol('func')
  const that = this // func
  thisObj[funcName] = that
  const result = thisObj[funcName](...arguments)
  delete thisObj[funcName]
  return result
}

Function.prototype.apply = function(thisObj) {
  thisObj = thisObj || window
  const funcName = Symbol('func')
  const that = this // func
  const args = arguments[1] || []
  thisObj[funcName] = that
  const result = thisObj[funcName](...[thisObj, ...args])
  delete thisObj[funcName]
  return result
}

Function.prototype.bind = function(thisObj) {
  thisObj = thisObj || window
  const that = this // func
  const outerArgs = [...arguments].slice(1)
  return function(...innerArgs) {
    return that.apply(thisObj, outerArgs.concat(innerArgs))
  }
}
复制代码

最后

往期精彩:

关注公众号能够看更多哦。

感谢阅读,欢迎关注个人公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号能够拉你进讨论群,有任何问题都会回复。

公众号
相关文章
相关标签/搜索