# JS-This (二)

🐴

# 前言

上一篇文章中我们从this指向的多种情况来认识了一下this, 这篇文章中我们从ECMAScritp 规范来看看this 是如何确定的。

JavaScript中,this是一直讨论的东西,并且在每个上下文创建时,this就被创建了,另外还有三个重要的属性也会被创建:变量对象作用域链。最近抽出了部分时间将JavaScriptthis的知识汇总了一下。

ECMAScript 镜像版规范 (opens new window)

# ECMAScript 中的数据类型

js中的数据类型分为语言类型和规范类型:

  • 语言类型 ECMAScript 语言类型 是 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 语言变量的值。

# 引用类型(Reference)

注意

该引用类型是规范中的数据类型,并不是javascript实际代码中的引用类型(因为我们常说Object是引用类型,这里的Reference并不是指的它)

上面说了ECMAScript中的数据类型,其中规范类型中的引用类型Reference (opens new window)this有这亲密的关系。引用类型Reference 是规范中的类型,在js中并不存在,只是存在于ECMAScript底层。引用类型Reference包含有三个重要的属性:

  • 基值(base):属性所在的环境对象,可以是 undefined, Object,Boolean,String,Number, environment record中的任意一个,当所处的环境是全局环境(在全局中,或setTimeoutsetInterval),或执行环境中时值为environment record (opens new window)
  • 引用名称(referenced name):该引用名称,是一个字符串
  • 严格引用标志 (strict reference):是否是严格模式,true为严格模式

例如:

var a = 1;

// a的引用类型Reference, a处于全局环境
AReferenceObj = {
  base:EnvironmentRecord, 
  name:'a',
  strict: false,
}

// 第二种基值为EnvironmentRecord情况 
//f执行时处于函数fun的执行环境中
function fun(){
  function f(){}
  f()
}

fun()
//函数f的引用类型Reference
FReferenceObj = {
  base:EnvironmentRecord, 
  name:'f',
  strict: false,
}

在例如:

var obj = {
    fun:function(){

    }
}

// fun对应的Reference是:
var FunReferenceObj = {
    base: obj,
    name: 'fun',
    strict: false
};

另外引用类型Reference除了拥有三个重要的属性外,还提供了一些方法比如:

  • GetBase(V)。 返回引用值 V 的基值, base的值。
  • 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

另外ECMAScript 还提提供了一个GetValue(v) (opens new window) 方法,它的作用就是获取Reference中的具体值。当使用GetValue(v)方法时,返回的是具体值,不在是一个引用类型Reference 例如:

var a = 1;

// a的引用类型Reference
AReferenceObj = {
  base:EnvironmentRecord, // 全局对象
  name:'a',
  strict: false,
}
GetValue(AReferenceObj)  // 返回值为1 ,1可不再是引用类型

# 函数中This的确定

# 函数执行中的规范

ECMAScript 规范中,函数的调用 (opens new window)解释了this是如何确定的

  1. ref 为解释执行 MemberExpression 的结果

...

  1. 如果 Type(ref)Reference,那么 如果 IsPropertyReference(ref)true,那么 令 thisValueGetBase(ref). 否则 , ref 的基值是一个环境记录项 令 thisValue 为调用 GetBase(ref)ImplicitThisValue 具体方法的结果
  2. 否则 , 假如 Type(ref) 不是 Reference. 令 thisValueundefined. 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表

上面函数调用时确定this的值说清楚点就是:

  1. MemberExpression解析后的结果赋值给ref (MemberExpression (opens new window)就是函数调用时括号左边的部分,如:foo()中的MemberExpressionfoo)
  2. 使用Type(ref)判断ref是不是Reference类型,判断时有如下几种情况
  • 如果 ref 是 引用类型Reference,并且 基值是否是对象(IsPropertyReference(ref)true), 那么 this 的值为 基值GetBase(ref)
  • 如果 ref 是 引用类型Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)(js中为window
  • 如果 ref 不是 引用类型Reference,那么 this 的值为 undefined,(js中严格模式下为undefined,非严格模式下为window,可以通过IsStrictReference(ref)来确定是否是严格模式)

上面我们说了函数调用时MemberExpression (opens new window)就是括号左边的,如:

function f(){} 

f() // MemberExpression 为f

var obj = {
  fun:function(){}
}
obj.fun() // MemberExpression 为obj.fun

function fun(){
  return function(){}
}
fun()()// MemberExpression 为fun()

obj['funName']()// MemberExpression 为obj['funName']

第二步我们很重要的就是需要判断ref 是否是引用类型,一般ref是引用类型的情况有两种:

  • 函数标示符
  • 属性访问器(点.属性访问器或中括号[]属性访问器)

# 实际例子

下面我们来使用实际列子看看函数调用时this的值是如何确定的

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

function fun(){
  console.log(this)
}
foo.bar(); // 例子1
(foo.bar)(); // 例子2
(foo.bar = foo.bar)(); // 例子3
(false || foo.bar)(); // 例子4
(foo.bar, foo.bar)(); // 例子5
fun() // 例子6
  1. foo.bar()在执行时,MemberExpressionfoo.bar, 并将MemberExpression赋值给了ref;然后通过Type(ref)判断foo.bar是不是引用类型,因为foo.bar是属性访问器形式,所以foo.bar是引用类型; 然后判断基值是否是对象IsPropertyReference(ref), foo.bar是对象所以this确定为foo对象(通过GetBase(ref)获取 this = GetBase(ref))

  2. (foo.bar)()在执行时()并没有使用GetValueMemberExpression进行处理,所以MemberExpressionfoo.bar, 接下来的情况和例一就一样了。

  3. (foo.bar = foo.bar)()在执行时,在左边括号中foo.bar = foo.bar因为使用了赋值运算=,此时调用了GetValue 方法,将foo.bar最终的值不在是引用类型(上面说了调用GetValue方法返回的是具体值,不在是一个引用类型Reference), 所以thisundefined,之后还会通过IsStrictReference(V)判断是否在严格模式下,如果不是thisundefined时,this将隐士转换为全局对象windowglobal

  4. (false || foo.bar)()在执行时,逻辑运算符也会调用GetValue 方法,所以最终this结果和例三一样

  5. (foo.bar, foo.bar)()在执行时,逗号运算符也会调用GetValue 方法,所以最终this结果和例三一样

  6. fun()在执行时,因为fun是一个标识符所以ref会是引用类型;但是引用类型的基值是EnvironmentRecord,它并不是一个对象,通过IsPropertyReference(ref)判断返回false,此时this值为ImplicitThisValue(ref),ImplicitThisValue(ref)函数通常会返回undefined声明式环境 (opens new window),对象式环境 (opens new window));之后还会通过IsStrictReference(V)判断是否在严格模式下,如果不是thisundefined时,this将隐士转换为全局对象windowglobal

# 构造函数中的This

在构造函数中this还是很好确定的,在构造函数中this永远指向通过构造函数创建的对象。

function people(){
  this.name = "king";
  this.age = 52
}

var one = new People()
var two = new People()

上面在构造函数中的this会指向通过new创建出来的对象。第一种情况下people中的this指向 one对象,第二种情况下people中的this指向 two对象

tom大叔

针对上面的构造函数new运算符调用people函数的内部的[[Construct]] 方法,接着,在对象创建后,调用内部的[[Call]] 方法。 所有相同的函数people都将this的值设置为新创建的对象。

# 改变This指向的方式

js我们可以使用callapplybind、方法可以来改变this的指向的。下面我们来看看着几个方法的使用。

# call 方法

call方法可以执行一个函数,并改变一个函数中this的指向,call方法中有多个参数,第一个参数是this指向对象,后面的参数就是调用函数时传入的参数。

var obj = {
  name: "King",
  age:23
}

var name = "global";

function fun(age,sum){
  console.log(this.name)
  console.log(age,sum)
}

fun.call(obj,50,30) //King; 50 30

我们会发现通过call调用函数fun后,fun函数内部的this指向了对象obj,并且传入了两个参数50 30

# apply 方法

apply方法也是可以执行一个函数,并改变一个函数中this的指向,和call方法不同的是,apply方法的第二个参数传入的是一个数组。

var obj = {
  name: "King",
  age:23
}

var name = "global";
function fun(age,sum){
  console.log(this.name)
  console.log(age,sum)
}

fun.call(obj,[50,30]) //King; 50 30

# bind 方法

jsbind方法可以给函数中this绑定一个固定的对象,并返回一个新函数

var obj = {
    name: "King",
    age:23
}

var name = "global";

function fun(age,sum,a){
   console.log(this.name)
}

var newFun = fun.bind(obj)
newFun() //King
setTimeout(newFun,2000) // King

通过例子我们可以看到,通过bind方法处理后,返回的新函数中的this永远都会指向同一个对象,而不会随着函数的调用方式不同而改变。

# DOM事件中This

DOM事件中绑定的函数中的this指向触发该事件的DOM元素,这点毋庸置疑。

DOM事件中This

关于DOM事件中This 虽然知道指向触发该事件的DOM元素,但是在ECMAScript规范中是如何实现的,我目前不太确定,希望知道的同学可以提供一下帮助。

参考链接:

最近更新时间: 7/2/2021, 11:27:27 AM