一、声明提升
- v8引擎会先编译代码,再执行。
思考下述代码的执行结果是什么?
showName();
console.log(myname); // 输出:undefined
var myname='zs';
function showName(){
console.log('函数showNamw 执行了');
}
这段代码的执行结果是先打印函数showNamw 执行了,再输出undefined,其原因主要与JavaScript中的声明提升机制有关。
1、变量声明提升
在js中,使用var声明的变量会被提升到函数或全局作用域的顶部,但只有变量的声明会被提升,而不是变量的初始化赋值操作。也就是说,var myname='zs'会被拆分成两步:
- 在代码执行之前,
var myname被提升到全局作用域的顶部,此时myname被声明为一个变量,其初始值为undefined; - 代码自上而下执行,当执行到
console.log(myname)时,myname已经被声明,但此时它还没有被赋值为'zs',故输出undefined。
2、函数声明提升
函数声明也会被提升到其所在作用域的顶部。function showName(){...}是一个函数声明,它会被提升到全局作用域的顶部。因此,当代码执行到showName()时,showName函数已经被声明并可用,会正常执行并打印函数showNamw 执行了。
由于声明提升的存在,代码的实际执行顺序可以理解为:
var myname; // 变量声明被提升,myname初始值为undefined
function showName(){ // 函数声明被提升
console.log('函数showNamw 执行了');
}
showName(); // 调用函数,打印“函数showNamw 执行了”
console.log(myname); // 此时myname的值仍为undefined,所以输出undefined
myname='zs'; // 变量赋值操作
二、调用栈
- js 引擎追踪函数的一个机制,管理一份代码的执行关系,栈底是全局上下文,栈顶是当前执行的上下文。
- 函数执行时,会创建一个执行上下文,入栈;函数执行完,出栈。
- 调用栈不能设计的太大,否则,js 引擎在查找上下文时会花费大量时间。
var a=2;
function add(){
var b=10;
return a+b;// 12
}
add();
执行过程:在全局作用域中声明了一个变量a和一个add函数。在add函数局部作用域内声明了一个变量b。函数内部返回a和b的和时,当前作用域没有a,可往外层全局作用域中查找。
三、块级作用域
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) // d 没有被声明 // 报错
}
foo()
执行过程:在foo函数的作用域中声明变量a和b。由于var声明的变量具有函数作用域,a在整个函数foo中都有效。let声明的变量具有块级作用域,b只在函数foo的作用域内有效。在块级作用域{}(代码块)中声明变量b,c,d。同上,var声明的变量c会被提升到函数foo的顶部,因此c在整个函数foo中都有效。b和d形成小型的栈结构,只在该块级作用域中有效。块级作用域结束,块级作用域的内容销毁,即出栈。
总结:
let声明的变量只在声明它们的块级作用域内有效。 在块级作用域外访问这些变量会导致错误。var声明的变量具有函数作用域,即使在块级作用域中声明,也会提升到函数的顶部,因此在整个函数中都有效。
四、作用域链
- 在js中,每个块级作用域都与包含它的外部作用域形成层级关系,构成作用域链。
- js 引擎在查找变量时,会先从当前作用域查找,如果没有,就会向上一级作用域查找,直到找到为止,如果没有,就会报错。
- 作用域链的下一级是谁,是由 outer 指针决定的。
例 1:
function bar(){
console.log(myname) // 输出:hh
} // 指针指向全局(词法作用域)——看声明在哪里
function foo(){
var myname="xx";
bar();// 函数调用
}// 指针指向全局
var myname="hh";
foo()
执行环境:
例 2:
var num =10
function a(){
var count = 18;
function b(){
var num = 20;
c()
}
function c(){
console.log(num) // 输出:10
}
b()
}
a()
执行过程:
五、闭包
- 根据作用域链的查找规则,内部函数一定有权利访问外部函数的变量。另外,一个函数执行完毕后它的执行上下文一定会被销毁。那么当函数 A 内部声明了一个函数 B,而函数 B 被拿到 A 的外部执行时。为了保证以上两个规则正常执行,A 函数在执行完毕后会将 B 需要访问的变量保存在一个集合中,并留在调用栈当中,这个集合就是闭包。
- 缺点:内存泄漏,闭包会一直存在于内存中,不会被垃圾回收机制回收。
function foo(){
var myname='ll'
var age=18
return function bar(){
console.log(myname); // 输出:ll
}
}
var baz = foo() // 规则1:foo函数体执行完,foo函数执行上下文销毁
// 规则2-作用域链的查找规则:内部的作用域一定有权力去访问它外部的作用域的变量,即bar函数可访问foo函数
// 二则规则冲突
baz()
执行过程:
小例题:
var arr=[] // function(){} function(){} function(){} function(){} function(){}
for(var i =1; i <= 5; i++){
arr.push(function(){
console.log(i); // 输出:6 6 6 6 6
})
}
// i=1 func*1 i=2 // 函数体没有触发执行,只是放入栈中
// i=2 func*2 i=3
// i=3 func*3 i=4
// i=4 func*4 i=5
// i=5 func*5 i=6
// i=6 循环终止 此时打印栈中的函数体
// i 是全局作用域的变量,函数体声明在全局,outer指针指向全局
for(var j=0; j<arr.length;j++){
arr[j]() // arr数组调用,函数触发
}
如何将输出值改为1 2 3 4 5 ?
解1:
- 用
let声明变量i,形成块级作用域。
var arr=[]
for(let i =1; i <= 5; i++){
arr.push(function(){
console.log(i); // 输出:1 2 3 4 5
})
}
//i 和 func 形成块级作用域,函数体指针指向该块级作用域,循环五次分别创建五个块级作用域
解2:
- 形成闭包
var arr=[]
for(var i =1; i <= 5; i++){
function foo(j){
arr.push(function(){ //匿名函数被拿到 foo 外面 // 分别形成五个闭包
console.log(j); // 输出:1 2 3 4 5
})
}
foo(i)
// (function(j){
// arr.push(function(){
// console.log(j); // 输出:1 2 3 4 5
// })
// })(i) // ()()自执行函数
}