1. JavaScript 解析和执行过程
1.1 介绍
JavaScript是一种描述型的脚本语言,是一种解析语言,由浏览器动态解析,不同种类的浏览器不同版本的浏览器对于 js 解析有微小的差别,不同浏览器的 js 解析引擎效率也有高低。
js 的执行过程分为两大部分:
第一部分:解析过程
,也称预编译期
。主要工作就是对于 js 的代码中声明的所有变量和函数进行预处理。需要注意的是,再此进行处理的仅是函数声明,并开辟出一块内存空间,不进行赋值操作。
第二部分:执行过程
。在执行过程中,浏览器的 js 引擎对于每个代码块进行顺序执行,如果有外部引用的 js 且 js 互有关联,此时需要注意,不同的 js 引入顺序,如果声明到吗块在调用代码块后调用,则将不会达到预期的效果。
总的来说, js 的执行分为两个部分,解析过程和执行过程。解析是按照代码块,一段一段进行解析,执行时按照代码块顺序逐行执行,解析一个代码块,执行一个代码块。
因为是解释性语言,所以 js 如果在解析过程中有错误,则不会提示,也可以理解为 js 不会出现编译错误,但如果出现了运行时错误,出现错误以下的所有 js 代码将不会继续执行。
1.2 全局预处理阶段
预处理
:创建一个此法环境 (LexicalEnvironment,简写LE),扫描 js 中用声明的方式声明的函数,用 var 定义的变量并将它们加到预处理阶段的词法环境中去。
预处理阶段先读取代码块,不是一行一行的解析执行定义的方法和用 var 定义的变量,会放到一个(不同的环境,会有得应的词法环境)词法环境中。
1 | var a = 1; // 用var定义的变量,已赋值 |
词法环境
1 | LE{ // 此时LE相当于window |
预处理的函数必须是 js 中声明的方式声明的函数(不是函数表达式)。
示例:
1 | d() |
词法环境
1 | LE { |
1.3 命名冲突
变量和函数同名冲突–函数优先,函数是一等公民。在既有函数声明又有变量声明的时候,函数声明的权重高一些。
1 | console.log(typeof f) // function |
1 | console.log(typeof f) // function |
变量和变量同名冲突,函数和函数同名冲突,后者会覆盖前者。
1 | f() // ff |
1 | var f = 1; |
1.4 执行阶段
1 | console.log(a); |
1.5 函数冲突原则
- 处理函数声明有冲突时,会覆盖。
- 处理变量声明时有冲突,会忽略。以传出参数的值为准。
预处理阶段传入参数值一一对应。
1 | function f(a, b) { |
1 | LE { |
没有被var
声明的变量,会变成最外部的成员,即全局变量
1 | function a() { |
2. 作用域
2.1 作用域
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。
1 | function outFun() { |
从上面的例子中可以体会到作用域的概念,变量inVariable在全局作用域中没有声明,所以在全局作用域下取值就会报错。我们可以这么理解:作用域是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名的遍历不会有冲突。
ES6之前JavaScript没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了块级作用域,可通过新增关键字let
和const
来体现。
2.2全局作用域和局部作用域
在代码中任何地方都能访问的对象拥有全局作用域,一般来说一下几种情形拥有全局作用域:
- 最外层函数和在最外层函数外面定义的变量拥有全局作用域
1 | var outVariable = "外部变量" // 全局变量 |
- 所有未定义直接复制的变量自动声明为拥有全局作用域(
要避免
)
1 | var outVariable = "外部变量" // 全局变量 |
- 所有window对象的属性拥有全局作用域
一般情况下,window对象的内置属性都拥有全局作用域,例如 window.name、window.location 、window.top 等等
全局作用域有个弊端,如果我们写了很多 js 代码,变量定义都没有使用函数包括,那么它们全都在全局作用域中。这样就会污染全局命名空间,容易引起命名冲突。
1 | // 张三写的代码 |
这就是为何jQuery、zepto等库的源码,所有的代码都会放在(funciton(){})()中。因为放在里面的所有变量,都不会被外泄和暴露
函数作用域是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内科访问到。
1 | function doSomething() { |
作用域是分层的, 内层作用域可以访问到外层作用域的变量,反之则不行。例子:
1 | function foo(a) { |
2.3 块级作用域
块级作用域可通过新增关键字let
和const
关键字来声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况下被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部。
let声明的语法与 var 的语法一致。你基本上可以使用 let 来代替 var 进行变量声明,但将会将变量的作用域限制在当前代码块中。块级作用域有以下几个特点:
- 声明不会被提成到当前代码块的顶部。因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块可用。
1 | console.log(c) //ReferenceError: Cannot access 'c' before initialization |
- 禁止重复声明
如果一个标识符已经在代码内部被定义,那么在此代码块内使用同一个标识符进行let声明就会导致抛出错误。例如:
1 | var count = 30; |
如果在嵌套的作用域内使用let声明一个同名的新变量,则不会抛出错误。
1 | var conut = 30; |
- 循环中绑定块级作用域的妙用
开发者可能最希望实现for循环的块级作用域了,因为可以把成声明的计数器变量限制在循环内,例如:
1 | var btns = document.querySelectorAll('button'); |
如果使用let声明i。
1 | var btns = document.querySelectorAll('button'); |
2.4 JavaScript中的作用域链
2.4.1 自由变量
当前作用域中没有定义的变量,就称为自由变量。自由变量的值如何得到 – 向父级作用域寻找(注意:这种说法并不严谨,以下会解释)。在如下代码中,console.log(a)
中变量a,在当前作用域并没有定义,就是自由变量。
1 | let a = 100 |
2.4.2 什么是作用域链
如果父级也没有,再一层一层向上寻找,直到找到全局作用域还没有找到,就报错is not defined
, 这样一层一层的关系,就是作用域链。
1 | let a = 100 |
关于自由变量的取值
1 | function fn() { |
在函数 fn 中,取自由变量 a 的值时,要到哪个作用域中取? — 要到创建 fn 函数的那个作用域中取,无论 fn 函数在哪里调用。所以更贴切的说法为:要到创建这个函数的那个作用域中取值。这里强调“创建”,而不是调用,切记 – 其实这就是所谓的“静态作用域”。
1 | let a = 10 |
1 | let a = 10 |
fn() 返回的事 bar 函数,赋值给 x。执行x(),即执行 bar 函数代码,取 b 的值时,直接在 fn 的作用域去除,取 a 的值时,试图在 fn 作用域取,但是取不到,只能转向创建 fn 的那个作用域中去查找,结果找到了,所以最后的结果是 30。
2.5 变量提升和函数提升
2.5.1 变量提升
通常 js 引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端。然后进行接下来的处理。(注:当前流行的 js 的引擎大都对源码进行了编译,由于引擎的不同,编译形式袁辉有所差异,这里所说的预编译和提升其实是抽象出来的,易于理解的概念)。
我们在一个函数中声明了一个变量,不过这个变量声明是在 if 语句块中:
1 | function variable() { |
js 引擎将变量提升到了函数的顶部,初始值为 undefined。函数内部有 var 修饰的变量,就不会使用父级作用域的变量。
1 | var foo = 1; |
如果 let 定义变量,会把变量的作用域限定在 if 代码块中,在外部使用会抛出错误ReferenceError: foo is not defined
。
1 | function fn() { |
2.5.2 函数提升
1 | function fn() { |
为什么函数可以在声明之前就可以调用,并且跟变量声明不同的是,它还能得到正确的结果,其实引擎是把函数声明整个地提成到了当前作用域的顶部,预编译之后的代码逻辑如下:
1 | function fn() { |
如果一个作用域中存在多个同名函数声明,后面出现, 的将会覆盖前面的函数声明:
1 | function fn() { |
对于函数,除了使用函数声明,我们还会使用函数表达式,下面是函数声明和函数表达式的对比:
1 | // 函数声明 |
可以看到,匿名函数表达式,其实是将一个不带名字的函数声明赋值给了一个变量,而具名函数表达式,则是带名字的函数赋值给一个变量,需要注意的是,这个函数只能在此函数内部使用。其实函数表达式可以通过变量访问,所以也存在变量提升同样的效果。
1 | // 函数声明 |
函数为什么会提升?
解决函数相互递归(A 函数内部会调用 B 函数,B 函数也会调用到 A 函数)问题。
1 | function a() { |
最佳实践:
变量提升是不应该存在的。无论变量还是函数,都必须先声明再使用。
使用 let/const 代替 var 声明变量。
2.6 变量的本质
2.6.1 本质
变量是什么:用直白的语言表述就是,有一个数据保存起来了,当需要使用这个数据的时候,需要在保存这个数据的位置把它拿出来,一般的解决方式就是用一个名称与这个数据对应起来,下次要用数据直接使用这个名称就行,这个名称就是变量。
变量的本质:当一段数据保存在计算机内存中,在运行程序的某一时刻需要读取这段数据时应该如何找到这个内存地址呢,解决方案就是变量 — 变量保存的就是这个内存地址的编号,读取变量的值即是使用变量保存的地址编号去查看该地址段当前保存的值是什么。
内存的分类:栈空间和堆空间,基本类型变量在第一次赋值被分配到栈空间。
1 | let name = "kobe" // 被分配到栈空间,此时 name 指向栈空间的“kobe” |
对象(引用)类型的变量第一次赋值被分配到堆空间,对象类型变量都是内存地址,并不是真正的值,
1 | const info = {name: "kobe"} |
声明方式
const 关键字用于修饰常量,定义的变量不可修改,而且必须初始化。
1 | function fn() { |
2.6.2 变量的产生和死亡
声明在函数外部的变量
- 产生: js 加载到该变量所在行时产生。
- 死亡:js 代码加载完毕,变量死亡
声明在函数内部的变量
- 前提:该变量所在的函数被调用。
- 产生: js 执行到该变量所在行时产生。
- 死亡:该变量所在的函数执行结束。