函数作用域与全局作用域
- 函数作用域
function foo(){
var a = 'bar';
console.log(a);
}
foo();//bar
- 全局作用域
var b = 'bar';
function foo(){
console.log(b);
}
foo();//bar
- 函数作用域是彼此独立的
function bar(){
var b = 'bar';
}
function foo(){
console.log(b);
}
foo();//Uncaught ReferenceError: b is not defined
- 作用域链
var b = 'bar';
function bar(){
function foo(){
console.log(b);//变量作用域的查找是一个扩散过程,就像各个环节相扣的链条,逐次递进
}
foo();
}
bar();//bar
块级作用域和暂时性死区
- 变量提升(var)
function foo(){
console.log(bar);
var bar = 3;
}
foo();//undefined
//等价于
function foo(){
var bar;
console.log(bar);
bar = 3;
}
foo();//undefined
- 封闭的块级作用域(let const)
function foo(){
console.log(bar);
let bar = 3;
}
foo();// can't access lexical declaration 'bar' before initialization
- 暂时性死区(起始于函数开头,终止于相关变量声明语句所在行)
function foo(){
let bar = 3;
console.log(bar);
}
foo();//3
- 除自身作用域内的foo3,bar2函数还可以访问foo2、foo1;但是bar1函数却无法访问bar2函数内定义的foo3
- 死区
- 默认参数赋值时的暂时性死区
function foo(arg1 ,arg2 = arg1){
console.log(`${arg1} ${arg2}`);
}
foo('arg1','arg2');//arg1 arg2
//如果没有传入第二个参数,则会使用第一个参数作为第二个实参
// 当第一个参数设置为默认值时,执行arg1 = arg2 会被当作暂时性死区处理
function foo2(arg1 = arg2 ,arg2){
console.log(`${arg1} ${arg2}`);
}
foo2(undefined,'arg2');//can't access lexical declaration 'arg2' before initialization
执行上下文
function foo(arg1){
let arg1;
}
foo();// redeclaration of formal parameter arg1
//等价于
function foo(){
var arg1;
let arg1;
}
- 执行上下文就是当前代码的执行环境/作用域,和作用域链相辅相成,但又是完全不同的两个概念。直观上看,执行上下文包含了作用域链,同时他们又像是一条河流的上下流:有了作用域链,才会有执行上下文的一部分。
JS代码执行的两个阶段
-
代码预编译阶段
预编译阶段是前置阶段,这一阶段会由编译器将JavaScript代码编译成可执行的代码。这里的预编译和传统的编译不同,传统的编译非常复杂,涉及分词、解析、代码生成等过程。这里的预编译是JavaScript中的独特概念,虽然JavaScript是解释型语言,编译一行执行一行。但是在代码执行前,JavaScript引擎确实会做一些“预先准备工作”。 在通过语法分析,确认语法无误后,便会在预编译阶段JavaScript代码中变量的内存空间进行分配,比如变量提升就是在这个阶段完成的。
- 对变量进行声明
- 对变量进行提升,值为undefined
- 对所有非表达式的函数声明进行提升
-
代码执行阶段
执行阶段的主要任务是执行代码逻辑,执行上下文在这个阶段会全部创建完成。
function bar(){ console.log("bar1"); } var bar = function(){ console.log("bar2"); } bar();//bar2
var bar = function(){ console.log("bar2"); } function bar(){ console.log("bar1"); } bar();//bar2
两段代码输出的都是bar2,因为在预编译阶段虽然对变量bar进行了声明,但是不会对其进行赋值;函数bar则被创建并提升。在代码执行阶段,变量bar才会(通过表达式)被赋值,赋值的内容为是函数体为
console.log("bar2")
的函数,输出结果为bar2。在预编译阶段只是对变量声明进行提升,值为undefined,在代码执行阶段才会赋值,所以通过var关键字定义的函数相对于对非表达式的函数来说他是后赋值的,所以他会覆盖掉非表达式函数函数
bar();//bar1 var bar = function(){// console.log("bar2"); } function bar(){//在预编译阶段对非表达式的函数声明进行提升 console.log("bar1"); } //在预编译阶段只是对变量声明进行提升,值为undefined,在代码执行阶段才会赋值,所以通过var关键字定义的函数相对于对非表达式的函数来说他是后赋值的,所以他会覆盖掉非表达式函数函数 bar();//bar2
foo(10);
function foo(num){
console.log(foo);
foo = num;
console.log(foo);
var foo;
}
console.log(foo);
foo = 1;
console.log(foo);
//输出
//undefined
//10
//function foo(num)
//1
在foo(10)
执行时,会在函数体内进行变量提升,此时执行函数体内的第一行会输出undefined
,执行函数体内的第三行会输出foo
。
接着代码运行到函数体外的console.log(foo)
语句时,会输出foo
函数的内容(因为foo
函数内的foo=num
,被赋值给函数作用域内的foo
变量)。
得出的结论是,作用域在预编译阶段确立但是作用域链是在执行上下文的创建阶段完成的,因为函数在调用时才会开始创建对应的执行上下文。执行上下文包括变量对象、作用域链、及this
的指向。
代码执行的整个过程就像生产流水线。
第一步是在预编译阶段创建变量对象(Variable Object
,VO),此时只是创建,并未赋值。
第二步是代码执行阶段,变量对象会转为激活对象(Acyive Object
,AO),即完成VO向AO的转换。
此时作用域链也将被确定,他由当前执行环境的变量对象和所有外层已经完成的激活对象组成。这道工序保证了变量和函数的有序访问,即如果为在当前作用域中找到变量,则会继续向上查找,直到全局作用域。
js代码执行的过程便是JavaScript
引擎执行机制最基本的原理
调用栈
-
在执行一个函数时,如果这个函数又调用了另外一个函数,而这“另外一个函数”又调用了另外一个函数,这样便形成了一系列的调用栈
function foo1(){ foo2(); } function foo2(){ foo3(); } function foo3(){ foo4(); } function foo4(){ console.log('foo4'); } foo1();//foo4
以上代码的调用关系为foo1
—foo2
—foo3
—foo4
。
具体过程是foo1先入栈,紧接着foo1调用foo2,foo2再入栈,foo2调用foo3,foo3再入栈,foo3调用foo4,foo4再入栈;直到foo4执行完。然后foo4先出栈,foo3再出栈,接着foo2出栈,最后foo1出栈。这个过程满足先进后出的规则,因此形成调用栈。
正常来讲在函数执行完毕并出栈时,函数内的局部变量在下一个垃圾回收(GC)节点会被回收,该函数对应的执行上下文将会被销毁,这也正是我们在外界无法访问函数内定义的变量的原因。也就是说,只有在函数执行时,相关函数才可以访问该变量,该变量会在预编译阶段被创建,在执行阶段被激活,在函数执行完毕后,其相关上下文会被销毁。
闭包
-
函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可以访问,进而形成了闭包。
function numGenerator(){ let num = 1; num++; return ()=>{ console.log(num); } } var getNumber = numGenerator(); getNumber();
在这个简单了闭包示例中,
numGenerator
创建了一个变量num
,接着返回打印num
值得箭头函数,这个函数引用了变量num
,使得在外部可以通过调用getNumber
方法访问变量num
,因此在numGenerator
执行完毕后,变量num
不会消失,仍然有机会被外界访问。 -
闭包的基本原理
在函数嵌套函数时,外层函数返回了内层函数,而且返回的内层函数使用了外层函数的变量,那么外界便能通过这个返回的函数获取原函数(外层函数)内部的变量值。
从直观上看闭包的概念为JavaScript中访问函数内部变量提供了途径和便利。这样做的好处有很多,比如可以利用闭包实现‘模块化’;
内存管理
内存管理是计算机科学中的概念。不论使用哪种编程语言开发,内存管理都是指针对内存生命周期的管理,而内存的生命周期无外乎分配内存、读写内存、释放内存
var foo = "bar";//分配内存
alert(foo);//读写内存
foo = null;//释放内存
内存的空间可以分为栈空间和堆空间。
- 栈空间:由操作系统自动分配释放,存放函数的参数值、局部变量的值等,操作方式类似于数据结构中的栈。
- 堆空间:一般由开发者分配释放,这部分空间需要考虑垃圾回收的问题。
在JavaScript
中,数据类型包括基本数据类型和引用数据类型
- 基本数据类型:undefined、null、number、boolean、string、symbol等
- 引用数据类型:Object、array、function等
一般情况下,基本数据类型按照值大小保存在栈空间中,占有固定大小的内存空间;引用数据类型保存在堆空间中,内存空间大小并不是固定的,需要引用情况来进行访问。
var a = 11;
var b = 10;
var c = [1,2,3];
var d = {e:66};
对于分配内存的读写行为,所有语言都较为一致,但是释放内存的行为在不同语言之间有差异。
JavaScript依赖宿主浏览器的垃圾回收机制,一般情况下不用程序员操心;但是某些情况下也会出现内存泄漏现象。
内存泄漏是指内存空间已经应该不再被使用了,但是由于某种原因没有被释放的现象。这是一种非常“玄学”的概念,因为内存空间是否还在使用在某种程度上是不可判定的,或者判定成本很高。
内存泄漏的危害非常直观:他会直接导致程序运行缓慢,甚至崩溃。
<style>
#element{
width: 100px;
height: 100px;
background-color: #bfa;
}
</style>
</head>
<body>
<button>清除节点</button>
<div id="element">
element
<div>div2</div>
</div>
<script>
var btn = document.querySelector("button");
var element = document.getElementById("element");
function remove(){
element.parentNode.removeChild(element);
element = null;
}
btn.onclick = function(){
if(element){
remove();
}
}
</script>
</body>
这样看起来button
元素已经从DOM中移除了,但是由于其事件处理句柄还在,所以该节点变量依然无法回收。因此还需要加上removeEventListene
r函数,防止内存泄漏
var element = document.getElementById("element");
element.innerHTML = `<button id="button">点击</button>`;
var button = document.getElementById("button");
button.addEventListener('mousemove',function(){
console.log(666);
});
element.innerHTML = ``;
removeEventListener('mousemove',button);
function foo(){
let i = 0;
var s = window.setInterval(function(){
i++;
console.log(i);
if(i>=5){
clearInterval(s);
}
},1000);
}
foo();
浏览器垃圾回收
- 对于浏览器垃圾回收,除了开发者主动保证回收外,大部分场景下浏览器都会依靠
标记清除
、引用计算
两种算法进行回收。
var array = [];
function createNodes(){
let i = 100;
let frag = document.createDocumentFragment();
for(;i<0;i++){
div = document.createElement("div");
div.appendChild(document.createTextNode(i));
frag.appendChild(div);
}
document.body.appendChild(frag);
}
function badCode(){
array.push([...Array(100000).keys()])
createNodes()
setTimeout(badCode,1000)
}
badCode();
//这里递归调用了badCode,该函数每次向array数组写入新的由10000项0-1数字组成的新的数组,badCode函数使用全局变量array后并没有手动释放内存,垃圾回收机制不会处理array,导致内存泄漏;同时badCode函数调用了createNode函数,每秒会创建100个div节点。
- 示例一
const foo = (function(){
var v = 0;
return ()=>{
return v++;
}
}());
for(let i = 0; i < 10;i++){
foo();
}
console.log(foo());//10
foo
是一个立即执行函数,在循环执行foo
时,引用自由变量10次,v自增10次,最后执行foo
时,得到10。这里的自由变量是指没有在相关函数作用域中声明,但却被使用了的变量。
- 示例二
const foo=()=>{
var arr = [];
var i;
for(i = 0;i < 10;i++){
arr[i] = function(){
console.log(i);
}
}
return arr[0];
}
foo()();
自由变量为i,类似上一段代码,执行foo
返回的是arr[0]
,arr[0]
此时是函数,其中变量i的值为10
- 示例三
var fn = null;
const foo = ()=>{
var a = 2;
function innerFoo(){
console.log(a);
}
fn = innerFoo;
}
const bar = ()=>{
fn();
}
foo();
bar();//2
正常来讲,根据调用栈的知识,foo
函数执行完毕后,其执行环境声明周期会结束,所占用的内存会被垃圾收集器释放,上下文消失。但是通过将innerFoo
函数赋值给全局变量fn,foo的变量对象a也会被保留下来。所以函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象,输出结果为2
- 示例四
var fn = null;
const foo = ()=>{
var a = 2;
function innerFoo(){
console.log(c);
console.log(a);
}
fn = innerFoo;
}
const bar = ()=>{
var c = 100;
fn();
}
foo();//ReferenceError: c is not defined
bar();
在中执行fn
时,fn
已经被赋值为innerFoo
,变量c并不在其作用域链上,c只是bar函数的内部变量,因此会报错ReferenceError: c is not defined