这是我参与更文挑战的第6天,活动详情查看: 更文挑战
首先我们知道,JS是门解释型语言,包括了两个阶段解释阶段、执行阶段。
作用域
前面已经提到了JS在解释阶段就确定了作用域规则。那什么是作用域?
作用域是在运行时代码中变量,函数和对象可访问性的集合[1]。
作用域可分为词法作用域(静态作用域) 和 动态作用域。
动态作用域并不关心函数在何处声明,只关心它们从何处调用。静态作用域在书写时就已经能够确认了。简而言之,词法作用域是在定义时确定的,而动态作用域是在运行时确定的。JS采用的就是静态作用域。实际上动态作用域是JS另一个重要机制this的体现。一些人之所以对作用域混乱,多数是因为静态的作用域和动态的this机制相混淆。那为什么this是动态的呢?后文有解释。
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();//2 因为foo是定义在全局环境中的
把foo放在bar函数里面:
var a = 2;
function bar() {
var a = 3;
function foo() {
console.log( a );
}
foo();
}
bar();//3 因为foo是定义在bar函数环境中的
作用域链
作用域链本质上是一个指向变量对象的指针列表,这个指针列表的第0个位置始终都是当前执行代码所在环境,下一个位置指向定义时的包含环境,在下一个位置指向下一个定义时的包含环境,这样一直延续到作用域的的最后的全局环境。标识符的解析就是通过这个作用域链一级一级查找标识符的过程。
执行上下文
执行上下文是JavaScript执行一段代码时的运行环境。当程序遇到可执行代码时,就进入了一个执行上下文,执行上下文是一个逻辑上的堆栈结构。堆栈中最顶层的执行上下文就是正在运行的执行上下文。每个执行上下文都有变量环境、词法环境、this。
当JS初始化全局或执行函数的时候(JS执行阶段):
- 首先会创建上下文。执行上下文分为两种:全局执行上下文、函数执行上下文。
- 然后就把创建的上下文压入调用栈中,执行代码,执行完毕,弹出调用栈。
- 垃圾回收。 而执行上下文生命周期又可分为:创建阶段、执行阶段
创建阶段:
-
变量环境和词法环境被定义。
-
确定this的指向(所以说this是要到执行阶段才能确定的)。
-
建立作用域链。
变量环境:通过var声明的变量存在这里
词法环境:通过let、const、with()、try-catch、function声明的变量存在这里。
词法环境由环境记录与对外部环境引入记录两个部分组成。 全局上下文与函数上下文的外部环境引入记录不一样,全局为null,函数为全局环境或者其它函数环境。 环境记录也不一样,全局叫对象环境记录,函数叫声明性环境记录。
词法环境是一种规范类型(specification type),它定义了标识符和ECMAScript代码中的特定变量及函数之间的联系。而词法环境中,包含了一个环境记录和一个指向外部词法环境的引用,而这个引用的值可能为null。
执行阶段:变量赋值、函数的引用。
this
由上文可知,this的指向时在上下文创建的时候才能确定的,这个要和JS的静态作用域区分。this指向的是执行是所处的执行环境。指向无非就五种:
调用方式 | 例子 | 指向 |
---|---|---|
函数直接调用 | xxx() | window |
属性调用 | a.b() | a |
new调用 | new xx() | 一个新对象 |
箭头函数调用 | ()=>{} | 父级环境 |
显式调用 | call(xx)、apply(xx)、bind(xx) | xx |
function foo(){
console.log(this)
};
let a = 2;
let obj = {
a:1,
fn1:foo,
fn2:()=>{
console.log(this)
}
};
let exe1 = obj.fn1;
let exe2 = obj.fn2;
exe1();//this=>window
exe1.call(a);//this=>Number {2}
exe1.call(obj.a);//this=>Number {1}
exe1.call(obj.fn1);//foo
obj.fn1();//this=>obj
obj.fn1.call(a);//this=>Number {2}
obj.fn1.call(obj.a);//this=>Number {1}
obj.fn1.call(obj.fn1);//this=>foo
exe2();//this=>window
exe2.call(a);//this=>window
exe2.call(obj.a);//this=>window
exe2.call(obj.fn1);//this=>window
//箭头函数的this无法改变
obj.fn2();//this=>window
obj.fn2.call(a);//this=>window
obj.fn2.call(obj.a);//this=>window
obj.fn2.call(obj.fn1);//this=>window
//箭头函数的this无法改变
其实大白话this就是,你想函数在哪个环境执行,你想在window环境执行,就直接执行它;想在某个对象的环境执行,就把它当成对象的属性去执行;想自定义执行环境,就用call、apply、bind吧。
一个函数还没执行之前,这个函数的this指向哪个环境都有可能,所以当你写一个函数的时候,就不要再去纠结它这个this究竟指向谁,到你调用的时候想指向谁就指向谁。
调用栈
这么多执行上下文,如何管理这些上下文?这就需要到 执行上下文栈了。
这个用来存放、管理执行上下文的栈就被称为调用栈。
可以认为执行栈底部永远有个全局执行上下文,除非把程序停了或者把浏览器关了,否则执行栈底部部永远有个全局执行上下文。
闭包
我比较认同的说法是,闭包是由函数以及声明该函数的词法环境组合而成的。这个说法来源于MDN。闭包不是一个函数,而是函数和词法环境组成的。在闭包场景下,确实存在一个函数有权访问另外一个函数作用域中的变量,但闭包不是函数。
另外一种说法是,闭包是指有权访问另外一个函数作用域中的变量的函数。
总结
(番外)作用域链与原型链的区别
当访问变量时,解释器会先在当前作用域查找,如果没有找到就去创建函数时所在的作用域找,作用域链顶端是全局对象window,如果window都没有这个变量则报错。
当在对象上访问某属性时,会查找当前对象,如果没有就顺着原型链往上找,原型链顶端是null,如果全程都没找到则返一个undefined,而不是报错。
(番外)变量对象(VO)/活动对象(AO)
当你阅读其它有关执行上下文的文章,一定有疑问,执行上下文创建过程不是应该解释this,作用域与变量对象/活动对象才对吗?
在阅读ECMAScript规范时,没有找到这些关键词。查阅资料可以知道,变量对象与活动对象的概念是ES3提出的老概念,从ES5开始就用词法环境和变量环境替代了。
词法环境的概念与变量对象是可以对应上的:变量对象(VO)与活动对象(AO)其实都是变量对象,变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(AO)来表示变量对象,正好对应到了词法环境。
而且由于ES6新增的let const不存在变量提升,于是正好有了词法环境与变量环境的概念来解释。
参考: