<>彻底搞懂JavaScript中this指向

在面向对象的编程语言中几乎都有类似this的关键字,如Java、C++、Swift中的this,Python、Object-C中的self,但是Javascript中的this和它们还不太一样,面向对象的编程语言中this通常会出现在类方法中,代表着当前调用对象,而Javascript中的this则更加灵活复杂。JavaScript中this是一个很特别的关键字,被自动定义在所有函数的作用域中,MDN中提到JavaScript的this是当前执行上下文的一个属性,在非严格模式下,总是指向一个对象,在严格模式下可以是任意值。那么,JavaScript中到底为什么要用到this?它的this又是以一种什么样的机制进行绑定的呢?

<>一、为什么使用this

我们看下面这样一段代码,如果没有this,我们想要获取到这个对象的的名字必须通过对变量的引用Tom.name
来实现,这种显式引用的方法显然是不合适的,如果有一天需要对变量名称进行修改,那么对象中的引用也必须全部进行修改。
var Tom = { name: 'Tom', speaking: function () { console.log(`${Tom.name} say
boh`) }, language: function () { console.log(`${Tom.name}'s mother tongue is
Chinese`) }, hobby: function () { console.log(`${Tom.name} love eating,
sleeping and playing peas`) }, }
当我们有了this以后,完全可以以一种更加优雅的方式进行引用,使得代码更加简洁,复用性更强。实际上,this给我们带来的便利远不止这么简单。
var Tom = { name: 'Tom', speaking: function () { console.log(`${this.name} say
boh`) }, language: function () { console.log(`${this.name}'s mother tongue is
Chinese`) }, hobby: function () { console.log(`${this.name} love eating,
sleeping and playing peas`) }, }
<>二、this到底指向什么

<>2.1 两个误解

<>2.1.1 误解一:指向自身

以下面这个计数器为例:输出的结果是0而不是10,显然在这个例子中this并不指向它自身
function counter() { this.count++ } counter.count = 0 for (let i = 0; i < 10; i
++) { counter(i) } console.log(counter.count) // 0
<>2.1.2 误解二: 指向其作用域

以下面这个函数嵌套调用为例:输出的结果是undefined,而并没有输出作用域上的值
function func1() { var a = 1 func2() } function func2() { var a = 2 console.log
(this.a) } func1() // undefined
<>2.2 this绑定机制

事实上,this是在函数运行时进行绑定的,它和this的定义位置没用关系,但是和this的调用位置和调用方式有关,他有四种绑定方式,即
默认绑定、显式绑定、隐式绑定、new绑定。

<>2.2.1 默认绑定

在函数独立调用时会使用到默认绑定规则,可以认为它是不符合其他绑定规则之后最后的决定,在非严格模式下,默认绑定会将this绑定到全局变量window
上,而在严格模式中,this会绑定到undefined。(注:此处不考虑nodejs)
var a = 1 function func1() { console.log(this.a) } function func2() { 'use
strict' console.log(this.a) } console.log(this) func1() // 1 func2() //
TypeError
<>2.2.2 隐式绑定

如果函数是通过某个对象进行调用,那么this会指向调用函数的对象。
let obj = { showThis: function () { console.log(this===obj) } } obj.showThis()
// true
那么如果进行了链式调用呢?看下面这个例子,显而易见,在这种链式调用中只用最后一层调用在起作用。
let obj = { showThis: function () { console.log(this===obj) }, } let obj2 = {
obj: obj, } obj2.obj.showThis() //true
当然不是通过这种.调用的方式就会绑定到.前面的对象上,值得注意的是,有时候会存在隐式丢失的情况,下面这个例子中输出的是window而不是obj
,因为在进行传递时丢失了隐式绑定,传入的可以看作是函数本身,和obj没有任何关系。
function func1() { console.log(this) } var obj = { func: func1, } var func2 =
obj.func func2() // window
<>2.2.3 显式绑定

隐式绑定有诸多的限制条件,在对象的内部必须有一个指向函数的属性
,并通过这个属性间接的引用函数,从而把this绑定到对象上,那么如果我们不想在对象的内部包含这样一个函数引用,又想把this绑定到这个对象上进行函数调用该怎么办呢?这个时候可以使用**
call、apply或者bind方法**进行强制绑定,需要注意的是,这里的绑定和对象的[[Prototype]]相关,因此箭头函数是无法进行显式绑定的。
function func() { console.log(this); } func.call(window); // window func.apply(
{}); // {} func.bind(1)(); // Number {1} 这里是数字类型的对象1
这里使用bind绑定又叫做硬绑定,可以有效的解决隐式绑定丢失的问题,其已在JavaScript内部实现,其内部实现原理非常简单:
function bind(func, obj) { return function() { return func.apply(obj, arguments
); } }
在很多地方中,都会给出多一个参数进行显式绑定,如setTimeout、forEach等,其内部也都是通过call或apply
实现的,可以有效的帮助我们少些点代码

<>2.2.4 new绑定

使用new关键字进行函数调用通常被称为构造函数调用,实际上在JavaScript中除了箭头函数外都可以通过这种方法调用,调用时会自动执行以下操作:

* 创建一个全新的对象
* 这个新对象会被执行Prototype连接
* 这个新对象会绑定到函数调用的this上
* 如果函数没有返回其他对象,表达式会自动返回这个新对象 function Person(name) { console.log(this); this.
name= name; } var tom = new Person('Tom'); // Person {} console.log(tom); //
Person {name: 'Tom'}
<>三、绑定优先级

如果一个函数调用位置、方法中使用了上述四条规则中的多种会使用那种优先级呢?

<>3.1 优先级是什么

答案是:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

<>3.2 显式绑定 > 隐式绑定

输出的是obj2说明显式绑定生效了,隐式绑定没有生效
function foo() { console.log(this); } var obj1 = { foo: foo } var obj2 = { foo:
foo} obj1.foo.call(obj2); // function foo() { console.log(this) } var obj1 = {
name: 'obj1', foo: foo, } var obj2 = { name: 'obj2', foo: foo, } obj1.foo.call(
obj2) // {name: 'obj2', foo: ƒ}, 说明 显式绑定 > 隐式绑定
<>3.3 new绑定 > 显式绑定

new与call、apply是同时使用在JavaScript中时不允许的,会报错TypeError
,但是可以和bind硬绑定后的函数同时使用,这里输出的是foo,说明new绑定生效了
function foo() { console.log(this) } var obj = { name: "obj" } var bar = foo.
bind(obj) var baz = new bar() // foo {}, 说明 new绑定 > 显式绑定
<>四、永远有一些意外

<>4.1 被忽略的显式绑定

如果在显示绑定中,传入一个null或者undefined,那么这个显示绑定就会被忽略,使用默认绑定。
function foo() { console.log(this) } foo.call(null) // window
那么如果就是想不把this绑定到任何对象上我们该怎么做呢?显式绑定一个空对象即可,注意是空对象不是null,它的创建方法为:
Object.create(null)

<>4.2 间接引用

在使用函数的间接引用时也会使用默认绑定规则,什么是间接引用可以先从简单的例子看起:
var num1 = 2 var num2 = 3 var res = (num2 = num1) console.log(num1, num2, res)
// 2 2 2
对于函数,同样是这种形态,称之为间接引用
function foo() { console.log(this) } var obj1 = { name: "obj1", foo: foo }; var
obj2= { name: "obj2" } obj1.foo() (obj2.foo = obj1.foo)(); // window
<>五、总结

那么,我们最终如何判断this指向呢?通常通过以下几步即可:

* 判断是否为箭头函数,箭头函数的this总是指向其最近的执行上下文
* 判断是否为new绑定,new绑定优先级最高,this指向new出来的对象
* 判断是否为显式绑定,this指向绑定的对象
* 判断是否为隐式绑定,this通常指向.前面那个调用函数的对象
* 最后考虑是否为默认绑定,根据JavaScript执行模式判断为window或这undefined
特殊情况需考虑隐式丢失、显式绑定null、间接引用情况

参考书籍:《你不知道的JavaScript上卷》

技术
下载桌面版
GitHub
百度网盘(提取码:draw)
Gitee
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:766591547
关注微信