一、函数的调用
全局环境的this指向全局对象,在浏览器中就是我们熟知的window对象
说到this
的种种情况,就离不开函数的调用,一般我们调用函数,无外乎以下四种方式:
- 普通调用,例如
foo()
。 - 作为对象方法调用,例如
obj.foo()
。 - 构造函数调用,例如
new foo()
。 - 使用
call
、apply
、bind
等方法。
除箭头函数外的其他函数被调用时,会在其词法环境上绑定this
的值,我们可以通过一些方法来指定this
的值。
- 使用
call
、apply
、bind
等方法来显式指定this
的值。function foo() { console.log(this.a) } foo.call({a: 1}) // 输出: 1 foo.apply({a: 2}) // 输出: 2 // bind方法返回一个函数,需要手动进行调用 foo.bind({a: 3})() // 输出: 3 复制代码
- 当函数作为对象的方法调用时,
this
的值将被隐式指定为这个对象。let obj = { a: 4, foo: function() { console.log(this.a) } } obj.foo() // 输出: 4 复制代码
- 当函数配合
new
操作符作为构造函数调用时,this
的值将被隐式指定新构造出来的对象。
二、ECMAScript规范解读this
上面讲了几种比较容易记忆和理解this
的情况,我们来根据ECMAScript规范来简单分析一下,这里只说重点,一些规范内具体的实现就不讲了,反而容易混淆。
其实当我们调用函数时,内部是调用函数的一个内置[[Call]](thisArgument, argumentsList)
方法,此方法接收两个参数,第一个参数提供this
的绑定值,第二个参数就是函数的参数列表。
ECMAScript规范: 严格模式时,函数内的
this
绑定严格指向传入的thisArgument
。非严格模式时,若传入的thisArgument
不为undefined
或null
时,函数内的this
绑定指向传入的thisArgument
;为undefined
或null
时,函数内的this
绑定指向全局的this
。
所以第一点中讲的三种情况都是显式或隐式的传入了thisArgument
来作为this
的绑定值。我们来用伪代码模拟一下:
function foo() {
console.log(this.a)
}
/* -------显式指定this------- */
foo.call({a: 1})
foo.apply({a: 1})
foo.bind({a: 1})()
// 内部均执行
foo[[Call]]({a: 1})
/* -------函数构造调用------- */
new foo()
// 内部执行
let obj = {}
obj.__proto__ = foo.prototype
foo[[Call]](obj)
// 最后将这个obj返回,关于构造函数的详细内容可翻阅我之前关于原型和原型链的文章
/* -------作为对象方法调用------- */
let obj = {
a: 4,
foo: function() {
console.log(this.a)
}
}
obj.foo()
// 内部执行
foo[[Call]]({
a: 1,
foo: Function foo
})
复制代码
那么当函数普通调用时,thisArgument
的值并没有传入,即为undefined
,根据上面的ECMAScript规范,若非严格模式,函数内this
指向全局this
,在浏览器内就是window。
伪代码模拟:
window.a = 10
function foo() {
console.log(this.a)
}
foo() // 输出: 10
foo.call(undefined) // 输出: 10
// 内部均执行
foo[[Call]](undefined) // 非严格模式,this指向全局对象
foo.call(null) // 输出: 10
// 内部执行
foo[[Call]](null) // 非严格模式,this指向全局对象
复制代码
根据上面的ECMAScript规范,严格模式下,函数内的this
绑定严格指向传入的thisArgument
。所以有以下表现。
function foo() {
'use strict'
console.log(this)
}
foo() // 输出:undefined
foo.call(null) // 输出:null
复制代码
需要注意的是,这里所说的严格模式是函数被创建时是否为严格模式,并非函数被调用时是否为严格模式:
window.a = 10
function foo() {
console.log(this.a)
}
function bar() {
'use strict'
foo()
}
bar() // 输出:10
复制代码
三、箭头函数中的this
ES6新增的箭头函数在被调用时不会绑定this
,所以它需要去词法环境链上寻找this
。
function foo() {
return () => {
console.log(this)
}
}
const arrowFn1 = foo()
arrowFn1() // 输出:window
// 箭头函数没有this绑定,往外层词法环境寻找
// 在foo的词法环境上找到this绑定,指向全局对象window
// 在foo的词法环境上找到,并非是在全局找到的
const arrowFn2 = foo.call({a: 1})
arrowFn2() // 输出 {a: 1}
复制代码
切记,箭头函数中不会绑定this
,由于JS采用词法作用域,所以箭头函数中的this
只取决于其定义时的环境。
window.a = 10
const foo = () => {
console.log(this.a)
}
foo.call({a: 20}) // 输出: 10
let obj = {
a: 20,
foo: foo
}
obj.foo() // 输出: 10
function bar() {
foo()
}
bar.call({a: 20}) // 输出: 10
复制代码
四、回调函数中的this
当函数作为回调函数时会产生一些怪异的现象:
window.a = 10
let obj = {
a: 20,
foo: function() {
console.log(this.a)
}
}
setTimeout(obj.foo, 0) // 输出: 10
复制代码
我觉得这么解释比较好理解:obj.foo
作为回调函数,我们其实在传递函数的具体值,而并非函数名,也就是说回调函数会记录传入的函数的函数体,达到触发条件后进行执行,伪代码如下:
setTimeout(obj.foo, 0)
//等同于,先将传入回调函数记录下来
let callback = obj.foo
// 达到触发条件后执行回调
callback()
// 所以foo函数并非作为对象方法调用,而是作为函数普通调用
复制代码
要想避免这种情况,有三种方法,第一种方法是使用bind
返回的指定好this
绑定的函数作为回调函数传入:
setTimeout(obj.foo.bind({a: 20}), 0) // 输出: 20
复制代码
第二种方法是储存我们想要的this值,就是常见的,具体命名视个人习惯而定。
let _this = this
let self = this
let me = this
复制代码
第三种方法就是使用箭头函数
window.a = 10
function foo() {
return () => {
console.log(this.a)
}
}
const arrowFn = foo.call({a: 20})
arrowFn() // 输出:20
setTimeout(arrowFn, 0) // 输出:20
复制代码
五、总结
- 箭头函数中没有
this
绑定,this
的值取决于其创建时所在词法环境链中最近的this
绑定 - 非严格模式下,函数普通调用,
this
指向全局对象 - 严格模式下,函数普通调用,
this
为undefined
- 函数作为对象方法调用,
this
指向该对象 - 函数作为构造函数配合
new
调用,this
指向构造出的新对象 - 非严格模式下,函数通过
call
、apply
、bind
等间接调用,this
指向传入的第一个参数这里注意两点:
bind
返回一个函数,需要手动调用,call
、apply
会自动调用- 传入的第一个参数若为
undefined
或null
,this
指向全局对象
- 严格模式下函数通过
call
、apply
、bind
等间接调用,this
严格指向传入的第一个参数
有时候文字的表述是苍白无力的,真正理解之后会发现:this
不过如此。
六、小练习
例子来自南波的JavaScript之例题中彻底理解this
// 例1
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1() // ?
person1.show1.call(person2) // ?
person1.show2() // ?
person1.show2.call(person2) // ?
person1.show3()() // ?
person1.show3().call(person2) // ?
person1.show3.call(person2)() // ?
person1.show4()() // ?
person1.show4().call(person2) // ?
person1.show4.call(person2)() // ?
复制代码
选中下方查看答案:
person1 // 函数作为对象方法调用,this指向对象
person2 // 使用call间接调用函数,this指向传入的person2
window // 箭头函数无this绑定,在全局环境找到this,指向window
window // 间接调用改变this指向对箭头函数无效
window // person1.show3()返回普通函数,相当于普通函数调用,this指向window
person2 // 使用call间接调用函数,this指向传入的person2
window // person1.show3.call(person2)仍然返回普通函数
person1 // person1.show4调用对象方法,this指向person1,返回箭头函数,this在person1.show4调用时的词法环境中找到,指向person1
person1 // 间接调用改变this指向对箭头函数无效
person2 // 改变了person1.show4调用时this的指向,所以返回的箭头函数的内this解析改变
系列文章
欢迎前往阅读系列文章,如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
菜鸟一枚,如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误,与大家共同进步。