ECMA 笔记之详说 this

ECMA 笔记之详说 this

本文主要是学习并整合文末 学习资料 中的相关知识,加深自己的理解并记录。

什么是 this? 在 JavaScript 中 this 指的是函数运行时所在的环境。

this 出现的原因

JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系。

var obj = { foo: function () {} };

上面的代码将一个对象赋值给变量 obj

JavaScript 引擎会先在内存里面,生成一个对象 { foo: 5 },然后把这个对象的内存地址赋值给变量 obj

也就是说,变量 obj 是一个地址(reference)。后面如果要读取 obj.foo,引擎先从 obj 拿到内存地址,然后再从该地址读出原始的对象,返回它的 foo 属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。

image

{
  foo: {
    [[value]]: 5
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

注意,foo 属性的值保存在属性描述对象的 value 属性里面。

这样的结构是很清晰的,问题在于属性的值可能是一个函数。

var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给 foo 属性的属性描述对象的 value 属性。

image

{
  foo: {
    [[value]]: 函数的地址
    ...
  }
}

由于函数是一个单独的值(独立在内存中),所以它可以在不同的环境(上下文)执行。

var f = function () {};
var obj = { f: f };

// 单独执行
f()

// obj 环境执行
obj.f()

JavaScript 允许在函数体内部,引用当前环境的其他变量。

var f = function () {
  console.log(x);
};

上面代码中,函数体里面使用了变量 x。该变量由运行环境提供。

现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。

所以,this 就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境

var f = function () {
  console.log(this.x);
}

上面代码中,函数体里面的 this.x 就是指当前运行环境的 x

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 单独执行
f() // 1

上面代码中,函数 f 在全局环境执行。

image

this.x 指向全局环境的 x

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// obj 环境执行
obj.f() // 2

上面代码中,函数 fobj 环境执行。

image

this.x 指向 obj.x

如何判断 this 的运行环境

函数调用的方式有很多种,这里简单归为五种:

  • 独立调用:函数独立调用时,this 指向 window;

  • 方法调用:函数作为某个对象的一个方法(属性)调用时,this 指向这个对象;

  • 构造函数调用:构造函数调用时,函数内部的 this 指向创建的新对象

  • callapplybind 间接调用:当一个函数被 call()apply()bind() 间接调用时,this 的值指向传入的对象。如果传入的是 null 或者 undefined 就指向 window

  • 其他方式调用:在形如 (false || obj.method)()(obj.method = obj.method)() 等表达式调用时,this 指向 window;

一、独立调用函数

在全局环境中,this 的指向是 window

函数在全局环境中独立调用时,函数内部的 this 指向 window

console.log(this === window);  // true

var a = 1;
function globalFun() {
    console.log(this);      // Window        
    console.log(this.a);    // 1       
}
globalFun();

函数在其它环境中独立调用时,函数内部的 this 也指向 window

例如,在某个函数内:

function globalFun() {
    function localFun() {
        console.log(this);  // Window  
    }
    localFun();
}
globalFun();

例如,在某个方法内:

var glabalObj = {
    a: 2,
    showThis: function() {
        console.log(this);      // {a: 2, showThis: ƒ}
        function localFun() {
            console.log(this);  // Window  
        }
        localFun();
    }
}
glabalObj.showThis();

例如,函数声明与调用分开:

function localFun() {
    console.log(this);  
}
function globalFun() {
    localFun();
}
globalFun();  // Window 


var glabalObj = {
    a: 2,
    showThis: function() {
        console.log(this);      // {a: 2, showThis: ƒ}
        localFun();             // Window
    }
}
glabalObj.showThis();

所以,可以得出结论,无论函数是怎么定义的,无论是定义在哪儿的,只要是独立调用,函数内的 this 就指向 window

二、函数作为方法调用

当函数被作为对象的方法(对象的一个属性)调用时,函数内部的 this 指向该对象;

例如,作为 glabalObj 对象的方法:

var glabalObj = {
    a: 2,
    showThis: function() {
        console.log(this);      // {a: 2, showThis: ƒ}
    }
}
glabalObj.showThis();

例如,作为 glabalObj 对象的方法:

var a = "glabal_variable";

var glabalObj = {
    a: 2,
    getA: function() {
        console.log(this.a); 
    }
}

var glabalOtherObj = {
    a: 10,
    glabalObj: glabalObj,
    getA: glabalObj.getA
}
glabalObj.getA();       // 2
glabalOtherObj.getA();  // 10
glabalOtherObj.glabalObj.getA(); // 2  此时的调用对象仍然是 glabalObj

三、构造函数调用

当时用 new 关键字调用一个构造函数时:

  1. 创建一个新对象,作为将要返回的实例;
  2. 将新对象的原型 __proto__ 指向构造函数的 prototype 属性;
  3. 将构造函数内部的 this 指向新对象;
  4. 执行构造函数内部代码;
  5. 返回新对象。(所以在构造函数内部,不能使用 return 关键字)

所以,构造函数调用时,函数内部的 this 指向创建的新对象。

function Animal(name, age) {
    this.name = name;
    this.age = age; 
};
Animal.prototype.walk = function() {
    // walking
}
Animal.prototype.drink = function() {
    // drink water
}
Animal.prototype.communicate = function() {
    // communicate with another animal
    console.log(`hello, I am ${this.name}`);
}

var tony = new Animal('tony',6);
console.log(tony);  // Animal {name: "tony", age: 6}
tony.communicate();

在构造函数 Animal 的原型方法 prototype.communicate 中,函数的 this 也是指向创建的新对象。其实,不仅仅是构造函数的 prototype,即便是在整个原型链中,this 代表的也都是创建的新对象

实例对象 tony 继承自构造函数 Animal 的原型方法 communicate() 中的 this 指向实例对象 tony。满足二、函数作为方法调用中所说的:当函数被作为对象的方法(对象的一个属性)调用时,函数内部的 this 指向该对象

四、通过 call()apply()bind() 间接调用

当一个函数被 call()apply()bind() 间接调用时,this 的值指向传入的对象。如果传入的是 null 或者 undefined 就指向 window

var name = "glabal_variable";
var humanObj = {
    name: "tony",
    age: "19"
}
var anotherHumanObj = {
    name: "Tom",
    age: "24"
}

function introduce(num, num2) {
    console.log(`hello, I am ${this.name}, and I can do additions, such as ${num} + ${num2} = ${num + num2} !`); 
}

introduce.call(null, 1, 2);         // hello, I am glabal_variable, and I can do additions, such as 1 + 2 = 3 !
introduce.call(undefined, 1, 2);    // hello, I am glabal_variable, and I can do additions, such as 1 + 2 = 3 !

introduce.apply(humanObj, [1, 3]);      // hello, I am tony, and I can do additions, such as 1 + 3 = 4 !
introduce.bind(anotherHumanObj)(1, 4);  // hello, I am Tom, and I can do additions, such as 1 + 4 = 5 !

bind() 方法只返回函数;call() 方法和 apply() 方法是立即执行函数;

五、从 ECMAScript 规范解读 this(其他方式调用)

5.1 铺垫解读

为了从规范去理解 this 指向,需要了解的前置知识。

5.1.1 Types

首先是第 8 章 Types:

Types are further subclassified into ECMAScript language types and specification types.

An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.

A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record.

我们简单的翻译一下:

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

ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的 Undefined, Null, Boolean, String, Number, 和 Object

而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

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

今天我们要讲的重点是便是其中的 Reference 类型。它与 this 的指向有着密切的关联。

5.1.2 Reference

那什么又是 Reference ?

让我们看 8.7 章 The Reference Specification Type:

The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.

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

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

再看接下来的这段具体介绍 Reference 的内容:

A Reference is a resolved name binding. (Reference 是已解析的名称绑定。)

A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. (Reference 由三个组件组成,基值,引用名称和严格的引用标志(布尔值)。)

The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1). (基值是 undefined,Object,Boolean,String,Number 或环境记录(10.2.1)。)

A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String. (基值 undefined 表示无法将引用解析为绑定。引用的名称是一个 String。)

这段讲述了 Reference 的构成,由三个组成部分,分别是:

  • base value
  • referenced name
  • strict reference

可是这些到底是什么呢?

我们简单的理解的话:

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

referenced name 就是属性的名称。

举个例子:

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
};

而且规范中还提供了获取 Reference 组成部分的方法,比如 GetBaseIsPropertyReference

GetBase

GetBase(V). Returns the base value component of the reference V. ( 返回引用 V 的基值组件。)

IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false. (如果基值是对象或 HasPrimitiveBase(V)true,则返回 true;否则返回 false。)

5.1.3 GetValue

除此之外,紧接着在 8.7.1 章规范中就讲了一个用于从 Reference 类型获取对应值的方法: GetValue

简单模拟 GetValue 的使用:

var foo = 1;

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

GetValue(fooReference) // 1;

GetValue 返回对象属性真正的值,但是要注意:调用 GetValue,返回的将是具体的值,而不再是一个 Reference

5.2 如何确定 this 的值

关于 Reference 讲了那么多,为什么要讲 Reference 呢?到底 Reference 跟本文的主题 this 有哪些关联呢?如果你能耐心看完之前的内容,以下开始进入高能阶段:

看规范 11.2.3 Function Calls:

这里讲了当函数调用的时候,如何确定 this 的取值。

只看第一步、第六步、第七步:

1.Let ref be the result of evaluating MemberExpression. (让 ref 成为评估 MemberExpression 的结果)

6.If Type(ref) is Reference, then (如果 Type(ref) 的值是 Reference,那么)

__ a.If IsPropertyReference(ref) is true, then (如果 IsPropertyReference(ref) 的值为 true,那么)

____ i.Let thisValue be GetBase(ref). (让 thisValue 为 GetBase(ref) 的值)

__ b.Else, the base of ref is an Environment Record (否则,ref 的基础是环境记录)

____ i.Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref). (让 thisValue 为调用 GetBase(ref)的 ImplicitThisValue 具体方法的结果)

7.Else, Type(ref) is not Reference. (否则,Type(ref) 的值不是 Reference。)

__ a. Let thisValue be undefined. (让 thisValue 为 undefined )

简单描述一下:

  1. 计算 MemberExpression 的结果赋值给 ref

  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、计算 MemberExpression 的结果赋值给 ref

什么是 MemberExpression?看规范 11.2 Left-Hand-Side Expressions:

MemberExpression :

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

举个例子:

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

所以简单理解 MemberExpression 其实就是 () 左边的部分。

2、判断 ref 是不是一个 Reference 类型。

关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个 Reference 类型。

举最后一个例子:

var value = 1;

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

// 示例 1
console.log(foo.bar());
// 示例 2
console.log((foo.bar)());
// 示例 3
console.log((foo.bar = foo.bar)());
// 示例 4
console.log((false || foo.bar)());
// 示例 5
console.log((foo.bar, foo.bar)());
// 示例 6
console.log(f());
示例 1 foo.bar()

在示例 1 中,MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢?

// 示例 1
console.log(foo.bar());

查看规范 11.2.1 Property Accessors,这里展示了一个计算的过程,什么都不管了,就看最后一步:

Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict. (返回类型为 Reference 的值,其基值为 baseValue,其引用名称为 propertyNameString,其严格模式标志为 strict。)

我们得知该表达式返回了一个 Reference 类型!

根据之前的内容,我们知道该值为:

var Reference = {
  base: foo,
  name: 'bar',
  strict: false
};

接下来按照 2.1 的判断流程走:

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢?

前面我们已经铺垫了 IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true

base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true

这个时候我们就可以确定 this 的值了:

this = GetBase(ref)

GetBase 也已经铺垫了,获得 base value 值,这个例子中就是 foo,所以 this 的值就是 foo ,示例 1 的结果就是 2!

唉呀妈呀,为了证明 this 指向 foo,真是累死我了!但是知道了原理,剩下的就更快了。

示例 2 foo.bar)()

看示例 2:console.log((foo.bar)());

foo.bar() 包住,查看规范 11.1.6 The Grouping Operator (分组运算符)

直接看结果部分:

Return the result of evaluating Expression. This may be of type Reference. (返回评估 Expression 的结果。 这可能是参考类型。)

NOTE This algorithm does not apply GetValue to the result of evaluating Expression. (注意此算法不会将 GetValue 应用于评估 Expression 的结果。)

实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的。

示例 3 foo.bar = foo.bar)()

看示例 3,有赋值操作符,查看规范 11.13.1 Simple Assignment ( = ):

计算的第三步:

3.Let rval be GetValue(rref).

因为使用了 GetValue,所以返回的值不是 Reference 类型。

按照之前讲的判断逻辑:

2.3 如果 ref 不是 Reference,那么 this 的值为 undefined

this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。

示例 4 false || foo.bar)()

看示例 4,逻辑与算法,查看规范 11.11 Binary Logical Operators:

计算第二步:

2.Let lval be GetValue(lref).

因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined。

示例 5 foo.bar, foo.bar)()

看示例 5,逗号操作符,查看规范 11.14 Comma Operator ( , )

计算第二步:

2.Call GetValue(lref).

因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined

示例 6 的普通调用

最后,一个最最普通的情况:

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

foo(); 

MemberExpression 是 foo,解析标识符,查看规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值:

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

接下来进行判断:

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

因为 base value 是 EnvironmentRecord,并不是一个 Object 类型,还记得前面讲过的 base value 的取值可能吗? 只可能是 undefined, an Object, a Boolean, a String, a Number, 和 an environment record 中的一种。

IsPropertyReference(ref) 的结果为 false,进入下个判断:

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

base value 正是 Environment Record,所以会调用 ImplicitThisValue(ref)

查看规范 10.2.1.1.6,ImplicitThisValue 方法的介绍:该函数始终返回 undefined

所以最后 this 的值就是 undefined

揭晓结果

所以最后一个例子的结果是:

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

注意:以上是在非严格模式下的结果,严格模式下因为 this 返回 undefined,所以示例 3 会报错。

学习资料

es5 规范

JavaScript 深入之从 ECMAScript 规范解读 this

JavaScript 的 this 原理

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值