最近学了很多东西,发现 js 的很多内容只是凭借着我的 主观意识,或者说 感觉。所以这需要系统地学习 以及整理一下
1、变量提升
首先要明确的一点是,在 es6 出现以前,js 只有四种作用域 ,
- 全局作用域
- 函数作用域
- eval 作用域
- with 作用域 (在 vue 模板语法的编译之中,就广泛地使用到了 这个),也正是通过这个,在 模板语法中才能 不适用 this 直接读取到 对应的数据
后面三种 不谈,就谈谈 前面两个
console.log(name) // function name
console.log(age) // undefined
function name() {}
var name = 'dong'
var age = 24
可以看到,在这种情况下,就出现了 变量提升
- 在很多文章中,都提到了 变量提升是为了解决 函数 调用的问题,但是 var 又是怎么回事呢?
- 首先 js 在执行之前,会先进行一遍 编译
- 编译的过程中,并不会执行代码
- 像上面的代码所示,会 解析到 name ,那么会 将 name 保存的环境变量中,但是 当前是一个函数,有 较高的优先级,会覆盖掉 var 的undefined 定义,函数体 也被放到了 环境变量中
- 第二个 var name 就如 第四点 提到的,优先级 比函数低,所以内部的存储 依旧是 function
- 到了 age ,则是将 age 放到环境变量中,并且默认赋值 为 undefined
- 然后开始执行,输入如代码中所示
- 但是有一点是需要注意的,那就是 function 定义不能穿透 if 等语句,如下代码所示,就是 类似于 var ,变量提升了,但是却不能对其进行赋值
console.log(f) // undefined
if (true) {
function f () {}
}
2、let 和 const
正是因为 var 的变量提升以及 缺少 作用域 ,所以出现了 let 和const
- let 和 const 在 { } 中是存在作用域的,哪怕 那只是一个 空的 { }
- 在 上一点 提到的 编译过程之中,其实 let 和 const 在 执行之前也被 编译到了,那么 这个是如何做到 暂时性死区 和 作用域的呢?
- 在 编译到 let 和 const 之后,对应的值 就会被 放入 词法环境(相当于单独的环境变量)中,而不是 通常所说的 环境变量中,然后 引擎 在 执行的过程之中,会 特别 注意这一点,禁止 代码在 定义之前 进行访问
- 所以下面代码的执行结果 就很清晰了
- 当 为 var 的时候,由于 没有作用域的影响,var 变量提升了,变成了 全局作用域,5 个 i 指向了 同一个
- 当 为 let 的时候,由于 作用域的 影响,每一个 {} 都是一个单独的作用域,所以 出现了 5 个 不同 的 i
- 但是 在 for 循环中,实际上 出现了 两种作用域,一种似 for 循环本身的作用域,还有一种 是 { } 包裹的作用域
- 很多文章 都说 let 和 const 没有出现变量提升,但是在我看来,其实已经做了提升,但是 禁止你使用,这个就称作 暂时性死区,究其原因,就是 我 第四大点 提到的 ,js 先进行了一遍解析,之后再执行
for (let i = 0; i < 3; i ++) {
let i = 'hhh'
console.log(i)
}
// hhh
// hhh
// hhh
3、作用域链
讲作用域链,就肯定会谈到 闭包,以及 闭包的原理了
- 闭包的定义,只和 词法作用域 有关,而和在哪里执行无关
- 也就是说,一个函数的闭包,只和 你在哪里定义了 这个函数有关系
- 然后 引用 第二大点 的 let 和 const, 一个 作用域会先 从 词法环境 里面找,然后 再到 环境变量中找,一层一层得找下来,直到 匹配 或者 到全局环境 中发现未定义为止
- 这里又要讲到函数 调用栈的 概念了,也就是 先进后出 的 关系
- 在上文中提到的编译阶段,遇到函数的时候,会快速地 对这个函数 做一次 词法扫描,然后 将 函数中 用到的值保存到 堆空间中
- 所以 下面 输出 的是 outer 也是一目了然的事情了
3、this
由于 闭包的缺陷(不能 手动 指定 作用域 到底是什么 ),js 又引入了 this 的概念
- 严格模式下,函数调用的时候,内部指向的是 undefined ,而在非严格模式下,指向的是 window
- 嵌套函数中的 this 不会继承 外层函数的 this,这里就引出了 () => {}, 就像 下图 Vue 中的 mounted 一样,内部定义一个函数,结果 this 指向的是 undefined。。。这里其实就和 第一点说的一样,内部调用的时候,是 undefined
- 各种操作 this 的方法中,this 的指向 是有 权重的,总的来说,是 obj.fn < fn.bind(obj) / apply / call < new fn()
- 要注意的是,事件绑定(指向 当前 dom),以及 setTimeout 中的this 指向(指向 window)
4、那么 js 是怎么被解析的呢?
这里面就涉及到了 v8引擎的底层实现,我肯定是不知道的,只讲讲表面
- 正如上文所讲的,V8引擎 在执行之前 会编译一遍代码,
- 编译代码的时候,会将 你的代码 分成 一个又一个的 token,也就是 最小的不可分割的部分,例如 var 、 = 、 name 等等
- 再将 token 转化为 AST 抽象语法树, 闭包、变量提升 就是在这个阶段完成的(大名鼎鼎的 babel、ESlint 就是 基于 AST 语法树 来进行 编译的)
- 如果 成功便成 AST ,那么自然相安无事,如果 变不成,那么就是 你写的代码有问题,出现了语法错误,这个时候就会抛出一个错误
- 但是 AST 依旧处于高层,所以 V8 会将 语法树 编译成为 字节码
- 事实上 字节码 依旧不能直接在 计算机中运行,而是需要 解释器 便成 机器码 才行
- 那么 为什么 不直接转为 机器码呢?
- 因为 机器码 占用的内存实在是太多,在 移动端的场景之中,很快就会 把内存给吃掉
- 然后 在执行的 过程之中,V8 引擎 发现有一段代码执行了多次,那么 就会把这段代码 单独提出来,转化成 字节码,保存进内存中
- 这样 你会发现,一段 js 代码 会越来越快,浏览器占用的 内存也会越来越高
5、把抽象的理论转到可视化
前端就是可视化的直接呈现着,那么有没有办法把上面的那些东西可视化呢?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function n() {
var na = 'inner';
return function () {
debugger
console.log(na)
}
}
var fn = n()
fn()
</script>
</body>
</html>