this 指向问题
与其他语言相比,函数的 this 关键字在 JavaScript 中的表现略有不同。实际上,this 就是一个指针,指向调用函数的对象。 然而,要搞懂 this 指向问题,远远没这么简单。
this 绑定规则
为了弄清楚真正的指向,我们需要理解 this 的 4 个绑定规则:默认绑定、隐式绑定、显式绑定、new 绑定。
默认绑定
默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用,如 doSomething()
:
function sayHi() {
console.log(this.name)
}
var name = 'Global'
sayHi() // Global
sayHi()
应用了默认绑定,this 指向全局对象(非严格模式),全局对象的 name
属性为 Global
。
WARNING
在严格模式或 node 环境中,this 指向 undefined
,此时会报错。
隐式绑定
隐式绑定,调用位置上存在上下文对象,或者被某个对象拥有或者包含,表现形式为 person.doSomething()
:
function sayHi() {
console.log(this.name)
}
var person = {
name: 'Achilles',
sayHi: sayHi,
}
var name = 'Global'
person.sayHi() // Achilles
此时使用 person
上下文来引用函数,因此可以说函数被调用时 person
对象“拥有”或者“包含”它。隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象,在这个例子里,this 指向了person
。
有一点需要注意,对象属性链中只有最后一层会影响到调用位置:
function sayHi() {
console.log(this.name)
}
var person2 = {
name: 'Patroclus',
sayHi: sayHi,
}
var person1 = {
name: 'Achilles',
friend: person2,
}
person1.friend.sayHi() // Patroclus
即使像 XX...XX.sayHi()
有很多层引用,影响调用位置的只有最后一层。
另一个需要注意的点是 隐式丢失。隐式绑定的函数会丢失绑定对象,也就是说会应用默认绑定,从而把 this 绑定到全局对象或者 undefined
上,取决于是否是严格模式。如:
function sayHi() {
console.log(this.name)
}
var person = {
name: 'Achilles',
sayHi: sayHi,
}
var name = 'Global'
var Hi = person.sayHi
Hi() // Global
虽然 Hi
是 person.sayHi
的一个引用,但是实际上,它引用的是 sayHi
函数本身,与 person
已经没有任何关系了,此时的 Hi()
其实是一个独立函数调用,因此应用了默认绑定。
除了上面这种丢失之外,隐式绑定的丢失也可以发生在回调函数中(事件回调也是其中一种):
function sayHi() {
console.log(this.name)
}
function Hi(fn) {
// fn 其实引用的是 sayHi
fn() // <-- 调用位置!
}
var person = {
name: 'Achilles',
sayHi: sayHi,
}
var name = 'Global'
Hi(person.sayHi) // Global
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,fn
实际指向的是 sayHi
,和 person
没有任何关系了。
显式绑定
显式绑定,即通过 call、apply、bind
的方式,显式的指定 this 所指向的对象。
三个方法使用方式各有不同:
函数名称 | 语法 | 参数 | 返回值 |
---|---|---|---|
call | call(thisArg) call(thisArg, arg1, arg2, …argN); | thisArg :将要要绑定的 this 值。arg1, …, argN (可选):函数的参数 | 使用指定的 this 值和参数调用函数后的结果 |
apply | apply(thisArg); apply(thisArg, argsArray) | thisArg :将要要绑定的 this 值。argsArray (可选):一个类数组对象,用于指定调用 func 时的参数 | 使用指定的 this 值和参数调用函数的结果 |
Bind | bind(thisArg) bind(thisArg, arg1, arg2, …argN); | thisArg :在调用绑定函数时,作为 this 参数传入目标函数 func 的值arg1, …, argN (可选):在调用 func 时,插入到传入绑定函数的参数前的参数 | 使用指定的 this 值和初始参数(如果提供)创建的给定函数的副本。 |
可以看出,call
和 apply
的作用一样,只是传参方式不同。call
和 apply
都会执行对应的函数,而 bind
方法不会(只返回一个绑定后的新函数)。
function sayHi() {
console.log(this.name)
}
var person = {
name: 'Achilles',
sayHi: sayHi,
}
var name = 'Global'
sayHi.call(person) // Achilles
通过这种方式,明确地将 this 绑定在了 person
上。 另外,显式绑定同隐式绑定一样,也会发生 绑定丢失 的现象,这里就不再赘述了。
被忽略的 this
如果你把 null
或者 undefined
作为 this 的绑定对象传入 call
、apply
或者 bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则:
function foo() {
console.log(this.a)
}
var a = 2
foo.call(null) // 2
new 绑定
在前文 new 操作符 中,我们已经知道,当使用 new
来调用函数的时候,就会新对象绑定到这个函数的 this 上。
function Person(name) {
this.name = name
}
var p = new Person('conyu')
console.log(p.name) // conyu
绑定优先级
我们知道了 this 有四种绑定规则,但是如果同时应用了多种规则,怎么办?
此时由绑定优先级来决定,这四种绑定的优先级为:
new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定
如果有兴趣,可以写个 demo 测试一下。
箭头函数中的 this
ES6 中有一种特殊的函数类型:箭头函数。它和普通函数有一个很大的区别就在于:箭头函数没有自己的 this,而是根据外层(函数或者全局)作用域来决 定 this。
var name = 'Global'
var person = {
name: 'Achilles',
sayHi: function () {
console.log(this.name)
},
hi: () => {
console.log(this.name)
},
}
person.sayHi() // Achilles
person.hi() // Global,因为箭头函数不绑定this,只能往上找,即全局的this。
思考题
- 以下代码的输出结果是?
function foo() {
console.log(this.a)
}
function doFoo() {
foo()
}
var obj = {
a: 1,
doFoo: doFoo,
}
var a = 2
obj.doFoo()
查看答案 👀
输出结果:
2
解析: 在执行 foo 的时候,执行环境就是 doFoo 函数,执行环境为全局。
- 以下代码的输出结果是?
var a = 10
var obj = {
a: 20,
say: () => {
console.log(this.a)
},
}
obj.say()
var anotherObj = { a: 30 }
obj.say.apply(anotherObj)
查看答案 👀
输出结果:
10
10
解析: 箭头函数不绑定 this
- 以下代码的输出结果是?
function a() {
console.log(this)
}
a.call(null)
查看答案 👀
输出结果: Window 对象
解析: 根据 ECMAScript262 规范规定:如果第一个参数传入的对象调用者是 null
或者 undefined
,call
方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null
还是 undefined
,其 this 都是全局对象 window。所以,在浏览器上答案是输出 window 对象。
要注意的是,在严格模式中,null
和 undefined
不会被替换:
'use strict'
function a() {
console.log(this)
}
a.call(null) // null
a.call(undefined) // undefined
- 以下代码的输出结果是?
var obj = {
say: function () {
var f1 = () => {
console.log('1111', this)
}
f1()
},
pro: {
getPro: () => {
console.log(this)
},
},
}
var o = obj.say
o()
obj.say()
obj.pro.getPro()
查看答案 👀
输出结果:
1111 window对象
1111 obj对象
window对象
解析:
o()
,o
是在全局执行的,而f1
是箭头函数,它是没有绑定 this 的,它的 this 指向其父级的 this,其父级say
方法的 this 指向的是全局作用域,所以会打印出window
obj.say()
,谁调用say
,say
的 this 就指向谁,所以此时 this 指向的是obj
对象;obj.pro.getPro()
,我们知道,箭头函数时不绑定 this 的,getPro
处于pro
中,而对象不构成单独的作用域,所以箭头的函数的 this 就指向了全局作用域window
。
- 以下代码的输出结果是?
var myObject = {
foo: 'bar',
func: function () {
var self = this
console.log(this.foo)
console.log(self.foo)
;(function () {
console.log(this.foo)
console.log(self.foo)
})()
},
}
myObject.func()
查看答案 👀
输出结果:
bar
bar
undefined
bar
解析:
- 首先
func
是由myObject
调用的,this 指向myObject
。又因为var self = this
;所以self
指向myObject
。 - 这个立即执行匿名函数表达式是由 window 调用的,this 指向 window 。立即执行匿名函数的作用域处于
myObject.func
的作用域中,在这个作用域找不到self
变量,沿着作用域链向上查找self
变量,找到了指向myObject
对象的self
。
- 以下代码的输出结果是?
window.number = 2
var obj = {
number: 3,
db1: (function () {
console.log(this)
this.number *= 4
return function () {
console.log(this)
this.number *= 5
}
})(),
}
var db1 = obj.db1
db1()
obj.db1()
console.log(obj.number)
console.log(window.number)
查看答案 👀
输出结果:
15
40
解析:
- 执行
db1()
时,this 指向全局作用域,所以window.number * 4 = 8
,然后执行匿名函数, 所以window.number * 5 = 40
; - 执行
obj.db1()
;时,this 指向obj
对象,执行匿名函数,所以obj.numer * 5 = 15
。
- 以下代码的输出结果是?
var length = 10
function fn() {
console.log(this.length)
}
var obj = {
length: 5,
method: function (fn) {
fn()
arguments[0]()
},
}
obj.method(fn, 1)
查看答案 👀
输出结果:
10
2
解析:
- 第一次执行
fn()
,this 指向 window 对象,输出10
。 - 第二次执行
arguments[0]()
,相当于arguments
调用方法,this 指向arguments
,而这里传了两个参数,故输出 arguments 长度为2
。
- 以下代码的输出结果是?
var a = 1
function printA() {
console.log(this.a)
}
var obj = {
a: 2,
foo: printA,
bar: function () {
printA()
},
}
obj.foo() // 2
obj.bar() // 1
var foo = obj.foo
foo() // 1
查看答案 👀
输出结果:
2
1
1
解析:
obj.foo()
,foo
的 this 指向obj
对象,所以a
会输出2
;obj.bar()
,printA
在bar
方法中执行,所以此时printA
的 this 指向的是 window,所以会输出1
;foo()
,foo
是在全局对象中执行的,所以其 this 指向的是 window,所以会输出1
;
- 以下代码的输出结果是?
var x = 3
var y = 4
var obj = {
x: 1,
y: 6,
getX: function () {
var x = 5
return (function () {
return this.x
})()
},
getY: function () {
var y = 7
return this.y
},
}
console.log(obj.getX())
console.log(obj.getY())
查看答案 👀
输出结果:
3
6
解析:
- 我们知道,匿名函数的 this 是指向全局对象的,所以 this 指向 window,
obj.getX()
会打印出3
; getY
是由obj
调用的,所以其 this 指向的是obj
对象,会打印出6
。
- 以下代码的输出结果是?
var a = 10
var obt = {
a: 20,
fn: function () {
var a = 30
console.log(this.a)
},
}
obt.fn()
obt.fn.call()
obt.fn()
查看答案 👀
输出结果:
20
10
20
解析:
obt.fn()
,fn
是由obt
调用的,所以其 this 指向obt
对象,会打印出20
;obt.fn.call()
,这里call
没有参数,就表示null
,我们知道如果call
的参数为undefined
或null
,那么 this 就会指向全局对象 this,所以会打印出10
;(obt.fn)()
, 这里给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于obt.fn()
,所以会打印出20
; :::以下代码的输出结果是?
function a(xx) {
this.x = xx
return this
}
var x = a(5)
var y = a(6)
console.log(x.x)
console.log(y.x)
查看答案 👀
输出结果:
undefined
6
解析:
- 最关键的就是
var x = a(5)
,函数a
是在全局作用域调用,所以函数内部的 this 指向 window 对象。所以this.x = 5
就相当于:window.x = 5
。之后return this
,也就是说var x = a(5)
中的x
变量的值是window
,这里的x
将函数内部的x
的值覆盖了。然后执行console.log(x.x)
, 也就是console.log(window.x)
,而 window 对象中没有x
属性,所以会输出undefined
。 - 当指向
y.x
时,会给全局变量中的x
赋值为6
,所以会打印出6
。
- 以下代码的输出结果是?
function foo(something) {
this.a = something
}
var obj1 = {
foo: foo,
}
var obj2 = {}
obj1.foo(2)
console.log(obj1.a)
obj1.foo.call(obj2, 3)
console.log(obj2.a)
var bar = new obj1.foo(4)
console.log(obj1.a)
console.log(bar.a)
查看答案 👀
输出结果:
2
3
2
4
解析:
- 首先执行
obj1.foo(2)
; 会在obj
中添加a
属性,其值为2
。之后执行obj1.a
,a
是右obj1
调用的,所以 this 指向obj
,打印出2
; - 执行
obj1.foo.call(obj2, 3)
时,会将foo
的 this 指向obj2
,后面就和上面一样了,所以会打印出3
; obj1.a
会打印出2
;- 最后就是考察 this 绑定的优先级了,
new
绑定是比隐式绑定优先级高,所以会输出4
。