this
是一个令无数 JavaScript 编程者又爱又恨的知识点。它的重要性毋庸置疑,然而真正想掌握它却并非易事。希望本文可以帮助大家理解 this
。
JavaScript 引擎在查找 this
时不会通过原型链一层一层的查找,因为 this
完全是在函数调用时才可以确定的,让我们来看下面几种函数调用的形式。
普通的函数调用,这是我们使用较多的一种, foo
是以单独的变量出现而不是属性。其中的 this
指向全局对象。
function foo() {
console.log(this)
}
foo() // Window
函数作为对象的方法调用,会通过 obj.func
或者 obj[func]
的形式调用。其中的 this
指向调用它的对象。
const obj = {
name: 'lxfriday',
getName(){
console.log(this.name)
}
}
obj.getName() // lxfriday
通过 new Constructor()
的形式调用,其 this
会指向新生成的对象。
function Person(name){
this.name = name
}
const person = new Person('lxfriday')
console.log(person.name) // lxfriday
通过 foo.apply(thisObj)
或者 foo.call(thisObj)
的形式调用,其中的 this
指向 thisObj
。如果 thisObj
是 null
或者 undefined
,其中的 this
会指向全局上下文 Window
(在浏览器中)。
掌握以上的几种函数调用形式就基本可以覆盖开发中遇到的常见问题了,下面我翻译了一篇文章,帮助你更深入的理解 this
。
本文接下来的内容翻译自 https://blog.bitsrc.io/what-is-this-in-javascript-3b03480514a7,作者 Rajat S,内容有删改,标题有改动。
如果你已经使用过一些 JavaScript 库,你一定会注意到一个特殊的关键字 this
。
this
在 JavaScript 中很常见,但是有很多开发人员花了很多时间来完全理解 this
关键字的确切功能以及在代码中何处使用。
在这篇文章中,我将帮助您深入了解 this
其机制。
在深入了解之前,请确保已在系统上安装了 Node 。然后,打开命令终端并运行 node 命令。
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
。
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 中运行将不会出现我们预期的 Superman
和 Clark Kent
,我们将得到 undefined
。
这背后的原因是由于该函数不是以严格模式编写的,所以 this
引用了全局对象。
如果我们在严格模式下运行这段代码,会因为 JavaScript 不允许给 undefined
增加属性而出现错误。这实际上是一件好事,因为它阻止我们创建全局变量。
最后,以大写形式编写函数的名称意味着我们需要使用 new
运算符将其作为构造函数来调用。将上面的代码片段的最后两行替换为:
const superman = new Hero("Superman", "Clark Kent");
console.log(superman);
再次运行 node index.js
命令,您现在将获得预期的输出。
Constructor Pattern
JavaScript 没有任何特殊的构造函数。我们所能做的就是使用 new
运算符将函数调用转换为构造函数调用,如上一节所示。
进行构造函数调用时,将创建一个新对象并将其设置为函数的 this
参数。然后,从函数隐式返回该对象,除非我们有另一个要显式返回的对象。
在 hero
函数内部编写以下 return
语句:
return {
heroName: "Batman",
realName: "Bruce Wayne",
};
如果现在运行 node
命令,我们将看到 return
语句将覆盖构造函数调用。
当 return
语句尝试返回不是对象的任何东西时,将隐式返回 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
绑定到特定对象来解决此问题。
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
或者 undefined
给 call
、 apply
作为上下文,将会导致 this
指向全局对象。
function dialogue() {
console.log('this', this)
}
const hero = {
heroName: 'Batman',
}
console.log(dialogue.call(null))
上述代码,在严格模式下输出 null
,非严格模式下输出全局对象。
当我们将一个方法作为回调传递给另一个函数时,始终存在丢失该方法的预期接收者的风险,导致将 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
。
箭头函数和普通函数有很大的不同,引用阮一峰 ES6入门第六章中的介绍:
函数体内的 this
对象,就是定义时所在的对象,而不是使用时所在的对象;
不可以当作构造函数,也就是说,不可以使用 new
命令,否则会抛出一个错误;
不可以使用 arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest
参数代替;
不可以使用 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 ();
类是所有 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
的模拟实现大同小异,注意 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))
}
}
往期精彩:
前端面试必会 | 一文读懂现代 JavaScript 中的变量提升 - let、const 和 var
前端面试必会 | 一文读懂 JavaScript 中的闭包
前端面试必会 | 一文读懂 JavaScript 中的作用域和作用域链
前端面试必会 | 一文读懂 JavaScript 中的执行上下文
InterpObserver 和懒加载
初探浏览器渲染原理
CSS 盒模型、布局和包含块
详细解读 CSS 选择器优先级
关注公众号可以看更多哦。
感谢阅读,欢迎关注我的公众号 云影 sky,带你解读前端技术,掌握最本质的技能。关注公众号可以拉你进讨论群,有任何问题都会回复。
公众号 交流群