JavaScript中的this是一个对于新手来说特别吓人并且不友好的概念。对于一个前端萌新来说,这个概念既模糊,又看不到它存在的意义。本文将深度解析JavaScript中的this,并且逐一分析他在每个常见场景中的意义和指向。
目录
基本篇: 什么是this?
构造函数中的this
Call, Apply, 和 Bind改变this
箭头函数继承this
简而言之, this代表存在于函数的一个对象,而这个对象就是调用这个函数的那个对象。
假设我们有一个对象叫做counter, 其中有两个函数,分别是increment和log。 当你调用counter中的函数的时候,你就可以获得函数中的this对象。
首先我们先试用log函数,来看看这里面的this到底是什么。
let counter = {
count: 0,
increment() {
return this.count
},
log() {
console.log(this)
}
}
counter.log()//{count: 0, increment: ƒ, log: ƒ}
counter.log()的结果显示,log函数的this就是counter这个object,也就是log函数的调用者。
counter.increment()//0
以此类推,我们可以得出increment的结果是0,也就是对象中count的值。
但现在你也许会问,那如果我们在全局环境下不通过一个对象,直接调用函数,他的this回事什么呢?请看这个例子:
function printThis() {
console.log(this)
}
printThis()//Window
在全局环境下直接调用函数,this的指向将会是Window对象自身。但这并不是一个固定的情况。如果你在函数内或者他的全局环境内写明了 "use strict" 关键词,那么这个函数将在严格模式下执行,而此时,在window环境下直接调用函数将不再返回Window, 而是undefined
function printThis() {
"use strict"
console.log(this)
}
printThis()//undefined
你可能会问,什么是严格模式?简而言之,严格模式的存在是为了写更严谨的代码,从而减少bug,并为未来JS的版本做铺垫。 在我们的案例中,严格模式会改变this的指向,将无调用者的函数的this从Window变成undefined。
但是尽管在严格模式下的无调用者的函数中的this不会默认指向Window,如果你直接在全局作用域打印this,this仍然会指向Window。
"use strict"
let x = this
console.log(x)//Window
接着我们之前的话题,我们接着使用之前的counter对象,我们再声明一个新的函数 newLog, 而这个newLog的值就是counter中的log方法:
let counter = {
count: 0,
increment() {
console.log(this.count)
},
log() {
console.log(this)
}
}
let newLog = counter.log
当我们调用newLog的时候,你也许会认为,调用newLog的结果也会是counter对象,但是:
counter.log()//Window
newLog()//undefined
newLog的this并不是counter对象,而是Window,因为counter并没有亲自调用newLog。即使newLog和counter中的log的内容完全一样,他们指向的this并不一定相同。
如果你在一个html标签中定义this,那么这个this将会指向这个html标签,你也可以通过getAttribute来获得这个标签上的属性
i am a box
在构造函数中,this指向的是构造函数的new出来的实例对象。在接下来的例子中,我们有一个构造函数Person,我们在这个构造函数的原型上添加一个函数sayHi,然后我们我们构造一个他的实例对象newPerson,并调用sayHi来看看他的this指向哪里
function Person(name, age) {
this.name = name,
this.age = age
}
Person.prototype.sayHi = function() {
console.log(this, this.name, this.age)
}
let newPerson = new Person('tom', 30)
newPerson.sayHi()// {name: 'tom', age: 30} 'tom' 30
正如我们刚才所讲,这里newPerson的this并不指向刚刚的构造函数Person,而是实例对象本身。同理,class中的this也将指向实例对象
class User {
constructor(name){
this.name = name
}
sayName() {
console.log(this.name)
}
}
let user = new User('playerone')
user.sayName()//playerone
如果你接触JS的时间不长,你也许对这三个函数很陌生。但他们的基本概念和用法其实特别简单,并且他们三个本质上做同一件事并且用法类似,就是修改一个函数的this指向。他们的语法也不难,都是函数自带的原型方法:
function.apply(thisArg, argsArray)
function.call(thisArg, arg1, /* …, */ argN)
function.bind(thisArg,[, arg1[, arg2[, ...]]]) ==>(这种以[]嵌套的写法经常出现,代表该参数是选填的)
他们三个第一个参数都是一个对象,而这个对象将变成这个函数的新的this。后面跟着的是该函数的参数。唯一的区别是他们传参数的方式;apply会把所有函数的参数作为一个数组传入。call会以逗号隔开,分别作为多个参数传入。bind的情况稍微特殊一些,。最后,call和apply的使用会直接调用该函数,而bind不会。
let obj = {
username: 'lucy',
age: 30,
hobby: 'running'
}
function logAge() {
console.log(`${this.username} is ${this.age} years old`)
}
function logInfo(a, b) {
console.log(a, b)
}
function addArgs(a, b, c) {
console.log("a: " + a)
console.log("b: " + b)
console.log("c: " + c)
return a + b + c
}
// logAge() // undefined is undefined years old
// logAge.call(obj) // lucy is 30 years old
// logAge.apply(obj) // lucy is 30 years old
// logAge.bind(obj)() // lucy is 30 years old
// 后面的小括号()用来调用函数,因为bind并不会立刻调用函数
logInfo.apply(null, [20, 30]) // 20 30
logInfo.call(null, 20, 30) // 20 30
let newAddArgs = addArgs.bind(null, 30, 'haha', [1,2,3])
newAddArgs() // a: 30, b: 'haha', c: [1,2,3]
//因为bind中的参数会为该函数设置预设参数,即使你自己再传参也不会有任何效果
newAddArgs('bb', 'cc') // a: 30, b: 'haha', c: [1,2,3]
箭头函数是一个ES6的主要特性之一,虽然我们在代码中经常遇到他,但你知道很多时候他存在的真正意义并不是为了更加简洁的语法,而是因为他独特的this指向。箭头函数自身并没有this对象。我们在开头讲过,函数中this一般指向他的调用者,而箭头函数的this, 它只会从自己作用域上一层继承this。也就是说,它的this和定义自己的“主人”的this相同。如果上一侧也没有,就继续往上一层去找,直到找到Window。call和apply也对箭头函数无效,声明严格模式也没有影响。
在下面这段代码中,箭头函数的this和普通函数的指向并不相同
let obj = {
name: 'kevin',
printA: function () {
console.log(this)
},
printB: () => {
console.log(this)
}
}
obj.printA()//{name: 'kevin', printA: ƒ, printB: ƒ}
obj.printB()//Window
在第二种情况中,箭头函数定义在另一个函数内,那么他将继承定义他的这个函数的this,也就是obj2这个对象
let obj2 = {
name: 'john',
printThis() {
console.log(this)
},
parentFunc() {
const arrowPrint = () => [
console.log(this)
]
printThis() // Window
arrowPrint() // {name: 'john', printThis: ƒ, parentFunc: ƒ}
console.log(this) // {name: 'john', printThis: ƒ, parentFunc: ƒ}
}
}
obj2.parentFunc()
第三种情况,如果我们分别把两个匿名的普通函数和箭头函数绑定给两个按钮,并且在全局作用域下定义另外一个箭头函数并在回调函数中调用它。那么, 不论如何,全局下的箭头函数的this都会指向window,因为箭头函数是在Window下定义的:定义它的对象。并且,你可以发现,普通函数如果直接作为元素的回调函数,那么他的this会指向该元素。
但是,当你在普通函数中加入严格模式,那么他的this指向将在匿名回调中变成undefined,正如我们之前所说,普通函数的this指向他的调用者。
function logThis() {
"use strict"
console.log(this)
}
btn1.addEventListener('click', function () {
logThis() // undefined
})
btn2.addEventListener('click', logThis) //
以上就是JavaScript中的this在各种常见情况下的行为表现。下面这段W3Schools的话可以比较好地总结我们所讲的内容:
In an object method, this refers to the object. 在对象方式中,this指向该对象 |
Alone, this refers to the global object. 自己被调用的话,指向全局对象 |
In a function, this refers to the global object. 在函数中,this也指向全局对象 |
In a function, in strict mode, this is undefined . 在函数中的严格模式下,指向undefined |
In an event, this refers to the element that received the event. 在事件中,指向触发事件的元素 |
Methods like call(), apply(), bind()可以把this重定向为任何对象 |