JS进阶系列-第七篇-this

Posted by Kylen on 2019-07-22

前言

一直以来对this的粗浅理解:如果一个函数被一个对象所拥有,在调用的时候以[对象.函数]的形式调用,那么函数内部的this则指向这个对象;如果是直接调用函数内部的this默认指向undefined,非严格模式下,会被自动指向全局对象。

就如同下面的例子一般,前者输出2,后者输出1:

1
2
3
4
5
6
7
8
9
10
var value = 1
var obj = {
value: 2,
foo: function(){
console.log(this.value)
}
}
obj.foo() // 2
var bar = obj.foo
bar() // 1

然而看到下一个例子我就懵逼了:

1
2
3
4
5
6
7
8
9
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}
console.log((false || foo.bar)()); // 1

从规范层面确定this的值

Types和Reference

ECMAScript的类型分为语言类型和规范类型

语言类型就是我们平时常说的基本类型(Null Undefined Boolean String Number Symbol)和复杂类型(Object);而规范类型相当于 meta-values,是用算法来描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

我们只要知道在 ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑

这里只需要简单了解 Reference 类型

Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的

Reference的组成由三个部分组成,分别是:

  • base value
  • reference name 属性的名称
  • staict reference
    base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。

举个例子:

1
2
3
4
5
6
7
8
var foo = 1;

// 对应的Reference是:
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};

在举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = {
bar: function () {
return this;
}
};

foo.bar(); // foo

// bar对应的Reference是:
var BarReference = {
base: foo,
propertyName: 'bar',
strict: false
};

需要了解的Reference组成部分的两个方法:

  • GetBase
    返回Reference的base value

  • IsPropertyReference
    如果base value是一个对象返回true,否则返回false

从Reference类型获取对应值的方法GetValueGetValue返回的是具体的值,不再是一个Reference

注意GetBase IsPropertyRerence GetValue都是底层的规范,在浏览器上是无法直接使用的。

确定this的步骤

  1. 计算MemberExpression的结果赋值给ref
    简单理解:MemberExpression 其实就是()左边的部分。
    关键就在于看规范是如何处理各种 MemberExpression
    举个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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
  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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1
//示例6
function foo() {
console.log(this.value)
}
console.log(foo()); // 1
  • foo.bar()
    根据规则返回的Reference为

    1
    2
    3
    4
    5
    var Reference = {
    base: foo,
    name: bar,
    strict: false
    }

    根据规则首先ref是一个reference,IsPropertyReference(ref)是true,所以this的值为GetBase(ref),即foo

  • (foo.bar)()
    ()没有对MemberExpression进行计算,所以结果和示例1一样

  • (foo.bar = foo.bar)()
    规范:

    2.Let lval be GetValue(lref).

    有赋值操作符,会使用GetValue方法,然后返回的就不是一个reference,this为undefined,非严格模式下this为undefined会被隐式的转换成window

  • (false || foo.bar)()
    规范:

    3.Let rval be GetValue(rref).

    逻辑与算法,使用GetValue方法,之后和示例3一样

  • (foo.bar, foo.bar)()
    规范:

    2.Call GetValue(lref).

    逗号运算符,使用GetValue方法,之后和示例3一样

  • foo()
    这是很常见的场景,this也是指向的window,但是本质却和示例3、4、5不一样
    根据规则返回的Reference为

    1
    2
    3
    4
    5
    var Reference = {
    base: EnvironmentRecord,
    name: bar,
    strict: false
    }

    ref是reference,但 IsPropertyReference(ref) 是 false,那么this的值为 ImplicitThisValue(ref),ImplicitThisValue 函数始终返回 undefined。所以在非严格模式下,this指向window。

ps:箭头函数的this是父级(词法上的)作用域的this

小结

其实之前我们简单粗暴的方式能够应付大多数情况下this的判断,但是不严谨,无法判断类似(false || foo.bar)()的情况,而且有时即使判断this的值是正确的,但是背后的逻辑却是不同的。所以需要从规范层面来理解this确定的过程。