深刻JavaScript系列(四):完全搞懂this

1、函数的调用

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

说到this的种种状况,就离不开函数的调用,通常咱们调用函数,无外乎如下四种方式:github

  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的值将被隐式指定新构造出来的对象。

2、ECMAScript规范解读this

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

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

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

因此第一点中讲的三种状况都是显式或隐式的传入了thisArgument来做为this的绑定值。咱们来用伪代码模拟一下:ui

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。this

伪代码模拟:spa

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。因此有如下表现。prototype

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
复制代码

3、箭头函数中的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
复制代码

4、回调函数中的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
复制代码

5、总结

  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不过如此。

6、小练习

例子来自南波的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 进行提问或勘误,与你们共同进步。

相关文章
相关标签/搜索