Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解 this #4

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

理解 this #4

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

理解 this

JavaScript 中的 this 有时让人难以理解,一般情况下,函数的调用方式决定了 this 的值。并且 this 不能在执行期间被赋值

let obj = {
	a: 1
};

let a = 10;

function func() {
	this = obj;
}

test(); // ReferenceError: Invalid left-hand side in assignment

全局环境

在全局环境下,无论是否在严格模式下,this 都指向全局对象

console.log(this);
// 在浏览器中 this === window
// 在 node 中,this ==== global

函数环境

  1. 简单调用

    不是在严格模式下,且 this 的值不是由该调用设置的,那么 this 的值默认指向全局对象

    // 在浏览器环境中
    function func() {
    	console.log(this === window); // true
    }
    
    func();
    

    在调用 func 函数时,没有对 this 做任何设置,所以 this 等于 window

    在严格模式下,this 将保持它进入环境是的值。

    'use strict';
    
    function func() {
    	console.log(this === undefined); // true
    }
    
    func();
    

    在严格模式中,func 中的 this 没有被执行环境定义,this 是没有值得,所以 this === undefined

  2. 作为对象的方法

    当函数作为对象的方法被调用时,this 指向调用该函数的对象

    let a = 1;
    
    let obj = {
    	a: 10,
    	func: function() {
    		console.log(this.a); // 10
    	}
    };
    
    obj.func();
    

    原型链中的 this 也同样适用这个方法

    let obj = {
    	func: function() {
    		console.log(this.a + this.b);
    	}
    };
    
    let res = Object.create(obj);
    res.a = 10;
    res.b = 20;
    
    res.func(); // 30
    

    通过 res 调用 func 函数时,res 本身没有 func 函数,res 通过原型链查找到 func 函数,func 函数中的 this 也是指向 res

    getter 和 setter 中的 this 也适用

    let obj = {
    	a: 1,
    	b: 2,
    	get sum() {
    		return this.a + this.b;
    	}
    };
    
    console.log(obj.sum); // 3
    
  3. new 绑定

    new Foo(...) 执行时,会进行如下步骤:

    • 一个继承自 Foo.prototype 的新对象被创建;
    • 使用指定的参数调用构造函数 Foo, 并将 this 绑定到新创建的对象;
    • 有构造函数返回的对象是 new 表达式的结果。如果构造函数没有显示返回一个对象,则使用步骤1创建的对象。
    let a = 1;
    
    function Foo(a) {
    	this.a = a;
    }
    
    let obj = new Foo(10);
    
    console.log(obj.a); // 10
    

    Foo 中的 this 会绑定到新创建的对象上,因为 Foo 没有显示返回对象,所以 obj 等于新创建的对象。所以 this === obj

  4. 箭头函数中的 this

    箭头函数中的 this 对象,和封闭词法环境的 this 保持一致,也就是说箭头函数中 this 被设置为箭头函数被创建时环境的 this

    var a = 1;
    
    let func = () => {
    	console.log(this.a); // 1
    };
    
    let obj = {
    	a: 10
    };
    
    func.call(obj);
    

    func 函数被创建的时候,词法环境中的 this 就是全局对象,func 函数函数中的 this 就固定为全局对象,即使通过 call 调用还是不能改变 this 的指向

    function foo() {
    	setTimeout(() => {
    		console.log(this.id); // 20
    	}, 0);
    }
    
    var id = 10;
    
    foo.call({id: 20});
    

    setTimeout 参数是一个箭头函数,这个箭头函数是在调用 foo.call({id: 20}) 时创建的,此时这个箭头函数的 this 指向它的词法环境中的 this,也就是 foo 的 this。如果 setTimeout 参数不是箭头函数,那么输出的是 10

    一般说箭头函数的 this 不可改变指的是调用箭头函数时不能修改 this(比如说通过 call 之类的是该改变不了的)。但是词法环境的 this 是可以改变的

    var a = 1
    
    function foo() {
      let obj = {
    		bar: () => {
          console.log(this.a)
    		}
    	}
    	
    	obj.bar()
    }
    
    let obj = {
      a: 2
    }
    
    foo() // 1
    foo.call(obj) // 2
    

    因为 obj.bar 是箭头函数,this 绑定到 foo 函数中的 this ,所以改变 foo 中的 this ,obj.bar 中的 this 自然也就改变了。

    在实际中不会这样书写,这儿只是做说明使用

  5. DOM 事件处理函数

    当函数被用作事件处理函数时,它的 this 指向绑定事件的元素。一些浏览器在使用非 addEventListener 的函数动态添加监听函数时不遵守这个约定

    document.getElementById('id').addEventListener('click', function(e) {
    	console.log(this === e.currentTarget); // 总是 true
    	console.log(this === e.target) // 当触发事件的元素就是绑定元素,也就是 e.target === 																			 // e.currentTarget 时,才为 true
    });
    

知道了上这些规则,有时也会判断出错,比如下面这些

var a = 1;

let obj = {
	a: 10,
	func: function() {
		console.log(this.a); // 1
	}
};

let foo = obj.func;
foo();
var a = 1;

let obj = {
	a: 10,
	func: function() {
		console.log(this.a); // 1
	}
};

(false || obj.func)();

这些可能和想象中的不一样,下面从 ECMAScript 规范中解读 this,实际上判断 this 最准确的是从规范入手。

ECMAScript 规范解读 this

此处只针对函数中的 this

函数中的 this 值(更加准确的讲,这里的 this 是调用者提供的 thisArg,因为在进入函数代码时,会根据情况绑定 this)是在函数调用(Function Calls)时确定的:

产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :

  1. 令 ref 为解释执行 MemberExpression 的结果。
  2. 令 func 为 GetValue(ref)。
  3. 令 argList 为解释执行 Arguments 的结果,产生参数值们的内部列表(参见 11.2.4)。
  4. 如果 Type(func) 不是 Object,抛出一个 TypeError 异常。
  5. 如果 IsCallable(func) 为 false,抛出一个 TypeError 异常。
  6. 如果 Type(ref) 为 Reference,那么
    1. 如果 IsPropertyReference(ref) 为 true,那么
      1. 令 thisValue 为 GetBase(ref)。
    2. 否则,ref 的基值是一个环境记录项
      1. 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果。
  7. 否则,Type(ref) 不是 Reference
    1. 令 thisValue 为 undefined
  8. 返回调用 func 的 [[Call]] 内置方法的结果,传入 thisValue 作为 this 值和列表 argList 作为参数列表。

和 this 相关的在 1、6、7 点

第一点

  • 令 ref 为解释执行 MemberExpression 的结果

    MemberExpression 具体是什么也需要看规范

    MemberExpression :
    PrimaryExpression // 原始表达式
    FunctionExpression // 函数定义表达式
    MemberExpression [ Expression ] // 属性访问表达式
    MemberExpression . IdentifierName // 属性访问表达式
    new MemberExpression Arguments // 对象创建表达式
    

    所以 MemberExpression 可以简单的理解为函数括号左边的部分

    let obj = {
    	foo: function() {}
    };
    
    let func = obj.foo;
    
    func(); // func 就是 MemberExpression
    obj.foo(); // obj.foo 就是 MemberExpression
    

第六点

  • 如果 Type(ref) 为 Reference, 那么

    • 如果 IsPropertyReference(ref) 为 true, 那么

      • 令 thisValue 为 GetBase(ref)
    • 否则,ref 的基值是一个环境记录项

      • 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果

    这里的 Reference 是一个只存在规范的抽象类型,尤雨溪有一个比较容易理解的回答。根据规范可以得出几点:

    1. Reference 作用是说明 delete、typeof、赋值运算符这些运算符的行为

    2. 一个引用是已解析的命名绑定。(比如 let a = 1 会有一个引用绑定到 a 标识符上)

    3. Reference 由三部分组成:基值、引用名称和一个严格引用标志(布尔值)。基值是 undefined、Object、Boolean、String、Number、环境记录项(EnvironmentRecord)中的任何一个。

      在规范中给出了抽象操作引用的部分:

      • GetBase(V):返回引用值 V 的值部分。
      • GetReferencedName(V):返回引用值 V 的引用名称部分。
      • IsStrictReference(V):返回引用值 V 的严格引用部分。
      • HasPrimitiveBase(V):如果值是 BooleanStringNumber,那么返回 true
      • IsPropertyReference(V):如果值是个 ObjectHasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false
      • IsUnresolvableReference(V):如果值是 undefined 那么返回 true,否则返回 false

    下面先给出 Reference 三部分的结果

    function foo() {
    	console.log(this);
    }
    
    // 对应的 Reference 是:
    var fooReference = {
    	base: EnvironmentRecord,
    	name: 'foo',
    	strict: false
    };
    

    根据规范查找

      MemberExpression :
           PrimaryExpression
           FunctionExpression
           MemberExpression [ Expression ]
           MemberExpression . IdentifierName
           new MemberExpression Arguments
    
      PrimaryExpression :
           this 
           Identifier
           Literal
           ArrayLiteral
           ObjectLiteral
           ( Expression )
    

    foo 是 Identifier(标识符),所以最后变成执行标识符。根据标识符引用:

    Identifier 的执行遵循 10.3.1 所规定的标识符查找。标识符执行的结果总是一个 Reference 类型的值

    标识符解析

    标识符解析是指使用正在运行的执行环境中的词法环境组件,通过一个 Identifier 获得其对应的绑定的过程。在 ECMA 脚本代码执行过程中,PrimaryExpression : Identifier 这一语法产生式将按以下算法进行解释执行:

    1. 令 env 为正在运行的执行环境的 词法环境组件
    2. 如果正在解释执行的语法产生式处在严格模式下的代码中,则仅 strict 的值为 true,否则令 strict 的值为 false
    3. 以 env、Identifier 和 strict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果。

    解释执行一个标识符得到的结果必定是引用类型的对象,且其引用名属性的值与 Identifier 字符串相等。

    根据规范标识符解析得到的结果必然是引用类型的对象。引用名与 identifier 字符串相等,所以 name 为 'foo',因为是在非严格模式下,所以 strict 等于 false。

    因为 Reference 的 IsPropertyReference 通过基值来返回布尔值。那么基值是怎么得来的呢。

    这里 env 为执行环境的词法环境组件,词法环境组件在这里简单理解为执行环境中的变量、函数声明。至于涉及的外部词法环境简单理解就是嵌套词法环境的上一层词法环境。

    根据 GetIdentifierReference 函数就可以得出结果

    当调用 GetIdentifierReference 抽象运算时,需要指定一个词法环境 lex,一个标识符字符串 name 以及一个布尔型标识 strict。lex 的值可以为 null。当调用该运算时,按以下步骤进行:

    1. 如果 lex 的值为 null,则:
      1. 返回一个类型为引用的对象,其基值为 undefined,引用的名称为 name,严格模式标识的值为 strict。
    2. 令 envRec 为 lex 的环境数据
    3. 以 name 为参数 N,调用 envRec 的 HasBinding(N) 具体方法,并令 exists 为调用的结果。
    4. 如果 exists 为 true,则:
      1. 返回一个类型为引用的对象,其基值为 envRec,引用的名称为 name,严格模式标识的值为 strict。
    5. 否则:
      1. 令 outer 为 lex 的外部环境引用
      2. 以 outer、name 和 strict 为参数,调用 GetIdentifierReference,并返回调用的结果。

    所以到现在就能得出 Reference 三部分的组成。

    thisValue 等于 GetBase(ref) 返回值,所以 thisValue 等于 Reference 的基值

  • 如果 IsPropertyReference(ref) 不为 true,thisValue 等于 ImplicitThisValue 函数返回值。而 ImplicitThisValue 函数默认返回 undefined。所以此时 thisValue 等于 undefined

第七点

如果 Type(ref) 不是 Reference ,thisValue 是 undefined

总结

  1. 找到 MemberExpression
  2. 判断 MemberExpression 是否为 Reference
  3. 如果不是 Reference,this 等于 undefined,如果是 Reference,就需要根据 Reference 取值

判断 MemberExpression 是否为 Reference 这个步骤很复杂,因为要根据 MemberExpression 不同形态来判断。上面举例是 foo 标识符来判断。

现在我们可以解答开始提出的两个例子了

var a = 1;

let obj = {
	a: 10,
	func: function() {
		console.log(this.a); // 1
	}
};

let foo = obj.func;
foo();

这里只要清楚调用函数是 foo(),而不是 obj.func()。那么就和上面分析的一样了。MemberExpression 是 foo,foo 是一个标识符,肯定也是 Reference:

fooReference = {
	base: EnvironmentRecord,
	name: 'foo',
	strict: false
};

所以 this 等于 EnvironmentRecord。

第二个例子

var a = 1;

let obj = {
	a: 10,
	func: function() {
		console.log(this.a); // 1
	}
};

(false || obj.func)();

MemberExpression 是 (false || obj.func),判断 (false || obj.func) 是否为 Reference,首先要执行这个表达式。

第一步是括号运算符(群组运算符)

返回执行 Expression 的结果。这可能是一个 Reference

括号运算符对 MemberExpression 没有影响。

第二步是逻辑运算 ||

产生式 LogicalORExpression : LogicalORExpression || LogicalANDExpression 按照下面的过程执行 :

  1. 令 lref 为解释执行 LogicalORExpression 的结果。
  2. 令 lval 为 GetValue(lref)。
  3. 如果 ToBoolean(lval) 为 true,返回 lval。
  4. 令 rref 为解释执行 LogicalANDExpression 的结果。
  5. 返回 GetValue(rref)。

这里 ToBoolean(LogicalORExpression) === ToBoolean(false) === false, 所以这里只看 GetValue(rref)。GetValue 返回具体的值,不会是 Reference。这一点很重要,如果看到最后一步运算结果是通过 GetValue 返回的值,那么 MemberExpression 不是 Reference。所以这里的 this 等于 undefined。

但是这里深入学习 GetValue(V)

  1. 如果 Type(V) 不是引用,返回 V。
  2. 令 base 为调用 GetBase(V) 的返回值。
  3. 如果 IsUnresolvableReference(V),抛出一个 ReferenceError 异常。
  4. 如果 IsPropertyReference(V),那么
    1. 如果 HasPrimitiveBase(V) 是 false,那么令 get 为 base 的 [[Get]] 内部方法 , 否则令 get 为下面定义的特殊的 [[Get]] 内部方法。
    2. 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法,返回结果。
  5. 否则,base 必须是一个环境记录项
  6. 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue 具体方法,返回结果。

根据规范,用伪代码描述:

function GetValue(value) {
	if (Type(value) != Reference) {
		return value;
	}
	
	var base = GetBase(value);
	
	if (base === null) {
		throw new ReferenceError;
	}
	
	if (IsPropertyReference(value) === true) {
		return Base.[[Get]](GetPropertyName)(Value);
	} else {
		// Base 必须是环境记录项
		return Base.GetBindingValue(name, strict);
	}
}

GetValue(obj.func) 此时又要转移到执行 obj.func 也就是要查看属性访问

我们知道调用 obj.func() 时,this 是 obj。在这里就可以解释清楚了

产生式 MemberExpression : MemberExpression [ Expression ] 按照下面的过程执行:

  1. 令 baseReference 为解释执行 MemberExpression 的结果。
  2. 令 baseValue 为 GetValue(baseReference)。
  3. 令 propertyNameReference 为解释执行 Expression 的结果。
  4. 令 propertyNameValue 为 GetValue(propertyNameReference)。
  5. 调用 CheckObjectCoercible(baseValue)。
  6. 令 propertyNameString 为 ToString(propertyNameValue)。
  7. 如果正在执行中的语法产生式包含在严格模式代码当中,令 strict 为 true,否则令 strict 为 false
  8. 返回一个值类型的引用,其基值为 baseValue 且其引用名为 propertyNameString,严格模式标记为 strict。

首先看最后一点,返回一个值类型的引用,引用的基值是 baseValue。baseValue === GetValue(baseReference) === GetValue(obj)。obj 是一个标识符,所以根据标识符解析结果是 Reference:

let objReference = {
	base: EnvironmentRecord,
	name: 'foo',
	strict: false
}

所以 GetValue(objReference) === Base.GetBindingValue(name, strict) === obj 。因此 obj.func 的基值是 obj,obj.func() 的 this 就是 obj。

开始我们说这里的 this 是调用函数的调用者提供的 thisArg,和执行函数时的 this 还有区别。实际上是在进入函数时根据调用者提供的 thisArg 生成最终的 this

进入函数

当控制流根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入函数代码的执行环境时,执行以下步骤:

  1. 如果函数代码严格模式下的代码,设 this 绑定 为 thisArg。
  2. 否则如果 thisArg 是 nullundefined,则设 this 绑定全局对象
  3. 否则如果 Type(thisArg) 的结果不为 Object,则设 this 绑定ToObject(thisArg)。
  4. 否则设 this 绑定 为 thisArg。
  5. 以 F 的 [[Scope]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。
  6. 词法环境组件 为 localEnv。
  7. 变量环境组件 为 localEnv。
  8. 令 code 为 F 的 [[Code]] 内部属性的值。
  9. 10.5 描述的方案,使用函数代码 code 和 argumentList 执行声明式绑定初始化化步骤。

和 this 有关的是前4点。

参考资料

ECMAScript 英文文档

ECMAScript 维基百科 中文文档

JavaScript深入之从ECMAScript规范解读this

ECMA-262-3 详解 第三章 This

MDN this

JavaScript:this

AST 在线解析

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant