从冴羽的《JavaScript深入之从ECMAScript规范解读this》引起的思考

栏目: JavaScript · 发布时间: 5年前

内容简介:声明:本人看了冴羽一篇文章后,感觉那边文章写的很好,但是不够详细,所以在这里丰富了一下,然后也希望分享给更多的人,一起学习javascript 的this这个关键字我相信有很多人会像我一样对他既陌生有熟悉。相信每一个学习JavaScript的前端工程师都做过这么一个事那就是到某个搜索引擎 key in ( javscript this),然后看过几个博客后感觉自己懂了this!来来让我们看看大部分博客会怎么写:

声明:本人看了冴羽一篇文章后,感觉那边文章写的很好,但是不够详细,所以在这里丰富了一下,然后也希望分享给更多的人,一起学习

javascript 的this这个关键字我相信有很多人会像我一样对他既陌生有熟悉。相信每一个学习JavaScript的前端工程师都做过这么一个事那就是到某个搜索引擎 key in ( javscript this),然后看过几个博客后感觉自己懂了this!

来来让我们看看大部分博客会怎么写:

  1. MDN
  2. Javascript 的 this 用法--阮一峰
  3. [译] this(他喵的)到底是什么 — 理解 JavaScript 中的 this、call、apply 和 bind -- 掘金
  4. The JavaScript this Keyword
    .....
    此外不就不列举了。

反正基本上都 第一步 ,先告诉你一个事实『 this 是在函数被调用时发生的绑定,指向上下文对象,即被调用函数所处的环境,也就是说,this 在函数内部指向了调用函数的对象。 』; 第二步 ,然后要不通过绑定方式的角度给你从new 绑定,显示绑定等多个角度告诉你this是谁,或者从函数调用情况,构造函数情况,bind调用等多个情况给你分析一波。然后不知道你会不会想我一样深深的记住调用对象是谁,this就是谁,然后把不太明白的特殊情况牢牢的记下,做个笔记什么的。然后觉得我终于弄明白this了,然后在真正使用的时候偶尔还会发生,怎么又忘记这种情况了!!!纳尼?what?脸好疼有没有?

然后在去看网上的博客,看看有没有这种情况的解释,看了冴羽大大的这篇《 JavaScript深入之从ECMAScript规范解读this 》文章后我觉得我可以从一个新的角度再去学习一下,让我们可以更深一步的去理解this,甚至找出一种方法,找this的时候通过“公式”去找到this的指代。

PS.在正式开始之前,本人先声明以下的文章会啰嗦一点,如果你还不是很懂this,希望你有时间去耐心的读下去,希望也可以给你一种新的认识。

2.this的前世今生

在深入了解 JavaScript 中的 this 关键字之前,有必要先退一步,看一下为什么 this 关键字很重要。 this 允许复用函数时使用不同的上下文 。换句话说, “this” 关键字允许在调用函数或方法时决定哪个对象应该是焦点。

2.1 this 是在函数被调用时发生的绑定,那么函数被调用的时候,JavaScript引擎都干了啥?

【ECMAScript规范 10.4.3节 进入函数代码】 是这么解释的: 当控制流根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入 函数代码 的执行环境时,执行以下步骤:

  1. 如果函数代码是严格模式下的代码 ,设 this 绑定为 thisArg(调用者)。(严格等于调用者)
  2. 如果不是严格模式下的代码, 判定thisArg 是不是 null 或 undefined,是则设 this 绑定为 全局对象 。
  3. 否则如果 Type(thisArg) 的结果不为 Object,则设 this 绑定为 ToObject(thisArg)。
  4. 否则设 this 绑定为 thisArg。
  5. 以 F 的 [[Scope]] 内部属性为参数调用 NewDeclarativeEnvironment,并令 localEnv 为调用的结果。
  6. 设词法环境为 localEnv。
  7. 设变量环境为 localEnv。
  8. 令 code 为 F 的 [[Code]] 内部属性的值。
  9. 按描述的方案,使用 函数代码 code 和 argumentList 执行定义绑定初始化步骤。

因为我们是研究this,所以我们着重关注前4条就可以了,简单总结一下就是:当调用函数的时候会创建函数执行上下文,在创建的过程中有一步就是创建this ,并根据是否在严格模式下,调用者是不是NUll或者undefined,调用者是不是对象决定this的指向。

2.2 我们搞清楚在调用的时候的this的指向规则,里面有个很扎眼的词 thisArg ,它是什么鬼?

那我们来看看 【ECMAScript规范 11.2.3节 函数调用】

  1. 令 ref 为解释执行 MemberExpression 的结果 .
  2. 令 func 为 GetValue(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,那么 令 thisValue 为 GetBase(ref). 如果 Type(ref) 不是 Reference , ref 的基值是一个环境记录项,令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果
  7. 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.
  8. 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表

这里的 thisValue的值 其实就是我们要找的thisArg了!!

那么根据规范我们就要先去找MemberExpression 的结果是什么了?

那我们看一看 MemberExpression的语法

  • PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
  • FunctionExpression // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式

原来MemberExpression 的结果就是:执行函数名部分 表达式的结果 (简单理解 MemberExpression 其实就是()左边的部分 表达式的结果 )。

下面我们要看看Reference是什么了?

Reference type :按字面翻译就是引用类型,但是它并不是我们常说的JavaScript中的引用类型,它是一个规范类型(实际并不存在),也就是说是为了解释规范某些行为而存在的,比如delete、typeof、赋值语句等。规范类型设计用于解析命名绑定的(A Reference is a resolved name binding.),它由三部分组成:

  • 基 (base) 值,
  • 引用名称(referenced name)
  • 布尔值 严格引用 (strict reference) 标志。

基值就是属性所在的对象或者就是 EnvironmentRecord,基值是 undefined, 一个 Object, 一个 Boolean, 一个 String, 一个 Number, 一个 environment record 中的任意一个。基值是 undefined 表示此引用可以不解决一个名字的绑定(A base value of undefined indicates that the reference could not be resolved to a binding.)。

PS.最后一句话我的理解是这个引用不需要一个名字和他关联起来,比如我们创建的匿名函数,他的base应该就是undefined。

举个栗子:chestnut::

var foo = 1;

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

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

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

IsPropertyReference(ref),GetBase(ref)和ImplicitThisValue()是什么?

  1. IsPropertyReference(V)。 如果引用的基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。(Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.)
  2. GetBase(V)。 返回引用值 V 的基值组件。(Returns the base value component of the reference V.)
  3. ImplicitThisValue()。 环境记录分为声明式环境记录和对象式环境记录,不同的略有不同。
    • 声明式环境记录项永远将 undefined 作为其 ImplicitThisValue 返回。
    • 对象式环境记录项的 ImplicitThisValue 通常返回 undefined,除非其 provideThis 标识的值为 true。
      • 令 envRec 为函数调用时对应的声明式环境记录项。
      • 如果 envRec 的 provideThis 标识的值为 true,返回 envRec 的绑定对象。
      • 否则返回 undefined。

2.3 this指代计算伪代码

把2.1 和2.2的知识进行总结可以写出如下伪代码:

FUNCTION.this = {
    ref =  MemberExpression 的结果;                        //'()'左边的表达式的结果 
    IF Type(ref) == Reference THEN                        //如果左边的表达式的结果是引用
        IF IsPropertyReference(ref) THEN                    //判定引用的基值是不是一个对象
            thisArg = ref.base                              //如果是,thisArg是这个引用的基值
        ELSE                
            thisArg = ref.base.ImplicitThisValue()        //如果引用的基值不是对象,说明是个环境记录,thisArg是
                                                          //它的ImplicitThisValue,绝大部分情况下是Undefined
    ELSE
        thisArg = undefined;                            //如果左边的表达式的结果不是引用,thisArg是undefined
    
   IF thisArg == undefined                          
        IF 'using strict' THEN                      //thisArg如果是undefined
            return   undefined                       //在严格模式下,函数的this就是undefined
        ELSE
            return   全局对象                       //在严格模式下,函数的this就是undefined
    ELSE
        return  thisArg                             //thisArg不是undefined,函数的this就是这个基值对象
} 
复制代码

3.复习巩固

举几个栗子:chestnut::

var value = 1;

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

var fooObj = {
  value: 2,
  bar: function () {
    return this.value;
  },
  fooIn:{
      value:3,
      foo:foo
  }
}
//示例1
console.log(foo());
//示例2
console.log(fooObj.bar());
//示例3
console.log((fooObj.bar)());
//示例4
console.log((fooObj.bar = fooObj.bar)());
//示例5
console.log((false || fooObj.bar)());
//示例6
console.log((fooObj.bar, fooObj.bar)());
//示例7
console.log(fooObj.fooIn.foo());
//输出结果是:1 2 2 1 1 1 3
复制代码
  1. foo() 的MemberExpression 是 foo,他的base是声明式环境记录,声明式环境记录的隐性的this值(ImplicitThisValue)是undefined ,在非严格模式下所以this指向window。

  2. fooObj.bar()的MemberExpression 是fooObj.bar是个引用,他的base 值是 fooObj,所以this指向 fooObj。

  3. (fooObj.bar)()的MemberExpression 是(fooObj.bar) 一个分组表达式,查看 11.1.6分组表达式 的规范:

    The production PrimaryExpression : ( Expression ) is evaluated as follows: Return the result of evaluating Expression. This may be of type Reference
    

    我们要这个表达式的结果,结果是:fooObj.bar,所以和2是一样的。

  4. (fooObj.bar = fooObj.bar)()的MemberExpression 是(fooObj.bar = fooObj.bar) 我们分组表达式的返回值是 fooObj.bar = fooObj.bar,我们就要计算这个表达式的结果,查看 11.13.1简单赋值的规范 :

    令 lref 为解释执行 LeftH 和 SideExpression 的结果 .

    令 rref 为解释执行 AssignmentExpression 的结果 .

    令 rval 为 GetValue(rref).

    抛出一个 SyntaxError 异常,当以下条件都成立 : Type(lref) 为 Reference IsStrictReference(lref) 为 true Type(GetBase(lref)) 为环境记录项 GetReferencedName(lref) 为 "eval" 或 "arguments"

    调用 PutValue(lref, rval).

    返回 rval.

    由蓝色部分可以知道(fooObj.bar = fooObj.bar) 不是一个Reference,所以thisArg是一个undefined,非严格模式是window.

  5. (false || fooObj.bar)()的MemberExpression 是(false || fooObj.bar) ,类比4查看 11.11 二元逻辑运算符 ,我们知道(false || fooObj.bar) 是个值不是Reference所以this也是指向window

  6. (fooObj.bar, fooObj.bar)()的MemberExpression 是(false || fooObj.bar) ,类比4查看 11.14 逗号运算符 ,,我们知道(false || fooObj.bar) 是个值不是Reference所以this也是指向window。

  7. fooObj.fooIn.foo()的MemberExpression 是fooObj.fooIn.foo,他是个Reference,基值是fooObj.fooIn,所以this指向fooObj.fooIn

总结:

看了本篇文章后应该可以解决绝大多数的this的指向问题,还有传说中的构造函数调用模式 ,箭头函数调用模式 , call、apply、bind 调用模式 ,大家应该也是都可以通过规范解释的清楚,在这里不再一一列举。

问题

var name = "The Window";
  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }
  };
  alert(object.getNameFunc()());//The Window

复制代码

这个我没有在规范里面找到,所以只能当个问题给大家写出来,如果写的不对,还希望有大神在评论中指出来。

object.getNameFunc()()的MemberExpression 是object.getNameFunc(),这个函数的运行结果是return的匿名函数的指针,是个Reference但是它的base不是对象,所以是一个环境记录项,没有被bind,this,call改变所以ImplicitThisValue()为undefined 所以非严格模式下就是window


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

亿级流量网站架构核心技术

亿级流量网站架构核心技术

张开涛 / 电子工业出版社 / 2017-4 / 99

《亿级流量网站架构核心技术》一书总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。本书分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。 不管是软件开发人员,还是运维人员,通过阅读《亿级流......一起来看看 《亿级流量网站架构核心技术》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试