【2】this

光速编译
• 阅读 1207

this

说到this,需要明确三方面内容:

  • this何时被赋值
  • this被赋了什么值
  • 内置函数如何使用this的

this何时被赋值

进入函数代码

当控制流根据一个函数对象 F、调用者提供的 thisArg 以及调用者提供的 argumentList,进入函数代码的执行环境时,执行以下步骤:

  1. 如果函数代码是严格模式下的代码,设 this 绑定 为 thisArg。
  2. 否则如果 thisArg 是 null 或 undefined ,则设 this 绑定为全局对象。
  3. ……
以上信息来源:进入函数代码

从上诉信息中可以知道

  • this 与调用者提供的 thisArg 密切相关
  • this 在严格模式下为 thisArg
  • this 在非严格模式下为 thisArg 或 全局对象

那么 thisArg 又是怎么来的呢?下面来看下函数调用过程:

函数调用

  1. 令 ref 为解释执行 MemberExpression 的结果。
  2. 令 func 为 GetValue(ref)。
  3. 令 argList 为解释执行 Arguments 的结果,产生参数值们的内部列表(参见 11.2.4)。
  4. 如果 Type(func) 不是 Object,抛出一个 TypeError 异常。
  5. 如果 IsCallable(func) 为 false,抛出一个 TypeError 异常。
  6. 如果 Type(ref) 为 Reference,那么

    • 如果 IsPropertyReference(ref) 为 true,

      • 那么令 thisValue 为 GetBase(ref)。
    • 否则,ref 的基值是一个环境记录项。

      • 令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果。
  7. 否则,Type(ref) 不是 Reference。

    • 令 thisValue 为 undefined。
  8. 返回调用 func 的 [[Call]] 内置方法的结果,传入 thisValue 作为 this 值和列表 argList 作为参数列表。
以上信息来源:函数调用

从上诉信息中可以知道

  • thisArg 即 thisValue
  • thisValue 与 ref 的类型密切相关
  • 如果 ref 的类型是 Reference(引用规范类型

    • 如果 ref 是属性引用,通过 GetBase(ref)(返回引用值ref的基值部分) 获取 thisValue
    • 否则,通过 ImplicitThisValue 方法获取 thisValue
  • 否则,thisValue 为 undefined

那么,了解到这里可能有许多新的疑问,比如:

  • Reference 是怎样的类型
  • ref 是怎么来的
  • ref 什么时候是 Reference,什么时候不是。
  • GetBase(ref) 和 ImplicitThisValue 是如何产生结果的
Reference 是怎样的类型

首先先解释下 Reference。

其实ES中的类型分为ECMAScript语言类型规范类型

ECMAScript语言类型对应的是程序员使用 ECMAScript 语言直接操作的值,如 Undefined、Null、Boolean、String、Number、Object等。
规范类型可用来描述 ECMAScript 表达式运算的中间结果,但这样的值不能储存为对象的属性或 ECMAScript 语言的变量值。引用尤雨溪的解释:

这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。
ref 是怎么来的

从上诉函数调用中可以知道,ref 是解释执行 MemberExpression 的结果。
下面详细看下 MemberExpression 的解析过程:

产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :

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

从上诉信息中分析可以知道

  • 解释执行 MemberExpression 的结果是一个引用规范类型(Reference)
  • 这个引用规范类型包含三部分信息:

    • baseValue
    • propertyNameString
    • strict
  • thisValue 的值取决于 baseValue
  • baseValue 是调用 GetValue 获得的。
  • GetValue 得到的基值是 undefined、Object、Boolean、String、Number、环境记录项中的任意一个(详见:引用规范类型),而不是引用规范类型。
GetValue 详细过程见:GetValue
ref 什么时候是 Reference,什么时候不是 Reference

一般来说,ref 是MemberExpression解析的结果,都将是 Reference。
但是,如果 MemberExpression 是其函数表达式的一部分,则可能将改变最终解析结果的类型。
而改变解析结果类型的主要原因取决于是否调用了 GetValue 方法,如果调用了 GetValue ,函数中间值 ref 将是 Object 类型。

那么哪些表达式不使用 GetValue 呢?

更多表达式详见:表达式
GetBase(ref) 和 ImplicitThisValue 是如何产生结果的
  • GetBase:返回引用值ref的基值部分
  • ImplicitThisValue : 声明式环境记录项永远将 undefined 作为其 ImplicitThisValue 返回。

this被赋了什么值

其实在 this何时被赋值 部分已经介绍了 this被赋了什么值。下面总结三种赋值过程:

第一种this赋值过程:

var v = 1; 
var obj = {
    v: 2,
    fn: function(){
        console.log(this.v);
    }
}
obj.fn(); // 2
(obj.fn)(); // 2
  1. 调用表达式解析 obj.fn ,返回引用规范类型,baseValue 为 obj。
  2. 函数调用,由于 1 过程返回的为引用规范类型,且为属性引用,调用 GetBase 将 baseValue (obj) 作为返回值,返回给 thisValue。
  3. 进入函数代码,thisArg 为 obj,将其赋值给 this。

如果对步骤1中,baseValue 为 obj 有疑问,详见属性访问, 产生式 MemberExpression : MemberExpression [ Expression ] 执行过程。
相当于有两个过程:

  1. 解析obj,baseValue 为声明式环境记录项。
  2. 解析obj.fn,baseValue 为 obj。

第二种this赋值过程:

function foo(){
    console.log(this);
}
foo(); // Window
  1. 调用表达式解析 foo, 返回引用规范类型,baseValue 为声明式环境记录项(函数声明时绑定)。
  2. 函数调用,由于 1 过程返回的为引用规范类型,且不为属性引用,调用 ImplicitThisValue 方法,返回 undefined 。
  3. 进入函数代码,thisArg 为 undefined,非严格模式下将 this 赋值为全局对象。

第三种this赋值过程:

var v = 1; 
var obj = {
    v: 2,
    fn: function(){
        console.log(this.v);
    }
}
var fn2 = obj.fn;
fn2(); // 1
(obj.fn, obj.fn)(); // 1
  1. 调用表达式解析 fn2,(obj.fn, obj.fn) ,由于赋值表达式、逗号表达式都使用了 GetValue 方法,返回函数(Object 类型)。
  2. 函数调用,由于 1 过程返回的不是引用规范类型,所以 thisValue 为 undefined`。
  3. 进入函数代码,thisArg 为 undefined,非严格模式下将 this 赋值为全局对象。

内置函数如何使用this的

内置函数修改 this 是通过给 func 的 [[Call]] 内置方法传递 thisArg 来实现的。

内置方法

内置方法模拟实现

使用成员表达式模拟内置方法[[Call]]的效果:

apply
Function.prototype.apply_ = function (context, arr) { 
    var context = Object(context) || window;
    var result;

    // 临时记录需要调用的function 
    context.fn = this;
      
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        // 使用成员表达式指定context.fn执行时this为context 
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
call
Function.prototype.call_ = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    // 获取参数列表 
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    } 
    // 使用成员表达式指定context.fn执行时this为context  
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}  
bind
Function.prototype.bind_  = function (context) {
    // 记录bind的函数 
    var self = this;
    var args = [];
    // 获取绑定的参数列表 
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    } 

    // 创建新函数 
    var fbound = function () {
        // 获取未绑定的参数列表 
        var bindArgs = Array.prototype.slice.call(arguments);
        // fbound被当做构造函数使用,this指向实例。否则,指向 context
        self.apply(this instanceof self ? this : context, args.concat(bindArgs));
    } 
    // 维护原型关系   
    fbound.prototype = self.prototype || new Function().prototype ;
    return fbound;
}

参考文档

点赞
收藏
评论区
推荐文章
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java成员变量的初始化
类变量(static变量,不需要实例化对象也可以引用)实例变量(非static变量,需要实例化对象)局部变量(类的成员函数中的变量)初始化方式:构造函数初始化变量声明时初始化代码块初始化java自动初始化(在构造函数执行之前执行) java保证所有变量被使用之前都是经过初始化的(声明并且定义过,被赋值
Wesley13 Wesley13
3年前
java13天
构造函数一个对象的建立,构造函数只运行一次。而一般方法可以被该对象调用多次。personp2newperson();//初始化构造函数p2.p2content();//调用函数的内容,可重复调用构造代码块中定义的是不同对象共性的初始化内容classtest{{System.out.prin
Wesley13 Wesley13
3年前
3、Angular JS 学习笔记 – Controllers [翻译中]
理解控制器在Angular中,一个控制器是一个javascript构造函数用于填充Angular作用域。当一个控制器通过使用ngcontroller指令附加到DOM上的时候,Angular将初始化一个新的Controller对象,使用指定的控制器构造函数。一个新的子作用域将可以作为一个参数$scope被注入到控制器构造函数。控制器用
Stella981 Stella981
3年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Wesley13 Wesley13
3年前
JS函数高级
原型与原型链所有函数都有一个特别的属性:prototype:显式原型属性所有实例对象都有一个特别的属性:__proto__:隐式原型属性显式原型与隐式原型的关系函数的prototype:定义函数时被自动赋值,值默认为{},即用为原型对象
Stella981 Stella981
3年前
IDA Pro 权威指南学习笔记(十)
栈帧(stackframe)是在程序的运行时栈中分配的内存块,用于特定的函数调用如果一个函数没有执行则不需要内存,当函数被调用时就需要用到内存1.传给函数的参数的值需要存储到函数能够找到它们的位置2.函数在执行过程中可能需要临时的存储空间,通过声明局部变量来分配这类临时空间,这些变量在函数内部使用,函数调用完后,就无法再访问它们
Wesley13 Wesley13
3年前
J2EE项目异常处理
       为什么要在J2EE项目中谈异常处理呢?可能许多java初学者都想说:“异常处理不就是try….catch…finally吗?这谁都会啊!”。笔者在初学java时也是这样认为的。如何在一个多层的j2ee项目中定义相应的异常类?在项目中的每一层如何进行异常处理?异常何时被抛出?异常何时被记录?异常该怎么记录?何时需要把checkedExc
Stella981 Stella981
3年前
ES6中箭头函数与普通函数this的区别(转)
看到一篇别人的博客,对this的理解又加深了一些。普通函数中的this:1\.this总是代表它的直接调用者,例如obj.func,那么func中的this就是obj2.在默认情况(非严格模式下,未使用'usestrict'),没找到直接调用者,则this指的是window3.在严格模式下,没有直接调用者的函数中的thi
Wesley13 Wesley13
3年前
C++ 拷贝构造函数和赋值运算符
  这篇文章主要介绍拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数,什么情况下调用赋值运算符。      拷贝构造函数和赋值运算符在默认情况下(用户没有定义,但是也没有显示的删除),编译器会自动隐式生成一个拷贝构造函数和赋值运算符,但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也
Wesley13 Wesley13
3年前
C++进阶
///任何时候都不要在构造函数或析构函数中调用虚函数/classdog{public:stringm_name;