深入ECMAScript系列(四):彻底搞懂this
栏目: JavaScript · 发布时间: 5年前
内容简介:全局环境的this指向全局对象,在浏览器中就是我们熟知的window对象说到除箭头函数外的其他函数被调用时,会在其词法环境上绑定
全局环境的this指向全局对象,在浏览器中就是我们熟知的window对象
说到 this
的种种情况,就离不开函数的调用,一般我们调用函数,无外乎以下四种方式:
-
普通调用,例如
foo()
。 -
作为对象方法调用,例如
obj.foo()
。 -
构造函数调用,例如
new foo()
。 -
使用
call
、apply
、bind
等方法。
除箭头函数外的其他函数被调用时,会在其词法环境上绑定 this
的值,我们可以通过一些方法来指定 this
的值。
-
使用
call
、apply
、bind
等方法来显式指定this
的值。function foo() { console.log(this.a) } foo.call({a: 1}) // 输出: 1 foo.apply({a: 2}) // 输出: 2 // bind方法返回一个函数,需要手动进行调用 foo.bind({a: 3})() // 输出: 3 复制代码
-
当函数作为对象的方法调用时,
this
的值将被隐式指定为这个对象。let obj = { a: 4, foo: function() { console.log(this.a) } } obj.foo() // 输出: 4 复制代码
-
当函数配合
new
操作符作为构造函数调用时,this
的值将被隐式指定新构造出来的对象。
二、ECMAScript规范解读this
上面讲了几种比较容易记忆和理解 this
的情况,我们来根据ECMAScript规范来简单分析一下,这里只说重点,一些规范内具体的实现就不讲了,反而容易混淆。
其实当我们调用函数时,内部是调用函数的一个
内置 [[Call]](thisArgument, argumentsList)
方法
,此方法接收两个参数,第一个参数提供 this
的绑定值,第二个参数就是函数的参数列表。
ECMAScript规范:严格模式时,函数内的 this
绑定严格指向传入的 thisArgument
。非严格模式时,若传入的 thisArgument
不为 undefined
或 null
时,函数内的 this
绑定指向传入的 thisArgument
;为 undefined
或 null
时,函数内的 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 复制代码
五、总结
-
箭头函数中没有
this
绑定,this
的值取决于其创建时所在词法环境链中最近的this
绑定 -
非严格模式下,函数普通调用,
this
指向全局对象 -
严格模式下,函数普通调用,
this
为undefined
-
函数作为对象方法调用,
this
指向该对象 -
函数作为构造函数配合
new
调用,this
指向构造出的新对象 -
非严格模式下,函数通过
call
、apply
、bind
等间接调用,this
指向传入的第一个参数这里注意两点:
-
bind
返回一个函数,需要手动调用,call
、apply
会自动调用 -
传入的第一个参数若为
undefined
或null
,this
指向全局对象
-
-
严格模式下函数通过
call
、apply
、bind
等间接调用,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解析改变
系列文章
欢迎前往阅读系列文章,如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
菜鸟一枚,如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误,与大家共同进步。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 【1】JavaScript 基础深入——数据类型深入理解与总结
- 深入理解 Java 函数式编程,第 5 部分: 深入解析 Monad
- 深入理解 HTTPS
- 深入理解 HTTPS
- 深入浅出Disruptor
- 深入了解 JSONP
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序是怎样跑起来的
[日] 矢泽久雄 / 李逢俊 / 人民邮电出版社 / 2015-4 / 39.00元
本书从计算机的内部结构开始讲起,以图配文的形式详细讲解了二进制、内存、数据压缩、源文件和可执行文件、操作系统和应用程序的关系、汇编语言、硬件控制方法等内容,目的是让读者了解从用户双击程序图标到程序开始运行之间到底发生了什么。同时专设了“如果是你,你会怎样介绍?”专栏,以小学生、老奶奶为对象讲解程序的运行原理,颇为有趣。本书图文并茂,通俗易懂,非常适合计算机爱好者及相关从业人员阅读。一起来看看 《程序是怎样跑起来的》 这本书的介绍吧!