主页
主页
文章目录
  1. 前言
  2. 一个问题
  3. 上下文环境
  4. 全局上下文中的 this
  5. 函数上下文中的 this
    1. 1. 全局函数
    2. 2. 作为对象的方法
    3. 3. 作为构造函数
    4. 4. 函数调用 apply、call、 bind 时
    5. 5. 箭头函数
  6. “一个问题”的解答
  7. 总结

完全理解 JavaScript 中的 this 关键字

前言

王福朋老师的 JavaScript原型和闭包系列 文章看了不下三遍了,作为一个初学者,每次看的时候都会有一种 “大彻大悟” 的感觉,而看完之后却总是一脸懵逼。原型与闭包 可以说是 JavaScirpt 中理解起来最难的部分了,也是这门面向对象语言很重要的部分,当然,我也只是了解到了一些皮毛,对于 JavaScript OOP 更是缺乏经验。这里我想总结一下 Javascript 中的 this 关键字,王福朋老师的在文章里也花了大量的篇幅来讲解 this 关键字的使用,可以说 this 关键字也是值得重视的。

作者:正伟

原文链接:this关键字

注:原创文章,转载请注明出处

一个问题

一道很常见的题目:下面代码将会输出的结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 关键字的使用分为两种情况:

  1. 全局上下文的 this
  2. 函数上下文的 this

(你也可以选择其他的方式分类。当然,这也不重要了)

全局上下文中的 this

在全局执行上下文中(在任何函数体外部),this 都指向全局对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在浏览器中, 全局对象是 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)

1
2
3
4
5
6
7
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 ):

1
2
3
4
5
6
7
8
var foo = function () {
return this
}
// 在浏览器中
foo() === window // true

// 在 node 中
foo() === global //true

但值得注意的是,以上代码是在 非严格模式 下。然而,在 严格模式 下,this 的值将保持它进入执行上下文的值:

1
2
3
4
5
6
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 调用全局变量:

1
2
3
4
5
6
var a = 'global this'

var foo = function () {
console.log(this.a)
}
foo() // 'global this'
1
2
3
4
5
6
7
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 值是在函数被调用时确定的,在定义函数时确定不了(箭头函数除外)。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的值同时也只受最靠近的成员引用的影响:

1
2
3
4
5
6
7
8
//接上面代码
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 的值为当前对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 出来的对象:

1
2
3
4
5
6
7
8
9
10
11
12
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 的指向便是前面讨论的,指向全局变量:

1
2
// 接上面代码
Foo() // window {...}

构造函数同样可以看作是一个普通的函数(只不过函数名称第一个字母大写了而已咯),但是在用 new 关键字调用构造函数创建对象时,它与普通函数的行为不同罢了。

4. 函数调用 applycallbind

当一个函数在其主体中使用 this 关键字时,可以通过使用函数继承自Function.prototypecallapply 方法将 this 值绑定到特定对象。即 this 的值就取传入对象的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
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'

applycall 不同,使用 bind 会创建一个与 foo 具有相同函数体和作用域的函数。但是,特别要注意的是,在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论之后如何调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 最终指向的是那个调用它的对象” 这句话就不通用了,函数调用 callapplybind 方法是一个特殊情况。下面还有一种特殊情况:箭头函数

5. 箭头函数

箭头函数是 ES6 语法的新特性,在箭头函数中,this 的值与创建箭头函数的上下文的 this 一致。

在全局代码中,this 的值为全局对象:

1
2
3
4
5
var foo = (() => this)
//在浏览器中
foo() === window // true
// 在node中
foo() === global // true

其实箭头函数并没有自己的 this。所以,调用 this 时便和调用普通变量一样在作用域链中查找,获取到的即是创建此箭头函数的上下文中的 this。若创建此箭头函数的上下文中也没有 this,便继续沿着作用域链往外查找,直到全局作用域,这时便指向全局对象(window / global)。

当箭头函数在创建其的上下文外部被调用时,箭头函数便是一个闭包,this 的值同样与原上下文环境中的 this 的值一致。由于箭头函数本身是不存在 this,通过 callapplybind 修改 this 的指向是无法实现的。

作为对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

箭头函数 foothis 被设置为创建时的上下文(在上面代码中,也就是全局对象)的 this 值,而且无法通过其他调用方式设定 foothis 值。

与普通函数对比,箭头函数的 this 值是在函数创建时确定的,而且无法通过调用方式重新设置 this 值。普通函数中的 this 值是在调用的时候确定的,可通过不同的调用方式设定 this 值。

“一个问题”的解答

回到开篇的问题上,输出结果为:

1
2
3
// undefined
// undefined
// undefined

因为箭头函数是在对象 obj1 内部创建的,在对象内部属于全局上下文(注意只有全局上下文和函数上下文),this 同样是指向全局对象,即箭头函数的 this 指向全局对象且无法被修改。

在全局对象中,没有定义变量 a,所以便输出三个了 undefined

1
2
3
4
const obj1 = {
a: 'a in obj1',
foo: () => { console.log(this.a) }
}

总结

this 关键字的值取决于其所处的位置(上下文环境):

  1. 在全局环境中,this 的值指向全局对象( windowglobal )。

  2. 在函数内部,this 的取值取决于其所在函数的调用方式,也就是说 this 的值是在函数被调用的时候确定的,在创建函数时无法确定(详解:this关键字)。以下四种调用方式:

    1. 全局中调用:指向全局对象 window / globalfoo 相当于 window.foo 在严格模式下有所不同;

    2. 作为对象的方法属性:指向调用函数的对象,在调用继承的方法时也是如此;

    3. new 关键字调用:构造函数只不过是一个函数名称第一个字母大写的普通函数而已,在用 new 关键字调用时,this 指向新创建的对象;

    4. call / apply / bindcall/apply/bind 可以修改函数的 this 指向,bind 绑定的 this 指向将无法被修改。

    当然,箭头函数是个例外,箭头函数本身不存在 this,而在箭头函数中使用 this 获取到的便是创建其的上下文中的 this


参考: