引言
你是不是在判断this指向的时候,第一反应就是默认绑定、隐式绑定、显示绑定、new绑定等,其中还要区分各种情况,比如隐式绑定丢失等情况。传统的理解方式(默认绑定、隐式绑定、显示绑定、new绑定)虽然有效,但往往需要记忆很多规则和例外情况,那么有没有一种统一的方法论来快速的判定this指向呢?答案是:必须有啊!
如何快速判定this指向
关于如何判定this的指向,其实这个方法就藏在ECMAScript文档中,好了不卖关子了,直接揭晓答案,那就是Reference类型。如果你对这个类型很陌生,那么你就慢慢的看下去,如果你了解这种判定方法,那也可以温故而知新
ECMAScript 规范
ECMAScript 规范中定义了两大类类型:
-
Language Type(语言类型)
- 我们在开发中直接使用的类型 包括:number、string、boolean、object、function、undefined、null
-
Specification Type(规范类型)
- 用于描述 ECMAScript 语言结构和语言类型的底层实现,包括:Reference、List、Completion、Property Descriptor、Property Identifier、Lexical Environment、Environment Record
- 这些类型在代码中不可见,仅用于描述语言底层的行为逻辑
Reference 类型
Reference 是 ECMAScript 规范中定义的规范类型(Specification Type),用于描述变量、属性或方法调用的底层引用关系。它直接影响:
- this 的绑定
- 赋值操作
- delete 操作
Reference 的组成结构
| 组成部分 | 描述 | 示例(obj.fn) |
|---|---|---|
| base | 引用的基础对象(对象、环境记录或 undefined) | base = obj |
| referencedName | 引用的名称(字符串或 Symbol) | referencedName = "fn" |
| strict | 布尔值,表示是否在严格模式下 | strict = false |
| thisValue | 用于确定函数调用时的 this 值 |
thisValue 如何生成?
根据 Reference 的组成结构({ base, name, strict }),thisValue 的生成规则如下:
| 情况 | thisValue 的值 | 示例 |
|---|---|---|
base 是对象 | base 本身 | obj.fn() → this = obj |
base 是环境记录(变量) | undefined(严格模式)或 window | fn() → this = window |
注意: thisValue 是引擎内部的“this 计算器”,根据 Reference 的 base 快速确定 this。可以理解为它是base值映射成this绑定的结果,可以简单的将thisvalue值看成和base值的等价关系,故之后介绍只介绍base,referencedName、strict.
举个例子:
var a = 10
var obj = {
a: 20,
say(){
console.log(this.a)
}
}
obj.say()
obj.say()的Reference 结构为
{
base: obj, // 基础对象
referencedName: 'say', // 引用的属性名
strict: false // 非严格模式
}
代码输出结果为20,因为base为对象,thisValue等于base,所以obj.say()执行时的this就是obj,所以输出结果就是20.
Reference 类型生成规则?
- 计算 MemberExpression(如 obj.fn)得到 ref
- 判断 ref 类型:
- 如果是 Reference:
- IsPropertyReference(ref) = true ⇒ this = GetBase(ref)
- base 是环境记录 ⇒ this = ImplicitThisValue(ref)
- 如果不是 Reference ⇒ this = undefined/window
- 如果是 Reference:
什么是 MemberExpression? 在ECMAScript规范 13.3 Left-Hand-Side Expressions:最后的解释为: MemberExpression :
- PrimaryExpression // 原始表达式
- FunctionExpression // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
function test() {
console.log(this)
}
test(); // MemberExpression 是 test
function test() {
return function() {
console.log(this)
}
}
test()(); // MemberExpression 是 test()
var test = {
play: function () {
return this;
}
}
test.play(); // MemberExpression 是 test.play
总结一下 MemberExpression 就是()左边的部分,
实例分析:
var obj = {
name: 'obj',
fn: function() {
console.log(this);
}
};
obj.fn(); // this = obj
执行过程:
1. 计算 obj.fn,得到 ref(Reference 类型,base = obj)。
2. IsPropertyReference(ref)为 true(因为obj是对象)。
3. this = GetBase(ref)= obj。
var baz = obj.fn;
baz(); // this = undefined(严格模式)或 window(非严格模式)
执行过程:
1. baz 是一个变量,计算baz得到的是 函数本身(非 Reference 类型)。
2. 由于ref不是 Reference 类型,this = undefined(严格模式)或 window(非严格模式
(obj.fn)(); // this = obj
执行过程:
1. (obj.fn) 仍然是 obj.fn,ref 是 Reference 类型,base = obj。
2. IsPropertyReference(ref) 为true,this = obj。
(baz = obj.fn)(); // this = undefined(严格模式)或 window(非严格模式)
执行过程:
1. baz = obj.fn 是一个赋值表达式,返回的是函数本身(非 Reference 类型)。
2. ref不是 Reference 类型,this = undefined(严格模式)或 window(非严格模式)。
总结
| 情况 | MemberExpression 计算 | ref 类型 | IsPropertyReference | this 值 |
|---|---|---|---|---|
obj.fn() | obj.fn | Reference | true | obj |
baz() | baz(变量) | 非 Reference | - | undefined/window |
(obj.fn)() | obj.fn | Reference | true | obj |
(baz = obj.fn)() | 赋值表达式返回函数 | 非 Reference | - |
注意: Reference 判断规则只适用于普通函数,对于NEW和箭头函数等函数底层属性本来就有改变this属性的,上述说的MemberExpression 就是()左边的部分不适用
实战案例
光说不练假把式,趁着手热,找几道题试试
案例1
var obj = {
name: 'wsw',
fun: function(){
console.log(this.name);
}
}
obj.fun() // wsw
ref = {
base: obj, // 基础对象是 obj
referencedName: "fun", // 引用的属性名是 "fun"
strict: false // 非严格模式
}
| 操作 | Reference 视角 | 结果 |
|---|---|---|
obj.fun | { base: obj, referencedName: "fun" } | 生成 Reference |
IsPropertyReference | base 是对象 obj → true | this = obj |
obj.fun() | 执行 fun 时 this = obj | 输出 "wsw" |
案例2
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
输出结果: 10 2
执行步骤
1:fn() fn 的 Reference 类型
fn是直接作为参数传递的 函数本身,不是属性访问,因此:
ref_fn = fn // 非 Reference 类型(直接函数值)
-
this绑定规则:- 非严格模式:
this = window(全局对象)。 - 严格模式:
this = undefined。 - 输出:
this.length→window.length→10(全局变量)。
- 非严格模式:
2:arguments[0](): arguments[0] 的 Reference 类型
arguments[0] 是 fn,但通过 arguments 对象访问,其 Reference 结构为:
ref_arguments = {
base: arguments, // arguments 对象
referencedName: "0", // 属性名 "0"
strict: false
}
-
this绑定规则:IsPropertyReference(ref_arguments)为true(base是arguments对象)。this = GetBase(ref_arguments)→arguments。- 输出:
this.length→arguments.length→2(obj.method(fn, 1)传入了 2 个参数)。
3. 关键点总结
| 调用方式 | Reference 类型 | this 绑定 | 输出结果 |
|---|---|---|---|
fn() | 非 Reference(直接函数值) | window(非严格模式) | 10 |
arguments[0]() | { base: arguments, name: "0" } | arguments | 2 |
4. 为什么 arguments[0]() 的 this 是 arguments?
arguments[0] 是 属性访问(类似 obj.fn),因此生成 Reference 类型:
{
base: arguments, // arguments 对象
referencedName: "0",
strict: false
}
根据规则:
IsPropertyReference(ref)为true→this = GetBase(ref)→arguments。arguments.length是传入的参数个数(这里是2)。
案例3
var a = 1;
function printA(){
console.log(this.a);
}
var obj={
a:2,
foo:printA,
bar:function(){
printA();
}
}
obj.foo(); // 2
obj.bar(); // 1
var foo = obj.foo;
foo(); // 1
执行步骤
1:obj.foo()
-
obj.foo的 Reference 类型:ref_foo = { base: obj, // 基础对象是 obj referencedName: "foo", // 属性名是 "foo" strict: false // 非严格模式 } -
this绑定规则:-
IsPropertyReference(ref_foo)为true(base是对象obj)。 -
this = GetBase(ref_foo)→obj。 -
输出:
this.a→obj.a→2。
-
2:obj.bar()
-
obj.bar的 Reference 类型:ref_bar = { base: obj, referencedName: "bar", strict: false } -
执行
bar():-
this绑定到obj(规则同obj.foo())。 -
但
bar内部的printA()是 直接调用,其 Reference 类型:
ref_printA = printA // 非 Reference 类型(直接函数值) -
-
this绑定:- 非严格模式:
this = window(全局对象)。 - 严格模式:
this = undefined。 - 输出:
this.a→window.a→1(全局变量)。
- 非严格模式:
3:foo()
-
foo的赋值:-
var foo = obj.foo,foo直接引用printA函数。 -
foo是 独立变量,没有base对象,因此:
-
ref_foo = {
base: Global Environment Record, // 全局环境记录
referencedName: "foo",
strict: false
}
- 执行
foo():foo是全局变量,this由ImplicitThisValue(ref_foo)决定:- 非严格模式:
this = window。 - 严格模式:
this = undefined。
- 非严格模式:
- 输出:
this.a→window.a→1(全局变量)。
3. 关键点总结
| 调用方式 | Reference 类型 | this 绑定 | 输出结果 |
|---|---|---|---|
obj.foo() | { base: obj, name: "foo" } | obj | 2 |
obj.bar() | bar 的 this 是 obj,但内部的 printA() 是直接调用 | window | 1 |
foo() | { base: 全局环境记录 } | window | 1 |
4. 为什么 obj.bar() 输出 1?
-
bar虽然是obj的方法,但 bar内部的printA()是独立调用:printA()没有通过对象调用(如obj.printA()),因此this绑定到全局对象。
-
对比:
obj.foo()是 方法调用 →this = obj。printA()是 函数调用 →this = window。
5. 最终输出
obj.foo(); // 2(this = obj)
obj.bar(); // 1(this = window)
foo(); // 1(this = window)
6. 总结
- 方法调用(
obj.fn()):this绑定到obj(Reference 的base是对象)。 - 函数调用(
fn()):this绑定到全局对象(非严格模式)。 - 赋值后调用(
var x = obj.fn; x()):this丢失原始绑定,变为全局对象。
案例4
function foo(something){
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4)
console.log(obj1.a); // 2
console.log(bar.a); // 4
-
obj1.foo的 Reference 类型ref = { base: obj1, referencedName: "foo", strict: false }
this绑定规则-
IsPropertyReference(ref)为true→this = GetBase(ref)→obj1。 -
执行
foo(2):this.a = 2→obj1.a = 2。 -
输出:
console.log(obj1.a)→2。
-
-
obj1.foo.call(obj2, 3)执行过程-
call的作用:强制将foo的this绑定到obj2。 -
执行
foo.call(obj2, 3):this = obj2→obj2.a = 3。 -
输出:
console.log(obj2.a)→3。
-
new obj1.foo(4)(new 绑定)
-
new的作用:创建一个新对象bar,并将foo的this绑定到bar。 - 执行
new foo(4):this = bar(新创建的对象)→bar.a = 4。obj1.a未被修改(仍然是2)。
- 输出:
console.log(obj1.a)→2(未变)。console.log(bar.a)→4。
| 调用方式 | this 绑定规则 | this 的值 | 修改的对象 | 输出结果 |
|---|---|---|---|---|
obj1.foo(2) | 方法调用 | obj1 | obj1.a = 2 | obj1.a = 2 |
foo.call(obj2, 3) | call | obj2 | obj2.a = 3 | obj2.a = 3 |
new obj1.foo(4) | new 绑定 | bar | bar.a = 4 | bar.a = 4 |
案例5
var x = 3;
var y = 4;
var obj = {
x: 1,
y: 6,
getX: function() {
var x = 5;
return function() {
return this.x;
}();
},
getY: function() {
var y = 7;
return this.y;
}
}
console.log(obj.getX()) // 3
console.log(obj.getY()) // 6
执行步骤
-
调用
obj.getX()-
obj.getX的 Reference 类型:ref_getX = { base: obj, referencedName: "getX", strict: false } -
执行
getX():-
this绑定到obj(方法调用)。 -
在
getX内部:-
定义局部变量
x = 5(不影响全局或对象属性)。 -
返回一个 立即执行的匿名函数:
function() { return this.x; // this 绑定规则? }()
-
-
-
匿名函数的
this:-
匿名函数是 直接调用(不是方法调用),因此:
- 非严格模式:
this = window(全局对象)。 - 严格模式:
this = undefined。
- 非严格模式:
-
this.x→window.x→3(全局变量x)。
-
-
输出:
console.log(obj.getX())→3。
-
-
调用
obj.getY()-
obj.getY的 Reference 类型:ref_getY = { base: obj, referencedName: "getY", strict: false } -
执行
getY():-
this绑定到obj(方法调用)。 -
在
getY内部:- 定义局部变量
y = 7(不影响全局或对象属性)。 this.y→obj.y→6。
- 定义局部变量
-
-
输出:
console.log(obj.getY())→6。
-
- 关键点总结
| 调用方式 | this 绑定规则 | this 的值 | 访问的 x 或 y | 输出结果 |
|---|---|---|---|---|
obj.getX() | 匿名函数是直接调用 | window | window.x(全局) | 3 |
obj.getY() | 方法调用 | obj | obj.y | 6 |
4. 为什么 obj.getX() 输出 3?
- 匿名函数
function() { return this.x; }()是 独立调用,this绑定到全局对象。 - 全局
x = 3,因此this.x返回3。 - 注意:局部变量
x = 5不影响结果,因为它不是通过this访问的。
5. 为什么 obj.getY() 输出 6?
getY是方法调用,this绑定到obj。obj.y = 6,因此this.y返回6。- 注意:局部变量
y = 7不影响结果,因为它不是通过this访问的。
案例6
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo);
console.log(self.foo);
(function() {
console.log(this.foo);
console.log(self.foo);
}());
}
};
myObject.func();
分步解析
1:myObject.func() 的 Reference 分析
-
计算
myObject.func:myObject.func是属性访问,生成 Reference 类型:```js ref_func = { base: myObject, // 基础对象是 myObject referencedName: "func", // 引用的属性名 strict: false // 非严格模式 } ``` -
this绑定:IsPropertyReference(ref_func)为true(base是对象myObject)。this = GetBase(ref_func)→myObject。
2:执行 func 函数
-
var self = this:this是myObject→self也指向myObject。 -
输出
this.foo和self.foo:console.log(this.foo):this是myObject→myObject.foo→"bar"。console.log(self.foo):self是myObject→myObject.foo→"bar"。
3:立即执行函数的 Reference 分析
-
立即执行函数的
this:-
匿名函数是 直接调用(
(function() { ... }())),没有通过对象调用。 -
其 Reference 类型:
ref_anon = 匿名函数本身 // 非 Reference 类型(直接函数值) -
this绑定:- 非严格模式:
this = window。 - 严格模式:
this = undefined。
- 非严格模式:
-
console.log(this.foo):this是window→window.foo→undefined。
-
-
立即执行函数的
self:self是外层函数func的变量,通过闭包保留了对myObject的引用。console.log(self.foo):self是myObject→myObject.foo→"bar"。
4. 关键点总结
| 代码位置 | Reference 类型 | this 绑定 | 输出结果 |
|---|---|---|---|
myObject.func() | { base: myObject, name: "func" } | myObject | "bar" |
console.log(this.foo) | - | myObject | "bar" |
console.log(self.foo) | - | myObject(通过闭包) | "bar" |
立即执行函数 this.foo | 非 Reference(直接函数值) | window | undefined |
立即执行函数 self.foo | - | myObject(通过闭包) | "bar" |
5. 为什么立即执行函数的 this 是 window?
-
立即执行函数是 独立调用,其 Reference 类型是 非 Reference(直接函数值)。
-
根据规则:
- 非严格模式:
this默认绑定到全局对象(window)。 - 严格模式:
this = undefined。
- 非严格模式:
-
window.foo未定义,因此输出undefined。
6. 为什么 self 仍然指向 myObject?
self是外层函数func的局部变量,通过闭包被内部函数引用。- 闭包保留了
self的值(myObject),因此self.foo始终访问myObject.foo。
案例7
var obj = {
say: function() {
var f1 = () => {
console.log("1111", this);
}
f1();
},
pro: {
getPro:() => {
console.log(this);
}
}
}
var o = obj.say;
o();
obj.say();
obj.pro.getPro();
分步解析
1:调用o()
-
o的赋值:-
var o = obj.say,o直接引用obj.say的函数。 -
o是一个 独立变量,没有base对象,因此:ref_o = { base: Global Environment Record, // 全局环境记录 referencedName: "o", strict: false }
-
-
执行
o():-
o()是 独立函数调用,this由ImplicitThisValue(ref_o)决定:- 非严格模式:
this = window。 - 严格模式:
this = undefined。
- 非严格模式:
-
say内部的箭头函数f1继承say的this(即window)。
-
-
输出:
console.log("1111", this)输出1111 window。
2: 调用obj.say()
-
计算
obj.say的 Reference:ref_say = { base: obj, referencedName: "say", strict: false } -
this绑定:IsPropertyReference(ref_say)为true(base是对象obj)。this = GetBase(ref_say)→obj。
-
执行
say():箭头函数f1继承say的this(即obj)。 -
输出:
console.log("1111", this)输出1111 obj。
3:调用obj.pro.getPro()
-
计算
obj.pro.getPro的 Reference:ref_getPro = { base: obj.pro, // pro 是对象 referencedName: "getPro", strict: false } -
箭头函数的
this绑定:getPro是箭头函数,this由词法作用域决定。getPro定义在pro对象中,但pro不是函数作用域,因此this继承外层全局作用域的this(即window)。
-
输出:
console.log(this)输出window。
4. 关键点总结
| 调用方式 | 函数类型 | this 绑定规则 | this 的值 | 输出结果 |
|---|---|---|---|---|
o() | 普通函数 | 独立调用 ⇒ this = window | window | 1111 window |
obj.say() | 普通函数 | 方法调用 ⇒ this = obj | obj | 1111 obj |
obj.pro.getPro() | 箭头函数 | 词法作用域 ⇒ this = window | window | window |
5. 为什么 obj.pro.getPro() 的 this 是 window?
- 箭头函数
getPro的this在定义时确定,而pro对象只是一个普通对象,不是函数作用域。 - 因此
getPro的this继承自全局作用域(window),与obj.pro无关。
Reference 类型与函数 this 绑定的终极总结
1. Reference 类型的核心作用
-
概念:ECMAScript 规范中的抽象类型,描述变量/属性/方法的引用关系。
-
结构:
{ base, referencedName, strict }base:基础对象(对象、环境记录或undefined)referencedName:引用的名称(字符串或 Symbol)strict:是否严格模式
2. 普通函数的 this 绑定规则
| 调用方式 | Reference 类型 | this 绑定规则 | 示例 |
|---|---|---|---|
方法调用 obj.fn() | { base: obj, name: "fn" } | this = GetBase(ref) → obj | obj.fn() → this=obj |
独立调用 fn() | { base: 环境记录 } | this = window(非严格模式) | let x=obj.fn; x() → this=window |
| call/apply/bind | - | 显式指定 this | fn.call(obj) → this=obj |
| new 调用 | - | this = 新创建的对象 | new Fn() → this=newObj |
3. 箭头函数的特殊性
-
规则:
this继承定义时的外层作用域,完全无视 Reference 规则 -
示例分析:
const obj = { fn: () => console.log(this) // this 继承全局作用域(window) } obj.fn(); // 输出 window(而非 obj)
4. 匿名函数与自执行函数
| 类型 | this 绑定规则 | 典型场景 | 示例输出 |
|---|---|---|---|
| 匿名函数 | 同普通函数(取决于调用方式) | 作为回调函数时可能丢失 this | setTimeout(function(){ console.log(this) }, 100) → window |
| 自执行函数 | 默认绑定到 window | 立即执行的独立作用域 | (function(){ console.log(this) })() → window |
| 闭包中的匿名函数 | 通过变量保留外层 this | 解决回调函数 this 丢失问题 | let self=this; function(){ console.log(self) } |
5. 经典对比案例
const obj = {
a: 1,
normalFn: function() { console.log(this.a) },
arrowFn: () => console.log(this.a)
};
// 场景1:直接调用
obj.normalFn(); // 1(Reference.base=obj)
obj.arrowFn(); // undefined(继承全局)
// 场景2:间接调用
const fn1 = obj.normalFn;
const fn2 = obj.arrowFn;
fn1(); // undefined(base=环境记录)
fn2(); // undefined(箭头函数已固化this)