浏览器开辟内存(栈内存/执行上下文/作用域)供js执行代码、存储变量(变量存储区)及基本数据类型的值(值存储区)。最先创建的作用域为全局作用域
词法解析阶段,会将var
声明及function
进行变量提升。对于var
定义的变量,只是进行提升声明,并不赋值,默认值为undefined
。对于function
定义的函数会进行声明且赋值,开辟一个新的内存空间,将代码字符串内容存储在堆内存中。
console.log(sum(10, 20)) // 30
function sum(a,b){ return a + b }
- 但是如果以函数表达式的方式声明函数,即将函数赋值给一个变量,则只会对该变量进行变量提升。
下面的代码会报错,因为词法解析阶段只对var sum
进行了提前声明,并未进行赋值,后面的函数体也不会被关联到sum
。
console.log(sum(10, 20)) // TypeError: sum is not a function
var sum = function(a,b){ return a + b }
- 在
全局作用域
下,对于普通变量来说,不用var
声明,不是变量声明。a=13
只是一个属性赋值,浏览器环境中,相当于window.a=13
console.log(a) // ReferenceError: a is not defined
a = 13
console.log(a)
a = 13
console.log(window.a === a) // true
- 浏览器环境中,在
全局作用域
下用var
声明的变量是全局变量,会挂在全局对象window
上。
var a = 20
console.log(a === window.a) // true
总结:浏览器环境中,在全局作用域
下,使用var
或者不使用任何关键字进行的声明都是挂载在全局对象window上。区别在于不使用任何关键字的声明不是变量声明,只是给window添加属性。
开辟栈内存->创建全局作用域->词法解析->变量提升->代码进栈执行->变量值存储在值存储区,并将变量和值进行关联->...
由于 js是先进行词法解析,因此下面整个代码不会执行,因为在词法解析阶段就已经出现了语法错误,不会再往下执行。
console.log('aaa')
let a = 1
var a = 2
//SyntaxError: Identifier 'a' has already been declared
而let重复声明不是语法错误,只有执行到的时候会报错。因此下面的代码第一行会执行输出
console.log('aaa')
console.log(a)
let a = 10
/*
aaa
ReferenceError: Cannot access 'a' before initialization
*/
函数定义也可以被重复声明(覆盖)
function fn(){console.log(1)}
var fn = function(){console.log(3)}
fn() // 3
fn()
function fn(){console.log(1)}
fn()
function fn(){console.log(2)}
fn()
var fn = function(){console.log(3)}
fn()
function fn(){console.log(4)}
fn()
function fn(){console.log(5)}
fn()
/*
5
5
5
3
3
3
*/
词法解析阶段,扫描代码,并进行变量提升:
1.fn()
没有var
或者function
声明,不再词法解析阶段进行变量提升。
2.扫描到function fn(){console.log(1)}
时,创建堆内存 A1,由于是function
函数定义,声明变量fn
,并进行赋值,将fn
指向内存A1
的地址
3.fn()
没有var
或者function
声明,不再词法解析阶段进行变量提升。
4.扫描到function fn(){console.log(2)}
时,创建堆内存 A2,不再重复声明,但要重新进行赋值,将fn
指向内存A2
地址
5.fn()
没有var
或者function
声明,不再词法解析阶段进行变量提升。
6.扫描到var fn = function(){console.log(3)}
,只对var fn
进行变量提升,而不赋值;但fn
已存在,不再重复声明。
7.fn()
没有var
或者function
声明,不再词法解析阶段进行变量提升。
8.扫描到function fn(){console.log(4)}
时,创建堆内存 A4,不再重复声明,但要重新进行赋值,将fn
指向内存A4
地址
9.fn()
没有var
或者function
声明,不再词法解析阶段进行变量提升。
10.扫描到function fn(){console.log(5)}
时,创建堆内存 A4,不再重复声明,但要重新进行赋值,将fn
指向内存A5
地址
11.fn()
没有var
或者function
声明,不再词法解析阶段进行变量提升。
因此,变量提升结束时,fn
指向的是存储代码console.log(5)
的堆内存地址 A5。
代码执行阶段:
1.fn()
开始执行,fn
指向的是存储代码console.log(5)
的堆内存地址 A5。取出代码进行执行栈执行代码,并输出 5
2.function fn(){console.log(1)}
词法解析阶段已经做了变量提升,不再处理。
3.fn()
开始执行,fn
指向的是存储代码console.log(5)
的堆内存地址 A5。取出代码进行执行栈执行代码,并输出 5
4.function fn(){console.log(2)}
词法解析阶段已经做了变量提升,不再处理。
5.fn()
开始执行,fn
指向的是存储代码console.log(5)
的堆内存地址 A5。取出代码进行执行栈执行代码,并输出 5
6.var fn = function(){console.log(3)}
词法解析阶段只对fn
进行了变量提升,但未做赋值。此时开始赋值操作,将变量fn
指向存储代码console.log(3)
的堆内存地址 A3
7.fn()
开始执行,fn
指向的是存储代码console.log(3)
的堆内存地址 A3。取出代码进行执行栈执行代码,并输出 3
8.function fn(){console.log(4)}
词法解析阶段已经做了变量提升,不再处理。
9.fn()
开始执行,fn
指向的是存储代码console.log(3)
的堆内存地址 A3。取出代码进行执行栈执行代码,并输出 3
10.function fn(){console.log(5)}
词法解析阶段已经做了变量提升,不再处理。
11.fn()
开始执行,fn
指向的是存储代码console.log(3)
的堆内存地址 A3。取出代码进行执行栈执行代码,并输出 3
因此答案是:5 5 5 3 3 3
先来看一段代码,它的结果容易让人困扰😴
console.log(a,b)
var a=12,b=12;
function fn(){
console.log(a,b)
var a = b = 13
console.log(a,b)
}
fn()
console.log(a,b)
/*
undefined undefined
undefined 12
13 13
12 13
*/
核心要点:
1.声明方式的不同
var a=12,b=12; //=> var a =12;var b =12;
var a = b = 13; //=> var a =13; b = 13;
2.执行过程
词法分析阶段:变量提升
(1)console.log(a,b)
无需要提升的变量声明,不处理
(2)var a=12,b=12;
对var
声明的a
,b
进行声明提升(放入值存储区)但不赋值(不和值进行关联)
(3)function fn(){...}
对function
声明进行提升并且赋值,为函数开辟新的堆内存空间存储代码字符串,并将变量fn
指向该内存地址
(4)fn()
无需要提升的变量声明,不处理
(5)console.log(a,b)
无需要提升的变量声明,不处理
代码执行阶段:
(1)执行console.log(a,b)
,a
,b
均有声明但有赋值,默认值为undefined
,因此打印出undefined undefined
(2)var a=12,b=12;
变量已经在词法解析阶段进行了变量提升,不再次进行声明,但要开始赋值,将12
和变量a
关联,将12
和变量b
关联
(3)function fn(){...}
已在词法解析阶段进行了提升和赋值,不处理
(4)fn()
函数执行,创建私有作用域(执行上下文)。并进栈执行
变量提升:
console.log(a,b)
不处理
var a = b = 13
只有var a
进行变量提升,将a
存储在变量存储区
console.log(a,b)
不处理
代码执行:
console.log(a,b)
,从私有函数作用域取出a,a
此时还未赋值,值为undefined
。私有函数作用域没有b
变量,从上层作用域中找到b
变量,值为 12.
var a = b = 13
,对a
进行赋值,将变量a
与13
进行关联;从外层作用域中找到b
并将其值改为13
console.log(a,b)
,从私有函数作用域取出a,值为13
;私有函数作用域没有b
变量,从上层作用域中找到b
变量,此时值为 13,因此打印出结果13 13
函数执行结束,出栈
(5)console.log(a,b)
,此时是在全局作用域下,a
为值 12,而b
已经被改变为13
,因此打印出结果12 13
1.函数声明时,只会开辟堆内存空间,存储代码字符串,并将变量和函数进行关联
2.函数执行时,形成函数作用域,在作用域内先进行变量提升,再执行代码
3.函数作用域链在函数声明时已经确定。函数的堆内存在哪个作用域下创建,它的上层作用域就是哪一个。和函数执行的位置和时机是无关的。
console.log(a,b,c)
var a = 12, b = 13, c = 14
function fn(a){
console.log(a,b,c)
a = 100;
c = 200;
console.log(a,b,c)
}
b = fn(10)
console.log(a,b,c)
/*
undefined undefined undefined
10 13 14
100 13 200
12 undefined 200
*/
代码分析:
- 词法解析,进行变量提升:
将带var
的进行变量提升,但不赋值,a,b,c
在全局作用域下进行声明
将function fn
声明进行变量提升,并赋值。开辟新的堆内存空间,存储它的代码字符串,并将该堆内存的地址赋值给变量fn
- 代码执行
(1)console.log(a,b,c)
,全局作用域下有a b c
的声明但未赋值,且值都为undefined
(2)var a = 12, b = 13, c = 14
,变量声明已经进行,只做赋值操作,将a
指向值12
,b
指向值13
,c
指向值14
(3)function fn(a)
函数声明和堆内存创建已经完成,不再处理
(4)b = fn(10)
,先将函数fn
执行,传入参数10
,并将函数返回值赋值给b
函数执行:形成函数私有作用域
变量提升:无var
或者function
声明,无变量提升
形参赋值:将传入的参数10
赋值给形参a
(函数的私有变量)
代码执行:
(a) console.log(a,b,c)
, a
是私有变量,值为 10,b
和c
在该函数作用域无声明,根据作用域链向上层查找,在全局作用域得到b
的值为 13,c
的值为 14
(b) a = 100;
变量a
在当前作用域有定义,将函数作用域内的私有变量a
重新赋值为 100
(c) c = 200;
变量c
在当前作用域无定义,根据作用域链向上层查找,在全局作用域得到c
,并将其值重新赋值为 200
(d) console.log(a,b,c)
打印得到值100 13 200
函数执行结束,无返回值,默认返回undefined
,退出当前执行栈
将undefined
赋值给全局作用域
下的变量b
5.console.log(a,b,c)
全局作用域下的a,b,c
分别为12 undefined 200
注意📢:何为函数私有变量?
函数私有变量:函数作用域中变量存储区存储的变量
(1)函数中使用var
,let
,const
,function
声明的变量
(2)形参是函数的私有变量
总结:
(1)注意不同的执行阶段:词法分析(变量提升)、代码执行阶段
(2)注意不同的作用域:全局作用域,函数作用域(以及块级作用域)
(3)注意不同的变量声明方式:不以任何关键字定义的变量(无论在哪里声明),都相当于给全局对象window赋加属性
(4)注意作用域链的形成时机:在函数创建(声明)阶段已经形成,函数的堆内存在哪个作用域下创建,它的上层作用域就是哪一个。和函数执行的位置和时机无关
(5)注意函数的私有变量:函数形参也是函数的私有变量,在函数内声明的变量是不能够和形参同名的
下面的代码执行结果可能让人困惑:
var arr = [1,2,3]
function fn(arr){
console.log(arr)
arr[0] = 100
arr = [100]
arr[0] = 0
console.log(arr)
}
fn(arr)
console.log(arr)
/*
[ 1, 2, 3 ]
[ 0 ]
[ 100, 2, 3 ]
*/
来看一下代码分析:
- 词法分析阶段,变量提升:
arr
进行变量提升但不赋值;fn
进行变量提升并赋值,为函数开辟新的内存空间,存储代码字符串,并将变量fn指向该内存空间地址
- 代码执行阶段
2.1 为arr
赋值,因为[1,2,3]
是引用类型,因此在堆内存开辟空间,存储数据,并将该堆内存空间地址赋值给arr
2.2 函数fn
执行,创建函数私有作用域。
(a) 形参赋值:将全局变量arr
的存储的数组空间的地址值传给函数的形参arr
,假设上面数组的空间地址为A
,则fn(arr)
等价于fn(A)
。这个形参是函数的私有变量,与全局变量arr
不同,但此时函数中的私有变量arr
与全局变量arr
指向的是同一个内存地址。
(b) 词法分析与变量提升:无需要进行提升的变量
(c) 代码执行:
console.log(arr)
,从函数私有变量arr
取值,为内存地址A
,因此打印出结果[1,2,3]
arr[0] = 100
,将私有变量arr
指向的内存地址空间的数组值[1,2,3]
的第一项改为了 100,此时该堆内存地址中的数组为[100,1,2]
。需要注意的是此处全局作用域下的变量arr
也是指向该地址的,因此如果此时用全局作用域下的变量arr
去访问该堆内存中的数据,则会是改变后的数据。
arr = [100]
, 创建一个新数组,需要创建一个新的堆内存存储该数组,并将函数作用域下的私有变量arr
指向该堆内存地址,假设该内存地址为B
,则这个变量arr
指向B
。此时它与全局作用域下的arr
指向的是不同的内存地址。
arr[0] = 0
,是将堆内存地址为B
的第一项改为0
,此时函数作用域下的私有变量arr
指向的堆内存地址数据为[0]
console.log(arr)
,取出函数作用域下的私有变量arr
指向的堆内存地址中的数据,打印出[0]
函数执行结果,无返回值,退出执行栈。
2.3 console.log(arr)
从全局作用域下取出变量arr
保存的堆内存地址,打印出结果[100,2,3]