对于前端工程师来说,或多或少都遇到过以下情况:在处理某一个回调方法时,用this去取某个值,例如this.a,结果却是undefined。大部分人都会第一时间想到,this的指向有问题,所以用self = this,或者func.bind(this),this.a就可以取到值了,this到底是什么呢?以下内容借鉴了《你不知道的JavaScript(上)》和一些自己的理解
常见误解
指向自身
这是个很常见的误解,JS开发者通常会有这么一个错误认知:因为函数是一个对象,在调用函数时,就可以存储属性的值,这样的方式从理论上来说是可行的,但是JS并没有这么设计看下面的代码
function foo(num){
console.log('foo:' + num);
this.count++;
}
foo.count = 0;
for(let i = 0; i < 10; i++) {
if(i > 5){
foo(i);
}
}
//foo:6
//foo:7
//foo:8
//foo:9
//0
console.log(foo.count);
很明显看出这种理解是错误的
指向函数的词法作用域
还有一种常见的误解是this指向函数的作用域,看下面的代码
function foo() {
a = 2;
bar();
}
function bar() {
console.log( "this.a:" + this.a );
}
foo();
//this.a: 2
这样写是可以打印出a的,这样很容易让人认为this是指向了foo的作用域, 这是错误的理解,之所以能打印出a是因为bar()进行了隐式绑定,this在任何情况下都不会指向函数的词法作用域,虽然说作用域也是一个对象,但是我们无法通过JS访问;
解决方法,this指向哪
很大一部分人遇到this指向问题时都会使用bind或者Function.call来解决;这种方法确实很有效只知道怎么解决不是我们的最终目的,我们需要弄懂为什么这么解决
this是什么(1)
this是在运行时绑定的,它的指向取决于函数调用的方式,在函数被调用时会产生一个执行上下文,其中包括函数调用栈,调用方法,传入参数等等,this就是其中的一个属性
调用位置
既然this是根据调用方式决定的,我们就需要知道什么是函数的调用位置
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置
函数的调用栈可以看做函数调用链,在开发中我们可以从开发工具中查看函数的调用栈(下图是vs Code的调用栈查看)来分析函数的调用栈

绑定规则
下面列出了一些绑定规则,可以根据函数的调用栈和下面的规则来分析this究竟指向什么
默认绑定
直接函数调用,可以把这条规则看作是无法应用其他规则时的默认规则,代码示例
function foo() {
console.log( this.a );
}
a = 2;
foo(); // 2
可以看到,这里的this.a被解析成了全局变量a,this指向了全局对象,这里是直接调用的foo,不带任何修饰的函数引用进行调用的,所以使用默认绑定规则;在严格模式下this无法使用默认绑定指向全局对象
function foo() {
"use strict";
console.log( this.a );
}
a = 2;
foo(); // 2
这里还有两个细节,只有在foo运行在严格模式下默认绑定才会失效
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
混用严格模式和非严格模式会有个有趣的现象,下面的代码两个a都可以打印出来,可以猜测,默认绑定是在第一次函数体中第一次调用this时执行的
function foo() {
console.log( this.a );
"use strict";
console.log( this.a );
}
var a = 2;
foo();
// 2
// 2
隐式绑定
考虑调用位置是否有上下文对象或者说是否被某个对象所包含,看以下代码
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
这种调用方式会引用obj的上下文,即this.a就是obj.a,下面一种情况会造成隐式绑定丢失
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名
a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
这种情况,bar()属于不带任何修饰的调用情况,适用于默认绑定;还有一种情况是在传入回调函数时
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
这种情况我们很容易遇到的回调函数this指向丢失的问题,在这里this指向了全局对象而不是obj,参数的传递其实是一种隐式赋值,所以和上个例子一样;这里只是丢失了this的绑定,还有一种可能是this绑定到了其他对象上;
显式绑定
有时我们需要让this强制指向某个对象,这样可以避免隐式绑定带来的问题,可以使用call(...)和apply(...)来实现,看下面的代码
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
这样可以把this强制绑定到obj上,这也是我们常用的一种做法;(如果你传入了一个原始值,则会进行装箱操作,(也就是new String(..)、new Boolean(..)或者new Number(..))来当作this的绑定对象。)这种做法有时候也无法解决我们的问题,比如说上面的隐式绑定丢失的问题,即使使用doFoo.call(obj,obj.foo),结果还是一样的,下面的方法就可以解决
硬绑定
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
};
function doFoo(fn) {
// fn 其实引用的是 foo
fn.call(obj); // <-- 调用位置!
}
a = "oops, global"; // a 是全局对象的属性
doFoo(foo); // 2
这样做的话,就可以让this绑定到obj上,这个例子和上个例子都是使用的call,但是从设计理念上看是不同的;这个就是我们使用的Function.prototype.bind的大致原理了(实际上bind方法很复杂),使用bind方法就是以下代码
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
};
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
a = "oops, global"; // a 是全局对象的属性
foo = foo.bind(obj);
doFoo(foo); // 2
API调用的“上下文”
JS的许多内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(...) 一样,确保你的回调函数使用指定的 this。
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到
obj [1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
new绑定
JS中的new操作符和其他语言不一样,JS中的构造函数其实是使用new操作符时被调用的普通函数,也就是说,JS中不存在构造函数,只有普通函数的构造调用,是用new来操作的,在使用new是会有以下操作
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[原型]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
使用new的情况
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
可以看到,new让this绑定到了一个新生成的对象上
优先级
默认绑定规则是其他都不适用的时候才会生效,所以考虑其他的
显示绑定和隐式绑定
先看先显式绑定和隐式绑定的优先级,看下面代码
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo:foo
};
var obj2 = {
a: 3,
foo:foo
}
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
可以看出显示绑定的优先级是高于隐式绑定的
硬绑定和 new绑定
new 和 call 不能同时使用,所以使用硬绑定来测试
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
可以看到 new bar(3)改变了bind所绑定的this
判断this
this的判断可以根据以下顺序
- 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象 var bar = new foo()
- 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是 指定的对象。 var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到 全局对象。 var bar = foo()
箭头函数
之前的规则可以判断大部分this的绑定,但是箭头函数比较例外,箭头函数是指=>所定义的,不是function定义的匿名函数,看下面的代码
function foo() {
return ()=>{
console.log(this.a);
}
}
var obj1 = {a:2};
var obj2 = {a:3};
var obj3 = {a:4};
var bar = foo.call(obj1);
bar.call(obj2); // 2
var bar2 = bar.bind(obj3);
bar2(); // 2
可以看到bar即使使用call或者bind方法都不可以改变this的绑定,箭头函数的this获取的调用时的this,不可更改(原书中说new调用也不可能更改,我试验的结果是new不可调用箭头函数)
追溯源头
为什么会产生这些this绑定的规则呢?我查阅了ECMA-262从中找到了一些定义可以解决大部分的疑问
this关键字做了什么
关于this的定义
12.2.2.1 Runtime Semantics: Evaluation
PrimaryExpression : this
1. Return ? ResolveThisBinding().
8.3.4 ResolveThisBinding ( )
The abstract operation ResolveThisBinding determines the binding of
the keyword this using the LexicalEnvironment of the running
execution context. ResolveThisBinding performs the following steps:
1. Let envRec be GetThisEnvironment().
2. Return ? envRec.GetThisBinding().
大概意思就是先找到 envRec,再取得envRec.GetThisBinding()
GetThisEnvironment
8.3.3 GetThisEnvironment ( )
The abstract operation GetThisEnvironment finds the Environment Record that currently supplies the binding of the keyword this. GetThisEnvironment performs the following steps:
1. Let lex be the running execution context's LexicalEnvironment.
2. Repeat,
a. Let envRec be lex's EnvironmentRecord.
b. Let exists be envRec.HasThisBinding().
c. If exists is true, return envRec.
d. Let outer be the value of lex's outer environment reference. e. Assert: outer is not null.
f. Set lex to outer.
循环取到envRec.HasThisBinding()为true的envRec并返回然后调用,HasThisBinding可以为true的有三种
Global Environment Records -> true
Module Environment Records -> true
Function Environment Records
这里我们重点看第三个
8.1.1.3.4 GetThisBinding ( )
1. Let envRec be the function Environment Record for which the method was invoked.
2. Assert:envRec.[[ThisBindingStatus]]isnot"lexical".
3. If envRec.[[ThisBindingStatus]] is "uninitialized", throw a ReferenceError exception.
4. Return envRec.[[ThisValue]].
这里说明了,取得是调用时的 function Environment Record,也就是说this是调用时绑定的
GetThisBinding
这里用来取得this的值,我们重点关注两个 Global Environment Records和Function Environment Records
Global Environment
8.1.1.4.11 GetThisBinding ( )
1. Let envRec be the global Environment Record for which the method was invoked.
2. Return envRec.[[GlobalThisValue]].
Function Environment Records
8.1.1.3.4 GetThisBinding ( )
1. Let envRec be the function Environment Record for which the method was invoked.
2. Assert:envRec.[[ThisBindingStatus]]isnot"lexical".
3. If envRec.[[ThisBindingStatus]] is "uninitialized", throw a ReferenceError exception.
4. Return envRec.[[ThisValue]].
可以看到Global Environment也就是全局作用域,会有[[GlobalThisValue]],
Function Environment Records有[[ThisValue]];
这就是我们使用this所取到的
[[thisValue]]
我们来简单看下Function Environment Records的ThisValue是什么,ThisValue是调用 8.1.1.3.1 BindThisValue ( V )来设置的,bindThisValue在标准里有两处提到了,这里我们看其中一处(9.2.1.2OrdinaryCallBindThis)
OrdinaryCallBindThis
这个是this的绑定,这里摘出一部分,具体的可以自己查阅ECMA-262的标准
5. If thisMode is strict, let thisValue be thisArgument.
6. Else,
a. If thisArgument is undefined or null, then
i. Let globalEnv be calleeRealm.[[GlobalEnv]].
ii. Let globalEnvRec be globalEnv's EnvironmentRecord. iii. Assert: globalEnvRec is a global Environment Record. iv. Let thisValue be globalEnvRec.[[GlobalThisValue]].
b. Else,
i. Let thisValue be ! ToObject(thisArgument).
ii. NOTE: ToObject produces wrapper objects using calleeRealm.
...
10. Return envRec.BindThisValue(thisValue).
这里提到了thisMode is strict我猜测应该是严格模式下,直接使用thisArgument;非严格模式下,如果是undefined 或 null 使用globalEnvRec.[[GlobalThisValue]],否则Let thisValue be ! ToObject(thisArgument);这里应该就是严格模式无法默认绑定,默认绑定会绑定到全局对象,以及原始值装箱操作的原因了
OrdinaryCallBindThis 在 9.2.2 [[Construct]] ( argumentsList, newTarget ) 和 9.2.1 [[Call]] ( thisArgument, argumentsList )中有调用,这里应该是Function Objects的call 和 构造调用
总结
关于this的指向问题,本文简单介绍了下,应该可以应付日常工作中所遇到的问题,JS中还有很多的复杂的机制和特性需要去深入学习,本文借鉴了Kyle Simpson大神的《你不知道的JavaScript(上卷)》还包括一些自己搜集的知识和自己的认知,对于ECMA-262标准的理解有些不准确的地方,希望大佬指正