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

栏目: JavaScript · 发布时间: 5年前

内容简介:全局环境的this指向全局对象,在浏览器中就是我们熟知的window对象说到除箭头函数外的其他函数被调用时,会在其词法环境上绑定

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

程序是怎样跑起来的

程序是怎样跑起来的

[日] 矢泽久雄 / 李逢俊 / 人民邮电出版社 / 2015-4 / 39.00元

本书从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制、内存、数据压缩、源文件和可执行文件、操作系统和应用程序的关系、汇编语言、硬件控制方法等内容,目的是让读者了解从用户双击程序图标到程序开始运行之间到底发生了什么。同时专设了“如果是你,你会怎样介绍?”专栏,以小学生、老奶奶为对象讲解程序的运行原理,颇为有趣。本书图文并茂,通俗易懂,非常适合计算机爱好者及相关从业人员阅读。一起来看看 《程序是怎样跑起来的》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

随机密码生成器
随机密码生成器

多种字符组合密码

html转js在线工具
html转js在线工具

html转js在线工具