堆和栈本身是数据结构,堆(链表结构)、栈(栈结构)。
程序在运行的时候内存中按照逻辑分为了堆内存和栈内存。
栈:栈结构的内存,比较小,速度快,操作系统会自动分配、回收。
特点:先进后出。
堆:堆结构的内存,比较大,速度慢。一般都是程序员使用的时候自己分配的。要让内存回收必须手动释放。
特点:先进先出(排队)
在底层语言中是这样的,在js中我们的各种内存机制几乎都是自动的。
JavaScript的运行环境分为两种:
每个运行环境我们通常称为执行上下文。
代码在执行的时候开辟出来一段栈空间用来说明代码的执行顺序,这个栈我们叫执行栈。
什么是作用域?
作用域:就是变量起作用的范围。
作用域的作用?
隔离变量。函数外部定义的变量和函数内部定义的变量没有关系。
作用域确定的时机?
作用域是在进入到全局执行环境或函数执行环境时就确定好的。
作用域种类?
ES5里面作用域分为两种:全局作用域和局部作用域(函数作用域)
ES6里面加入了块级作用域。
当你一执行JavaScript就进入了全局执行环境就有了全局作用域,当执行某个函数时候就进入了函数执行环境就有了某个函数的作用域。
如果执行环境销毁其作用域也会被销毁。
__同一个函数调用多次将会产生不同的执行环境,每次调用都会产生一个。__因为每一次执行的环境分配的地址空间都不相同。
全局变量、局部变量
全局变量:定义在全局执行环境中的变量。(函数外部的变量,全局作用域)
局部变量:定义在局部环境中的变量。(函数内部,函数作用域)
局部变量:只能在自己的作用域中起作用,只能在函数内部使用,外部是用不到的。
function test(){ var a = 1; var b = 2; console.log(a); } test(); console.log(a);
全局变量:在整个程序的所有地方都可以使用(包括函数内部)。
var c = 3; function test() { var a = 1; var b = 2; console.log(c); } test(); console.log(c);
尽量少定义全局变量,把变量的权限最小化。
作用域链
函数内部是可以嵌套函数的。
var a = 0; function test() { var a = 1; function test1() { console.log(a);//1 } test1(); console.log('test');//test } test();
上面的代码输出1,test为什么是1是由于作用域链导致的。
作用域是变量起作用的范围,作用域链用来描述查找变量的一系列的过程。
一个函数在定义的时候会生成一个属性[[scope]]
属性(带[[]]
的都是不能访问),这个属性中存储了定义函数时作用域层级。
作用域链执行的时候才有:作用域链=当前的函数作用域+[[scope]]
作用域是执行的时候有的;
[[scope]]
定义的时候有的;
作用域链执行的时候才会最终生成。作用域链=当前的函数作用域+[[scope]]
函数当前的作用域是作用域链的顶端,全局作用域是作用域链的最后端。
执行环境被销毁,作用域链也会被销毁。
var定义变量就是将作用域限定在最近的函数作用域上。如果不是在函数中定义的变量在全局中定义这个变量,属于全局作用域。
局部作用域中使用变量的查找过程:
局部作用域中是否有同名的形参,如果有就使用。
<script> function test(a){ console.log(a); } test(1); </script>
局部作用域中是否有同名的函数(函数内部也可以定义函数),如果有就使用。并且在定义之前或之后使用都可以。
function test(){ /* function a(){} a(); a(); */ a(); function a(){ console.log('a'); } a(); } test();
局部作用域中是否有同名的变量声明,如果有就使用(在声明之前使用将会是undefined,声明之后使用将是值)
function test() { /* var a; console.log(a); a = 1; console.log(a); */ console.log(a);//undefined var a = 1; console.log(a);//1 } test();
如果同时使用了同名的形参、变量:变量会覆盖形参,在形参下马上使用将会是实参的值,在变量定义下使用将会是变量的值。
<script> function test(a){//a=2 /* var a ;//忽略 重复声明 console.log(a);//2 a = 1; console.log(a);//1 */ console.log(a);//2 var a = 1; console.log(a);//1 } test(2); </script>
同时定义了同名的形参、函数:函数会覆盖形参,在形参下马上使用或在函数定义下马上使用都是函数。
function test(a) { console.log(a);//函数体 function a() { console.log('a'); } console.log(a);//函数体 } test(2);
如果同时定义了同名的变量、函数:函数第一行使用将会是子函数,变量定义下使用将会是变量的值。
function test() { console.log(a);//函数 var a = 1; console.log(a);//1 function a() {//子函数 console.log('a'); } console.log(a);//1 } test();
如果同时定义了同名的形参、函数、变量声明:子函数覆盖形参、函数第一行使用时将会是子函数、变量定义下使用将会是变量值。
function test(a) { console.log(a);//函数 var a = 1; console.log(a);//1 function a() { console.log('a'); } console.log(a);//1 } test(2);
如果局部作用域中没有该变量,则沿着作用域链向上层开始查找。
在局部作用域中设置变量的过程:
看函数中是否定义了同名的变量,如果定义了则直接修改本作用域中的。
<script> function test(){ var a = 1; console.log(a);//1 a = 2; console.log(a);//2 } test(); </script>
如果本作用域中没有同名的变量,则沿着作用域链向上层查找,如果找到设置的是上层作用域的。
function test1(){ var a = 'xyz';//1 2 function test(){ console.log(a);//xyz a = 1; console.log(a);//1 a = 2; console.log(a);//2 } test(); console.log(a);//2 } test1();
如果到最后都没有找到则看全局作用域中是否有,如果有就修改,如果没有就声明。
var a = '000'; function test1(){ function test(){ console.log(a+'!');//000! a = 1; console.log(a+'@');//1@ a = 2; console.log(a+'#');//2# } test(); console.log(a+'$');//2$ } test1(); console.log(a + '%');//2%
如:
function test1() { function test() { a = 1; console.log(a + '@');//1@ a = 2; console.log(a + '#');//2# } test(); console.log(a + '$');//2$ } test1(); console.log(a + '%');//2%
小总结:作用域链函数执行的时候才会生成,因为作用域链等于:当前作用域+[[scope]]
当前的作用域:函数调用的时候生成。
[[scope]]
属性:函数定义的时候就有了,函数即使不调用也有。
<script> var num = 10; function fun() {//[[scope]]->全局作用域 var num = 20; fun2();//fun2的作用域+全局作用域 } function fun2() {//[[scope]]->全局作用域 console.log(num); } fun();//fun的作用域+全局作用域 </script>
fun的局部环境中可以调用全局中的fun2,fun2执行的时候找num,这个时候fun2的作用域链中只包含fun2的执行环境+全局执行环境
分析1:
<script> var a = 100; console.log(a); </script>
进入到全局执行环境,全局执行环境发现有变量声明,在栈空间中用一段空间来存储a=100,当你执行到console.log(a);
的时候从栈内存中查找a这个标识符然后找到它的值。
变量如果是基本数据类型(String、Number、Boolean、undefined、null)会在栈内存开辟空间存储标识符,然后将基本类型的值也存储在栈内存中并且和标识符对应起来。
分析3:
引用类型的值存储在堆内存中,存储引用类型的时候会给堆内存一个内存地址,但是标识符还是在栈内存中,为了能够让标识符和真正的数据结合起来在栈中存储的值是引用类型的地址。
内存地址都是十六进制0x开头。
变量如果是引用类型,会在栈内存开辟一段空间存储标识符,引用类型的值存储在堆内存中,堆内存中对应内存地址,这个内存地址会被存储到栈内存中和标识符对应起来。
查找变量和执行环境、作用域、作用域链有关系,和堆没有关系,因为是查找标识符,标识符存储在栈里面。
引用类型赋值的时候赋的是地址而不是值。
赋址和赋值
分析6:
分析7:
程序开始~结束执行了什么:
程序一开始执行,碰见了全局执行环境,创建全局执行环境压入到执行栈,全局代码执行的时候依赖的是全局环境中的东西。
全局变量:如果是基本数据类型,值存储在栈内存,如果是引用类型这些数据存储在堆内存里面,标识符存储在栈内存中。然后要和堆内存中的地址要对应起来。
当程序遇到了函数调用,就有自己的执行环境了,然后将自己的执行环境压入到执行栈。
局部变量:局部变量的数据存储和全局变量的存储套路是一样的只不过执行环境不一样。
只有函数执行,局部变量才会出现。函数执行完成之后,函数环境要销毁,局部变量也就不存在了。
当函数调用完成之后,要继续执行全局里面的代码,一直到所有代码都执行完成,代表程序结束了,程序结束,全局环境最后出栈(销毁)
执行环境:
创建阶段:将带var的变量的声明以及函数的声明放在当前作用域的顶部,执行阶段:开始在原来的代码的本身应该在位置开始进行赋值和处理其他逻辑。
console.log(a); var a = 1; 但是实际执行的不是这样,实际执行的是 var a;//在创建阶段声明的 undefined console.log(a);//执行阶段 这时候要用到a,但是上面的a还没有赋值所以打印出来值是undefined a=1//执行阶段时候执行 执行重新赋值 test(); function test(){ console.log('!'); } test(); 实际执行的时候: function test(){ console.log('!'); } test(); test();
注意:
不管是全局执行环境还是函数执行环境每个执行环境都会对变量、函数进行提升操作。
只有function声明的函数将会被提升函数表达式和函数构造器声明的函数不会被提升。
<script> /* var foo;//undefined foo();//foo不是函数。 如果foo这个变量真的不存在的话那么就是foo not defined foo = function(){ } */ foo(); var foo = function (){ console.log('!!!'); } </script>
函数声明和变量都会被提升,但是函数会被首先提升,然后才是变量(在JS中函数是一等公民)
/* function foo(){//定义了一个函数foo console.log(1); } //重复定义foo将会被忽略。 var foo; foo(); foo = function(){ console.log(2); } */ foo(); var foo; function foo(){ console.log(1); } foo = function(){ console.log(2); }
解析过程中foo()
函数会被提升到最前面,然后是var foo
但是因为函数已经被声明了所以var foo
属于重复定义将会被忽略,然后执行foo()
函数所以输出1,然后又定义了函数表达式foo = function()
所以覆盖了前面的foo函数。
函数同名,提升过程中,后面的覆盖前面的。
第一个坑
<script> alert(a); a = 0; </script>
报错,不是正常的变量定义,不会出现提升操作。
第二个坑
/* var a ; alert(a);//undefined a = 0; alert(a);//0 */ alert(a);//undefined var a = 0; alert(a);//0
第三坑
<script> /* function a(){} var a; alert(a); a = '我是变量'; alert(a); */ alert(a); var a = '我是变量'; function a() { alert('我是函数') } alert(a); </script>
第四坑
<script> /* function a(){} var a; alert(a);//函数 a++;//NaN alert(a);//NaN a='我是变量'; alert(a);//我是变量 */ alert(a); a++;//函数转换数值是NaN。 alert(a); var a = '我是变量'; function a() { alert('我是函数') } alert(a); </script>
第五坑
第六坑
第七坑
第八坑