Skip to content

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

@logan70

Description

@logan70

一、函数的调用

全局环境的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 进行提问或勘误,与大家共同进步。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions