在JavaScript中,变量的作用域有两种,全局作用域、函数作用域,ES6中用let、const声明的变量有块级作用域。
全局作用域:任何未经声明的变量直接赋值,此变量为全局变量,归window所有;一切声明的全局变量,都是window 的属性函数作用域:在函数作用域中定义的变量,函数外部无法访问块级作用域:{ }中定义的所有变量在括号外都是不可见的,称之为块级作用域。我们会发现,javascript代码执行会出现下面这种情况
console.log(fn1()); //可以在声明之前调用 // 生效的是函数f1 ,而不是变量f1 ? //函数声明提升 会把函数的声明整体提升到最顶端 console.log(fn2()); //报错 fn2 is not a function //变量声明提升 提升声明,不提升赋值 var fn1 = "a"; //函数声明 function fn1() { return 1; } //函数表达式 var fn2 = function () { return 2 }我们会发现,javascript代码执行前,会做一些我们看不见的事,比如变量提升,也就是说js代码执行有自己的执行环境,里面有执行代码需要的信息,即执行上下文
概念: 某个函数或全局代码的执行环境,环境中包含执行代码需要的所有信息 <执行上下文就是一个对象,对象中包含了执行代码需要的信息> 执行之前建立执行上下文。
call stack(执行上下文栈ECS):组织管理程序运行过程中的执行上下文。
执行上下文的内容:
VO:(变量对象)存放函数或全局代码执行过程中需要用到的局部变量作用域this现在主要说的就是VO: VO是一个对象,调用函数或执行全局代码时创建,创建一个vo,需要需要三步
确定函数形参的值(包括arguments对象)确定函数中所有的函数字面量声明 (1)该函数必须是字面量函数声明 function a(){},此函数提升后可以认为该声明失效 (2)如果该声明中出现同名属性,直接覆盖确定函数中所有的变量声明(var声明的),将其提取到上下文中,值为undefined 注:如果此变量名已经存在于当前vo中,忽略此变量分析上述例子,创建的VO对象如下:
// VO { // fn1:fn , // //fn1变量已存在,忽略 // fn2:undefined , // }可以发现,变量f1被忽略,值为函数的变量f2在vo中(代码执行前)是undefined,执行f2报错,f2不是一个函数,console.log(fn2)时,打印 undefined
作用域链:要得到一个变量的值,若当前作用域没有定义,就到父级作用域寻找。如果父级作用域中也没找到,就再向一层去寻找,直到找到全局作用域。这种一层一层的关系,就是作用域链。
var value = { name: 'cc' }; (function fn1() { var a = 1; (function fn2() { var b = 2; console.log('value', value) //{name:"cc"},value向上顺着作用域寻找 window的 console.log('a', a) //1 fn1的 console.log('b', b) //2 当前作用域变量 console.log('c', c) //c is not defined })(); //立即执行fn2 })(); //立即执行fn1内部函数被保存到了外部会产生闭包。比如:两个函数互相嵌套,把里面函数保存到了外部(全局),必然会生成闭包,里面函数在外面执行一定可以调用他原来在的那个函数里边的变量。
var a = null; function father() { var n = 1; function son() { n++; console.log(n) } a = son //把son保存给全局a } father() a() //2 a() //3分析:执行函数father,执行完会销毁,释放作用域;但是father函数中有函数son定义,son函数使用father中变量n(作用域链找到的n),此时,把函数son赋值给全局变量a,执行a,那么father中的变量n一直在全局中有引用,导致father作用域链得不到释放,导致内存泄漏(内存占用的多了剩的就越少)。
1 . 变量的生命周期:从定义一个变量到不再使用这个变量
局部变量的生命周期在函数执行完成后就到头了全局变量的生命周期在页面关闭后就到头了 function fn() { var a = 1; } fn() console.log(a) // a is not defined // 无法访问 因为当fn()执行后变量a生命周期到头了 // 垃圾回收机制收走了2 . 垃圾回收机制:释放内存,检测这个数据有没有在其他地方被引用,或在其他地方使用,没有的话就会被垃圾回收机制回收这个数据,内存被释放
回收策略: 标记清除:给每个数据打上标记(如:声明变量标记进入环境,变量不再使用标记离开环境,回收)引用计数:标记使用次数注意:如果这个数据有引用的关系则不会被回收
3 . 执行上下文:当前代码的执行环境 EC
全局环境函数环境eval环境遇到遇上以上三种环境会放到执行上下文栈 ECS (函数调用栈 call stack)
分析上述代码:也可以把son返回出去
function father() { var n = 0; function son() { n++ console.log(n) } return son; } var result = father() //father有引用 不会被垃圾回收机制回收 result() //1执行过程:
//father() //执行代码之前要执行环境 创建阶段 fatherEC = { vo: { //变量队对象 n: undefined, son: 'son在内存里的引用地址', }, scope: [ //存储作用域链 声明的环境 Global.AO ], this: 'window' } //执行阶段 fatherEC = { Ao: { n: 30, //变量赋值 son: fn, //函数赋引用值 }, scope: [ fatherEC.AO, Global.AO ], this: 'window' } //son() //执行代码之前要执行环境 创建阶段 sonEC = { vo: { //变量队对象 }, scope: [ //存储作用域链 声明的环境 fatherEC.AO, Global.AO ], // this: 'window' } //执行阶段 fatherEC = { Ao: { }, scope: [ sonEC.AO,fatherEC.AO, Global.AO ], // this: 'window' }例子
for (var i = 0; i < 5; ++i) { setTimeout(function () { console.log(i + " "); }, 100); } //5个5js单线程,setTimeout要放在执行队列,等主线程清空才能执行,执行时for循环已经结束,i变量会被提升并且每次循环覆盖,最后一次结束 i=5 ,所以,打印5个5
解决方法1:立即执行函数
for (var i = 0; i < 5; ++i) { (function (i) { setTimeout(function () { console.log(i + " "); }, 100); })(i) } //0 1 2 3 4解决方法2:ES6 let ,块级作用域
for (let i = 0; i < 5; ++i) { setTimeout(function () { console.log(i + " "); }, 100); } //0 1 2 3 4