本文主要介绍
JavaScript
中this
的问题,这是每个学习JavaScript
都绕不开研究的问题 ,而且有时候会越学越迷糊,本文从实用的角度出发来理解this
,因为要彻底的弄懂this并不是一件容易的事,这涉及到ECMAScript标准规范
,浏览器就是一局这套规范来解析this
的指向的,学完本篇文章可以应付实际开发中80%的场景,要想彻底弄懂this
可以参考此篇文章JavaScript深入之从ECMAScript规范解读this。
作用域
在学习 this
之前先来学习下 JavaScript
中的作用域,因为 this
的理解机制与作用域刚好相反,理解了作用域的机制能更好的理解this
的精髓。几乎每个编程语言都有作用域
的概念,作用域又分为 静态作用域
和 动态作用域
,在 JavaScript
中作用域是静态作用域
。在 ES6
以前,唯一产生作用域的方法就是 function
,每一个 function
都有自己的作用域,在作用域外面你就存取不到这个 function
內部所定义的变量。然而 ES6
的时候引入了 let
和 const
关键字,多了 block
的作用域。
var a = 100
function echo() {
console.log(a) // 100 or 200?
}
function test() {
var a = 200
echo()
}
test()
上面的例子输出什么?只要迟疑了,说明还没有理解透彻。所谓 静态作用域
是指函数在定义的时候就决定了函数的作用域,与之后函数如何调用没有关系了,总结为一句话就是:函数在定义的时候就已经确定好了作用域!所以不管 echo
是在哪里调用的,其外层作用域就是全局作用域而不是 test
函数的作用域,由于 echo
函数内部没有 a
这个变量,所以会向外层作用域进行寻找,此时就找到了全局作用域下的 a = 100
这个变量,打印出来就是 100
。
经典的例子
再来看一个经典的例子,上面我们说过,在 ES6
以前,唯一产生作用域的方法就是 function
,每一个 function
都有自己的作用域,其他方式是无法产生作用域的。
var btn = document.querySelectorAll('button')
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
假设有五个按钮,上面的代码当我们点击每个按钮时会显示对应按钮的序号吗? 答案是全部显示 5
,是不是感觉狠惊讶?下面我们来分析。
分析
由于只有函数可以产生作用域,而由于传入的函数内没有定义 i
这个变量,所以当点击按钮的时候会去其外层作用域寻找,而此时由于 for
循环已经执行完毕,此时的i的值已经是 5
了,所以无论点击哪个按钮都会显示 5
。
改如何才能显示对应的序号呢?
其实解决办法有二个,一个是让传入的函数自己的作用域内能有对应的 i
,另外一个是假如传入的函数的作用域内没有对应的 i
,在它向外层作用域寻找时能找到对应的 i
。
解决方案一
要让函数内部的作用域内能有对应变量,首先想到的一种方式就是把变量当做参数传入即可。
function getAlert(num) {
return function() {
alert(num)
}
}
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', getAlert(i))
}
或者:
function getAlert(num) {
return function() {
alert(num)
}
}
for(var i=0; i<=4; i++) {
btn[i].addEventListener('click', getAlert(i))
}
解决方案二
要想去外层作用域内找到不同的变量,那就得每个函数的外层作用域都不能一样,而不是都去全局的作用域或者同一个作用域下进行寻找。
for(let i=0; i<=4; i++) {
btn[i].addEventListener('click', function() {
alert(i)
})
}
发现了吗,只是将关键字 var
改成了 let
,因为 ES6
的 let
关键字会生成块作用域
也就是代码块,所以建议开发中全部采用let
关键字,上面的代码可以看成这样的组织形式:
{ // 块级作用域
let i=0
btn[i].addEventListener('click', function() {
alert(i)
})
}
{ // 块级作用域
let i=1
btn[i].addEventListener('click', function() {
alert(i)
})
}
...
上面的我们简要的阐述了作用域的概念,我们始终要记住最核心的东西就是:
- 函数在定义的时候就已经确定好了作用域!
-
let
和const
关键字,多了block
的作用域。
this的理解
现在我们正式切入 this
这个老生常谈的问题,首先请记住:this
的具体指向要看这个函数是怎么调用的。这和作用域的机制是相反的,作用域是在函数定义的时候就确定了,与函数的调用是无关的,而 this
且是在函数的调用时才决定的,不同的调用会导致 this
的指向是不一样的,这就有点动态的概念。
var x = 10
var obj = {
x: 20,
fn: function() {
var test = function() {
console.log(this.x)
}
test()
}
}
obj.fn()
上面这个应该输出多少?在讲解之前请容我再次强调一句:this
的具体指向要看这个函数是怎么调用的。最后的 this
是在 test
中显示的,test
最后是谁在调用,这里我们可以看成window
在调用(非严格模式下)也就是 window.test()
,所以会找到全局作用域下的 x=10
。
关于call,apply和bind
这几个函数都可以改变覆盖 this
的指向从而更改 this
的指向,call
和 apply
基本上相同,只是传参的方式不太一样,apply
接受的是一个数组。
class Car {
hello() {
console.log(this)
}
}
const myCar = new Car()
myCar.hello() // myCar instance
myCar.hello.call('yaaaa') // yaaaa
myCar.hello.apply('yaaaa') // yaaaa
bind
是返回一个新的函数:
'use strict';
function hello() {
console.log(this)
}
const myHello = hello.bind('my')
myHello() // my
可以发下 call
,apply
和 bind
都可以改变 this
的指向,需要注意的是一旦用 bind
绑定 this
后,在使用 call
来想改变 this
的指向是不行的。
'use strict';
function hello() {
console.log(this)
}
const myHello = hello.bind('my')
myHello.call('call') // my
用call来确定this
只要把函数的调用转成 call
的方式调用,就能狠容易的看出 this
指向的是什么。
const obj = {
value: 1,
hello: function() {
console.log(this.value)
},
inner: {
value: 2,
hello: function() {
console.log(this.value)
}
}
}
const obj2 = obj.inner
const hello = obj.inner.hello
obj.inner.hello()
obj2.hello()
hello()
上面的函数调用在转化成 call
的方式后如下:
obj.inner.hello() // obj.inner.hello.call(obj.inner) => 2
obj2.hello() // obj2.hello.call(obj2) => 2
hello() // hello.call() => undefined
箭头函数中的this
箭头函数中的 this
则是遵循"在定义它的地方的 this
是什么,它的 this
就是什么"。直白点就是箭头函数在哪个作用域定义的,那个作用域的 this
是什么,箭头函数中的 this
就是什么,当然作用域中的 this
也要结合上面的原则进行判断。
const obj = {
x: 1,
hello: function(){
// 这里的this是什么,test的this就是什么
// 就是我说的:
// 在定义它的地方的this是什么,test的this就是什么
console.log(this)
const test = () => {
console.log(this.x)
}
test()
}
}
obj.hello() // 1
const hello = obj.hello
hello() // undefined
上面的例子可以看出,不用方式调用 hello
会导致 console.log(this)
这句代码的作用域中 this
的指向不用,进而导致箭头函数中的 this
指向也不一样。
总结
- 作用域是在函数定义的时候就确定了,与函数的调用是无关的。
-
this
的具体指向要看这个函数是怎么调用的 - 用call的方式转化函数的调用比较好理解
this
指向。 - 箭头函数中的
this
是由定义它的地方的作用域下的this
决定的。