介绍
本文是 JavaScript 高级深入浅出系列的第六篇
,详细介绍了 JS 中的 this 指向
正文
1. 为什么需要 this
在常见的编程语言中,几乎都会有this
关键字(Objective-C中是self
)。但是 JS 中的 this 和常见的面向对象的 this 不同:
- 常见的面向对象的编程语言中:this 通常出现在类的方法中(特别是实例方法中),指的是当前的调用对象
- 但是 JS 中的 this 更加灵活,无论是它出现的位置还是代表的含义
const foo = {
name: "foo",
greeting() {
// 在实际的开发中,想要获取当前对象中的属性,有没有 this 其实都是可以的
// 但是如果不使用 this ,那么在开发中将会非常的不方便
console.log(`Hello, My name is ${this.name}`)
console.log(`Hello, My name is ${foo.name}`)
}
}
2. this 的指向问题
2.1 在全局环境中
// 在大多数情况下,this 都是在类中使用,但是全局其实也可以访问 this
console.log(this)
- 浏览器环境下,全局
this
指向 GlobalObject,也就是window
- Node 环境下,全局
this
指向一个空对象{}
2.2 在函数中
在开发中,我们一般不再全局使用 this,通常是在函数中使用。
- 所有的函数在调用时,都会创建一个函数执行上下文
- 这个上下文中记录着函数的调用栈,AO对象等
- this 也存在与这个上下文中
function foo() {
console.log(this)
}
// 1. 全局调用
foo() // window
const bar = {
name: `bar's name`,
foo,
}
// 2. 放在对象中使用
bar.foo() // bar 本身
// 3. 通过 apply 调用
foo.apply('abc') // abc
同一个函数,调用的方式不同,那么 this 就不同。说明:this 指向和函数的位置是没有关系的,和函数被调用的方式有关系
- 函数在调用时, JS 会给函数一个默认的 this 值
- this 的绑定和函数所处的位置是没有关系的
- this 的绑定和调用方式以及调用的位置有关系
- this 是在运行时绑定的
3. this 的绑定规则
3.1 默认绑定
独立调用函数的时候,绑定规则是默认绑定,独立调用函数,就会指向全局。
独立调用函数我们可以理解为函数没有被绑定在某个对象上调用
function foo1() {
console.log(this)
}
function foo2() {
console.log(this)
foo1()
}
function foo3() {
console.log(this)
foo2()
}
foo3() // 独立调用函数,3个函数的 this 均指向 window
// 案例二
var obj = {
name: `obj's name`,
foo() {
console.log(this)
}
}
var bar = obj.foo
bar() // 独立调用,还是 window
// 案例三
function foo() {
return function() {
console.log(this)
}
}
var obj = {
name: `obj's name`,
foo
}
var bar = obj.foo()
bar() // 独立调用,this 一定会是 window
3.2 隐式绑定
通过某个对象调用的可以触发隐式绑定,隐式绑定,this 指向调用该函数的对象本身。也就是说它的调用位置中,是通过某个对象发起的函数调用
// 案例
const obj = {
name: `obj's name`,
greeting() {
console.log(`my name is ${this.name}`)
}
}
obj.greeting() // 这里的 this 就是 obj 本身
// 案例二
const obj1 = {
name: `obj1's name`,
greeting() {
console.log(`my name is ${this.name}`)
}
}
const obj2 = {
name: `obj2's name`,
greeting: obj1.greeting
}
obj2.greeting() // 这里的 this 绑定的是 obj2
3.3 显式绑定
隐式绑定有一个前提条件:
- 必须在调用的对象内部有一个对函数的引用(比如一个属性)
obj.foo = function() { console.log(this) }
- 如果没有这样的引用,在进行调用时,就无法找到此对象中的函数,也就无法执行此代码
- 正是因为这个引用,间接的将
this
绑定到了调用函数的对象上
如果我们不希望在该对象上有这样的某个属性,但是又希望函数的 this 指向的时这个对象,这个时候需要显式绑定:
- JS 中所有的函数都可以使用
call
、apply
call
和apply
的区别在于,call 后面的参数是单独的,apply 后面的参数是一个数组
- 这两个函数的第一个参数都要求传入一个对象,该对象就是修改函数的 this 所使用的
- call 和 apply 在执行函数时,是可以明确绑定 this,这个绑定规则称之为显式绑定
function foo() {
console.log(this)
}
foo() // 直接调用,触发默认绑定,this => window
const obj = {
name: `obj's name`
}
// 使用 call 方法,第一个参数就修改了此函数内部的 this 指向
foo.call(obj) // { name: `obj's name` } 指向 obj 本身
call 和 apply 的区别
function sum(num1, num2) {
console.log(num1 + num2, this)
}
sum(10, 20) // 30, window
const obj = {
name: `obj'name`,
}
// 除了第一个参数,后面的参数就是传入函数的参数
// 区别在于,call的参数需要一个一个的
sum.call(obj, 10, 20)
// 但是 apply 的参数是一个数组
sum.apply(obj, [10, 20])
bind
call 和 apply 没有返回值
function foo() {
console.log(this)
}
// 多次调用很麻烦
foo.call('aaa')
foo.call('aaa')
foo.call('aaa')
bind
返回一个绑定了 this 的新函数
function foo() {
console.log(this)
}
// 此时 newFoo 就是已经将 this 修改为 "bar" 的新函数了
const newFoo = foo.bind('bar')
newFoo() // "bar"
3.4 new 绑定
JS 中的函数可以当作一个类的构造函数来使用,可以使用new
关键字
使用 new 关键字时,有以下过程:
- 创建一个全新的对象
- 这个新对象会被执行 prototype 连接
- 这个新对象会绑定到函数调用的 this 上(this 的绑定在这个步骤中完成)
- 如果函数没有返回其他对象,表达式会返回这个新对象
new
绑定时,this = 创建的新对象(也就是我们常说的实例对象)
4. 绑定规则的优先级
- 默认规则的优先级最低
- 显式绑定优先级高于隐式绑定
- new 绑定优先级高于隐式绑定
- new 绑定优先级高于 bind
- new 绑定和 call、apply 是不允许同时使用的,因此不存在冲突问题
- new 绑定可以同时用 call,但是 new 高于 call
new > 显式绑定 > 隐式绑定 > 默认绑定
5. 内置函数的 this 分析
定时器
setTimeout/setInterval
setTimeout(function() {
console.log(this) // window
}, 1000)
DOM 事件
const div = document.querySelector('.box')
div.addEventListener('click', function() {
console.log(this) // div 元素本身
})
数组内置函数
const names = ['Alex', 'John', 'Tom']
names.forEach(function(item) {
console.log(item, this) // window
})
// forEach 默认的 this 是 window
// 但是可以传入第二个参数,第二个参数可以修改函数中的值
names.forEach(function(item) {
console.log(item, this) // "aaa"
}, 'aaa')
// map、filter 等数组内置函数 this 都默认是 window,都可以传入第二个参数,修改内部 this 的指向
6. this 的特殊绑定
之前的 4 个规则已经能够应对日常的开发,但是总会有一些语法会跳出规则之外
6.1 忽略显式绑定
function foo() {
console.log(this)
}
foo.apply("aaa") // aaa 没问题
foo.apply(null) // window
foo.apply(undefined) // window
如果显式绑定(apply、call、bind 都是这样)传入指向为null/undefined
,this 将自动被绑定为全局对象
6.2 间接函数引用
const obj1 = {
name: 'obj1',
foo() {
console.log(this)
},
}
const obj2 = {
name: 'obj2',
}
obj2.bar = obj1.foo
obj2.bar() // this 指向 obj2
// 这里记得加一个分号,不然小括号在进行词法分析中将和上文的声明 obj2 视为一个整体
;(obj2.bar = obj1.foo)() // 这里视为一个单独的函数调用,this 指向 window
这种写法最好不要在生产环境中使用
7. 箭头函数中的 this 指向
7.1 什么是箭头函数
箭头函数是 ES6 之后的一种声明函数的方法
- 箭头函数不会绑定 this 、arguments 属性
- 箭头函数不能作为构造函数使用(不能和 new 一起使用,会抛出错误)
箭头函数如何编写:
- () 参数,如果只有一个参数可以省略 ()
- => 箭头
- {} 函数体,如果函数体内只有一行代码,则会视为该行代码的结果为返回值,同时这种情况下可以省略 {}
() => {}
num => { // do something... }
const sum = (num1, num2) => num1 + num2
console.log(sum(10, 20)) // 30
// 箭头函数使用例子:需求:将数组中所有的偶数 * 100后相加
const nums = [2, 12, 24, 35, 48]
const res = nums
.filter(num => num % 2 === 0)
.map(num => num * 100)
.reduce((prev, curr) => prev + curr)
console.log(res)
7.2 箭头函数简写
- 如果只有一个参数,可以省略 (),没有参数不能省略 ()
- 如果只有一行代码,可以省略 {},同时这行代码的计算结果将作为返回值
- 如果返回的是一个对象,还想要省略 {} 的话,需要在外层使用 () 包裹
// 这种写法,引擎不知道 {} 到底是对象字面量还是函数执行体
const foo = () => { name: 'alex', age: 18 }
// 需要这样
const foo = () => ({ name: 'alex', age: 18 })
7.3 箭头函数中的 this 指向
箭头函数不根据上文中的 4 中绑定规则来确定 this 指向,而是根据外层作用域去确定 this
const foo = function() {
console.log(this.message)
}
const obj = {
message: 'alex',
}
// 一个正常的函数,是根据 4 种绑定规则来确定 this 指向的
foo.call(obj) // alex
window['message'] = 'global message'
const bar = () => {
console.log(this.message)
}
const obj = {
message: `obj's message`,
}
// 但是箭头函数是跟着外层作用域的 this 走的
bar.call(obj) // global message
看一个案例
const obj = {
message: `obj's name`,
foo: function() {
return () => {
console.log(this.message)
}
},
}
const obj2 = {
message: `obj2' name`,
}
obj.foo().call(obj2) // 最终打印 obj's name
// 虽然使用 call 显式修改了 this 指向,但是由于箭头函数的 this 跟着外层作用域走,而 foo 函数的 this 是 obj,最终箭头函数的 this 其实也是 obj,而不是 obj2
应用场景
const obj = {
data: [],
fetchData() {
// ES6之前没有箭头函数的解决方案
// var _this = this 。以下文 _this 代指 this,即可访问到 obj
setTimeout(function() {
// 这段代码其实是有问题的
// 因为 setTimeout 的 this 指向是 window
// 所以无法访问到期望的 this 也就是 obj
console.log('fetched Data !!!')
this.data = [1, 2, 3]
this.getData()
}, 3000)
},
getData() {
console.log('data is', this.data)
},
}
obj.fetchData()
// 将代码修改为箭头函数
const obj = {
data: [],
fetchData() {
// 由于箭头函数的 this 指向是外层作用域的 this,因为这里 fetchData 的 this 指向 obj,所以箭头函数定时器回调函数就可以成功访问到 obj
setTimeout(() => {
console.log('fetched Data !!!')
this.data = [1, 2, 3]
this.getData()
}, 3000)
},
getData() {
console.log('data is', this.data)
},
}
obj.fetchData()
8. 面试题 this 指向案例
8.1 第一题
var name = 'window'
var person = {
name: 'person',
sayName: function() {
console.log(this.name)
},
}
function sayName() {
var sss = person.sayName
sss() // window
person.sayName() // person
(person.sayName)() // person
;(b = person.sayName)() // window,上文中说的间接函数引用,这里相当于就是一个单独的函数调用
}
sayName()
这一题还是比较简单的,直接根据 4 种绑定规则去判断就可以,默认绑定和隐式绑定,唯一的一个难点在于(person.sayName)()
,这个可以看作是person.sayName()
,所以是person
8.2 第二题
var name = 'window'
var person1 = {
name: 'person1',
foo1: function() {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function() {
return function() {
console.log(this.name)
}
},
foo4: function() {
return () => {
console.log(this.name)
}
},
}
var person2 = { name: 'person2' }
person1.foo1() // person1,隐式绑定
person1.foo1.call(person2) // person2 显式绑定优先级大于隐式绑定,所以是 person2
person1.foo2() // window 箭头函数不绑定 this,因此继承 person1 的上层作用域就是全局作用域
person1.foo2.call(person2) // window 虽然绑定了 foo2 的 this 给了 person2,但是还是继承了 person2 上层的作用域,也就是全局
person1.foo3()() // window 默认绑定,因为 person1.foo() 返回一个函数,接着就单独调用此函数,所以是 window
person1.foo3.call(person2)() // 还是 window,这个和上面的一样
person1.foo3().call(person2) // person2,这里把返回的函数显式绑定了 this 给 person2
person1.foo4()() // person1,箭头函数不绑定 this,因此继承 foo4 的 this
person1.foo4.call(person2)() // person2,这里把 foo4 的 this 显式绑定给了 person2,因此是 person2
person1.foo4().call(person2) // person1,同是箭头函数不绑定 this
8.3 第三题
这道题一定要注意,和上一题不一样,要好好看解析
var name = 'window'
function Person(name) {
this.name = name
this.foo1 = function() {
console.log(this.name)
}
this.foo2 = () => console.log(this.name)
this.foo3 = function() {
return function() {
console.log(this.name)
}
}
this.foo4 = function() {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.foo1() // person1(隐式绑定)
person1.foo1.call(person2) // person2(显式绑定高于隐式绑定)
person1.foo2() // person1 这里需要说一嘴,虽然 foo2 是一个箭头函数,但是在声明函数的时候使用的是 this.xx 所以上层作用域就找到了 this,也就是实例对象 person1
person1.foo2.call(person2) // person1 箭头函数不绑定 this,继承于 this.foo2 的作用域,即this
person1.foo3()() // window,这里还是 person1.foo3() 返回一个函数,然后紧接着单独调用这个函数,适用于默认绑定
person1.foo3.call(person2)() // window,和上一个一样
person1.foo3().call(person2) // person2,这里是将返回的函数的 this 显式绑定给了 person2
person1.foo4()() // person1,箭头函数不绑定 this,继承上层,也就是 this
person1.foo4.call(person2)() // person2,这里是将 foo4 的作用域绑定给了 person2
person1.foo4().call(person2) // person1,箭头函数不绑定 this,虽然显式绑定了,但是还是继承的是 person1.foo4 的this
8.4 第四题
var name = 'window'
function Person(name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function() {
return function() {
console.log(this.name)
}
},
foo2: function() {
return () => {
console.log(this.name)
}
},
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
person1.obj.foo1()() // window,foo1 函数返回一个函数,紧接着独立调用此函数
person1.obj.foo1.call(person2)() // window,还是独立调用此函数
person1.obj.foo1().call(person2) // person2,这里将返回的函数内部的 this 显式绑定给了 person2
person1.obj.foo2()() // obj,箭头函数不绑定 this,将继承 foo2 的 this 也就是 obj
person1.obj.foo2.call(person2)() // person2,这里将 foo2 的 this 显式绑定给了 person2
person1.obj.foo2().call(person2) // obj,同箭头函数不绑定 this,将继承 foo2 也就是 obj
总结
本文中,你学习到了 5 个知识点:
- 为什么需要 this:提高开发效率
- this 的指向问题:this 在运行时动态绑定
- this 的绑定规则:4 种绑定规则,但是箭头函数并不适用于这四种,而是继承于外层作用域的 this
- 绑定规则的优先级:new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
- this 的特殊绑定:某种情况下(开发中极少极少会用到),this 的绑定时很特殊的