- this值在进入上下文的时候确定,并且在上下文运行期间永久保持不变。
- 全局代码中的this:
- 函数代码中的this:
- 所以对于this的理解,下面从应用角度和规范角度分别去理解。
- 1. 应用角度(一般情况)
- 2. 规范角度
js在执行一段可执行代码时,会创建对应的执上下文,对于每个执行上下文,都有三个重要属性:
- 变量对象
- 作用域链
- this
当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this
就指向这个执行上下文。
this值在进入上下文的时候确定,并且在上下文运行期间永久保持不变。
全局代码中的this:
在全局代码中,this指向全局对象本身。
函数代码中的this:
首先,在通常情况下,this是由激活上下文代码的调用中提供的,即调用函数的父上下文。
但是像以下这种就不适用以上判断:
var foo = {
bar: function () {
console.log(this);
}
};
// foo.bar()
(false || foo.bar)(); // global
所以,为了充分理解this值,不能仅仅通过调用函数的父上下文来决定,还需要根据js的一种规范去判定。 ———- 引用类型(Reference Type)
所以对于this的理解,下面从应用角度和规范角度分别去理解。
1. 应用角度(一般情况)
在js中,this的绑定规则,大致可以分为以下5种。
- 默认绑定
- 隐式绑定
- 显式(硬)绑定
new
绑定ES6
箭头函数绑定
1.1 默认绑定
默认绑定,通常是函数独立调用,不包含其他规则。非严格模式下,this指向window,严格模式下指向undefined。
例子1:
let a = 1;
const b = 2;
var c = 3;
function print() {
console.log(this.a); // undefined
console.log(this.b); // undefined
console.log(this.c); // 3
}
print();
console.log(this.a); // undefined
为什么a和b没有被正常打印,这是因为let
和 const
声明的变量,会生成块级作用域,并且存在暂时性死区( temporal dead zone,简称TDZ ),并没有挂载到window
对象上。
例子2:
a = 1;
function foo() {
console.log(this.a);
}
const obj = {
a: 10,
bar() {
foo();
}
}
obj.bar();
上述代码打印结果是1,原因是foo
虽然在obj
的bar
函数中,但foo
函数仍然是独立运行的,foo
中的this
依旧指向window
对象。
例子3:
a = 1;
(function(){
console.log(this);
console.log(this.a)
}())
function bar() {
b = 2;
(function(){
console.log(this);
console.log(this.b)
}())
}
bar();
默认情况下,自执行函数的
this
指向window
1.2 隐式绑定
函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是XXX.func()这种调用模式。
例子1:
a = 1
var obj = {
a: 2,
foo() {
console.log(this.a)
}
}
let foo = obj.foo;
foo();
打印结果是1。
js
的引用类型,其地址是存放于栈内存中的,本体是存放于堆内存中。上述代码,将obj.foo
赋值给foo
,然后直接调用,相当于直接运行堆内存中的方法,和obj无关了。
不要把这里理解成
window.foo
执行,如果foo
为let/const
定义,foo
不会挂载到window
上,但不会影响最后的打印结果
例子2:
name = 'javascript' ;
let obj = {
name: 'obj',
A (){
this.name += 'this';
console.log(this.name)
},
B(f){
this.name += 'this';
f();
},
C(){
setTimeout(function(){
console.log(this.name);
},1000);
}
}
let a = obj.A;
a(); // 直接调用,和obj无关
obj.B(function(){
console.log(this.name); // 形参传入,导致调用关系丢失,和obj无关
});
obj.C(); // setTimeout中,this指向window
console.log(name); // name已经被修改
上述打印结果都是javascriptthis。
1.3 显式绑定
通过call()、apply()、bind()
等方法,强行改变this
指向。
var obj = {
a: 'obj',
foo: function () {
console.log('foo:', this.a)
return function () {
console.log('inner:', this.a)
}
}
}
var a = 'window'
var obj2 = { a: 'obj2' }
obj.foo()() // foo:obj inner:window
obj.foo.call(obj2)() // foo: obj2 inner:window
obj.foo().call(obj2) // foo:obj inner: obj2
1.4 new绑定
使用new
来构建函数,会执行如下四部操作:
- 创建一个空的简单
JavaScript
对象(即{}
); - 为步骤1新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
经典例子:
function Foo(){
getName = function(){ console.log(1); };
return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){ console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2 相当于new (Foo.getName)()
new Foo().getName(); // 3 (new Foo()).getName()
new new Foo().getName(); // 3 相当于 new ((new Foo()).getName())()
1.5 箭头函数
箭头函数没有自己的this
,它的this
指向外层作用域的this
,且指向函数定义时的this
而非执行时。
this指向外层作用域的this
: 箭头函数没有this
绑定,但它可以通过作用域链查到外层作用域的this
;指向函数定义时的this而非执行时
:JavaScript
是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。
例子1:
name = 'tom'
const obj = {
name: 'xwk',
intro: () => {
console.log('My name is ' + this.name)
}
}
obj.intro()
打印结果是,My name is tom。原因箭头函数intro
外层的作用域是window
,所有this指向window
。
例子2:
name = 'tom'
const obj = {
name: 'xwk',
intro: function () {
return () => {
console.log('My name is ' + this.name)
}
},
intro2:function () {
return function() {
console.log('My name is ' + this.name)
}
},
intro3:function() {
console.log('My name is ' + this.name)
}
}
obj.intro2()() // My name is tom
obj.intro3() // My name is xwk
obj.intro()() // My name is xwk
2. 规范角度
ECMAScript 的类型分为语言类型和规范类型。
语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。
而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。
其实,就是js中还有一种存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。
引用类型(Reference type)
尤雨溪:
这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。
引用类型的构成,有三个组成部分,分别是:
- base value ( 属性所在的对象或者就是 EnvironmentRecord(global) )
- referenced value (属性的名称,key)
- strict reference (是否是严格模式,严格模式下,this如果为undefined则不会自动转成global)
其中base value 的值,只可能是undefined,string,number,boolean,objcet,environmentRecord类型
var foo = 1;
// 对应的Reference是:
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
var foo2 = {
bar: function () {
return this;
}
};
foo2.bar(); // foo
// bar对应的Reference是:
var BarReference = {
base: foo2,
name: 'bar',
strict: false
};
另外,规范中还提供了几个方法,
-
GetBase
返回reference的base value。
-
IsPropertyReference
如果base value 是一个对象,就返回true。
-
GetValue
返回对象属性真正的值,需要注意:这个值是具体的值,不再是一个Reference。
那么如何通过Reference确定一个this的值呢?
-
计算 MemberExpression 的结果赋值给 ref
-
判断 ref 是不是一个 Reference 类型
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) 2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref) 2.3 如果 ref 不是 Reference,那么 this 的值为 undefined
具体分析
-
计算 MemberExpression 的结果赋值给 ref
那么什么是MemberExpression呢,规范中解释如下:
- PrimaryExpression // 原始表达式
- FunctionExpression // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
function foo() { console.log(this) } foo(); // MemberExpression 是 foo function foo() { return function() { console.log(this) } } foo()(); // MemberExpression 是 foo() var foo = { bar: function () { return this; } } foo.bar(); // MemberExpression 是 foo.bar (false || foo.bar)(); // MemberExpression 是 (false || foo.bar)
简单理解,MemberExpression就是()左边的部分。
-
判断
ref
是不是一个Reference
类型
关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。
var value = 1
var foo = {
value: 2,
bar: function() {
return this.value
}
}
console.log(foo.bar()) // 2
console.log((foo.bar)()) // 2
console.log((foo.bar = foo.bar)()) // 1 有赋值操作符 使用了GetValue,所以返回的值不是Refercence类型
console.log((false || foo.bar)()); // 1
console.log((foo.bar, foo.bar)()); // 1
上述代码分析如下:
-
foo.bar()
和(foo.bar)()
一样,MemberExpression计算结果是foo.bar
,首先foo.bar
肯定是一个Reference类型,该值为:var Reference = { base: foo, name: 'bar', strict: false };
并且Reference.base值是一个对象,所以IsPropertyReference(ref)
返回true,this = GetBase(ref)
。
-
` (foo.bar = foo.bar)()
,MemberExpression计算结果是
(foo.bar = foo.bar),由于使用了赋值操作符,这过程必然使用了
GetValue方法,前面讲过,使用
GetValue方法将返回真正的value值,不再是一个Reference,所以参考2.3,ref值不是Referfence,
this = undefined`。 -
(false || foo.bar)()
,MemberExpression计算结果是(false || foo.bar)
,逻辑与算法,参考规范,2.Let lval be GetValue(lref).
因为使用了 GetValue,所以返回的不是 Reference 类型,this = undefined
。
-
(foo.bar, foo.bar)()
,MemberExpression计算结果是(foo.bar, foo.bar)
,逗号操作符,参考规范,2.Call GetValue(lref).
因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined
还有最后一种情况,
2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)
function foo() {
console.log(this)
}
foo();
上述代码:
MemberExpression 是 foo,
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
根据规则2.2,base value值并不是一个对象,IsPropertyReference(ref)
返回是false,所以this
的值是ImplicitThisValue(ref)
。
查看规范 10.2.1.1.6(http://yanhaijing.com/es5/#128),ImplicitThisValue
方法的介绍:该函数始终返回 undefined。
所以最终结果是undefined,非严格模式下就是Window
对象。
ECMAScript 5.1 规范地址:http://yanhaijing.com/es5/#115
参考:
-
https://www.teqng.com/2021/12/01/%E3%80%8A2w%E5%AD%97%E5%A4%A7%E7%AB%A0-38%E9%81%93%E9%9D%A2%E8%AF%95%E9%A2%98%E3%80%8B%E5%BD%BB%E5%BA%95%E7%90%86%E6%B8%85js%E4%B8%ADthis%E6%8C%87%E5%90%91%E9%97%AE%E9%A2%98/
-
https://github.com/mqyqingfeng/Blog/issues/7