作用域与执行环境
执行环境与作用域不是同一种东西!
作用域
作用域是指在程序中定义变量的区域,决定了变量的生命周期,是一套存储规则。作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期
- 全局作用域
其中的对象在代码中任何地方都能访问,其生命周期伴随着页面的生命周期 - 函数作用域
在函数内部定义的变量包括函数只能在函数内部被访问。函数执行结束后函数内部定义的变量会被销毁 - 注意
- 除了全局作用域,只有函数才能创建作用域即函数作用域 ,JS没有块级作用域
- 作用域可以嵌套,不可以重叠
var a=1; //全局作用域
function fn1(){
var a=2; //fn1作用域
}
执行环境
即执行上下文,是当前代码的运行环境,与this
关键字相关联,所有的变量都存在其中
上下文环境主要分为全局环境和局部(函数)环境,每个函数执行的上下文环境都不同
this.a=1; //全局执行上下文
function fn1(){
this.a=2; //fn1执行上下文
}
var obj=new fn1();
- 创建执行上下文的条件
- 当js执行全局代码时会编译全局代码并创建全局执行上下文。在整个页面生存周期内,全局执行上下文只有一份
- 调用一个函数时,函数体内的代码被编译并创建函数执行上下文;函数执行结束,函数执行上下文被销毁
- 使用
eval()
方法时,eval()
中的代码被编译并为其创建执行上下文
两者区别
- 作用域是在定义时即编译阶段确定,不会改变;执行上下文是在执行、调用阶段才创建
- 作用域是静态观念的,而执行上下文环境是动态上的
- 一个作用域下可能包含若干个上下文环境,也可能没有,但其中处于活动状态的执行上下文环境只有一个
JS代码的执行流程
- 编译阶段
输入一段代码经过编译后会生成两部分内容:- 执行上下文
执行上下文是js执行一段代码时的运行环境。其中包括一个变量环境的对象,该对象中保存了变量提升的内容;还包括一个词法环境,ES6中let
和const
声明的变量保存在其中 - 可执行代码
js引擎将声明以外的代码编译为字节码,成为可执行代码
- 执行上下文
- 执行阶段
js引擎开始按照顺序执行可执行代码
变量提升
指在js代码执行过程中,js引擎把变量和函数声明部分提升到代码开头的行为。变量被提升后会被设置默认值undefined
变量对象
每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中
若环境是函数,则其活动对象会作为变量对象,其中最开始只包含一个变量即arguments
对象
- 注意
var
声明的变量会自动被添加到最接近的环境中;若没有使用var
声明,则会自动被添加到全局环境
作用域链
访问规则,保证对执行环境有权访问的所有变量和函数的有序访问
- 每进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链
- 每个执行环境都包含一个外部引用即
outer
,指向其外部的执行环境。若某个函数中使用了外部变量,则js引擎沿着这条作用域链进行变量查找 - 作用域链的前端是当前执行代码所在环境的变量对象,最后一个对象始终是全局执行环境的变量对象。搜索从作用域链的前端开始逐级向后回溯访问,寻找目标。
- 内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境。环境之间的联系是线性的、有次序的
函数的创建与调用
- 创建
- 首先js引擎编译全局代码并创建全局执行上下文,其中包含着全局变量对象,代码中的全局变量包括声明的函数都保存在其中
- 在全局中创建一个函数,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在函数内部的
[[Scope]]
属性中
- 调用
- 为函数创建一个执行环境,然后通过复制函数的
[[Scope]]
属性中的对象,构建起执行环境的作用域链 - 该函数执行环境的活动对象(作为变量对象)被创建并推入作用域链的前端,并生成可执行代码
- 执行完毕后,局部活动对象即函数执行环境就会被销毁,内存中仅保存全局执行环境的变量对象
- 为函数创建一个执行环境,然后通过复制函数的
- 例子1
function compare(value1, value2){ // code... } var result = compare(5, 10);
- 例子2
function bar() { console.log(myName); } function foo() { var myName = '123'; bar(); } var myName = '456'; foo(); //456 /* 因为bar函数和foo函数都在全局作用域中被声明创建,所以它们的外部引用outer都指向的是全局上下文 因此bar函数中js引擎查找myName变量沿着作用域链向上一级找到的是全局作用域中的myName */
执行上下文栈
js引擎利用执行上下文栈(调用栈)来管理执行上下文,追踪函数执行
- 例子:
var a = 2; function add(b, c) { return b+c; } function addAll(b, c) { var d = 10; result = add(b, c) return a+result+d } addAll(3, 6);
- 创建全局上下文并将其压入栈底
- 执行全局代码,执行到调用
addAll
函数,js引擎编译该函数,为其创建一个执行上下文并压入栈中,再执行函数中的代码 - 执行到调用
add
函数,js引擎编译该函数,为其创建一个执行上下文并压入栈中,再执行函数中的代码 add
函数执行完毕,该函数执行上下文从栈顶弹出addAll
函数执行完毕,执行上下文从栈顶弹出- 全局上下文留在栈中
- 栈溢出
调用栈有大小,当入栈的执行上下文超过一定数目就会报错,写递归代码时易发生栈溢出错误 - 调用栈信息查看
- 在浏览器控制台Sources中可以给代码加断点,执行代码时执行流程会在断点处暂停,然后可以在Call Stack查看调用栈情况
- 在代码中加上
console.trace()
来输出当前函数调用关系
块级作用域
ES6引入let
和const
关键字实现块级作用域
let
声明的是变量,值可以被修改const
声明的是常量,值不可以被修改
块级作用域的实现
例子:
//例子
function foo() {
var a = 1;
let b = 2;
{
let b = 3;
var c = 4;
let d = 5;
console.log(a); //1
console.log(b); //3
}
console.log(b); //2
console.log(c); //4
console.log(d); //报错
}
foo();
- 编译阶段
- 函数内部通过
var
声明的变量在编译阶段被存放到变量环境中 - 函数内部通过
let
声明的变量被存放到执行上下文的词法环境中 - 在函数块级作用域内部
let
声明的变量没有被放到词法环境中
- 函数内部通过
- 执行阶段
词法环境内部维护了一个小型栈结构,栈底是函数最外层的let
或const
变量,进入一个块级作用域后就会把该作用域块中的let
或const
变量压到栈顶,当作用域块执行完成后该作用域的信息就会从栈顶弹出
闭包
指有权访问另一个函数作用域中变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数
function createComparisonFunction(propertyName) {
return function(object1, object2){ //匿名函数
//code...
};
}
//创建函数
var compare = createComparisonFunction("name");
//调用函数
var result = compare({ name: "Nicholas" }, { name: "Greg" })
//解除对匿名函数的引用(以便释放内存)
compareNames = null;
createComparisonFunction()
函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍在引用这个活动对象。即当 createComparisonFunction()
函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()
的活动对象才会被销毁
- 注意
- 匿名函数的执行环境具有全局性,因此
this
对象通常指向window - 闭包只能取得包含函数中任何变量的最后一个值
function createFunctions(){ var result = new Array(); for (var i=0; i < 10; i++){ result[i] = function(){ return i; }; } return result; } /* 因为result数组中每个函数作用域链中都保存着其包含函数的活动对象 所以它们引用的都是同一个变量i 所以每个函数都返回10 */
- 匿名函数的执行环境具有全局性,因此
- 优点
- 延长外部函数局部变量生命周期
- 可以重复使用变量,并且不会造成变量污染
- 缺点
比普通函数更占用内存,会导致网页性能变差,在IE下容易造成内存泄露
闭包的回收
- 若引用闭包的函数是一个局部变量,等函数销毁后,js引擎执行垃圾回收时判断闭包不再被使用就会将这块内存回收
- 若引用闭包的函数是一个全局变量,则闭包会一直存在直到页面关闭;但若此闭包以后不再使用,会造成内存泄漏
- 内核泄漏
由于外部函数的活动对象被引用于内部匿名函数的作用链中,若匿名函数存在,即使外部函数调用完销毁后,它所占用的内存也不会被回收,这样就容易导致内存泄漏。因此要手动设置引用匿名函数的变量为null
解除引用
- 内核泄漏
参考:
① 极客时间《浏览器工作原理与实践》
② https://www.cnblogs.com/wangfupeng1988/p/4000798.html
③ https://blog.csdn.net/qq_38563845/article/details/78206729