Blog - wingsico

勾起我有想法去写这样一个文章,一是因为我从来没有总结过this的用法,也经常对这个概念有些模糊;二是在学习ES6的时候,被一个简单的例子给弄懵了,感觉自己之前学的好像是假的?例子如下:

var doSomething = () => {
  console.log(this)
}

var obj = {  }

doSomething() // 很明显,打印的是Window对象

var doAnother = doSomething.bind(obj)
doAnother() // Window

doSomething.call(obj) // Window
doSomething.apply(obj) // Window

这下我就很懵逼了,在我之前的认知中this是这样的:

在函数内部时,this指向的是函数运行时所在的上下文,
且通过bind/call/apply可以改变函数内部的this指向,指向其第一个参数(type => Object).

这让我很慌,于是将代码改成了这样:

var doSomething = function () {
  console.log(this)
}

var obj = {  }

doSomething() // Window

var doAnother = doSomething.bind(obj)
doAnother() // obj

doSomething.call(obj) // obj
doSomething.apply(obj) // obj

诶,这个时候又很正常,跟我的理解来看没有偏差,于是认定是箭头函数的关系。 于是去阮一峰看了一下箭头函数的相关描述,又参考了另外几篇博客以及一些例子之后,我觉得自己已经弄得比较明白了,关于上面那题,容我先卖个关子,这篇博客主要目的不在于解决这个问题,而更多的是为了彻底理清this在各种情况以及在普通函数和箭头函数之间表现的差别。所以,我们一步步从简单的开始。


理解this

什么是this呢?很久以前,大概刚学js没多久,我知道js不像c/c++,他是没有指针的,但我记得有人跟我说过,this就相当于一个指针,且这个指针是根据其所处的执行环境不同而指向不同,且

this永远指向的都是一个对象

这条性质决定了我们如何使用this,在定义类中,也就是构造器函数中,我们通常使用this.xxx来定义公共实例变量。在使用对象的时候,我们也经常使用obj.xxx来使用其属性,从用法上来看,点操作符一般只存在于对象中,也可以从侧面再说明this指向的就是一个对象。我们来举个例子来说明:

function Person(name) {
  this.name = name
  console.log(this, typeof this)
}

var jack = new Person('jack') // Person { name: "jack" } "object"

除此之外,还要理解的就是一个概念就是:

在绝大多数情况下,在普通函数内部,this的指向由函数执行的环境决定。
也就是说,this在函数执行期间才被绑定到调用该函数时所处的上下文中,而不是声明时候所处的上下文。

可能上面这句话还不是特别好理解,那么我来举几个例子:

function fn() {
  console.log(this)
}

function wtf() {
  fn()
}

fn() // Window
wtf() // Window

可能有人又会有疑问了: 直接调用fn()很容易理解,因为调用fn所处的上下文为Window对象,所以打印Window,但是调用wtf时,fn明明在wtf的内部调用的啊,为什么不指向wtf呢? 这很容易理解,我们来把上述函数更改一下

function wtf() {
  console.log(this === window) // true
  console.log(window.fn === fn) // true
  window.fn() // Window
  fn() // Window
}

这下就很清晰易懂了,在wtf函数内部调用fn(),他的实际执行上下文仍然为Window对象。 我们再来看一个例子:

// 作为普通函数调用时,this必须指向一个对象。那就是全局对象(在浏览器内是Window对象)。
var getA = function() {
  console.log(this.a)  
}

window.a = 1
getA() // a

// 作为对象属性调用时,this指向的就是该对象
var obj = {
  a: 2,
  getA: function() {
    console.log(this.a)
  }
}
obj.getA() // 2 

var getOtherA = obj.getA // 这里将 function() { console.log(this.a) } 赋给了getOtherA,但它的执行环境仍是全局环境中,其this指向Window
getOtherA() // 1

根据以上的例子我们可以总结出:

在普通函数内部,this总是指向其执行环境上下文中,即要关注该函数的直接调用位置,再换种说法,即关注函数是如何调用的


那么,我们都有哪些调用方法呢?

一、函数调用


来看一个简单的例子介绍函数调用:

function hello(name) {
  return 'Hello ' + name
}

var message = hello('world')
console.log(message) // "hello world"

这样一个函数名加上一个左开括号加上一个逗号分隔的参数表达式再加上一个右开括号,就会执行该函数对象的函数调用。 还有一个高级的是IIFE,立即调用的函数表达式

var message = (function(name) {
  return 'hello ' + name
})('world)
console.log(message) // "hello world"

IIFE也是一个函数调用:第一对括号(function(name) {…})是一个表达式,它计算为一个函数对象,后跟一对带括号的’world’参数:(‘world’)。 理解了函数调用,我们再来看一下this在函数调用中的指向情况:

function test() {
  console.log(this === window)
  this.number = 20
  var number = 30
}

test()
console.log(window.number) // 20

这里的test()为js的函数调用,在执行期间,js将函数内部的this绑定到了全局对象上,即window 所以我们可以用一句话总结:

this在函数直接调用中总指向全局对象,全局对象是由执行环境决定的,在浏览器里,他是window对象

再举个例子,更深的理解这句话:

var obj = {
  name: 'jack',
  action: 'hello',
  doIt: function() {
    console.log(this === obj)
    function strConcat() {
       console.log(this === obj)
       return this.action + ' ' +this.action
    }
    return strConcat()
  }
}

var msg = obj.doIt() // true, false
console.log(msg) // "hello jack"

调用obj.doIt时,由于他是方法调用(在后面会说),显然doIt函数的上下文是obj对象,strConcat函数是doIt里面定义的,你可能会想它和doIt一样,this也指向obj。 其实不然,我们在看我们的那句话,函数调用的this指向全局对象,而在这里就是window,即使外部函数doIt有着obj作为函数对象,在这里也不会影响strConcat中this的指向。

函数调用的常见陷阱this是被认为在内部函数中与外部函数相同。
正确的应该是内部函数的上下文仅依赖于调用,而不依赖于外部函数的上下文。