重学JavaScript[3] this
this 是在函数运行时进行绑定的,它的上下文取决于函数调用时的各种条件
this 的指向与函数书写位置无关,而与调用位置有关
# 全局的 this
# 浏览器环境
在浏览器环境下,this 指向Window
对象:
console.log(this)
function fn() {
console.log(this)
}
fn()
这两段代码会得到同样的结果:Window
对象,也就是全局对象globalThis
# Node 环境
在 Node 环境下,全局对象是global
,但直接打印 this 会得到一个{}
,实际上指向的是module.exports
对象
// ...
console.log(this === module.exports)
// true
function fn() {
console.log(this === global)
}
fn()
// true
通过函数打印出的 this,指向global
对象
# 绑定规则
# 默认绑定
上一篇中提到过使用call
和apply
等来绑定函数以改变 this 指向
现在,我们不使用任何“绑定”的行为
this.num = 100
function fn() {
console.log(this.num)
}
fn()
// 100
不执行任何绑定行为时,this 会绑定到当前对象(在段代码里是全局对象)
请注意:如果使用严格模式,全局对象将无法使用默认绑定:
this.num = 100
function fn() {
'use strict'
console.log(this.num)
}
fn()
// TypeError报错
# 隐式绑定
考虑以下代码:
function fn() {
console.log(this.num)
}
this.num = 200
let obj = {
num: 100,
fn: fn,
}
fn()
obj.fn()
// 200
// 100
函数 fn 被单独写出来(好像不属于 obj 对象),在对象内被引用为一个属性,当通过obj.fn()
调用时,函数的 this 会根据隐式规则绑定到引用函数的上下文对象,也就是obj
如果有多个对象直接引用,形成了对象属性引用链时,只有“最接近”调用的一层会影响其调用位置:
function fn() {
console.log(this.num)
}
let child = {
num: 100,
fn: fn,
}
let father = {
num: 200,
child: child,
}
father.child.fn()
// 100
# 丢失
在某些时候,隐式绑定会丢失,这时候自动执行默认绑定规则,考虑以下代码:
function fn() {
console.log(this.a)
}
function doFn(f) {
f()
}
this.a = 'global'
let obj = {
a: 'obj',
fn: fn,
}
doFn(obj.fn)
// global
可以理解为:以传参方式传入的函数,其 this 也要根据实际调用情况来判定(不过往往你不知道类似doFn
这样的函数的实现时要尤其小心,它很有可能改变调用时的 this)
这一点如果放在实际应用里就很好理解,调用内置的setTimeout
函数:
function fn() {
console.log(this.a)
}
this.a = 'global'
let obj = {
a: 'obj',
fn: fn,
}
setTimeout(obj.fn, 100)
// global
# 显式绑定
在上一篇我们已经介绍过显式绑定的方法
在调用call
等函数时,需要传进一个thisArg
参数作为绑定的对象,通常我们会传递一个类似上面提到的obj
这样的复杂对象,但如果传递一个字符串、数字,或者布尔类型呢?
尝试以下代码:
function fn() {
console.log(this)
}
fn.call('hello')
在浏览器环境运行,你会在控制台得到一个String
类型的对象,而非'hello'
字符串本身
再传入一个数字试试:
function fn() {
console.log(this)
}
fn.call('hello')
也会得到类似的结果
打印出的对象形式说明:在这个过程中会对传入的原始值(基本数据类型)装箱,等价于:new String('hello')
和new Number(100)
# new 绑定
提到 new,不得不想起上一篇中介绍的“构造器函数”,事实上 JavaScript 并不存在什么“构造器函数”,任何函数都可以被直接执行,也可以在调用前面加上new
关键字(这样的调用方式被称为“构造器调用”)
比如这个函数:
function Person() {
this.name = '王昭君'
}
let person1 = new Person()
let person2 = Person()
console.log(person1)
console.log(person2)
第一次打印会得到一个{name: '王昭君'}
的对象
第二次打印会得到undefined
,这样不奇怪,因为 Person 函数什么也没返回
当一个函数被“构造器调用时”,会执行这些操作:
- 创建一个全新的对象
- 对这个新对象进行“原型”连接
- 这个新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式会自动返回这个新对象
我们主要关心第三步操作,至于其他步会在以后提到
执行构造器调用时,将函数的 this 绑定到新对象,称之为 new 绑定
# 绑定优先级
四种绑定优先级的关系:
默认 < 隐式 < 显式 < new
在判断时遵循优先级从大到小的顺序,如果发现有较大优先级存在,则根据较大优先级规则判断 this 指向
# 例外
如果你尝试了显式绑定中给出的例子,会发现如果传入的参数是null
或者undefined
,代码会打印出全局对象(在浏览器环境下是Window
对象,在 node 环境是global
对象)
function fn() {
console.log(this)
}
fn.call('hello')
实际上这里应用的是默认绑定规则,不过正常情况下都不会去传入这两个看起来就“不正常”的值作为thisArgs
在函数柯里化(后面的文章也许会提到)操作中,调用bind
时需要传入一个参数作为“占位”,如果这时候传入了这两个“不正常”的值,就有可能造成修改全局对象等不可预计的后果
# 箭头函数
在上一篇中我们提到了函数的几种书写方式
其中箭头函数并不是由function
关键字来定义的,而是使用了 lambda 表达式来定义
箭头函数并不遵循 this 的四种绑定规则,而是根据外层作用域来决定 this
考虑以下代码:
let obj = {
name: 'obj',
getName1: () => {
console.log(this.name)
},
getName2() {
this.getName1()
},
getName3() {
console.log(this.name)
},
}
this.name = 'global'
obj.getName1()
obj.getName2()
obj.getName3()
// global
// global
// obj
箭头函数在 this 绑定上也带来了很多优势,比如下面这段 React 代码:
class App extends React.Component {
constructor() {
this.state = {start: false}
}
handleClick() {
this.setState({start: true})
}
render() {
return <Button onClick={ this.handleClick.bind(this) }>开始<Button/>
}
}
为确保事件处理函数在调用时总能得到正确的 this,在书写时要手动绑定 this
React 官网提供的示例是:
<Button onClick={() => this.handleClick()}>开始<button />
如果使用箭头函数,可以省去这一过程:
class App extends React.Component {
constructor() {
this.state = {start: false}
}
handleClick = () => {
this.setState({start: true})
}
render() {
return <Button onClick={ this.handleClick }>开始<Button/>
}
}
# 手写 XX
在面试中经常会考察 this 绑定相关的知识,通常会要求手写call
、apply
、bind
等函数,这些内容我打算在单独的一篇文章中总结