JS代码真的是按严格按照顺序执行的吗?
讲解完宏观浏览器的大致流程后,接下来我们开始微观详细介绍更细节的js模块
这里涉及到了很多抽象的底层逻辑理解方面的问题,比如执行上下文,可执行代码,执行栈等,理解了这些东西,你才能对,变量提升,闭包,作用域和作用域链有一个清晰的理解,同时,对你使用开发者调试工具调试代码等也有很大帮助
先看下面这段代码
skill()
console.log(myname)
var myname = '凯隐'
function skill() {
console.log('裂舍影')
}
大家都知道js是按顺序执行的,但是如果按照这个逻辑来立即的话,那么
- 执行到第一行的时候,由于函数skill并未定义,所以执行应该会报错
- 同样执行到第二行,由于变量未定义,所以同样也会报错
但是执行结果并未如此(我用的node.js来执行)
第一行输出裂舍影,第二行输出undefined,这和我们想象的输出顺序不一样啊.
所以通过这个结果,我们知道了函数或者变量在定义之前就可以使用,那如果使用没有定义的变量或者函数,js代码还能继续执行吗? 验证一下:
skill()
console.log(myname)
// var myname = '凯隐'
function skill() {
console.log('裂舍影')
}
从上面的结果来看,我们可以得出几个结论
- 执行过程中,若使用了未被声明的变量,那么js会报错
- 在一个变量定义之前使用它,不会出错,但是该变量会被初始化为undefined,而不是定义的值
- 在一个函数声明之前调用它,函数是会正常执行的
那么问题来了:
- 为什么函数和变量能在声明之前就使用呢?这和js代码顺序执行不一样啊?
- 同样,为什么函数就能正常执行,而变量就是undefined呢?
变量提升&函数提升
要解释这两个问题,首先得搞清楚什么是变量提升和函数提升
首先弄明白什么是声明和赋值
var myname = '凯隐'
上面这行代码你可以看成是两行
var myname //声明
myname = '凯隐' //赋值
接下来看函数的声明和赋值
function foo(){console.log('foo');}
var bar = function(){console.log('bar')}
第一个是完整的函数声明,第二个是函数的变量式声明第二个也可以看成
var bar
bar = function(){console.log('bar')}
知道什么是声明和赋值之后
接下来就是变量提升了
所谓的变量提升,是指js代码运行过程中,js引擎将变量的声明部分和函数的完整声明部分提升到代码的开头的行为.变量被提升后,会给变量设置默认值,而这个默认值就是undefined
模拟实现就是这样:
//函数提升到开头
function skill() {
console.log('裂舍影')
}
//变量提升到开头并且给出初始值undefined
var myname = undefined
/接下来开始顺序执行
skill()
console.log(myname)
myname = '凯隐'
通过这段模拟的变量提升代码,大家已经明白了可以在定义之前使用变量或者函数的原因:函数和变量在执行之前都已经被提升到了代码开头
(这里说一下特殊情况不声明直接赋值 如代码直接写:myname=‘凯隐’,但是没有声明关键字var,这时myname会被默认提升到全局变量,这里我这篇博客最后会讲讲)
js代码执行流程
前面我们从概念上来看.’‘变量提升’'意味着变量和函数声明会在物理层面提升到代码的最前面,和我们模拟实现的代码一样.但是这并不准确,因为实际上变量和函数声明在代码里的位置是不会改变的,而是在编译阶段被js引擎放入内存中
js代码执行阶段大致分为3个流程
一段js代码=>编译阶段=>执行阶段
1.编译阶段
为了搞清楚编译阶段和变量提升到底有什么关系
我把刚刚的模拟代码分为两部分
第一部分:变量提升部分的代码
function skill() {
console.log('裂舍影')
}
var myname = undefined
第二部分:顺序执行部分的代码
skill()
console.log(myname)
myname = '凯隐'
接下来我细化流程:
首先输入一段js代码=>js引擎接收到代码生成执行上下文和可执行代码
这个过程输入是js代码,输出是执行上下文和可执行代码
什么是执行上下文
?执行上下文是指js执行一段代码时的运行环境(学过编译原理的同学应该很清楚),你可以理解为代码执行时候的一个工厂,而代码就在这个工厂里面执行,这个工厂叫做执行上下文,比如调用一个函数,就进入了这个函数的执行工厂(执行上下文)里面,并且确定该函数执行期间的诸如this,变量,对象以及函数等等
.
执行上下文具体是什么,我下一篇博客具体说说,现在我们大致了解.
现在我们只要知道,执行上下文中有个叫做变量环境的对象(可以理解为工厂里一个特定的区域),该对象中保存了变量myname和函数skill
可以简单的把变量对象看成是如下结构
VariableEnviroment:
myname->undefined
skill->function(){console.log('裂舍影')}
了解完变量环境对象后,接下来,我们再结合下面这段代码来分析是如何产生变量环境的
skill()
console.log(myname)
var myname = '凯隐'
function skill() {
console.log('裂舍影')
}
- 首先js引擎大致扫描整体代码,第一行和第二行不是声明操作,所以js引擎不做任何处理
- 第三行的时候,由于有var关键字声明,因此js引擎在变量环境中创建一个名为myname的属性,并且使用undefined对其初始化
- 第四行,js引擎发现了一个function函数的完整定义,同样在变量环境中创建一个skill属性将其指向堆中的函数的位置(堆,数据结构中的一个东西,简单来说,js中基本类型直接保存在栈,引用类型保存在堆),这样一个完整的变量环境构建完毕,接下来js引擎就会将其余的代码编译为字节码,至于字节码是什么,以后讲js引擎我具体说,现在字节码你大致理解为以下代码(可执行代码)
skill()
console.log(myname)
myname = '凯隐'
有了执行上下文和可执行代码接下来就是执行阶段
执行阶段
js引擎开始执行可执行代码,按照顺序来执行(我们说的按顺序执行其实是指这部分)
流程:
- 当执行到skill函数时,js引擎便在变量环境中查找这个函数,变量环境中保存了该函数的引用地址,所以js引擎执行该函数,输出’裂舍影’结果
- 接下来打印’myname’信息,js引擎继续在变量环境中查找’myname’属性,由于变量环境存在这个myname属性,并且值为undefinded,所打印undefined
- 接下来执行赋值操作,讲’凯隐’赋值给变量环境中的myname属性,覆盖undefined
此时:
VariableEnviroment:
myname->'凯隐'
skill->function(){console.log('裂舍影')}
好了 ,这就是一段js代码的大致执行流程了,实际上这里面非常的复杂,编译阶段学过编译原理的同学都知道存在词法分析,语法解析,代码优化,中间代码生成之类的东西,所以编译原理也要学好哦
几个特殊的问题
1.代码中出现相同的变量和函数怎么办?
function skill() {
console.log('裂舍影')
}
skill()
function skill() {
console.log('猫咪之镰')
}
skill()
上面这段代码打印的是什么?
没错都是猫咪之镰刀
因为js引擎整体扫描生成变量环境阶段的时候,先找到了第一个skill,然后继续找,找到了第二个skill,这时候就会覆盖掉原来的那个skill
,逻辑上也应该如此
另外如果变量提升阶段变量a和函数a同名,使用的时候优先使用函数,这个原则也叫做函数优先原则
var a
function a() {}
console.log(a);//function a{}
a = 2
console.log(a) //2 function a 被2覆盖
(事实上这跟js引擎有关,浏览器v8引擎会这样,而我使用node.js本地运行这段代码会报sikll is already declared的错误
)
2.没有声明变量而是直接赋值
var a = 0
if (1) {
a = 1
function a() {}
a = 21
console.log('内部', a, window.a)
}
console.log('外部', a, window.a)
你想的结果是什么?
哈哈哈 其实答案是: 内部 21 1 外部 1 1
这和我们之前讲的有些不一样,我们来分析一下:
- 首先a被变量提升a:undefined,function a(){} ,js之前没有块级作用域,所以if块里面的函数也被提升,作用域我下篇博客讲,然后a赋值为0,全局对象下var a 其实是将a挂载到window上,window.a = 0
- 然后进入 if块,此时由于if块中a没有声明,js默认会将其作为全局对象挂载到window上,此时window.a由0变为1,另外的,在代码块中也会定义一个a = 1.这里出现了块级作用域,if块中的a = 1,块级作用域下篇博客讲
- 然后继续走,a = 21,此时改变的是if块中的a,所以内部打印的是21 ,window.a是1
- 然后来到外部,外部理所当然的是1 1
有点绕,事实上由于js的这种特性,经常会出现一些稀奇古怪的bug,js本身是有很大缺陷的,所以今年才会特别流行type script,ts优化了js,本身是js的一种进阶版本.
通过上述问题,我们理解了,变量提升有时会造成一些意想不到的错误,而且我们在定义变量时应该先声明,否则也会出现一些很难想到的错误.
所以es6之后就出现了let关键字和const关键字
这个下篇讲,好了 就写到这