从 ECMA 规范上理解 this

773 阅读13分钟

深入理解 this

this 在 JavaScript 中的指向一般都是指向调用者,如果没有调用者则指向 undefined ,非严格模式下指向 undefined 会默认指向全局对象(window)。

上述指向是指一般情况,采用 callapplybind 函数进行特殊处理后,可以修改原有的 this 指向。

那么问题来了,除却特殊调用,this 真的默认指向调用者么?

本文会先阐述两个不相关的内容 Type(类型),Reference(引用类型), 在阐述在函数中 this 指向的规则处理。

Type

在ECMA规范的第八节中:类型又分为语言类型和规范类型

  • 语言类型:字符串(String),数值(Number),对象(Object),布尔值(Boolean) 等等
  • 规范类型:是描述 ECMA 语言构造与 ECMA 语言类型语意的算法所有的元值对应的类型。规范类型包括引用类,列表,完结,属性描述式,属性标示,词法环境,环境记录。规范类型可用来描述 ECMA 表示运算的中途结果,但是这些值不能存为对象的变量或是 ECMA 语言变量的值

从规范上可以知道,语言类型是我们使用 JS 的过程中可以声明及获取到的类型,规范类型是存在与 ECMA 中的,是为了维持 JS 引擎的运行而存在的,不能外部直接获取。

Reference(引用类型)

ECMA 在8.7节中提到:引用类型是用来说明 delete,typeof,赋值运算符这些运算符的行为。 一个 Reference(引用) 是个已解决的命名绑定。一个引用由三个部分组成

  • base:基值。可以是 undefined,Object,Boolean,String,Number,Environment record(环境记录项)
  • reference name:引用名称。字符串(String)
  • strict reference:严格引用标志。是一个 布尔值(Boolean)

可以参考下面这个对象

var foo = 1;

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

Reference(引用类型) 有几个方法,仅作了解(ECMA内部判断使用,外部无法调用)

  • GetBase(V)。返回引用值 V 的基值。返回一个 reference 的 base
  • GetReferencedName(V)。返回引用值 V 的引用名称。返回一个 reference 的 reference name
  • IsStrictReference(V)。返回引用值 V 的严格引用值。返回一个 reference 的 strict
  • HasPrimitiveBase(V)。如果基值是 Boolean,String,Number,那么返回 true
  • IsPropertyReference(V)。如果基值是个对象或 HasPrimitiveBase(V) 方法是 true,那么返回 true,否则返回 false
  • IsUnresolveableReference(V)。如果基值是 undefined 那么返回 true ,否则返回 false

GetValue

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

比较复杂,我采用了代码形式来表示了一下上述逻辑

// 伪逻辑
function GetValue(V) {
  // 如果 Type(V) 不是 Reference(引用类型),则返回 V
  if (Type(V) !== Reference) {
      return;
  }
  // 使 base 为调用 GetBase(V) 的返回值
  var base = GetBase(V);
  // 如果 IsUnresolveableReference(V) 为 true,则抛出一个 ReferenceError 异常
  if (IsUnresolveableReference(V)) {
  	throw Error('ReferenceError 异常');
  }
  // IsPropertyReference(V) 为 true
  if (IsPropertyReference(V)) {
    // 如果 HasPrimitiveBase(V) 是 false
     if (!HasPrimitiveBase(V)) {
      // 使 get 成为 base 的 [[Get]] 内部方法
      base.[[Get]] = get;
      // 将 base 作为 this 值,传递 GetReferencedName(V) 为参数,调用 get 内部方法。返回结果
      return base.[[Get]](GetReferencedName(V));
    }
  } 
  // IsPropertyReference(V) 为 false
  else {
    // 如果 IsPropertyReference(V) 为 false,base 必须是一个 environment record(环境记录)
    base = 'environment record';
    // 传递 GetReferencedName(V) 和 IsStrictReference(V) 为参数调用 base 的 GetBindingValue 具体方法,返回结果
    return base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))
  }
}

由上面可以看出,GetValue 调用内部的方法获取其 V 的参数值,而这个值,不是 Reference(引用类型) ,而是一个具体的值。

如何确定 this 值?

ECMA 第11.2.3 节:这章节说明了函数调用的情况

  1. 使 reference 为解释执行 MemberExpression 的结果
  2. 使 function 为 GetValue(reference)
  3. 使 argList 为解释执行 Arguments 的结果
  4. 如果 Type(function) 不是一个 Object 类型,抛出一个 TypeError 的异常
  5. 如果 IsCallable(function) 是 false,抛出一个 TypeError 的异常
  6. 如果 Type(reference) 为 Reference 类型,那么如果 IsPropertyReference(reference) 为 true,那么令 thisValue 为 GetBase(reference) 的 ImplictitThisValue 的具体方法的结果
  7. 否则,Type(reference) 不是 Reference,令 thisValue 为 undefined

第一条 MemberExpression 规范里11.2 左值表达式说明了

  • PrimaryExpression:原始表达式
  • FunctionExpression:函数定义表达式
  • MemberExpression [Expression]:中括号属性访问表达式。obj [name]
  • MemberExpression . IdentifierName:. 属性访问表达式。obj . name
  • new MemberExpression Arguments:new 关键字

第六条说明了如果经过 判断后是 Reference 类型,就将 thisValue 为 GetBase(reference) 的值 第七条说明 不是 Reference 类型,就将 thisValue 设为 undefined

thisValue 就是我们平常使用的 this 值

从上面的规范可以看到,确定 thisValue 的关键在于调用函数时解释 MemberExpression 的结果是不是一个 Reference 类型 现在我们使用一些例子,在配合 ECMA规范 11 节 中的各种能调用函数的表达式,来说明可能会产生调用的场景以及 this 的结果。 以下例子无特殊情况,都是使用此代码;

// 注意 严格模式下 内部的 this 不会指向 window,方便测试不采用严格模式
var value = 'global';

var foo = {
  value: 'internal',
  bar: function () {
    console.log(this.value);
  }
}

以下例子说明的时候有时候会产生简写,此处写入注解

  • lref:left reference
  • lval:left Value
  • rref:right reference
  • rval:right Value

11.1.6 分组表达式

产生 PrimaryExpression:(Expression) 按照下面的过程执行 返回 Expression 的结果,它可能是 Reference 类型

// 分组表达式
console.log((foo.bar)()); // internal

从 11.1.6节 的描述可以看出,分组表达式的返回值,并不会特意去处理值,所以相当于执行 foo.bar()。

11.2.1 属性访问

属性访问方式有两种

// 中括号访问
MemberExpression [ Expression ]
MemberExpression [ <identifier-name-string> ]

// .属性访问
MemberExpression . IdentifierName 

产生一个表达式 MemberExpression:MemberExpression [Expresssion]

  1. 使 baseReference 为 解释 + 执行 MemberExpression 的结果
  2. 使 baseValue 为 GetValue(baseReference) 执行后的结果
  3. 使 propertyNameReference 为 解释 + 执行 Expresssion 的结果
  4. 使 propertyNameValue 为 GetValue(propertyNameReference)
  5. 调用 CheckObjectCorercible(baseValue)
  6. 令 propertyNameString 为 ToString(propertyNameValue)
  7. 如果正在执行中的语法产生包含严格模式代码当中,使 strict 为 true,否则灵 strict 为 false
  8. 返回一个值类型的引用,其基值为 baseValue 且其引用名为 propertyNameString,严格模式标记为 strict
// 属性访问方式
console.log(foo.bar()); // internal
console.log(foo['bar']()); // internal

前面的步骤都是在说明通过对象来访问对象中属性的过程,第六条可以看出使用 [] 来访问属性会强制转换成字符串。第八条可以看出结果,返回一个 Reference 类型数据

11.11 二元逻辑运算符

二元逻辑运算符具备两种 产生式 LogincaANDExpression:LogincaANDExpression && BitwiseORExpression

  1. 使 lref 为 解释 + 执行 LogincaANDExpression 的结果
  2. 使 lval 为 GetValue(lref)
  3. 如果 ToBoolean(lval) 为 false,返回 lval
  4. 使 rref 为 解释 + 执行 BitwiseORExpression 的结果
  5. 返回 GetValue(rref)

产生式 LogincaORExpression:LogincaORExpression || LogincaANDExpression

  1. 使 lref 为解释 + 执行 LogincaORExpression 的结果
  2. 使 lval 为 GetValue(lref)
  3. 如果 ToBoolean(lval) 为 true,返回 lval
  4. 使 rref 为 解释 + 执行 LogincaANDExpression 的结果
  5. 返回 GetValue(rref)

由上面两个解释也可以看得出 && 和 || 自带的短路逻辑如何执行。 不管是 && 还是 || 这两种表达式都是用了 GetValue 函数作为返回值,所以,返回的是 global

// 二元逻辑运算符
console.log((true && foo.bar)()); // global
console.log((false || foo.bar)()); // global

11.12 条件运算符

产生式 ConditionalExpression:LogicalORExpression ? AssignmentExpression : AssignmentExpression 。

  1. 令 lref 为 解释 + 执行 LogicalORExpression 的结果
  2. 如果 ToBoolean(GetValue(lref)) 为 true,那么 令 trueRef 为 解释+执行第一个(也就是:左边) AssignmentExpression 的结果,返回 GetValue(trueRef)
  3. 否则 令 falseRef 为解释 + 执行 第二个 AssignmentExpression 的结果。返回 GetValue(falseRef)

由上述的逻辑可以看到 条件运算符也是 通过 GetValue() 进行返回的,所以打印的是 global

// 条件运算符
console.log((false ? '' : foo.bar)()); // global
console.log((true ? foo.bar : '')()); // global

11.13 赋值运算符

产生式 AssignmentExpression:LeftHandSideExpression = AssignmentExpression;

  1. 令 lref 为 解释 + 执行 LeftH 和 SideExpression 的结果
  2. 令 rref 为 解释 + 执行 AssignmentExpression 的结果
  3. 令 rval 为 GetValue(rref)
  4. 如果以下条件成立,抛出一个 SyntaxError 异常
    1. Type(lref) 为 Reference
    2. IsStrictReference(lref) 为 true
    3. Type(GetBase(lref)) 为 environment record(环境记录)
    4. GetReferencedName(lref) 为 'eval' 或 'arguments'
  5. 调用 PutValue(lref,rval)
  6. 返回 Rval

比较复杂,难懂,但是 只看 3即可,rval 为 GetValue 返回值,就与 Reference 类型数据无缘,所以为 global

注意:副作用调用与正常的, A 赋值给 B,B 调用方法不一样。

// 赋值运算符
console.log((foo.bar = foo.bar)()); // global

// 赋值运算符采用上面的情况几乎没有,一般都是下面这种情况
var obj = {
  value: 'ceshi'
}
obj.bar = foo.bar;
console.log(obj.bar()); // ceshi

11.14 逗号运算符

产生式 Expression:Expression,AssignmentExpression

  1. 令 lref 为解释 + 执行 Expression 的结果
  2. Call GetValue(lref)
  3. 令 rref 为解释 + 执行 AssignmentExpression 的结果
  4. 返回 GetValue(rref)

返回 GetValue() 的值,所以指向 window

// 逗号运算符
console.log((foo.bar, foo.bar)()); // global

call、apply 和 bind 如何修改 this 值

了解 call 、apply 和 bind 的时候,我们首先了解下一个函数 [[Call]]

[[Call]]

此处有两个 [[Call]] 需要分别清楚,一个是 ECMA 8.6.2 对象内部方法 中提到的 [[Call]],一个是 ECMA 15.3.4.5.1 [[Call]]

  • 8.6.2的 [[Call]]:没有代码逻辑,只有一段说明。
    • [[Call]]:SpecOp(any,a List of any) - any or Reference
    • 运行与此对象关联的代码。通过函数调用表达式调用。SpecOp 的参数是一个 this 对象和函数调用表达式传来的参数组成的列表。实现了这个这个内部方法的对象是可调用的。只有作为宿主对象的可调用对象才可能返回引用值。

8.6.2 的 [[Call]] 只有函数才能调用,第一个参数 any 代表所需要绑定的 this 对象, a List of any 代表执行函数时所传入的参数列表。apply 和 call,bind 都是调用此方法进行 this 环境绑定。

  • 15.3.4.5.1 [[Call]]:

当调用一个用 bind 函数创建的函数对象 F 的 [[Call]] 内部方法,传入一个 this 值 和一个参数列表 ExtraArgs,采用如下步骤

  1. 令 boundArgs 为 F 的 [[BoundArgs]] 内部属性值
  2. 令 boundThis 为 F 的 [[BoundThis]] 内部属性值
  3. 令 target 为 F 的 [[TargetFunction]] 内部属性值
  4. 令 args 为一个新列表,它包含与列表 boundArgs 相同顺序相同值,后面跟着与 ExtraArgs 是相同顺序相同值
  5. 提供 boundThis 作为 this 值,提供 args 为参数调用 target 的 [[Call]] 内部方法,返回结果

此方法只有用 bind 函数的使用才能使用。本质上还是调用 8.6.2 里的 [[Call]]

Function.prototype.[[Call]] = function () {
	var boundArgs = F.[[BoundArgs]];
  var boundthis = F.[[BoundThis]];
  var target = F.[[TargetFunction]];
  var args = [...boundArgs, ...ExtraArgs];
  target.[[Call]](boundthis, args);
}

由上可知,不管是通过 bind 函数调用的专属 [[Call]] 还是 由 apply,call 函数内部实现,都调用了 ECMA规范里 8.6.2 对象内部方法提到的 [[Call]],而看过这一节的应该知道了,[[Call]] 并不是每一个对象都有,而是特定对象才有,那么如果知道那些对象才有呢?内部提供了一个方法,用于判断那些对象具有 IsCallable 抽象操作 IsCallable 是用来判断对象是否可以调用内部的 [[Call]] 方法

类型结果
Undefinedfalse
Nullfalse
Booleanfalse
Numberfalse
Stringfalse
Object如果参数对象包含一个 Call 内部方法,则返回 true,否则返回 false

call

ECMA 规范 15.3.4.4 里有完善的逻辑说明 Function.prototype.call(thisArg, [,arg1[,arg2, ...]]) 当以 thisArg 和 可选的 arg1,arg2 等作为参数在一个 function(以下简称func) 对象上调用 call 方法,采用如下步骤

  1. 如果 IsCallable(func) 是 false,则抛出一个 TypeError 异常
  2. 令 argList 为一个空列表
  3. 如果调用这个方法的参数多于一个,则从 arg1 开始以从左到右的顺序将每个参数插入为 argList 的最后一个元素
  4. 提供 thisArg 作为 this 值,并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果
// 用简单的代码实现上面的伪逻辑,用一些现在的语法 取巧实现
Function.prototype.call = function (thisArg, ...arg) {
  // 因为调用此函数,根据文章上面所描述的 所以 this 指向调用者,又因为是函数本身调用的,所以
  // this 指向函数
	var func = this;
  if (!IsCallable(func)) {
  	throw Error('类型异常')
  }
 	// 令 argList 为一个空列表
  var argList = [];
  // 如果调用这个方法的参数多于一个,则从 arg1 开始以从左到右的顺序将每个参数插入为 argList 的最后一个元素
  if (arg instanceof Array && arg.length > 1) {
  	argList = arg;
  }
  // 提供 thisArg 作为 this 值,并以 argList 作为参数列表,调用 function 的 [[Call]] 内部方法,返回结果
  return func.[[Call]](thisArg, argList);
}

apply

ECMA 规范 15.3.4.3 里有完善的逻辑说明 Function.prototype.apply(thisArg, argArray) 当以 thisArg 和 argArray 为参数在一个 function(以下简称func) 对象上调用 apply 方法,采用如下步骤

  1. 如果 IsCallable(func) 是 false,则抛出一个 TypeError 异常
  2. 如果 argArray 是 null 或 undefined,则返回提供 thisArg 作为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果
  3. 如果 Type(arrArray) 不是一个 Object,则抛出一个 TypeError 异常
  4. 令 len 为 argArray 调用 length 的结果
  5. 令 n 为 ToUnit32(len)
  6. 令 argList 为一个空列表
  7. 令 index 为 0
  8. 只要 index < n 就重复
    1. 令 indexName 为 ToString(index)
    2. 令 nextArg 为 以 indexName 作为参数调用 argArray 的 [[Get]] 内部方法的结果。(简单来讲就是 arrArray[indexName])
    3. 将 nextArg 作为最后一个元素插入到 argList 里
    4. 设定 index 为 index + 1
  9. 提供 thisArg 作为 this 值,并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法,返回结果

上面 4 - 8 条看起来很多,其实只作了一件事情,将 argArray 赋值给 argList,但并不是直接赋值,而是将其里面一个个参数添加到 argList 中

// 用简单的代码实现下伪逻辑
Function.prototype.apply = function (thisArg, argArray) {
	var func = this;
  if (!IsCallable(func)) {
  	throw Error('类型异常')
  }
  if (Type(argArray) === 'undefined' || Type(argArray) === 'null') {
  	return func.[[Call]](thisArg, []);
  }
 	if (!(Type(argArray) === 'Object' && argArray instanceof Array)) {
    throw Error('类型异常')
  }
  var len = argArray.length;
  var index = 0;
  var argList = [];
  while (index < len) {
    var indexName = ToString(index);
    var nextArg = argArray[indexName];
    argList.push(nextArg);
  	index = index + 1;
  }
  return func.[[Call]](thisArg, argList);
}

bind

ECMA 规范 15.3.4.5 里有完善的逻辑说明 Function.prototype.bind(thisArg, [,arg1[,arg2, ...]]) bind 方法,需要一个或更多参数,thisArg 和 (可选的) arg1,arg2等,执行如下步骤返回一个新的函数对象

  1. 令 Target 为 this 值
  2. 如果 IsCallable(Target) 是 false,抛出一个 TypeError 异常
  3. 令 A 为一个新的参数列表,它包含按顺序的 thisArg 后面的所有参数
  4. 令 F 为一个新原生 ECMA 函数对象
  5. 依照 8.12 指定,设定 F 除了 [[Get]] 之外的所有内部方法
  6. 依照 15.3.5.4 指定,设计 F 的 [[Get]] 的内部属性
  7. 设定 F 的 [[TargetFunction]] 内部属性为 Target
  8. 设定 F 的 [[BoundThis]] 内部属性为 thisArg 的值
  9. 设定 F 的 [[BoundArgs]] 内部属性为 A
  10. 设定 F 的 [[Class]] 内部属性为 "Function"
  11. 设定 F 的 [[Prototype]] 内部属性为 15.3.3.1 指定的标准内置 Function 的 prototype 对象
  12. 依照 15.3.4.5.1 描述,设定 F 的 [[Call]] 内置属性
  13. 依照 15.3.4.5.2 描述,设定 F 的 [[Construct]] 内置属性
  14. 依照 15.3.4.5.3 描述,设定 F 的 [[Haslnstance]] 内置属性
  15. 如果 Target 的 [[Class]] 内部属性是 "Function",则
    1. 令 L 为 Target 的 length 属性减去 A的长度
    2. 设定 F 的 length 自定属性为 0 和 L 中更大的值
  16. 否则设定 F 的 length 自定属性为 0
  17. 设定 F length 自身属性的特性为 15.3.5.1 指定的值
  18. 设定 F 的 [[Extensible]] 内部属性为 true
  19. 令 thrower 为 [[ThrowTypeError]] 函数对象
  20. 以 "caller",属性描述符 { [[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false },和 false 作为 参数调用 F 的 [[DefineOwnProperty]] 内部方法
  21. 以 "arguments" ,属性描述符 { [[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false },和 false 作为参数调用 F 的 [[DefineOwnProperty]] 内部方法
  22. 返回 F

Function.prototype.bind 创建的对象不包括 prototype 属性或 [[Code]],[[FormalParameters]],[[Scope]] 内部属性

// bind 的实现可谓是最复杂的了,参考了规范里的许多东西
Function.prototype.bind = function (thisArg, ...arg)
	var Target = this;
  if (!IsCallable(func)) {
  	throw Error('类型异常')
  }
  var A = [...arg];
  var F = function () {};
	// 依照 8.12 设定 F 拥有 除了 [[Get]] 之外的全部方法
	F.[[GetOwnProperty]] = Object.[[GetOwnProperty]];
	F.[[GetProperty]] = Object.[[GetProperty]];
	F.[[CanPut]] = Object.[[CanPut]];
	F.[[Put]] = Object.[[Put]];
	F.[[HasProperty]] = Object.[[HasProperty]];
	F.[[Delete]] = Object.[[Delete]];
	F.[[DefaultValue]] = Object.[[DefaultValue]];
	F.[[DefineOwnProperty]] = Object.[[DefineOwnProperty]];
	
	// 依照15.3.5.4 设定 F 的 [[Get]]
	F.[[Get]] = Function.[[Get]]
	// 设定 F 的 [[TargetFunction]] 内部属性为 Target
	F.[[TargetFunction]] = Target;
	// 设定 F 的 [[BoundThis]] 内部属性为 thisArg 的值
	F.[[BoundThis]] = thisArg;
	// 设定 F 的 [[BoundArgs]] 内部属性为 A
	F.[[BoundArgs]] = A;
	// 设定 F 的 [[Class]] 内部属性为 "Function" 
	F.[[Class]] = "Function";
	// 设定 F 的 [[Prototype]] 内部属性为 15.3.3.1 指定的标准内置 Function 的 prototype 对象
	F.[[Prototype]] = Function.[[Prototype]];

	// 15.3.4.5.1,2,3 的描述 添加 [[Call]] [[Construct]] [[Haslnstance]]
	F.[[Call]] = Function.[[Call]];
	F.[[Construct]] = Function.[[Construct]];
	F.[[Haslnstance]] = Function.[[Haslnstance]];
	if (Target.[[Class]] === "Function") {
    var L = Target.length - A.length;
    F.length = Math.max(0, L);
  } else {
  	F.length = 0;
  }
	// 处理 length 属性
	F.[[DefineOwnProperty]])("length", {
    [[Writable]]: false,
    [[Enumerable]]: false,
    [[Configurable]]: false
  })
	F.[[Extensible]] = true;
	var thrower = [[ThrowTypeError]];
	// 处理 "caller" 属性
	F.[[DefineOwnProperty]])("caller", { 
    [[Get]]: thrower, 
    [[Set]]: thrower, 
    [[Enumerable]]: false, 
    [[Configurable]]: false 
  })
	// 处理 "arguments" 属性
	F.[[DefineOwnProperty]])("arguments", { 
    [[Get]]: thrower, 
    [[Set]]: thrower, 
    [[Enumerable]]: false, 
    [[Configurable]]: false 
  })
  return F;
}

总结

虽然 call、apply 和 bind 三者不同,但是对于底层实现逻辑而言,三者都是调用 Object 对象的内部方法 [[Call]] 来实现绑定。三者的区别也仅仅只是使用方法上的区别

  • call(thisArg, [,arg1[,arg2, ...]]):传入需要绑定的对象,和一连串参数,以逗号分割,是否返回参数由调用函数来决定
  • apply(thisArg,argList):传入需要绑定的对象和一个数组,数组里可以存放函数参数,以下标分割,是否返回参数由调用函数决定
  • bind(thisArg,[,arg1[,arg2, ...]]):传入需要绑定的对象,和一连串参数,以逗号分割,返回一个函数。使用返回的函数后是否返回参数由调用函数来决定

本文简单了些,只是说明了 this 的来源,以及各种使用情况下 this 的取值,最后 call、apply 和 bind 也缺少了代码实现,下次有机会,会补上。

文章有错误的地方可以提出,从语雀复制过来有格式上的错误

本文还少了一个环节,箭头函数的 this 说明,下次会补充也可以自行查看 ECMA规范ES6英文版

文章引用

JavaScript深入之从ECMAScript规范解读this

ECMA规范ES5.1版中文版

ECMA规范ES5.1版英文版