Skip to content

Latest commit

 

History

History
387 lines (241 loc) · 13.7 KB

变量提升机制.md

File metadata and controls

387 lines (241 loc) · 13.7 KB

变量提升

浏览器开辟内存(栈内存/执行上下文/作用域)供js执行代码、存储变量(变量存储区)及基本数据类型的值(值存储区)。最先创建的作用域为全局作用域

词法解析阶段,会将var声明及function进行变量提升。对于var定义的变量,只是进行提升声明,并不赋值,默认值为undefined。对于function定义的函数会进行声明且赋值,开辟一个新的内存空间,将代码字符串内容存储在堆内存中。

console.log(sum(10, 20)) // 30
function sum(a,b){ return a + b }

带var与不带var的区别

  1. 但是如果以函数表达式的方式声明函数,即将函数赋值给一个变量,则只会对该变量进行变量提升。

下面的代码会报错,因为词法解析阶段只对var sum进行了提前声明,并未进行赋值,后面的函数体也不会被关联到sum

console.log(sum(10, 20)) // TypeError: sum is not a function
var sum = function(a,b){ return a + b }
  1. 全局作用域下,对于普通变量来说,不用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
  1. 浏览器环境中,在全局作用域下用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进行赋值,将变量a13进行关联;从外层作用域中找到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
*/

代码分析:

  1. 词法解析,进行变量提升:

将带var的进行变量提升,但不赋值,a,b,c在全局作用域下进行声明

function fn声明进行变量提升,并赋值。开辟新的堆内存空间,存储它的代码字符串,并将该堆内存的地址赋值给变量fn

  1. 代码执行

(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,bc在该函数作用域无声明,根据作用域链向上层查找,在全局作用域得到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 ]
*/

来看一下代码分析:

  1. 词法分析阶段,变量提升:

arr进行变量提升但不赋值;fn进行变量提升并赋值,为函数开辟新的内存空间,存储代码字符串,并将变量fn指向该内存空间地址

  1. 代码执行阶段

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]