- Published on
深入 JavaScript 之 this
this 关键字是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。 但是即使是非常有经验的 JavaScript 开发者也很难说清它到底指向什么。
----《你不知道的 JavaScript》
绑定规则
this
有如下四种绑定规则:
默认绑定
定义:不带任何修饰的直接函数调用。
指向:在非严格模式下指向全局对象,严格模式下指向 undefined
。
代码实例:
var a = 10
function foo() {
var a = 1
console.log(this.a)
}
foo() //10.
此处的 foo()
等价于 window.foo()
,故 this
指向 window
。
特殊情况:function
作为函数返回值进行调用,如下例子:
var name = 'window'
var x = {
name: 'x',
getName: function () {
return function () {
console.log(this.name)
}
},
}
// 此处相当于(x.getName())(),也可看作var foo = x.getName();foo()。
x.getName()() // window
隐式绑定
定义:函数执行的时候有上下文对象(调用者),也就是说函数是作为对象的属性进行调用。
指向:上下文对象(调用者)。
代码示例:
var obj = {
a: 10,
foo: foo,
}
// this指向调用者obj,此处打印obj.a。
obj.foo() // 10。
隐式丢失:当作为对象属性的方法,被赋值给一个新的变量并调用时,会发生隐式丢失情况,此时将应用默认绑定,见下例:
var obj = {
a: 10,
foo: foo,
}
// this指向调用者obj,此处打印obj.a。
obj.foo() // 10。
var bar = obj.foo
bar() // undefined。隐式丢失,此时应用的是默认绑定,this 指向 window 对象。
隐式丢失的另一种情况是参数传递,这是一种隐式赋值(将实参赋值给形参),所以会有如下情况:
var name = 'foo'
setTimeout(function bar() {
console.log(this.name) // foo。此时作为参数的函数 bar 应用的是默认绑定。
})
PS:链式调用如 xx.yy.obj.foo()
,this
指向直接上级。
显式绑定
定义:通过bind
、call
、apply
显式的指定this
指向。
指向:显式指定的this
对象。
例:
var obj = { a: 3 }
function foo(a, b) {
console.log(a + b + this.a)
}
function foo1(a, b) {
console.log(a + b + this.a)
}
function foo2(a, b) {
console.log(a + b + this.a)
}
foo.call(obj, 'a', 'b') // ab3
foo1.apply(obj, ['a', 'b']) // ab3
foo2 = foo2.bind(obj)
foo2('a', 'b') // ab3
特殊情况:当指定的this
对象为null
或者undefined
时,此时函数内部的this
将应用默认绑定。
var a = 123
var obj = { a: 3 }
function foo(a, b) {
console.log(a + b + this.a)
}
// this 指向全局对象
foo.call(null, 'a', 'b') // ab123
PS:当无需指定this
时,使用空对象{}
代替null
作为call
/bind
/apply
方法的第一个参数是一个更好的选择,当然,如果使用Object.create(null)
就更好了,因为比空对象{}
要更“空”。
new 绑定
在JavaScript
中,使用new
操作符后,JavaScript
引擎会做如下操作:
- 创建一个原型指向构造函数
prototype
属性的新对象(继承构造函数的原型)。 - 执行构造函数,并将
this
指向第一步中创建的新对象。 - 如果第二步执行中返回了一个对象,那么 将这个对象返回作为结果,否则返回第一步中的新对象。
其中第 2 步就是new
绑定。
function foo() {
this.a = 10
console.log(this)
}
var obj = new foo() // {a: 10}
console.log(obj.a) // 10
如果构造函数返回一个对象,那么此时将丢失原来绑定this
的新对象。也就是说无法通过实例的属性来访问到原构造函数内部的this
(如下例中的obj.a
返回的是undefined
,而不是预期中的 10),除非显式将this
作为属性返回(如下例中的属性 c)。
function foo() {
this.a = 10
return { b: 2, c: this }
}
var obj = new foo()
console.log(obj) // { b: 2 },而不是 { a: 10 }
console.log(obj.a) // undefined,而不是 10
console.log(obj.c) // { a: 10 }
this 绑定的优先级
new
绑定 > 显式绑定 > 隐式绑定 > 默认绑定
判断思路
按以下步骤进行判定:
函数是否在
new
中调用(new 绑定)?如果是的话this
指向的新创建的对象。函数是否使用
call
、apply
、bind
处理(显式绑定)?如果是的话,this
指向指定的对象。函数是否在上下文对象中调用(隐式绑定)?如果是的话,
this
指向上下文对象(调用者)。如果以上条件均不符合(默认绑定)。
this
指向全局对象(在严格模式下指向undefined
)。
箭头函数
关于箭头函数的 this,阮一峰 老师有如下定义:
“箭头函数”的 this,总是指向定义时所在的对象,而不是运行时所在的对象。
其中的“对象”,并不是真的对象,确切的说应该是执行上下文。
即 箭头函数的this
指向它所定义的时候的上下文,而不是运行时候所在的上下文(普通函数中的 this 指向运行时的上下文)。
箭头函数本身并没有this
绑定,所以在箭头函数中使用 this 的时候,js 会从箭头函数开始,沿着它定义时所在的作用域向上寻找,直到找到第一个this
绑定为止,该this
的指向即为箭头函数的 this 指向。
function foo() {
setTimeout(() => {
console.log('id:', this.id)
}, 100)
}
// 箭头函数向上查找到foo的this并继承,此时foo的this指向对象{id:42}。
foo.call({ id: 42 }) //id: 42
箭头函数应用
箭头函数的一个常用应用,就是用于回调函数中,比如定时器或者事件处理器中的回调函数。
以定时器为例,setTimeout
中的普通回调函数中的this
默认指向全局对象。
var name = 'window'
var x = {
name: 'x',
getName: function () {
this.name = 'getName'
setTimeout(function () {
console.log(this.name)
}, 0)
},
}
x.getName() // window
如果想在回调函数中获取 getName 方法中的this
,我们一般会使用下面两种方式
1、将this
进行缓存。
var name = 'window'
var x = {
name: 'x',
getName() {
this.name = 'getName'
var self = this
setTimeout(function () {
console.log(self.name)
}, 0)
},
}
x.getName() // getName
2、使用bind
方法显式绑定this
。
var name = 'window'
var x = {
name: 'x',
getName() {
this.name = 'getName'
setTimeout(
function () {
console.log(this.name)
}.bind(this),
0
)
},
}
x.getName() // getName
也可以直接使用箭头函数,写法会更简洁明了。
var name = 'window'
var x = {
name: 'x',
getName: function () {
this.name = 'getName'
setTimeout(() => {
// 这里直接继承 getName 方法中的 this。
console.log(this.name)
}, 0)
},
}
x.getName() // getName