对于This的理解


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没有被正常打印,这是因为letconst 声明的变量,会生成块级作用域,并且存在暂时性死区( temporal dead zone,简称TDZ ),并没有挂载到window对象上。

例子2:

a = 1;
function foo() {
    console.log(this.a); 
}
const obj = {
    a: 10,
    bar() {
        foo(); 
    }
}
obj.bar(); 

上述代码打印结果是1,原因是foo虽然在objbar函数中,但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执行,如果foolet/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来构建函数,会执行如下四部操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回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而非执行时。

  1. this指向外层作用域的this: 箭头函数没有this绑定,但它可以通过作用域链查到外层作用域的this
  2. 指向函数定义时的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
};

另外,规范中还提供了几个方法,

  1. GetBase

    返回reference的base value。

  2. IsPropertyReference

​ 如果base value 是一个对象,就返回true。

  1. GetValue

    返回对象属性真正的值,需要注意:这个值是具体的值,不再是一个Reference。

那么如何通过Reference确定一个this的值呢?

  1. 计算 MemberExpression 的结果赋值给 ref

  2. 判断 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
    

具体分析

  1. 计算 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就是()左边的部分。

  2. 判断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

上述代码分析如下:

  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)

  1. ` (foo.bar = foo.bar)() ,MemberExpression计算结果是 (foo.bar = foo.bar),由于使用了赋值操作符,这过程必然使用了GetValue方法,前面讲过,使用GetValue方法将返回真正的value值,不再是一个Reference,所以参考2.3,ref值不是Referfence,this = undefined`。

  2. (false || foo.bar)(),MemberExpression计算结果是 (false || foo.bar),逻辑与算法,参考规范,

    2.Let lval be GetValue(lref).

​ 因为使用了 GetValue,所以返回的不是 Reference 类型,this = undefined

  1. (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

参考:

  1. 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/

  2. https://github.com/mqyqingfeng/Blog/issues/7