JavaScript:this(一)

1,138 阅读16分钟

  本文和大家聊聊thisthisJavaScript中可说是神一样的存在,灵活性太强了,早期对this做过一次梳理,但现在面对this的使用还是怕怕的,对this的理解不透彻,还是在猜结果,根据应用场景划分梳理,更多的是在硬记,面对复杂恶心人的应用场景,特别是在面试的时候出的题,还是不明白,对结果一头雾水。本文,努力从底层和规范梳理this,真正的理解this

  前面的文章有写到,程序执行调用前会创建对应的执行上下文,一个执行上下文可以理解为一个抽象的对象,其中this就是这个对象的一个属性:

executionContext: {
    variable object:vars, functions, arguments
    scope chain: variable object + all parents scopes
    thisValue: context object
}

  this值在进入上下文时就已经确定了,并且在上下文运行期间永久不变。也就是说this是在函数调用的时候确定的。

  我们看看ECMAScript规范(5.1)中怎么描述this

The this keyword evaluates to the value of the ThisBinding of the current execution context.

  意思就是说this执行为当前执行环境(执行上下文)的ThisBindingThisBinding就是this的值。

  this也是一个对象,与执行的上下文环境息息相关,也可以把this称为上下文对象,激活执行上下文的上下文。

  在描述this之前,我们先来看几个比较重要的概念。

类型

  我们都知道在JavaScript中对象是引用类型,函数和数组都属于引用类型,还有基本类型:NumberStringBoolean等。其实在ECMASciript5.1规范中将类型分为了两种:

Algorithms within this specification manipulate values each of which has an associated type. The possible value types are exactly those defined in this clause. Types are further subclassified into ECMAScript language types and specification types.

  规范的算法操作各个有类型的值,可处理的类型在算法相关叙述中定义。类型又再分为ECMAScript语言类型和规范类型。

An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.

  ECMAScript语言类型就是ECMAScript开发者使用ECMAScript语言直接操作的值对应的类型。ECMAScript语言类型包括未定义(Undefined)、空(Null)、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。也就是我们常说未定义(Undefined)、空(Null)、布尔值(Boolean)、字符串(String)、数值(Number)为基本数据类型,对象(Object)为引用类型。

A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record. Specification type values are specification artefacts that do not necessarily correspond to any specific entity within an ECMAScript implementation. Specification type values may be used to describe intermediate results of ECMAScript expression evaluation but such values cannot be stored as properties of objects or values of ECMAScript language variables.

  规范类型是描述ECMAScript语言构造与语言类型语意的算法所用的元值对应的类型。规范类型包括引用(Reference)、列表(List)、完结(Completion)、属性描述式(Property Descriptor)、属性标识(Property Identifier)、词法环境(Lexical Environment)、环境记录(Environment Record)。

  我们需要知道规范类型的值是不一定对应ECMAScript实现里的任何实体的虚拟对象。规范类型可以用来描述ECMAScript表示运算的中途结果,但这些值不能存成对象的属性或是ECMAScript语言变量的值。

  对于规范类型不理解的话,我们只需要知道,规范类型用来描述表达式求值过程的中间结果,是一种内部实现,是用来描述语言底层行为逻辑,不对开发者直接开放。

  尤雨溪大大在知乎中就Reference的提问也做来解答:

  ECMASciript规范对类型做了很明确的定义,类型分为ECMAScript语言类型和规范类型,其中规范类型中有讲到引用(Reference),需要注意的是这里的引用,不同与我们常说的对象(Object)为引用类型中的引用,这是两个概念,对象(Object)为引用类型中的引用更多的是强调在我们定义对象的时候,通过声明变量,将值赋值给变量,变量存的是这个对象的内存地址,并不是对象的值本身,这也就是引用传递。而规范类型中提及到的引用(Reference)并不是这个意思,规范中是怎么定义的,我们下面来看看,理解规范类型中提及到的引用(Reference)对我们理解this有非常大的帮助,是重点:

Reference

  Reference(引用),这里说的引用是规范类型中的引用。我们先来看看规范中怎么定义引用规范类型的:

The Reference type is used to explain the behaviour of such operators as delete, typeof, the assignment operators, the super keyword and other language features. For example, the left-hand operand of an assignment is expected to produce a reference.

  上面是ECMAScript6规范中的定义,比ECMASciript5.1定义更丰富。

  Reference类型是ECMAScript规范用来解释delete, typeof、赋值运算符、super关键字等语言特性的行为。例如在赋值运算中左边的操作数期望产生一个引用。

  Reference类型值可以理解为是对某个变量、数组元素或者对象属性所在的内存地址的引用,不是对其所在内存地址所保存值的引用。在 ECMAScript中,赋值运算符的左侧是一个引用(Reference),不是值。

  我们再来看看Reference的具体内容:

A Reference is a resolved name binding. A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1). A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

  一个引用(Reference)是一个已解决的命名绑定。一个引用(Reference)由三部分组成,基(base)值,引用名称(referenced name)和布尔值、严格引用(strict reference)标志。基值是 undefined, 一个Object, 一个Boolean, 一个String, 一个Number, 一个environment record中的任意一个。基值是undefined 表示此引用可以不解决一个绑定。引用名称是一个字符串。

  我们来看一个简单的实例,理解下引用(Reference):

// 实例一
var a = 1;

  上面就是一个很简单的声明变量,变量a对应的Reference是什么呢:

// var a = 1 对应的Reference
var aReference = {
  base: Environment Record,
  name: 'a',
  strict: false
}

  有人会觉得根据定义好像是这么写的,也有人会思考为什么是这么写:

  这是在全局环境下声明了一个变量,并且做了简单的赋值操作,我们来看看规范中怎么定义简单赋值的:

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. Return rval.

  我们只要看第一步就可以了,令lref解释执行LeftHandSideExpression的结果。

  LeftHandSideExpression也就是声明的变量a,我们知道声明的变量a可以称为是一个标识符(Identifier),为了帮助大家更好的理解,我们来看看LeftHandSideExpression中有没有标识符,先看下LeftHandSideExpression是怎么定义的:

LeftHandSideExpression :

NewExpression
CallExpression

  规范中写明了LeftHandSideExpression包含了两种,一是new表达式,二是函数调用表达式,并没有我们想要看到的标识符。我们再追根下,new表达式和函数调用表达式又包含了哪些内容:

NewExpression :

MemberExpression
new NewExpression

CallExpression :

MemberExpression Arguments
CallExpression Arguments
CallExpression [ Expression ]
CallExpression . IdentifierName

  上面的new表达式和函数调用表达式同样也没看到我们想要看到的标识符,我们就再追根下,我们看到new表达式中可以是成员表达式(MemberExpression),我们再来看看成员表达式中有没有我们想要的内容:

MemberExpression :

PrimaryExpression
FunctionExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
new MemberExpression Arguments

  在上面的内容中还是没有看到我们想要看到的标识符,怎么办,只有在追下了,我们再看看PrimaryExpression中有什么:

PrimaryExpression :

this
Identifier
Literal
ArrayLiteral
ObjectLiteral
( Expression )

  这下在PrimaryExpression看到了我们想要看到的标识符(Identifier)了,所以确保变量a是标识符无疑了,为了确定它也挺不容易的。

  既然我们现在知道了变量a是一个标识符,我们再来看看标识符是怎么执行的:

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类型的值。也就说明了a这个标识符是一个Reference类型的值。我们再来看看10.3.1写了什么:

10.3.1 Identifier Resolution

Identifier resolution is the process of determining the binding of an Identifier using the LexicalEnvironment of the running execution context. During execution of ECMAScript code, the syntactic production PrimaryExpression : Identifier is evaluated using the following algorithm:

  1. Let env be the running execution context’s LexicalEnvironment.
  2. 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.
  3. Return the result of calling GetIdentifierReference function passing env, Identifier, and strict as arguments.

The result of evaluating an identifier is always a value of type Reference with its referenced name component equal to the Identifier String.

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

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

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

  上面提到了GetIdentifierReference函数,我们再来看看这是什么:

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)值为envRec,引用的名称(name)为 name,严格模式标识的值为strict

  就本实例来说,base值为envRecenvRec也就是10.3.1传入的执行环境的词法环境。而一个词法环境是由一个环境记录项(Environment Record)和可能为空的外部词法环境引用构成。我们暂时不用管外部词法环境,我们需要知道环境记录项这个概念。

  在规范中,有两种环境记录项,一是声明式环境记录项,另一种是对象式环境记录项。声明式环境记录项定义那些将标识符与语言值直接绑定的ECMA脚本语法元素,例如函数定义,变量定义以及Catch语句。所以本实例的环境记录项是声明式环境记录项。

  说了这么多就是为了告诉大家本实例基(base)值是Environment Recordnamefoo,所以抽象数据结构为:

var aReference = {
  base: Environment Record,
  name: 'a',
  strict: false
}

  规范中也使用了以下抽象操作来接近引用:

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

  规范使用以下抽象操作来操作引用:

  • GetValue(v)。返回对象属性真正的值,是reference传入,会返回一个普通类型出来。比如fooreference,通过GetValue之后就是一个普通的object,也就是foo对应的js类型本身
  • PutValue(v,w)

  铺垫了这么多,现在我们来聊聊this了,有上面的理论基础理解this相对会简单很多:

  this绑定多数是出现在函数调用的时候,不同的场景下,this的指向不一样,我们先来看看函数调用在规范中怎么定义的:

11.2.3 Function Calls

The production CallExpression : MemberExpression Arguments is evaluated as follows:

  1. Let ref be the result of evaluating MemberExpression.
  2. Let func be GetValue(ref).
  3. Let argList be the result of evaluating Arguments, producing an internal list of argument values (see 11.2.4).
  4. If Type(func) is not Object, throw a TypeError exception.
  5. If IsCallable(func) is false, throw a TypeError exception.
  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).

  1. Else, Type(ref) is not Reference.

a. Let thisValue be undefined.

  1. Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.

  函数调用具体内容如下:

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

  规范中讲到了函数调用时如何确定this的值,我们下面就按照上述步骤来解析不同场景下this值是什么。

默认绑定

  我们先看一个最简单的场景:

// 实例二
function foo() {
  console.log(this); // window
}
foo();

  函数foo执行输出想必大家都知道输出是window,我们从规范中来分析下为什么this指向的是window

  我们现在只需要看第一步,令ref为解释执行MemberExpression的结果。MemberExpression是什么呢?我们来看看规范是怎么定义的:

11.2 Left-Hand-Side Expressions

MemberExpression :

PrimaryExpression // 主值表达式
FunctionExpression // 函数调用表达式
MemberExpression[Expression] // 属性访问表达式
MemberExpression.IdentifierName // 属性访问表达式
new MemberExpression Arguments // 对象创建表达式

  MemberExpression可以理解为()左边部分,在这里是foofoo是主值表达式(PrimaryExpression)中的一种,我们先看下PrimaryExpression是怎么定义的:

11.1 Primary Expressions

PrimaryExpression :

this
Identifier
Literal
ArrayLiteral
ObjectLiteral
( Expression )

  看的出来foo就是一个标识符(Identifier),前面的内容也已经讲过了,解释执行一个标识符得到的结果必定是引用类型的对象,且其引用名属性的值与Identifier字符串相等。

  所以foo的抽象数据结构如下:

foo_reference = {
	base: Environment Record,
	name: "foo",
	strict: false
}

  到这里第一步已经完成了,执行MemberExpression的结果就是foo_referenceref = foo_referenceref是一个Reference

  因为refReference,所以来到了第六步:如果Type(ref)Reference,那么如果IsPropertyReference(ref)true,那么令thisValueGetBase(ref). 否则, ref的基值是一个环境记录项,令 thisValue为调用GetBase(ref)ImplicitThisValue具体方法的结果。

  ref的基(base)值是一个环境记录项(Environment Record),不是一个对象,所以IsPropertyReference(ref)falsethis的值是调用GetBase(ref)ImplicitThisValue具体方法的结果。

  我们来看看ImplicitThisValue方法是什么:

10.2.1.1.6 ImplicitThisValue()

Declarative Environment Records always return undefined as their ImplicitThisValue.

  声明式环境记录项永远将undefined作为其ImplicitThisValue返回.

  函数foo执行到这里,thisValue = undefined,对应到JavaScript代码中的thisthis=undefined,还没有结束,规范中在进入函数代码有写到:

10.4.3 Entering Function Code

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

  如果thisArgnullundefined,则设this绑定为 全局对象 。所以函数foo执行时,this指向全局对象,也就是window

// 实例二
function foo() {
  console.log(this); // window
}
foo();

  所以这个实例控制台输出的就是window。这也就是从应用场景来区分的话,就是默认绑定。

  这就是this指向解析的全过程了,从规范中解读this的指向,从底层了解this的指向,不需要去区分默认绑定、隐性绑定、显性绑定等这种从应用场景来记this的指向,记不住也容易出错,遇到复杂的场景还是容易犯错,我们需要真正的从底层来理解this,对于this理解透彻。

  有了上面的分析方式,我们再看看几种比较常见的this应用场景:

隐性绑定

// 实例三
var obj = {
  foo: function() {
    console.log(this); // obj
  }
}
obj.foo();

  实例三中MemberExpression计算结果是obj.fooobj.foo是个什么,会是不是一个Reference呢?

  按照前面的分析方式,我们知道obj.fooMemberExpression.IdentifierName,是一个属性访问。

  我们再来看看规范中怎么定义属性访问(Property Accessors)的:

11.2.1 Property Accessors

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

  1. Let baseReference be the result of evaluating MemberExpression
  2. Let baseValue be GetValue(baseReference).
  3. Let propertyNameReference be the result of evaluating Expression.
  4. Let propertyNameValue be GetValue(propertyNameReference).
  5. Call CheckObjectCoercible(baseValue).
  6. Let propertyNameString be ToString(propertyNameValue).
  7. If the syntactic production that is being evaluated is contained in strict mode code, let strict be true, else let strict be false.
  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.

  上面主要讲的是:

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

  我们回到我们的实例三,obj.foo中的MemberExpressionobj,执行MemberExpression的结果返回一个Reference,假设是reference_objbaseReferencereference_obj

  baseValue就是GetValue(reference_obj)GetValue方法上面也有介绍,baseValue也就是obj

  同理propertyNameString就是foo

  再看最后一步(第八步),所以obj.foo最终执行是一个Reference,且数据结构如下:

reference_obj_foo = {
	base: obj,
	name: "foo",
	strict: false
}

  因为obj.foo一个Referenceref = reference_obj_foo,来到了函数调用的第六步:

If Type(ref) is Reference, then

a. If IsPropertyReference(ref) is true, then

i. Let thisValue be GetBase(ref).

  IsPropertyReference()方法前面也有介绍,如果基值是个对象或HasPrimitiveBase(V)true,那么返回true。本实例baseobj,是一个对象,所以IsPropertyReference(ref)返回true,也因为返回了true,所以thisValue = GetBase(ref)GetBase(V)方法前面也有介绍,返回引用值V的基值组件,即thisValue = GetBase(ref) = obj

  所以obj.bar()执行时this指向obj,这也就是this的隐性绑定。

  现在已经分析了this的基本指向,默认绑定和隐性绑定的指向问题,按照应用场景来分的话,还有显性绑定、new绑定以及箭头函数等,由于内容还有很多下篇文章再详细输出,本文就暂时写到这。本文也基本的讲述从规范的角度this的指向,从规范中帮助我们更好的理解this

结语

  文章如有不正确的地方欢迎各位大佬指正,也希望有幸看到文章的同学也有收获,一起成长!

-------------------------------本文首发于个人公众号------------------------------

最后,欢迎大家关注我的公众号,一起学习交流。