this关键字
前言
王福朋老师的 JavaScript原型和闭包系列 文章看了不下三遍了,作为一个初学者,每次看的时候都会有一种 "大彻大悟" 的感觉,而看完之后却总是一脸懵逼。原型与闭包 可以说是 JavaScirpt 中理解起来最难的部分了,也是这门面向对象语言很重要的部分,当然,我也只是了解到了一些皮毛,对于 JavaScript OOP 更是缺乏经验。这里我想总结一下 Javascript 中的 this
关键字,王福朋老师的在文章里也花了大量的篇幅来讲解 this
关键字的使用,可以说 this
关键字也是值得重视的。
作者:正伟
原文链接:this关键字
注:原创文章,转载请注明出处
一个问题
一道很常见的题目:下面代码将会输出的结果是什么?
const obj1 = {
a: 'a in obj1',
foo: () => { console.log(this.a) }
}
const obj2 = {
a: 'a in obj2',
bar: obj1.foo
}
const obj3 = {
a: 'a in obj3'
}
obj1.foo() // 输出 ??
obj2.bar() // 输出 ??
obj2.bar.call(obj3) // 输出 ??
复制代码
在弄明白 this 关键字之前,也许很难来回答这道题。
那先从上下文环境说起吧~
上下文环境
我们都知道,每一个 代码段 都会执行在某一个 上下文环境 当中,而在每一个代码执行之前,都会做一项 "准备工作",也就是生成相应的 上下文环境,所以每一个 上下文环境 都可能会不一样。
上下文环境 是什么?我们可以去看王福朋老师的文章(链接在文末),讲解的很清楚,这里不再赘述。
代码段 可以分为三种:
- 全局代码
- 函数体
eval
代码
与之对应的 上下文环境 就有:
- 全局上下文
- 函数上下文
(elav
就不讨论了,不推荐使用)
当然,这和 this
又有什么关系呢?this
的值就是在为代码段做 "准备工作" 时赋值的,可以说 this
就是 上下文环境 的一部分,而每一个不同的 上下文环境 可能会有不一样的 this
值。
这里我大胆的将 this
关键字的使用分为两种情况:
- 全局上下文的
this
- 函数上下文的
this
(你也可以选择其他的方式分类。当然,这也不重要了)
全局上下文中的 this
在全局执行上下文中(在任何函数体外部),this
都指向全局对象:
// 在浏览器中, 全局对象是 window
console.log(this === window) // true
var a = 'Zavier Tang'
console.log(a) // 'Zavier Tang'
console.log(window.a) // 'Zavier Tang'
console.log(this.a) // 'Zavier Tang'
this.b = 18
console.log(b) // 18
console.log(window.b) // 18
console.log(this.b) // 18
// 在 node 环境中,this 指向global
console.log(this === global) // true
复制代码
注意:在任何函数体外部,都属于全局上下文,
this
都指向全局对象(window
/global
)。在对象的内部,也是在全局上下文,this
同样指向全局对象(window
/global
)
window.a = 10
var obj = {
x: this.a,
_this: this
}
obj.x // 10
obj._this === this // true
复制代码
函数上下文中的 this
在函数内部,this
的值取决与函数被调用的方式。
this
的值在函数定义的时候是确定不了的,只有函数调用的时候才能确定 this
的指向。实际上 this
最终指向的是那个调用它的对象。(也不一定正确)
1. 全局函数
对于全局的方法调用,this
指向 window
对象(node下为 global
):
var foo = function () {
return this
}
// 在浏览器中
foo() === window // true
// 在 node 中
foo() === global //true
复制代码
但值得注意的是,以上代码是在 非严格模式 下。然而,在 严格模式 下,
this
的值将保持它进入执行上下文的值:
var foo = function () {
"use strict"
return this
}
foo() // undefined
复制代码
即在严格模式下,如果 this
没有被执行上下文定义,那它为 undefined
。
在生成 上下文环境 时:
- 若方法被
window
(或global
)对象调用,即执行window.foo()
,那this
将会被定义为window
(或global
);- 若被普通对象调用,即执行
obj.foo()
,那this
将会被定义为obj
对象;(后面会讨论)- 但若未被对象调用(上面分别是被
window
对象和普通对象obj
调用),即直接执行foo()
,在非严格模式下,this
的值默认指向全局对象window
(或global
),在严格模式下,this
将保持为undefined
。
通过 this
调用全局变量:
var a = 'global this'
var foo = function () {
console.log(this.a)
}
foo() // 'global this'
复制代码
var a = 'global this'
var foo = function () {
this.a = 'rename global this' // 修改全局变量 a
console.log(this.a)
}
foo() // 'rename global this'
复制代码
所以,对于全局的方法调用,this
指向的是全局对象 window
(或global
),即调用方法的对象。(注意严格模式的不同)
函数在全局上下文中调用,
foo()
可以看作是window.foo()
,只不过在严格模式下有所限制。
2. 作为对象的方法
当函数作为对象的方法调用时,它的 this
值是调用该函数的对象。也就是说,函数的 this
值是在函数被调用时确定的,在定义函数时确定不了(箭头函数除外)。
var obj = {
name: 'Zavier Tang',
foo: function () {
console.log(this)
console.log(this.name)
}
}
obj.foo() // Object {name: 'Zavier Tang', foo: function} // 'Zavier Tang'
//foo函数不是作为obj的方法调用
var fn = obj.foo // 这里foo函数并没有执行
fn() // Window {...} // undefined
复制代码
this
的值同时也只受最靠近的成员引用的影响:
//接上面代码
var o = {
name: 'Zavier Tang in object o',
fn: fn,
obj: obj
}
o.fn() // Object {name: 'Zavier Tang in object o', fn: fn, obj: obj} // 'Zavier Tang in object o'
o.obj.foo() // Object {name: 'Zavier Tang', foo: function} // 'Zavier Tang'
复制代码
在原型链中,this
的值为当前对象:
var Foo = function () {
this.name = 'Zavier Tang'
this.age = 20
}
// 在原型上定义函数
Foo.prototype.getInfo = function () {
console.log(this.name)
console.log(this.age)
console.log(this === tang)
}
var tang = new Foo()
tang.getInfo() // "Zavier Tang" // 20 // true
复制代码
虽然这里调用的是一个继承方法,但 this
所指向的依然是 tang
对象。
也可以看作是对象
tang
调用了getInfo
方法,this
指向了tang
。即this
指向了调用它的那个对象。
参考:《Object-Oriented JavaScript》(Second Edition)
3. 作为构造函数
如果函数作为构造函数,那函数当中的 this
便是构造函数即将 new
出来的对象:
var Foo = function () {
this.name = 'Zavier Tang',
this.age = 20,
this.year = 1998,
console.log(this)
}
var tang = new Foo()
console.log(tang.name) // 'Zavier Tang'
console.log(tang.age) // 20
console.log(tang.year) // 1998
复制代码
当 Foo
不作为构造函数调用时,this
的指向便是前面讨论的,指向全局变量:
// 接上面代码
Foo() // window {...}
复制代码
构造函数同样可以看作是一个普通的函数(只不过函数名称第一个字母大写了而已咯),但是在用
new
关键字调用构造函数创建对象时,它与普通函数的行为不同罢了。
4. 函数调用 apply
、call
、 bind
时
当一个函数在其主体中使用 this
关键字时,可以通过使用函数继承自Function.prototype
的 call
或 apply
方法将 this
值绑定到特定对象。即 this
的值就取传入对象的值:
var obj1 = { name: 'Zavier1' }
var obj2 = { name: 'Zavier2' }
var foo = function () {
console.log(this)
console.log(this.name)
}
foo.apply(obj1) // Ojbect {name: 'Zavier1'} //'Zavier1'
foo.call(obj1) // Ojbect {name: 'Zavier1'} //'Zavier1'
foo.apply(obj2) // Ojbect {name: 'Zavier2'} //'Zavier2'
foo.call(obj2) // Ojbect {name: 'Zavier2'} //'Zavier2'
复制代码
与 apply
、call
不同,使用 bind
会创建一个与 foo
具有相同函数体和作用域的函数。但是,特别要注意的是,在这个新函数中,this
将永久地被绑定到了 bind
的第一个参数,无论之后如何调用。
var f = function () {
console.log(this.name)
}
var obj1 = { name: 'Zavier1' }
var obj2 = { name: 'Zavier2' }
var g = f.bind(obj1)
g() // 'Zavier1'
var h = g.bind(ojb2) // bind只生效一次!
h() // 'Zavier1'
var o = {
name: 'Zavier Tang',
f:f,
g:g,
h:h
}
o.f() // 'Zavier Tang'
o.g() // 'Zavier1'
o.h() // 'Zavier1'
复制代码
到这里,“
this
最终指向的是那个调用它的对象” 这句话就不通用了,函数调用call
、apply
、bind
方法是一个特殊情况。下面还有一种特殊情况:箭头函数。
5. 箭头函数
箭头函数是 ES6 语法的新特性,在箭头函数中,this
的值与创建箭头函数的上下文的 this
一致。
在全局代码中,this
的值为全局对象:
var foo = (() => this)
//在浏览器中
foo() === window // true
// 在node中
foo() === global // true
复制代码
其实箭头函数并没有自己的 this
。所以,调用 this
时便和调用普通变量一样在作用域链中查找,获取到的即是创建此箭头函数的上下文中的 this
。若创建此箭头函数的上下文中也没有 this
,便继续沿着作用域链往外查找,直到全局作用域,这时便指向全局对象(window
/ global
)。
当箭头函数在创建其的上下文外部被调用时,箭头函数便是一个闭包,this
的值同样与原上下文环境中的 this
的值一致。由于箭头函数本身是不存在 this
,通过 call
、 apply
或 bind
修改 this
的指向是无法实现的。
作为对象的方法:
var foo = (() => this)
var obj = {
foo: foo
}
// 作为对象的方法调用
obj.foo() === window // true
// 用apply来设置this
foo.apply(obj) === window // true
// 用bind来设置this
foo = foo.bind(obj)
foo() === window // true
复制代码
箭头函数 foo
的 this
被设置为创建时的上下文(在上面代码中,也就是全局对象)的 this
值,而且无法通过其他调用方式设定 foo
的 this
值。
与普通函数对比,箭头函数的 this
值是在函数创建时确定的,而且无法通过调用方式重新设置 this
值。普通函数中的 this
值是在调用的时候确定的,可通过不同的调用方式设定 this
值。
“一个问题”的解答
回到开篇的问题上,输出结果为:
// undefined
// undefined
// undefined
复制代码
因为箭头函数是在对象 obj1
内部创建的,在对象内部属于全局上下文(注意只有全局上下文和函数上下文),this
同样是指向全局对象,即箭头函数的 this
指向全局对象且无法被修改。
在全局对象中,没有定义变量 a
,所以便输出三个了 undefined
。
const obj1 = {
a: 'a in obj1',
foo: () => { console.log(this.a) }
}
复制代码
总结
this
关键字的值取决于其所处的位置(上下文环境):
-
在全局环境中,
this
的值指向全局对象(window
或global
)。 -
在函数内部,
this
的取值取决于其所在函数的调用方式,也就是说this
的值是在函数被调用的时候确定的,在创建函数时无法确定(详解:this关键字)。以下四种调用方式:-
全局中调用:指向全局对象
window
/global
,foo
相当于window.foo
在严格模式下有所不同; -
作为对象的方法属性:指向调用函数的对象,在调用继承的方法时也是如此;
-
new 关键字调用:构造函数只不过是一个函数名称第一个字母大写的普通函数而已,在用
new
关键字调用时,this 指向新创建的对象; -
call / apply / bind:
call
/apply
/bind
可以修改函数的this
指向,bind
绑定的this
指向将无法被修改。
当然,箭头函数是个例外,箭头函数本身不存在
this
,而在箭头函数中使用this
获取到的便是创建其的上下文中的this
。 -
参考: