基础知识
引擎和它的好朋友
- 引擎:从头到尾负责整个JavaScript程序的编译和执行过程。
- 编译器:负责语法分析及代码生成的内容。
- 作用域:负责收集和维护所有声明的标识符(变量)组成的一系列 查询 ,并实施一套严格的规则,控制当前执行代码对标识符(变量)的访问权限。
编译器——编译原理
一般分为三个步骤:
- 词法分析:将字符组成的字符串分解为代码块(词组单元)
- 语法分析:将词组单元流转化为AST树(抽象语法🌲)
- 代码生成:将AST树转化为机器可执行的代码。
举第一个🌰:
var a = 1 ;变量的赋值操作,实际上分为两步:
- 编译器会在当前的作用域中声明一个变量(如果没声明过),也就是
var a;- 在运行时,引擎会在作用域中查找该变量,如果能找到就进行赋值,也就是
a = 2,找不到就会报错。
引擎——查询
分为LHS查询和RHS查询:
-
LHS:找到某个变量的容器本身,并对它赋值
- 例如在第一个🌰中,
a = 2就是一个LHS查询
- 例如在第一个🌰中,
-
RHS:查找某个变量,获取它的值
- 例如
console.log(a)就是一个RSH查询
- 例如
作用域嵌套
实际情况中,通常需要同时顾及几个作用域。在当前作用域找不到时,引擎就会在外层嵌套的作用域中继续查找,直到在全局作用域也找不到时,查找停止。
可能发生的两种异常类型:
- ReferenceError:作用域判别失败相关
- TypeError:作用域判别成功,但对结果的操作是不合法的。
词法作用域
- 词法作用域(
Lexical Scopes)是javascript中使用的作用域类型,词法作用域 也可以被叫做 静态作用域,与之相对的还有 动态作用域。 - 无论函数在哪里,在何时被调用,它的词法作用域都只在函数被声明时所处的位置决定。
- 词法作用域查找只会查找一级标识符。
欺骗词法(不推荐使用)
-
eval(..)函数
- 通常被用来执行动态创建的代码
- 接受一个字符串,插入的字符串会被当做原本就在那里一样来处理。
- 可能对原本的词法作用域进行了修改。
-
with 关键字
- 通常被当做重复引用同一个对象的多个属性快捷方式,可以不用重复引用对象本身
var obj = { a:1, b:2, c:3 } with(obj){ a = 3; b = 4; c = 5; }- with声明实际上是根据传递的对象,凭空创建了一个全新的词法作用域,当对象中的标识符在全局作用域也找不到时,会自动创建一个全局变量。
函数作用域
属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
-
在外部使用一个函数包裹,可以隐藏内部实现。
-
规避冲突
- 使用全局命名空间
- 使用模块管理
-
(function fn(){...})作为函数表达式,说明foo只能在...所代表的位置中被访问,外部作用域则不行,不会污染外部作用域。 -
匿名函数表达式和具名函数表达式
-
匿名的缺点
- 在栈追踪中不会显示有意义的函数名,调试困难
- 不方便自身引用
- 代码可读性差
-
-
立即执行函数表达式(IIFE)
-
(function(){})() -
用法
- 通过函数作用域解决命名冲突
- 把他们当做函数调用并传递参数进去
- 倒置代码的运行数据
-
块作用域
简单来说,花括号内 {...} 的区域就是块级作用域区域。
ES6 标准提出了使用 let 和 const 代替 var 关键字,来“创建块级作用域”。
变量提升
- 只有声明本身会提升,而赋值或者其他运行逻辑会留在原地。
- 每个作用域都会进行提升操作。
- 函数声明首先被提升,然后才是变量。
- 后面出现的函数声明,可以覆盖前面的。
闭包
什么是闭包
指有权访问另一个函数作用域中变量的函数
形成闭包的原因
内部的函数存在外部作用域的引用就会导致闭包。
闭包的作用
- 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
- 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化。
闭包的场景
1、return一个函数
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
return f
}
var x = fn()
x() // 21
fn()就是一个闭包,存在上级作用域的引用,即 n = 20,输出结果为21
2、函数作为参数
var a = 'merlin'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
fo()就是闭包,存在上级作用域的引用,即a = 'foo',console.log 结果为foo
3、自执行函数
var n = 'merlin';
(function p(){
console.log(n)
})()
/* 输出
* merlin
/
同样存在上级作用域的引用,即n = 'merlin',console.log 结果为merlin
4、循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包,所以能依次输出0-9,闭包形成了10个互不干扰的私有作用域。每个作用域里的i都不同
如果去掉自执行函数,就不存在外部作用域的引用,那么每一个setTimeout调用的都是全局的i。
由于js是单线程的,遇到异步代码会先入栈,不先执行,等到同步代码执行完后才执行,此时的全局i为10,所以输入的为10个10
5、回调函数
window.name = 'merlin'
setTimeout(function timeHandler(){
console.log(window.name);
}, 100)
闭包需要注意什么
容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。
过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。