目录html
1.执行上下文与执行上下文栈git
(1)什么是执行上下文?github
在 JavaScript 代码运行时,解释执行全局代码、调用函数或使用 eval 函数执行一个字符串表达式都会建立并进入一个新的执行环境,而这个执行环境被称之为执行上下文。所以执行上下文有三类:全局执行上下文、函数执行上下文、eval 函数执行上下文。算法
执行上下文能够理解为一个抽象的对象,以下图:浏览器
Variable object:变量对象,用于存储被定义在执行上下文中的变量 (variables) 和函数声明 (function declarations) 。app
Scope chain:做用域链,是一个对象列表 (list of objects) ,用以检索上下文代码中出现的标识符 (identifiers) 。ide
thisValue:this 指针,是一个与执行上下文相关的特殊对象,也被称之为上下文对象。函数
(2)什么是执行上下文栈?this
在全局代码中调用函数,或函数中调用函数(如递归)等,都会涉及到在一个执行上下文中建立另外一个新的执行上下文,而且等待这个新的上下文执行完毕,才会返回以前的执行上下文接着继续执行,而这样的调用方式就造成了执行上下文栈。es5
示例代码:
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 能够是如下五种表达式中的任意一种:
简单理解 MemberExpression 就是调用一个函数的()
左侧的部分。
第2~5步,获取调用函数的参数列表以及检测所调用的函数是否合法,不然抛出相应异常。
第六、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 方法:
而对于 ImplicitThisValue 方法,其属于环境记录项(Environment record)的方法。而环境记录项分为两种:
声明式环境记录项:每一个声明式环境记录项都与一个包含变量和(或)函数声明的 ECMA 脚本的程序做用域相关联。声明式环境记录项用于绑定做用域内定义的一系列标识符。其 ImplicitThisValue 永远返回 undefined。
对象式环境记录项:每个对象式环境记录项都有一个关联的对象,这个对象被称做 绑定对象 。对象式环境记录项直接将一系列标识符与其绑定对象的属性名称创建一一对应关系。其 ImplicitThisValue 一般返回 undefined,除非其 provideThis 标识的值为 true。具体以下:
对象式环境记录项能够经过配置的方式,将其绑定对象合为函数调用时的隐式 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 类型的函数调用
首先,须要j简单了解一下 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.总结
()
左侧的部分 MemberExpression 的解释执行的结果的类型是否是 Reference 类型直接关联。4.参考
this 关键字 - JavaScript | MDN - Mozilla
深刻理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)
深刻理解JavaScript系列(13):This? Yes,this!