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你到底指向哪里 #21

Open
wolfdu opened this issue Jan 6, 2018 · 0 comments
Open

this你到底指向哪里 #21

wolfdu opened this issue Jan 6, 2018 · 0 comments

Comments

@wolfdu
Copy link
Owner

wolfdu commented Jan 6, 2018

https://wolfdu.fun/post?postId=5a5079d5b890b156d0fae63d

终究还是逃不过,这次便将你连根拔起

this的绑定应该是在梳理JavaScript执行上下文时遗漏的点,因为关于this的指向,在我的学习过程中也经历了好几个阶段,所以觉得有必要单独去梳理一下。

this指向初体验

初次系统学习了解this的指向问题是在阅读You-Dont-Know-JS的时候,当时感觉就是一个this的指向问题就要记住4种规则去判定,还要根据优先级去使用对应的规则,感觉心好累 (눈_눈)

不过呢,至少可以纠正this指向的一些基本错误,同时慢慢接近this指向问题的真相。可以参考我之前记录的相关笔记,这里就不再赘述了About This&Comprehensive analysis This

通过EC摸索this方向

不过呢,在我们了解ja的执行上下文(EC)之后,我们视乎可以去跟本质的去观察this的指向问题。
如果你还不了解JavaScript的执行上下文<-可以戳这里 o‿≖✧

画个简单的流程图描述下之前总结执行上下文:

可以发现在之前梳理EC的时候,我并没有花力气去解释this的指向相关问题,为啥呢?是因为当时我的思路也不太清晰,所以这里单独在进行梳理。

那么我们要重点关注就是创建EC时的this指向的过程了。
其实从图中我就可以发现:**this的指向,是在函数被调用的时候确定的,也就是在创建EC阶段。**因此当函数的调用方式不同时,this的指向就发生了变化。

我们可以看从一个简单的荔枝观察this指向:

var a = 1;

function foo(){
    console.log(this.a)
}

var obj = {
    a: 2,
    fn: foo
};

foo(); // 1

obj.fn(); // 2

var bar = obj.fn;
bar(); // 1

是不是感觉变化多端呢?
我们再看一个荔枝:

var a = 1;

var obj = {
    a: 2
};

function foo(){
    // ERROR Uncaught ReferenceError: Invalid left-hand side in assignment
    this = obj; 
    console.log(this.a)
}

foo();

当我在foo中改变this的指向时,会抛出异常,也就是函数的this指向一旦确定后,就不能再改变了。

全局对象中this

在分析全局执行上下文的时候,我们知道,在全局执行上下文中,global object就是变量对象(variable object)。在浏览器端,global object被具象成window对象,也就是说 global object === window === 全局执行上下文的variable object。因此global object对于程序而言也是唯一可读的variable object。

我们可以在全局作用域下看看

// 浏览器
console.log(this); // Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}

// node
console.log(this); // global

所以在全局作用域中this指向的就是全局对象,在浏览器中指向window,在node中指向global。

函数中this指向

1. 函数直接调用

从最简单的场景开始:

var a = 1;

function foo(){
    console.log(this.a);
}

foo();

函数直接被调用,这个时候我们可以发现this指向了全局对象。

无论是否在严格模式下,在全局执行上下文中(在任何函数体外部)this都指代全局对象。

2. 作为对象方法调用

接下来复杂一点的栗子:

var a = 1;

var obj = {
    a: 2,
    c: this.a + 10,
    fn: function () {
        return this.a;
    }
}

console.log(obj.c);
console.log(obj.fn());

可以想一想,这里会输出什么样的结果呢?
我们先看看obj.fn()这里会输出2,也就是说这里的this指向当前调用函数的对象obj

但是让人疑惑的是obj.c为什么是11呢?
其实很好理解,我们使用{}是不会形成新的作用域的,所以为c赋值时的this.a中的this指向是全局对象。

如果调用函数,是某一个对象的方法,那么该函数在调用时,内部的this指向该对象

3. 严格模式下的this指向

通过这个栗子我们看看严格模式下的区别:

// 我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
    'use strict';
    console.log(this);
}

fn();  // fn是调用者,独立调用
window.fn();  // fn是调用者,被window所拥有

fn()为独立调用函数,在严格模式下this指向undefined,但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

window.fn()为全局变量调用fn,函数内部的this指向window,所以这里会输出全局对象window。

结合栗子的结果思考一下,严格模式与非严格模式区别。

到这里,会不会觉得this的指向问题是不是有迹可循了呢?
可以思考下面的代码会输出什么结果呢?

'use strict';
var a = 1;
function foo () {
    var a = 2;
    var obj = {
        a: 3,
        c: this.a + 10,
        fn: function () {
            return this.a;
        }
    }
    return obj.c;

}
console.log(foo());    // ?
console.log(window.foo());  // ?

call,apply,bind中的this指向

这里相关的方法前面的文章已经模拟实现过了,如果对于以上几种方法中的this指向还不是很清楚可以猛捶JavaScript模拟实现call,apply,bind等方法

new运算符中的this指向

不好意思,new运算符的this指向也被封装到了这里new运算符模拟实现,看过应该不会再对new的实现和this指向有疑问了。


分割线。。。
我们可以发现,从以上角度(全局下,函数独立调用,对象方法调用,new,call,apply,bind)探索this指向的文章其实有很多,感觉也是千篇一律,总结下来其实也是对应的场景和规则。那么为什么会有这些场景和规则呢?
既然有规则,那么我去翻翻ECMAScript规范吧,这里还有英文版规范

建议主要看英文版,因为中文翻译版中的有些译文和排版会让本来就很绕的规范更加难懂

从规范的角度去解读this的指向

我们先看一看规范中涉及到this指向的相关知识点,也是查看规范是所需要的背景知识。

涉及this指向的规范概览

Types

第8章 Types

类型又再分为 ECMAScript 语言类型 与 规范类型 。

我们先要清楚这两种类型:

  • ECMAScript语言类型:我们在使用ECMAScript 语言时操作的类型如:未定义 (Undefined)、 空值 (Null)、 布尔值(Boolean)、 字符串 (String)、 数值 (Number)、 对象 (Object)。
  • 规范类型: 是描述 ECMAScript 语言构造与 ECMAScript 语言类型语意的算法所用的元值对应的类型。规范类型包括 引用(Reference) 、 列表(List) 、 完结(Completion) 、 属性描述式(Property Descriptor) 、 属性标示(Property Identifier) 、 词法环境(Lexical Environment)、 环境纪录(Environment Record)。

这里我们要区别就是,规范类型可用来描述 ECMAScript 表式运算的中途结果,但是这些值不能存成对象的变量或是 ECMAScript 语言变量的值。也就是说规范类型它们是为了更好地描述ECMAScript语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。

下一个我们要关注的点就是规范类型中的Reference引用类型

Reference

第8.7 Reference

引用类型用来解释 deletetypeof,赋值运算符这些运算符的行为。

Reference由3部分组成:

  1. 基值(base value),指向引用的原值
  2. 引用名称(referenced name)
  3. 严格引用 (strict reference) 标志,该值是一个Boolean值标示是否严格模式

其中base value 是 undefined, 一个 Object, 一个 Boolean, 一个 String, 一个 Number, 一个 environment record 中的任意一个。

我们用实例来模拟下Reference

var foo = {
    bar: function () {
        return this;
    }
};

// 模拟foo对应的Reference
_fooReference = {
	base: EnvironmentRecord,
	propertyName: 'foo',
	strict: false
};

foo.bar(); // foo

// 模拟bar对应的Reference
_barReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

还需要了解Reference相关的几个方法

  • GetBase(V)。 返回引用值 V 的基值组件
  • HasPrimitiveBase(V)。 如果基值是 Boolean, String, Number,那么返回 true。
  • IsPropertyReference(V)。 如果基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。

MemberExpression

第11.2 MemberExpression

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

示例:

function foo() {
    console.log(this)
}

foo(); // MemberExpression: foo

var foo = {
    bar: function () {
        return this;
    }
}

foo.bar(); // MemberExpression: foo.bar

这里可以理解MemberExpression就是括号左边的部分(눈_눈)

Function Calls

第11.2.3 Function Calls
这里只用关注1,6,7步

The production CallExpression : MemberExpression Arguments is evaluated as follows:
1.Let ref be the result of evaluating MemberExpression.

6.If Type(ref) is Reference, then
 a. If IsPropertyReference(ref) is true, then
  i. Let thisValue be GetBase(ref).
 b. Else, the base of ref is an Environment Record
  i. Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).
7.Else, Type(ref) is not Reference.
 a. Let thisValue be undefined.

简单解释下:
1.将MemberExpression的执行结果赋值给ref
6.判断ref类型,如果类型为Reference,那么判断 IsPropertyReference(ref)为true那么this指向GetBase(ref),否则this值为undefined
7.如果ref类型不是Reference那么this值为undefined

相关步骤后文会详细说明

Entering Function Code

第10.4.3 Entering Function Code

The following steps are performed when control enters the execution context for function code contained in function object F, a caller provided thisArg, and a caller provided argumentsList:
...
2.Else if thisArg is null or undefined, set the ThisBinding to the global object.

当控制流进入函数代码的执行阶段时,对this值的处理。
我们重点关注第二步

GetValue(v)

8.7.1 GetValue(v)

关于GetValue我们只需要明白一点:如果是reference传入,会返回一个普通类型出来。比如 foo 为reference,通过GetValue之后就是一个普通的 object,也就是 foo 对应的 js 类型本身。
通过一个示例模拟下:

var foo = 1;

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

GetValue(_fooReference) // 1;

通过以上的理论梳理我们从函数调用过程大概可以得到如下流程:

准备工作就到这里啦,我们开车吧。

探索this指向

以下从规范角度解读this指向问题,其中我们只会关注相关过程或结果,可能会忽略掉一些过程性的东西,因为规范实在是太繁琐啦~~

在了解了以上关于this指向规范的理论之后,你一定很懵逼吧,没看太懂没关系,结合实例一步一对应就会慢慢看到this在像你招手了。

我们正式开始探索this指向。
最简单的例子->函数的独立调用开始:

function foo() {
    console.log(this);
}

foo();

先看第一步

1.Let ref be the result of evaluating MemberExpression.

将执行MemberExpression的结果赋值给ref
我们之前已经知道了MemberExpression就是foo,那么接下来就是执行foo标识符,那么这个foo的引用过程是什么样的呢?我们来看规范怎么说吧。

Identifier Reference(标识符引用)

第11.1.2 Identifier Reference

An Identifier is evaluated by performing Identifier Resolution as specified in 10.3.1. The result of evaluating an Identifier is always a value of type Reference.

这里直接告诉我们Identifier 的执行遵循 10.3.1 所规定的标识符查找。标识符执行的结果总是一个 Reference 类型的值。

既然都告诉我们章节了,我们就去看一看。

Identifier Resolution(标识符解析)

第10.3.1 Identifier Resolution

  1. Let env be the running execution context’s LexicalEnvironment.
  1. If the syntactic production that is being evaluated is contained in a strict mode code, then let strict be true, else let strict be false.
  2. Return the result of calling GetIdentifierReference function passing env, Identifier, and strict as arguments.

env为当前的词法环境,然后第三步以env,Identifier,strict为参数调动GetIdentifierReference函数(;´༎ຶД༎ຶ`) 有完没完,都看到这里了就,硬这头皮继续看下去吧,看完了我们再炸锅~

GetIdentifierReference(lex, name, strict)

第10.2.2.1 GetIdentifierReference(lex, name, strict)

...
Return a value of type Reference whose base value is envRec, whose referenced name is name, and whose strict mode flag is strict.

我们发现最终会返回一个Reference,并且基值(base value)为envRec,name为参数中name,strict为参数strict

咻~终于有个结果了,第一步也就执行完了:

ref = {
	base: LexicalEnvironment,
	propertyName: 'foo',
	strict: false
}

接下来执行第6步:
6. If Type(ref) is Reference, then

执行Type(ref),我们知道这里的ref是Reference

那么接下来执行IsPropertyReference(ref)

上文中提到过的Reference方法,这里先执行HasPrimitiveBase(ref)false,那么IsPropertyReference(ref)也为false

所以接下来执行6.b
b. Else, the base of ref is an Environment Record
  i. Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

ImplicitThisValue

第10.2.1.1.6 ImplicitThisValue

Declarative Environment Records always return undefined as their ImplicitThisValue.

那么我们知道这里thisValue = undefined,那么执行foo()this = undefined

最后一步

Entering Function Code

10.4.3 Entering Function Code

  1. Else if thisArg is null or undefined, set the ThisBinding to the global object.

this = global = window
٩(๑ᵒ̴̶͈᷄ᗨᵒ̴̶͈᷅)و功夫不负有心人,能到这里的都是勇士!!!


通过上面独立函数调用的分析过程,我们继续分析其他常见情况:

对象方法调用函数:

var foo = {
    bar: function () {
        console.log(this);
    }
};

foo.bar();

首先执行MemberExpression将其结果赋值给ref,此时MemberExpression为foo.bar我们看一看执行属性访问的过程:

Property Accessors

11.2.1 Property Accessors

The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:

8.Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

这里我们就只关注第8步了,我们可以知道返回的是一个Reference类型

ref = {
	base: foo,
	propertyName: 'bar',
	strict: false
}

接下来轻车熟路,判断是否是Reference类型?
我们已经知道ref是Reference类型了。

接下来判断ref的基值类型
那么接下来执行IsPropertyReference(ref)结果为true

a. If IsPropertyReference(ref) is true, then
  i. Let thisValue be GetBase(ref).

那么此时的this值为ref的基值thisValue = foo

是不是so easy ,所以当执行foo.bar()时bar中的this是指向foo的。


到这里我们已经知道了大致套路了。
最后再看一种情况

var foo = {
    bar: function () {
        console.log(this);
    }
};

(fn = foo.bar)();

执行MemberExpression将其结果赋值给ref,执行语句为fn = foo.bar,我们需要了解赋值语句执行过程:

Simple Assignment ( = )

11.13.1 Simple Assignment ( = )

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Let lref be the result of evaluating LeftHandSideExpression.
  2. Let rref be the result of evaluating AssignmentExpression.
  3. Let rval be GetValue(rref).

简答解释下,这里需要对右边的表达式执行结果执行GetValue(rref)方法,前面我们已经了解到了右边表达式(foo.bar)执行结果是一个Reference,那么GetValue(rref)执行将返回一个非Reference类型结果。

执行函数调用

7.Else, Type(ref) is not Reference.
 a. Let thisValue be undefined.

我们得到thisValue = undefined

然后进入函数执行阶段:
10.4.3 Entering Function Code

  1. Else if thisArg is null or undefined, set the ThisBinding to the global object.

this = global = window٩(๑ᵒ̴̶͈᷄ᗨᵒ̴̶͈᷅)و

吼啦~当你遇到你无法用理解的this指向问题时不妨试试从规范的角度去探寻一下,是一个很不错的选择哦。

小结

这里我们从传统的this解读视角对比ECMAScript规范视角解读this指向,也算是从现象到本质的一种探索了。
不得不说从规范去寻找this的指向过程要麻烦太多相对也要晦涩一点,但是当你耐心一点一点理顺了之后,会有新的收获,同时规范显得也没那么神秘难懂。

相信在你明白本文中所有内容的时候,你对于的this指向的理解应该清晰明了了。
不过无法理解规范相关的内容,也不用着急,过一段时间后在来翻阅,或许会有新的收获。

参考文章

根治JavaScript中的this-ECMAScript规范解读
JavaScript深入之从ECMAScript规范解读this

若文中有知识整理错误或遗漏的地方请务必指出,非常感谢。如果对你有一丢丢帮助或引起你的思考,可以点赞鼓励一下作者=^_^=

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

No branches or pull requests

1 participant