深入JavaScript系列(四):彻底搞懂this

原文链接:https://juejin.im/post/5c0fd1f8e51d4505fe6c4bde

一、函数的调用

全局环境的this指向全局对象,在浏览器中就是我们熟知的window对象

说到this的种种情况,就离不开函数的调用,一般我们调用函数,无外乎以下四种方式:

  1. 普通调用,例如foo()
  2. 作为对象方法调用,例如obj.foo()
  3. 构造函数调用,例如new foo()
  4. 使用callapplybind等方法。

除箭头函数外的其他函数被调用时,会在其词法环境上绑定this的值,我们可以通过一些方法来指定this的值。

  1. 使用callapplybind等方法来显式指定this的值。
    function foo() {
        console.log(this.a)
    }
    foo.call({a: 1}) // 输出: 1
    foo.apply({a: 2}) // 输出: 2
    // bind方法返回一个函数,需要手动进行调用
    foo.bind({a: 3})() // 输出: 3
    复制代码
  2. 当函数作为对象的方法调用时,this的值将被隐式指定为这个对象。
    let obj = {
        a: 4,
        foo: function() {
            console.log(this.a)
        }
    }
    obj.foo() // 输出: 4
    复制代码
  3. 当函数配合new操作符作为构造函数调用时,this的值将被隐式指定新构造出来的对象。

二、ECMAScript规范解读this

上面讲了几种比较容易记忆和理解this的情况,我们来根据ECMAScript规范来简单分析一下,这里只说重点,一些规范内具体的实现就不讲了,反而容易混淆。

其实当我们调用函数时,内部是调用函数的一个内置[[Call]](thisArgument, argumentsList)方法,此方法接收两个参数,第一个参数提供this的绑定值,第二个参数就是函数的参数列表。

ECMAScript规范: 严格模式时,函数内的this绑定严格指向传入的thisArgument。非严格模式时,若传入的thisArgument不为undefinednull时,函数内的this绑定指向传入的thisArgument;为undefinednull时,函数内的this绑定指向全局的this

所以第一点中讲的三种情况都是显式或隐式的传入了thisArgument来作为this的绑定值。我们来用伪代码模拟一下:

function foo() {
    console.log(this.a)
}

/* -------显式指定this------- */
foo.call({a: 1})
foo.apply({a: 1})
foo.bind({a: 1})()
// 内部均执行
foo[[Call]]({a: 1})

/* -------函数构造调用------- */
new foo()
// 内部执行
let obj = {}
obj.__proto__ = foo.prototype
foo[[Call]](obj)
// 最后将这个obj返回,关于构造函数的详细内容可翻阅我之前关于原型和原型链的文章

/* -------作为对象方法调用------- */
let obj = {
    a: 4,
    foo: function() {
        console.log(this.a)
    }
}
obj.foo()
// 内部执行
foo[[Call]]({
    a: 1,
    foo: Function foo
})
复制代码

那么当函数普通调用时,thisArgument的值并没有传入,即为undefined,根据上面的ECMAScript规范,若非严格模式,函数内this指向全局this,在浏览器内就是window。

伪代码模拟:

window.a = 10
function foo() {
    console.log(this.a)
}
foo() // 输出: 10
foo.call(undefined) // 输出: 10
// 内部均执行
foo[[Call]](undefined) // 非严格模式,this指向全局对象

foo.call(null) // 输出: 10
// 内部执行
foo[[Call]](null) // 非严格模式,this指向全局对象
复制代码

根据上面的ECMAScript规范,严格模式下,函数内的this绑定严格指向传入的thisArgument。所以有以下表现。

function foo() {
    'use strict'
    console.log(this)
}
foo() // 输出:undefined
foo.call(null) // 输出:null
复制代码

需要注意的是,这里所说的严格模式是函数被创建时是否为严格模式,并非函数被调用时是否为严格模式:

window.a = 10
function foo() {
    console.log(this.a)
}
function bar() {
    'use strict'
    foo()
}
bar() // 输出:10
复制代码

三、箭头函数中的this

ES6新增的箭头函数在被调用时不会绑定this,所以它需要去词法环境链上寻找this

function foo() {
    return () => {
        console.log(this)
    }
}
const arrowFn1 = foo()
arrowFn1() // 输出:window
           // 箭头函数没有this绑定,往外层词法环境寻找
           // 在foo的词法环境上找到this绑定,指向全局对象window
           // 在foo的词法环境上找到,并非是在全局找到的
const arrowFn2 = foo.call({a: 1})
arrowFn2() // 输出 {a: 1}
复制代码

切记,箭头函数中不会绑定this,由于JS采用词法作用域,所以箭头函数中的this只取决于其定义时的环境。

window.a = 10
const foo = () => {
    console.log(this.a)
}
foo.call({a: 20}) // 输出: 10

let obj = {
    a: 20,
    foo: foo
}
obj.foo() // 输出: 10

function bar() {
    foo()
}
bar.call({a: 20}) // 输出: 10
复制代码

四、回调函数中的this

当函数作为回调函数时会产生一些怪异的现象:

window.a = 10
let obj = {
    a: 20,
    foo: function() {
        console.log(this.a)
    }
}

setTimeout(obj.foo, 0) // 输出: 10
复制代码

我觉得这么解释比较好理解:obj.foo作为回调函数,我们其实在传递函数的具体值,而并非函数名,也就是说回调函数会记录传入的函数的函数体,达到触发条件后进行执行,伪代码如下:

setTimeout(obj.foo, 0)
//等同于,先将传入回调函数记录下来
let callback = obj.foo
// 达到触发条件后执行回调
callback()
// 所以foo函数并非作为对象方法调用,而是作为函数普通调用
复制代码

要想避免这种情况,有三种方法,第一种方法是使用bind返回的指定好this绑定的函数作为回调函数传入:

setTimeout(obj.foo.bind({a: 20}), 0) // 输出: 20
复制代码

第二种方法是储存我们想要的this值,就是常见的,具体命名视个人习惯而定。

let _this = this
let self = this
let me = this
复制代码

第三种方法就是使用箭头函数

window.a = 10
function foo() {
    return () => {
        console.log(this.a)
    }
}
const arrowFn = foo.call({a: 20})
arrowFn() // 输出:20
setTimeout(arrowFn, 0) // 输出:20
复制代码

五、总结

  1. 箭头函数中没有this绑定,this的值取决于其创建时所在词法环境链中最近的this绑定
  2. 非严格模式下,函数普通调用,this指向全局对象
  3. 严格模式下,函数普通调用,thisundefined
  4. 函数作为对象方法调用,this指向该对象
  5. 函数作为构造函数配合new调用,this指向构造出的新对象
  6. 非严格模式下,函数通过callapplybind等间接调用,this指向传入的第一个参数

    这里注意两点:

    1. bind返回一个函数,需要手动调用,callapply会自动调用
    2. 传入的第一个参数若为undefinednullthis指向全局对象
  7. 严格模式下函数通过callapplybind等间接调用,this严格指向传入的第一个参数

有时候文字的表述是苍白无力的,真正理解之后会发现:this不过如此。

六、小练习

例子来自南波的JavaScript之例题中彻底理解this

// 例1
var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()  // ?
person1.show1.call(person2)  // ?

person1.show2()  // ?
person1.show2.call(person2)  // ?

person1.show3()()  // ?
person1.show3().call(person2)  // ?
person1.show3.call(person2)()  // ?

person1.show4()()  // ?
person1.show4().call(person2)  // ?
person1.show4.call(person2)()  // ?
复制代码

选中下方查看答案:

person1 // 函数作为对象方法调用,this指向对象

person2 // 使用call间接调用函数,this指向传入的person2

window // 箭头函数无this绑定,在全局环境找到this,指向window

window // 间接调用改变this指向对箭头函数无效

window // person1.show3()返回普通函数,相当于普通函数调用,this指向window

person2 // 使用call间接调用函数,this指向传入的person2

window // person1.show3.call(person2)仍然返回普通函数

person1 // person1.show4调用对象方法,this指向person1,返回箭头函数,this在person1.show4调用时的词法环境中找到,指向person1

person1 // 间接调用改变this指向对箭头函数无效

person2 // 改变了person1.show4调用时this的指向,所以返回的箭头函数的内this解析改变

系列文章

深入ECMAScript系列目录地址(持续更新中...)

欢迎前往阅读系列文章,如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

菜鸟一枚,如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误,与大家共同进步。

展开阅读全文

没有更多推荐了,返回首页