阅读 832

完全理解JavaScript中的this关键字

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 关键字的使用分为两种情况:

  1. 全局上下文的 this
  2. 函数上下文的 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. 函数调用 applycallbind

当一个函数在其主体中使用 this 关键字时,可以通过使用函数继承自Function.prototypecallapply 方法将 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'
复制代码

applycall 不同,使用 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 最终指向的是那个调用它的对象” 这句话就不通用了,函数调用 callapplybind 方法是一个特殊情况。下面还有一种特殊情况:箭头函数

5. 箭头函数

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

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

var foo = (() => this)
//在浏览器中
foo() === window // true
// 在node中
foo() === global // true
复制代码

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

当箭头函数在创建其的上下文外部被调用时,箭头函数便是一个闭包,this 的值同样与原上下文环境中的 this 的值一致。由于箭头函数本身是不存在 this,通过 callapplybind 修改 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
复制代码

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

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

“一个问题”的解答

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

// undefined
// undefined
// undefined
复制代码

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

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

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


参考:

关注下面的标签,发现更多相似文章
评论