Open WJCHumble opened 3 years ago
对于变量提升这个问题,我想从事前端的同学都或多或少认为我懂这个。曾经,我也是这样认为的,我懂变量提升,并且可以从变量在 Chrome 中的内存分配讲起,以及中间发生了什么。
Chrome
但是,在一次面试中,我遇到了几个一起面前端的同学(当然技术水平参差不齐,并不是很高),在和他们聊这次笔试中的变量提升的问题时,发现大家都支支吾吾的,很多讲的都是值的覆盖。
当时的面试题是这样的:
function fn(a) { console.log(a) var a = 2 function a() {} console.log(a) } fn(1)
这个题目,最终会输出function a(){} 和 2。那么,为什么是这个答案,这个过程发生了什么很重要。所以,今天我们就来彻底刨析一下变量提升的过程。
function a(){}
2
在分析整个过程前,我们先来回顾一下 JavaScript 中变量在内存中的分配。
JavaScript
大家都知道的是,对于原始类型会存储在栈空间中,对于引用类型会将引用存储在栈空间中,将数据存储在堆空间中。
其实这个过程还牵扯到函数上下文的创建,而每一个函数上下文中又会创建一个变量环境、词法环境。有兴趣的同学可以去看李斌老师的浏览器工作原理与实践
所以,我们来看一个简单的栗子,分析一下它在内存中的分配: 栗子:
var a = 1 var b = 2 var student = {name: 'wjc', age: 22}
它内存中的分配:
众所周知,JavaScript 是一门动态类型的语言,即它是在运行时确定变量的类型,不同于静态类型语言的先编译再运行的过程。但是,事实是 V8 引擎在解析运行 JavaScript 之前是会进行一次简单的编译,也就是我们通常所说的初始化过程。
V8
这个初始化过程,会做这几件事:
undefined
那么,我们就来看一个简单的栗子:
console.log(a) sayHi() var a = 2 function sayHi() { console.log('Hi') }
那么按照我们上面所说,这段代码的赋值只有 var a = 2,函数声明只有进行编译阶段的代码会是这样的:
var a = 2
// 编译代码 var a = undefined var sayHi = function () { console.log('Hi') }
此时,它在内存中的分布:
然后,在执行阶段的代码会是这样:
// 执行代码 console.log(a) sayHi() a = 2
所以,也就是当我们真正执行的时候会走执行代码,所以很显然会输出:
undefined Hi
而当走完所有执行代码后,此时内存是这样的:
我想通过这个栗子,大家应该大致搞懂变量提升的过程。但是,仍然存在一个较为特殊的情况,就是当函数形参存在时的变量提升,也就是我们文章开头提及的面试题。
首先,我们需要对函数调用做一个简单的理解,在我们平常调用函数的时候,真正会经历两个步骤:
可以看到这里我们提到了当函数存在形参时,会先进行函数形参的编译和执行过程。
这里我们就来分析文章开头这个栗子:
首先,此时是存在函数形参的,那么函数形参的编译和执行会是这样:
var a = undefined a = 1
然后,才会进行函数体的编译和执行:
// 编译 a = function a() {} // 重点!!! // 执行 console.log(a) a = 2 console.log(a)
可以看到的是,如果函数体内的变量名和形参的变量名重复时,则不会进行普通变量的编译赋值 undefined 的过程。但是,如果存在该变量是函数时,那么则会进行函数变量的编译赋值,即直接指向函数在堆空间中的地址。
所以,我们这个栗子在编译后,可以看作是这样的:
function fn() { var a = undefined a = 1 a = function a() {} console.log(a) a = 2 console.log(2) }
很显然,它会输出会输出function a(){} 和 2
不知大家在深度理解过变量提升过程后,是否有和我一样的感受就是学习编程的本质是追溯本源。现今,虽然我们可以用 ES6 的 let 或 const 来声明变量来避免 var 的种种缺陷。但是,如果因为这样而不去思考 var 为什么会存在这些缺陷。我想这是非常遗憾的。
ES6
let
const
var
引言
对于变量提升这个问题,我想从事前端的同学都或多或少认为我懂这个。曾经,我也是这样认为的,我懂变量提升,并且可以从变量在
Chrome
中的内存分配讲起,以及中间发生了什么。但是,在一次面试中,我遇到了几个一起面前端的同学(当然技术水平参差不齐,并不是很高),在和他们聊这次笔试中的变量提升的问题时,发现大家都支支吾吾的,很多讲的都是值的覆盖。
当时的面试题是这样的:
这个题目,最终会输出
function a(){}
和2
。那么,为什么是这个答案,这个过程发生了什么很重要。所以,今天我们就来彻底刨析一下变量提升的过程。一、变量在内存中的分配
在分析整个过程前,我们先来回顾一下
JavaScript
中变量在内存中的分配。大家都知道的是,对于原始类型会存储在栈空间中,对于引用类型会将引用存储在栈空间中,将数据存储在堆空间中。
所以,我们来看一个简单的栗子,分析一下它在内存中的分配: 栗子:
它内存中的分配:
二、运行前的简单编译
众所周知,
JavaScript
是一门动态类型的语言,即它是在运行时确定变量的类型,不同于静态类型语言的先编译再运行的过程。但是,事实是V8
引擎在解析运行JavaScript
之前是会进行一次简单的编译,也就是我们通常所说的初始化过程。这个初始化过程,会做这几件事:
undefined
;二是对函数的初始化,即直接指向函数在堆空间中的内存那么,我们就来看一个简单的栗子:
那么按照我们上面所说,这段代码的赋值只有
var a = 2
,函数声明只有进行编译阶段的代码会是这样的:此时,它在内存中的分布:
然后,在执行阶段的代码会是这样:
所以,也就是当我们真正执行的时候会走执行代码,所以很显然会输出:
而当走完所有执行代码后,此时内存是这样的:
我想通过这个栗子,大家应该大致搞懂变量提升的过程。但是,仍然存在一个较为特殊的情况,就是当函数形参存在时的变量提升,也就是我们文章开头提及的面试题。
三、函数形参的编译执行
首先,我们需要对函数调用做一个简单的理解,在我们平常调用函数的时候,真正会经历两个步骤:
这里我们就来分析文章开头这个栗子:
首先,此时是存在函数形参的,那么函数形参的编译和执行会是这样:
然后,才会进行函数体的编译和执行:
可以看到的是,如果函数体内的变量名和形参的变量名重复时,则不会进行普通变量的编译赋值
undefined
的过程。但是,如果存在该变量是函数时,那么则会进行函数变量的编译赋值,即直接指向函数在堆空间中的地址。所以,我们这个栗子在编译后,可以看作是这样的:
很显然,它会输出会输出
function a(){}
和2
结语
不知大家在深度理解过变量提升过程后,是否有和我一样的感受就是学习编程的本质是追溯本源。现今,虽然我们可以用
ES6
的let
或const
来声明变量来避免var
的种种缺陷。但是,如果因为这样而不去思考var
为什么会存在这些缺陷。我想这是非常遗憾的。