this 在前端代码中几乎随时随刻都能看到,同时,它也是面试中面试官贼喜欢考验的一个问题,但是网上的关于this的介绍的文章云杂,有时候为了方便自己理解而按自己的思路给this下了有偏差的定义并一直延用的话,可能会在重要时刻带来巨大的错误后果,所以最近我又深深地研究了下,翻了几篇热度及赞同度相对较高的博客,对this重新了解一波,并总结了下,那么我们现在就开始吧。让我们先从浅的层面理解下
既然要开讲this,我们还是按照惯例先从基础的层面给大家进行一个梳理温固及了解,当然已经熟知的小伙伴就可以直接跳到后面深层理解去了哈~
this 关键字指的是行为当前执行环境的 ThisBinding,也就是我们执行上下文创建阶段的 this 所绑定的对象或值,亦或是说我们的this是在调用时决定的,而不是创建时决定的,this具有运行时绑定的特性。
不管是严格模式还是非严格模式,Web里this都是指向全局对象即window,Node里的this则是指向global。
让我们先来看下下面函数调用时的执行上下文
function aaa(){
console.log("aaa");
bbb();
}
function bbb(){
console.log("bbb");
ccc();
}
function ccc(){
console.log("ccc");
}
aaa();
aaa是在全局执行上下文中被调用的,bbb是在aaa执行上下文中被调用的,而ccc是在bbb执行上下文中被调用的,所以调用位置应该是当前正在执行的函数的前一个调用中。那么,在函数中this的指向是根据调用该函数的对象来确定的。
让我们再来看看下面的代码
var name = 'window';
var example = function(){
console.log(this.name);
}
example(); // 'window'
非严格模式中,函数是在全局执行上下文执行的,可看成是window.example,调用函数的对象是window,所以它的this指向全局对象window,但如果是在严格模式下
'use strict'
var name = 'window';
var example = function(){
console.log(this.name);
}
example(); // '报错,this -> undefined'
全局上下文的this不再默认为window而是undefined(这个后面会细说为什么),由于没有指向全局对象,那么调用example的对象就不存在了,所以从undefined中去找name的值就肯定会报错了。
所以在非严格模式,最外层的this一定能取到外层的全局变量么,也不一定!
让我们来看看下面这种情况
let name = 'window';
let example = function(){
console.log(this === window);
console.log(this.name);
}
example(); // true,undefined
上面这种情况true确实指向window,但由于let、const定义的变量不会绑定到window上,所以通过this.name是取不到值的。
var name = 'window';
var example = function(){
console.log(this.name);
}
var obj = {
name: 'objInnerFunc',
func: example,
sonfunc: {
name: 'objSonFunc',
func: example,
}
}
obj.func(); // 'objInnerFunc'
obj.sonfunc.func(); // 'objSonFunc'
// 用call类比则为:
obj.func.call(obj);
// 用call类比则为:
obj.sonfunc.func.call(obj.sonfunc);
上面可以很明显的看出函数调用时的调用对象,如果example是在全局环境下调用的话那this.name就是输出“window”了。
var anotherFunc = obj.func;
anotherFunc()
那如果是上面这种呢,上面这种并不是对象去调用函数,而是把对象里的函数赋给了一个变量,相当于取obj.func的值,一个函数赋给了anotherFunc,那么anotherFunc调用时this非严格模式是指向window的,而严格模式下则undefined。等同于 anotherFunc.call(undefined) -> 当然非严格模式下就不是undefined而是window了
函数调用对象由call、apply、bind第一个参数指定,也就是你自己自定义函数调用的对象,在非严格模式下第一个参数为undefined、null时this绑定到window上,如果是非对象原始值则会被包装,比如 1 会被包装为Number(1)。三者区别主要为call和apply是除第一个参数的传参方式不同,call为普通传参,apply第二参数为数组传参,也因此call的性能会好一点。bind跟前两者不同在于它会返回一个this被更改指定后的新函数,这个函数调用时this必指向你绑定的对象,但如果new这个返回的函数this就会被指向新对象了。
function Example(name){
this.name = name;
// return function f(){};
// return {};
}
var result = new Example('123');
new 一个实例时,会先创建一个对象,然后将this绑定到这个新对象上,再将这个对象赋给指定的标识符,所以this是指向你实例出来的对象,但前提是你这个构造函数不显式返回任何值,它才是返回新创建的对象。
同上,this也是指向生成的新对象,即使调用this的方法是在原型链上,但因为方法的调用是新生成的对象调用的,所以this指向当前新对象。
箭头函数没有自己的this、super、arguments和new.target,所以箭头函数不能作为构造函数来new,没有原型对象,也没有this的绑定,其this继承自上一层非箭头函数的this,否则指向全局对象(非严格模式),undefined(严格模式)
var name = 'window';
var example = {
name: '啦啦啦',
inner: function(){
// var self = this;
var arrowDoSth = () => {
// console.log(self.name);
console.log(this.name);
}
arrowDoSth();
},
arrowDoSth2: () => {
console.log(this.name);
}
}
example.inner(); // '啦啦啦'
example.arrowDoSth2(); // 'window'
这里你会说咦arrowDoSth2不也是example调用的吗,不应该this也指向example,刚我们说了箭头函数是没有this的,它的this是继承非箭头函数的this指向,相当于是上级传下来的this,那这里通过对象直接调用,他找不到可以继承的this那就会默认继承全局对象了,他自己没有指向不了啊,只得继承window,所以call、apply、bind绑定不了箭头函数。
this会绑定到该事件绑定的DOM元素,但有些特殊情况绑定到全局对象(比如IE6~IE8的attachEvent)
this严格模式undefined,非严格模式全局对象
灵活运用Object.create(null)能处理一些由this引发的问题
从基础层面认识完this后,接着我们再换个角度,从另一个层面,也就是深层次来说一说this~
ECMAScript 的类型分为 语言类型 和 规范类型。
语言类型是开发者可以直接操作的,如我们常用的七大基本类型。而规范类型是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的,包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。(这些类型稍稍了解下就行啦)
那这里跟this关系最密切的就是Reference类型了,Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的类型,是一个抽象类型,用来描述语言的底层逻辑行为,不存在于JS代码中,只能YY想像下,是不能找出来的。
Reference由三部分组成
base value -> 属性所在的对象或者EnvironmentRecord(全局属性)
referenced name -> 属性的名称
strict reference // 这个就不管了
For example:
var aaa = {
test: function () {
return this;
}
};
aaa.test(); // aaa
// bar对应的Reference是:
var TestReference = {
base: aaa, //test这个属性位于aaa对象里
name: 'test', //属性名称为test
strict: false
};
规范中还提供了获取Reference组成的方法,如
GetBase —— 用来获取Reference的base即属性所在对象
IsPropertyReference —— 用来判断是否是一个Reference类型,它的逻辑是base是一个对象则返回true,即判断是否是一个对象的属性
GetValue —— 获取对应的值,如下,就我们代码中等号所赋予的值啦,即赋值操作,可以是1或者一个函数表达式等等,其返回的不是一个Reference类型
var test = 1;
var TestReference = {
base: EnvironmentRecord,
name: 'test',
strict: false
};
GetValue(TestReference) // 1;
好了,步入正题
函数调用时:
1、计算 MemberExpression 的结果赋值给 ref
什么是MemberExpression
- PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
- FunctionExpression // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
怎么看MemberExpression呢,最简单的方法就是调用函数时括号左边的那一串
2、判断 ref 是不是一个 Reference 类型
3、是的话指向该reference的base对象,否则严格模式指向undefined,非严格模式指向全局对象
让我们来看下例子(以严格模式为例)
'use strict'
var value = 1;
var aaa = {
value: 2,
test: function () {
return this.value;
}
}
//示例1,这里MemberExpression是aaa.test
console.log(aaa.test());
//示例2,这里MemberExpression是(aaa.test)等同aaa.test
console.log((aaa.test)());
//示例3,这里MemberExpression是一个(aaa.test = aaa.test)
console.log((aaa.test = aaa.test)());
//示例4,这里MemberExpression是一个(false || aaa.test)
console.log((false || aaa.test)());
//示例5,这里MemberExpression是一个(aaa.test, aaa.test)
console.log((aaa.test, aaa.test)());
先看实例1,aaa.test,因为get Base得到对象aaa,所以是一个Reference类型,那么this指向test的base->aaa
实例2(aaa.test)不做求值操作,所以等同于实例1,this同样指向aaa
实例3(aaa.test = aaa.test)执行了一波赋值操作,调用了get Value的方法,因为get Value返回的不是一个Reference,所以此MemberExpression不是Reference类型,this指向undefined
实例4(false || aaa.test)因为或操作要进行逻辑换算,所以也将后面的aaa.test进行了一波值转换操作,所以aaa.test被使用了get Value,返回的不是一个Reference类型,那么this指向undefined
实例5逗号运算符也会进行计算操作,所以调用了getValue,同上返回的不是一个Reference类型,那么this指向undefined。
当然还有一种极为常见的情况
function test() {
console.log(this)
}
test();
//--------------
var fooReference = {
base: EnvironmentRecord,
name: 'test',
strict: false
};
定义在全局环境下的函数,它是个reference对象,但是它的base是EnvironmentRecord,当然了非严格模式就是window了,严格模式的话Environment Record会调用一个 ImplicitThisValue(ref) 函数,其返回的值undefined,所以这也就解释了严格模式下指向全局环境对象的this为啥是undefined了
var value = 1;
var aaa = {
value: 2,
test: function () {
return this.value;
}
}
console.log((false || aaa.test)()); // 1
按我们传统的认识这个可能就解释不出了,上面(false || foo.bar)相当于取aaa.test的值赋值给一个匿名变量然后执行,其this就是全局对象了。
https://juejin.cn/post/6844903746984476686
https://github.com/mqyqingfeng/Blog/issues/7