深入理解JS:执行上下文中的this(一)
目录
- 执行上下文与执行上下文栈
- this
- 全局环境
- 函数环境
- 总结
- 参考
1.执行上下文与执行上下文栈
(1)什么是执行上下文?
在 JavaScript 代码运行时,解释执行全局代码、调用函数或使用 eval 函数执行一个字符串表达式都会创建并进入一个新的执行环境,而这个执行环境被称之为执行上下文。因此执行上下文有三类:全局执行上下文、函数执行上下文、eval 函数执行上下文。
执行上下文可以理解为一个抽象的对象,如下图:
Variable object:变量对象,用于存储被定义在执行上下文中的变量 (variables) 和函数声明 (function declarations) 。
Scope chain:作用域链,是一个对象列表 (list of objects) ,用以检索上下文代码中出现的标识符 (identifiers) 。
thisValue:this 指针,是一个与执行上下文相关的特殊对象,也被称之为上下文对象。
(2)什么是执行上下文栈?
在全局代码中调用函数,或函数中调用函数(如递归)等,都会涉及到在一个执行上下文中创建另一个新的执行上下文,并且等待这个新的上下文执行完毕,才会返回之前的执行上下文接着继续执行,而这样的调用方式就形成了执行上下文栈。
示例代码:
function A() {
console.log('function A')
B()
}
function B() {
console.log('function B')
C()
}
function C() {
console.log('function C')
}
A()
上述示例代码,当执行到函数 C时,此时的执行上下文栈如下图:
2.this
首先需要清楚,this 是执行上下文的一个属性,而不是某个变量对象的属性,是一个与执行上下文相关的特殊对象。由于在开发中不推荐或应尽量避免使用 eval 函数,所以在这里我们主要讨论全局执行上下文(全局环境)和函数执行上下文(函数环境)中的 this。
(1)全局环境
无论是否在严格模式下,在全局环境中(在任何函数体外部的代码),this 始终指向全局对象(在浏览器中即 window)。
示例代码(浏览器中):
console.log(this === window) // true
a = 1;
console.log(window.a) // 1
console.log(this.a === window.a) // true
this.b = "test"
console.log(window.b) // test
console.log(b) //test
(2)函数环境
在大多数情况下,函数的调用方式决定了 this 的值。 this 是不能够在执行期间被赋值修改的,并且在每次函数被调用时其 this 可能不同(通过 apply 或 call 方法显示设置 this 等)。
另外,ES5 引入了 bind 方法来设置函数的 this 值,而不用考虑函数如何被调用的。ES6 引入了支持 this 词法解析的箭头函数(它在闭合的执行环境内设置 this 的值)。
接下来我们主要分析:函数的调用方式是如何决定 this 的值?(对于 bind 方法以及箭头函数将留于下一篇文章进行详细分析)
要弄明白这个问题,我们来看看 EcmaScript 5.1标准的规定,了解一下 函数调用 的规范:
11.2.3 函数调用
产生式 CallExpression : MemberExpression Arguments 按照下面的过程执行 :
- 令 ref 为解释执行 MemberExpression 的结果 .
- 令 func 为 GetValue(ref).
- 令 argList 为解释执行 Arguments 的结果 , 产生参数值们的内部列表 (see 11.2.4).
- 如果 Type(func) is not Object ,抛出一个 TypeError 异常 .
- 如果 IsCallable(func) is false ,抛出一个 TypeError 异常 .
- 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) 为 true,那么 令 thisValue 为 GetBase(ref). 否则 , ref 的基值是一个环境记录项 , 令 thisValue 为 GetBase(ref).ImplicitThisValue().
- 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.
- 返回调用 func 的 [[Call]] 内置方法的结果 , 传入 thisValue 作为 this 值和列表 argList 作为参数列表
产生式 CallExpression : CallExpression Arguments以完全相同的方式执行,除了第1步执行的是其中的CallExpression。
简单解析:
第1步,令 ref 为 MemberExpression 解释执行的结果。
在 11.2 左值表达式 中有提到,MemberExpression 可以是以下五种表达式中的任意一种:
- PrimaryExpression // 原始表达式
- FunctionExpression // 函数定义表达式
- MemberExpression [ Expression ] // 属性访问表达式
- MemberExpression . IdentifierName // 属性访问表达式
- new MemberExpression Arguments // 对象创建表达式
简单理解 MemberExpression 就是调用一个函数的()
左侧的部分。
第2~5步,获取调用函数的参数列表以及检测所调用的函数是否合法,否则抛出相应异常(GetValue的作用是获取 Reference 类型具体的值)。
第6、7步,就是决定函数调用的 this 的值的关键步骤,翻译一下,如同下面的伪代码:
var thisValue = getThisValue(ref)
function getThisValue(ref) {
// 判断 ref 的类型是否是 Reference,如果不是,直接返回 undefined
if(Type(ref) !== Reference) return undefined
// 是否是 Object, Boolean, String, Number
if(IsPropertyReference(ref)) {
return GetBase(ref)
} else {
// 是一个环境记录项(Environment record),调用其 ImplicitThisValue 方法
return GetBase(ref).ImplicitThisValue()
}
}
关于 GetBase 和 IsPropertyReference 方法:
- GetBase(V), 返回引用值 V 的基值 (Reference 的基值 base,详见下面提到的 Reference 的组成)。
- HasPrimitiveBase(V), 如果基值是 Boolean, String, Number,那么返回 true。
- IsPropertyReference(V), 如果基值是个对象或 HasPrimitiveBase(V) 是 true,那么返回 true;否则返回 false。
而对于 ImplicitThisValue 方法,其属于环境记录项(Environment record)的方法。而环境记录项分为两种:
声明式环境记录项:每个声明式环境记录项都与一个包含变量和(或)函数声明的 ECMA 脚本的程序作用域相关联。声明式环境记录项用于绑定作用域内定义的一系列标识符。其 ImplicitThisValue 永远返回 undefined。
对象式环境记录项:每一个对象式环境记录项都有一个关联的对象,这个对象被称作 绑定对象 。对象式环境记录项直接将一系列标识符与其绑定对象的属性名称建立一一对应关系。其 ImplicitThisValue 通常返回 undefined,除非其 provideThis 标识的值为 true。具体如下:
- 令 envRec 为函数调用时对应的声明式环境记录项。
- 如果 envRec 的 provideThis 标识的值为 true,返回 envRec 的绑定对象。
- 否则返回 undefined。
对象式环境记录项可以通过配置的方式,将其绑定对象合为函数调用时的隐式 this 对象的值。这一功能用于规范 With 表达式(12.10 章 )引入的绑定行为。该行为通过对象式环境记录项中布尔类型的 provideThis 值控制,默认情况下,provideThis 的值为 false。(只有使用了 with 表达式,才会将 provideThis 标识的值为 true)
而上面提到了两种新的类型: 引用规范类型 (Reference)与 环境记录项(Environment record)都是属于ECMAScript 的规范类型,相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。
而与规范类型相对于的就是语言类型:就是开发者直接使用的类型,即Undefined, Null, Boolean, String, Number, 和 Object。(ECMAScript的类型分为语言类型和规范类型)
从上面的伪代码中可以看到 thisValue 的值与 ref 是否是引用规范类型(Reference)有直接关联,即调用一个函数时,其()
左侧的部分的解释执行的结果的类型是不是 Reference 类型,将直接影响 thisValue 的值。
EcmaScript 5.1标准中的 Reference 的规范:
8.7 引用规范类型 (Reference)
Reference 类型是用来说明 delete,typeof,赋值运算符这些运算符的行为。
一个 Reference 是个已解决的命名绑定。其由三部分组成, 基值 (base) , 引用名称(referenced name) 和布尔值 严格引用 (strict reference) 标志。
基值是 undefined, Object, Boolean, String, Number, Environment record 中的任意一个。基值是 undefined 表示此引用可以不解决一个绑定。引用名称是一个字符串。严格引用标志表示是否在严格模式下解释执行的代码。
而引用规范类型(Reference)会被用在标识符解析中,标识符执行的结果总是一个 Reference 类型的值。
EcmaScript 5.1标准中的 标识符解析 的规范:
10.3.1 标识符解析
标识符解析是指使用正在运行的执行环境中的词法环境,通过一个 标识符 获得其对应的绑定的过程。在 ECMA 脚本代码执行过程中,PrimaryExpression : Identifier 这一语法产生式将按以下算法进行解释执行:
- 令 env 为正在运行的执行环境的 词法环境 。
- 如果正在解释执行的语法产生式处在 严格模式下的代码 中,则仅 strict 的值为 true,否则令 strict 的值为 false。
- 以 env,Identifier 和 strict 为参数,调用 GetIdentifierReference 函数,并返回调用的结果。
解释执行一个标识符得到的结果必定是 Reference 类型的对象,且其引用名属性的值与 Identifier 字符串相等。
而 GetIdentifierReference 函数就是返回一个 Reference 类型的对象,类似如下对象:
var valueOfReferenceType = {
base: <base object>, // Identifier 所处的环境(Environment Record)或者 Identifier 属性所属的对象
propertyName: <property name>, // 与 Identifier 字符串相等
strict: <boolean>
};
因此,我们可以来看一些相关的示例代码。
第一组:非严格模式和严格模式的全局函数
function foo() {
console.log(this)
}
function bar() {
'use strict'
console.log(this)
}
foo() // global
bar() // undefined
// foo 标识符对应的 Reference
var fooReference = {
base: EnvironmentRecord,
propertyName: 'foo',
strict: false
}
// bar 标识符对应的 Reference
var barReference = {
base: EnvironmentRecord,
propertyName: 'bar',
strict: true
}
上述代码中,对于 fooReference,根据函数调用规范可知其 this = getThisValue(fooReference) = GetBase(fooReference).ImplicitThisValue() = undefined
,而 barReference 也是一样。
但为什么 foo()
输出的是 global 全局对象而不是 undefined 呢?这是因为在非严格模式下, 当 this 的值为 undefined 时,会被隐式转换为全局对象。而在严格模式下,指定的 this 不再被封装为对象。
第二组:对象的属性访问
var foo = {
bar: function () {
console.log(this)
}
}
foo.bar() // foo
// foo 的 bar 属性对应的 Reference
var barReference = {
base: foo,
propertyName: 'bar',
strict: false
}
上述代码中,对于 barReference,根据函数调用规范可知 this = getThisValue(barReference) = GetBase(barReference) = foo
在 foo.bar()
中,MemberExpression 计算的结果是 foo.bar,为什么它是一个 Reference 类型呢?
EcmaScript 5.1标准中的 属性访问 的规范:
11.2.1 属性访问
- 返回一个 Reference 类型的值,其基值为 baseValue 且其引用名为 propertyNameString, 严格模式标记为 strict.
这里只引用了最后一步,属性访问最终返回的值是一个 Reference 类型。
第三组:非 Reference 类型的函数调用
首先,需要简单了解一下 GetValue 方法,其作用是获取 Reference 类型具体的值,返回结果不再是一个 Reference。例如:
var foo = 1
// foo 标识符对应的 Reference
var fooReference = {
base: EnvironmentRecord,
propertyName: 'foo',
strict: false
}
GetValue(fooReference) // 1
示例代码:
value = 1
var foo = {
value: 2,
bar: function () {
console.log(this.value)
}
};
foo.bar(); // 2
(foo.bar)(); // 2
(false || foo.bar)(); // 1
(foo.bar = foo.bar)(); // 1
(foo.bar, foo.bar)(); // 1
在上述示例代码中:
- 对于
(foo.bar)
,foo.bar 被 () 包住,使用了分组运算符,查看规范 11.1.6 分组操作符,可知分组表达式不会调用 GetValue 方法, 所以(foo.bar)
仍旧是一个 Reference 类型,因此 this 为 Reference 类型的 base 对象,即 foo。 - 对于
(false || foo.bar)
,有逻辑与算法,查看规范 11.11 二元逻辑运算符,可知二元逻辑运算符调用了 GetValue 方法,所以false || foo.bar
不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。 - 对于
(foo.bar = foo.bar)
,有赋值运算符,查看规范 11.13.1 简单赋值,可知简单赋值调用了 GetValue 方法,所以foo.bar = foo.bar
不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。 - 对于
(foo.bar, foo.bar)
,有逗号运算符,查看规范 11.14 逗号运算符,可知逗号运算符调用了 GetValue 方法,所以foo.bar, foo.bar
不再是一个 Reference 类型,因此 this 为 undefined,非严格模式下,被隐式转化为 global 对象。
3.总结
- 在全局环境(全局执行上下文)中(在任何函数体外部的代码),this 始终指向全局对象
- 在函数环境(函数执行上下文)中,绝大多数情况,函数的调用方式决定了 this 的值,这与调用函数的
()
左侧的部分 MemberExpression 的解释执行的结果的类型是不是 Reference 类型直接关联。
4.参考
this 关键字 - JavaScript | MDN - Mozilla
深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)
深入理解JavaScript系列(13):This? Yes,this!
JavaScript深入之从ECMAScript规范解读this
深入理解JS:执行上下文中的this(一)的更多相关文章
- 从一道看似简单的面试题重新理解JS执行机制与定时器
壹 ❀ 引 最近在看前端进阶的系列专栏,碰巧看到了几篇关于JS事件执行机制的面试文章,因为我在之前一篇 JS执行机制详解,定时器时间间隔的真正含义 博文中也有记录JS执行机制,所以正好用于作为测试自 ...
- 深入理解JS 执行细节
javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之前,我们需要引入几个相关的概念:执行环境栈.全局对象.执行环境.变量对象.活动对象.作用域和作用域链等 ...
- 深入理解JS执行细节(写的很精辟)
来源于:http://www.cnblogs.com/onepixel/p/5090799.html javascript从定义到执行,JS引擎在实现层做了很多初始化工作,因此在学习JS引擎工作机制之 ...
- 深入理解js——执行上下文
什么是"执行上下文"?暂且不下定义,先看一段代码: 第一句报错,a未定义,很正常.第二句.第三句输出都是undefined,说明浏览器在执行console.log(a)时,已经知道 ...
- 深入学习JS执行--创建执行上下文(变量对象,作用域链,this)
一.介绍 本篇继上一篇深入理解js执行--单线程的JS,这次我们来深入了解js执行过程中的执行上下文. 本篇涉及到的名词:预执行,执行上下文,变量对象,活动对象,作用域链,this等 二.预执行 在上 ...
- JS执行机制详解,定时器时间间隔的真正含义
壹 ❀ 引 通过结果倒推过程是我们常用的思考模式,我在上一篇学习promise笔记中,有少量关于promise执行顺序的例子,通过倒推,我成功让自己对于js执行机制的理解一塌糊涂,js事件机制,事件 ...
- 深入理解JS:执行上下文中的this(二)
目录 序言 Function.prototype.bind() 方法 箭头函数 参考 1.序言 在 深入理解JS:执行上下文中的this(一) 中,我们主要深入分析全局环境和函数环境中函数调用的 th ...
- js的基础(平民理解的执行上下文/调用堆栈/内存栈/值类型/引用类型)
与以前的切图比较,现在的前端开发对js的要求似乎越来越高,在开发中,我们不仅仅是要知道如何运用现有的框架(react/vue/ng), 而且我们对一些基础的知识的依赖越来越大. 现在我们就用平民的方法 ...
- 10分钟理解JS引擎的执行机制
首先,请牢记2点: (1) JS是单线程语言 (2) JS的Event Loop是JS的执行机制.深入了解JS的执行,就等于深入了解JS里的event loop 1.灵魂三问 (1) JS为什么是单线 ...
随机推荐
- HR问了我朋友什么是简单工厂模式,竟被质疑是否学过设计模式,是否是计算机专业?
越是简单的东西,越是容易被忽略,我来带你们好复习一下! 简单工厂模式 简单工厂模式也被称为静态工厂模式;使用简单工厂模式可以将产品的"消费"和生产完全分开,客户端只需要知道自己需要 ...
- muduo网络库源码学习————互斥锁
muduo源码的互斥锁源码位于muduo/base,Mutex.h,进行了两个类的封装,在实际的使用中更常使用MutexLockGuard类,因为该类可以在析构函数中自动解锁,避免了某些情况忘记解锁. ...
- Linux下swap到底有没有必要使用
周五看到QQ群里在讨论Linux主机上到底需不需要开启swap空间,而且目前公有云主机默认都是把swap关了的,很多公司也是没有开启swap,那到底需不需要开启呢? 我之前在看<鸟哥的Linux ...
- 使用python绘制世界人口地图及数据处理
本篇我们来说:下载和处理json格式的文件,并通过pygal中的地图工具来实现数据可视化 ------------------------------------------------------- ...
- turtle库应用实例-五角星绘制
五角星绘制 ...
- Spring官网阅读(四)BeanDefinition(上)
前面几篇文章已经学习了官网中的1.2,1.3,1.4三小结,主要是容器,Bean的实例化及Bean之间的依赖关系等.这篇文章,我们继续官网的学习,主要是BeanDefinition的相关知识,这是Sp ...
- Exception in thread "main" java.lang.NoSuchMethodError: scala.Predef$.wrapRefArray([Ljava/lang/Object;)Lscala/collection/mutable/WrappedArray
我是在用akka框架做简单的一个聊天,然而出师不利,刚开始学就遇到这个问题 遇事不决问百度,百度给出的结果是spark中scala版本和你使用的scala的版本不一致,所导致的错误 我用的是akka, ...
- STM32 CubeIDE快速创建工程(图文详解)
使用STM32CubeIDE快速创建STM32的HAL库工程. 文章目录 1 STM32CubeIDE Home 2 生成工程 3 程序下载 1 STM32CubeIDE Home 进入到官网的下载界 ...
- Linux内核驱动学习(五)KThread学习总结
文章目录 简介 例程 运行结果 参考 简介 使用内核线程需要包含头文件#include <linux/kthread.h>,下面整理了一下常用的api接口,如下表格所示: 函数 功能 st ...
- Spring中bean的四种注入方式
一.前言 最近在复习Spring的相关内容,这篇博客就来记录一下Spring为bean的属性注入值的四种方式.这篇博客主要讲解在xml文件中,如何为bean的属性注入值,最后也会简单提一下使用注解 ...