理解javascript中的this指向
前言:本文主要对javascript当中的this指针问题进行全面的分析和理解。熟悉js的同学们应该知道它是一门以词法作用域为基础的语言,而js当中的this指针却表现出不同于词法作用域下变量查找规则的行为。本文是在阅读《你不知道的javascript》一书后,结合个人理解与实践得到的较为全面的总结,包含了this的一些常规使用场景和一些特殊情况。
一:词法作用域
词法作用域又称静态作用域,javascript是一门基于词法作用域的语言,因此深入理解什么是词法作用域是理解js查找变量规则的基础。首先参考以下代码:
let a = 1;
function func1(){
console.log(a);
}
function func2() {
let a = 2;
func1();
}
func2();
打印出来的a的值会是多少呢?答案是1,对词法作用域不熟悉的同学可能会有疑问,离func1最近的是 a=2
,可为什么打印的是1?
词法作用域,顾名思义,根据词法来确定变量所属的作用域。我们将作用域看作一个容器,上述代码可以理解为:
- 全局作用域:包含变量 a、func1、func2、可能还有global
- func1作用域:包含变量 func1
- func2作用域: 包含变量 func2、a
执行过程如下:
- 执行func2,将func2压入调用栈,声明变量 a = 2
- 查找func1,由于func2的作用域下没有func1,因此会向上层作用域也就是全局作用域查找func1
- 执行func1,将func1压入调用栈,由于func1中没有变量 a ,因此将会继续向上层作用域也就是全局作用域(而非func2的作用域)查找,发现 a = 1,打印1
- 执行结束 func1出栈 func2 出栈
以上就是整个程序的执行以及变量查找的过程,一言以蔽之:**javascript的作用域是在代码编写时就决定的,而非运行时决定。**与之相对应的 动态作用域
在本例中就会打印出2而非1,动态作用域的具体细节还请读者们自行探索,在这里不过多赘述。
二:this 的绑定规则
this是javascript中一个非常特殊的变量,它并不遵循词法作用域的规则。在说明this绑定规则之前,我们首先要明白调用位置的概念
,仍然理解以下代码:
let a = 1;
function func1(){ <---- func1定义的位置
console.log(a);
}
function func2() {
let a = 2;
func1(); <---- func1执行的位置
}
func2();
调用位置很容易理解,既函数在何处被压如调用栈。而js中的this绑定主要依赖的就是函数的执行位置。this绑定遵循以下规则:
1. 默认绑定
独立函数调用在非严格模式下默认为this绑定全局变量。
var a = 1; // 这里注意如果用let声明则不会将a绑定到全局变量上,node环境下var也不会将a绑定到全局变量上。
function func1() {
console.log(this.a)
}
func1();
2. 隐式绑定
使用对象进行函数调用时,将为this隐式绑定该对象。链式调用中将绑定最后一个对象
function foo() {
console.log(this.a)
}
let obj = {
a: 1,
foo,
}
obj.foo(); // 1
obj.obj2 = Object.assign(obj);
obj.obj2.a = 2;
obj.obj2.foo(); // 2
3. 显式绑定
显示绑定分为两种,间接绑定和硬绑定。间接绑定只在调用时生效,而且必须通过Function.prototype.apply/call进行。
function foo() {
console.log(this.a)
}
let obj = {
a: 1,
}
foo.call(obj); // 1
foo.apply(obj); // 1
foo(); // undefined
硬绑定通过Function.prototype.bind函数实现,且会永久改变this绑定,bind函数返回一个将this硬绑定为传入对象的函数。
function foo() {
console.log(this.a)
}
let obj = {
a: 1,
}
const foo2 = foo.bind(obj); // 返回一个this硬绑定为obj的函数
foo2(); // 1 ,即使在全局作用域下直接调用foo2,this也指向obj
foo(); // undefined 原函数中的this并没有改变
4. new
在ES5以前其实没有构造函数的概念,在ES6加入了class关键字后才真正有了构造函数的概念。ES5以前的构造函数其实和普通函数是一样的。真正带来不同的是new关键字。在使用new关键字创建对象时的过程如下:
- 创建一个空对象
- 将函数中的this绑定为该对象
- 将该对象的原型指向构造函数的原型。
- 如果构造函数没有返回值,则默认返回该对象。
其实通过new关键字调用构造函数的过程,在es5之前应该叫做函数的构造调用而非构造函数的调用。
三: 绑定优先级
this绑定存在多种规则同时存在的情况,那必然也存在优先级的高低,否则具体场景下我们就无法判断到底遵循哪一个规则。首先直接给出结论:
new > 显式绑定 > 隐式绑定 > 默认绑定
由于优先级的判断是通过测试证明得出的,在这里就只对显式绑定和隐式绑定进行证明,感兴趣的读者可以自行证明其他规则。
function foo() {
console.log(this.a)
}
let obj = {
a: 1,
}
const foo2 = foo.bind(obj);
foo2(); // 1
foo(); // undefined
let obj2 = {
a: 3,
foo: foo2
}
obj2.foo(); // 1 因为硬绑定属于显式绑定,它比隐式绑定的优先级高,因此this仍指向obj。
然而凡事都有例外,例外虽有但极少,下面仅介绍一种经常遇到的典型场景。
function foo() {
console.log(this.a);
}
var a = 1;
foo.call(null); // 1,如果传入null,仍然采用的是默认绑定
结语:
由于javascript基于词法作用域的特点,很多初学者甚至是有一些经验的开发者在没有遇到过特殊情况前一直认为this也遵循词法作用域的特点(作者在写下这篇文章前也是这么认为的)。然而事实是this更偏向动态作用域,因为其绑定规则依赖于具体调用的位置。
this指向的判断遵循以下规则(优先级从高到低):
- 是否为构造调用(new),如果是,绑定为new创建的对象
- 是否通过apply call bind进行显式绑定
- 是否通过xxx.yyy()的方式调用,是则采用隐式绑定
- 是否直接调用,如果是,采用默认绑定