本文章仅针对我自己在看书过程中对一些不太清楚的知识点进行查漏补缺——《你不知道的JavaScript(上卷)》第一部分作用域和闭包
编译与执行
传统编译语言的编译过程:词法分析、语法分析、代码生成,而JavaScript语言则要更复杂
JS引擎不会用大量的时间进行优化,因为JS的编译过程不是发生在构建之前的
JS的编译发生在代码执行前的几微秒,JS引擎用了各种方法如JIT延迟编译甚至重编译来保证性能最佳
拿一段代码举例对JS编译与执行过程进行分析
var a = 2;
var b = c; // 报错ReferenceError
词法分析:编译器将这段程序分解成词法单元
语法分析:将词法单元解析成AST树
预编译:开辟内存空间,存放变量和函数
当前作用域没有这个变量才会声明
预编译阶段不赋值
匿名函数不参与预编译
解释执行:
执行代码、赋值等(随着作用域链进行LHS或RHS查询)
异常
ReferenceError(不成功的 RHS 引用,查询失败,即遇到未声明的变量,作用域链中没找到)
TypeError(作用域判别成功,但是对结果的操作不合法)
1~3阶段由编译器参与,3~4阶段作用域参与,4阶段JS引擎参与
LHS与RHS
在代码执行的过程中,JS引擎会拜托作用域进行LHS查询与RHS查询,书上描述得有点抽象,我的理解是LHS发生在变量赋值时变量赋值给谁要查谁,RHS发生在要使用某个变量时查询变量的值
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
上述代码3处LHS、4处RHS查询
LHS发生在c = ..;a = ..(给形参a赋值); b = ..;
RHS查询发生在foo(2); .. = a; a..; ..b.
思考以下代码
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4
console.log(a+b)中的b进行RHS引用,首先查询foo函数的作用域,发现没有则查询上一次作用域这里即全局作用域
而LHS与RHS的引用都会随着作用域链查找
eval与with的坏处
eval和with的作用和语法就不详细介绍了,毕竟这两个东西都不推荐使用,原因如下:
简单来说:如果代码中出现了eval或者with,可能JS引擎在编译阶段做的性能优化都白费了
详细来说:
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。
但如果引擎在代码中发现了 eval(..) 或 with ,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。
函数作用域
立即执行函数和普通函数作用域方面的区别
var a = 2;
function foo() { // <-- 添加这一行
var a = 3;
console.log(a); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log(a); // 2
(function foo() { // <-- 添加这一行
var a = 3;
console.log(a); // 3
})(); // <-- 以及这一行
console.log(a); // 2
立即执行函数外部作用域(这里是全局 作用域)不能访问
立即执行函数的函数名绑定在自己的作用域,而不是外部作用域(这里的外部是指全局)
块级作用域
for while if try-catch {}
let与const在块级作用域中声明,在该块级作用域外无法被访问
而var在块级作用域中定义后却不受块级作用域限制,会作用域提升到最上层(全局或函数作用域)
块级作用域有利于垃圾回收,JS引擎能够更直观的知道哪些变量不会再被使用而可以进行回收了
必看例子1——函数表达式声明不会提前,函数声明会提前
// foo() // TypeError 这里foo的值还是undefined 对undefined()操作是类型错误
// bar() // ReferenceError 全局作用域中没有bar
var foo = function bar() {
console.log(a) // undefined 而不是报错 因为var定义的变量作用域会提升
var a = 2
}
foo()
// bar() // ReferenceError 全局作用域中没有bar
必看例子2——函数优先、多个函数名重复后面的覆盖前面的
// var foo; // var定义在这里代码执行也结果也一样
foo(); // 3 var定义的变量名和函数名重复 函数优先 后面的函数声明覆盖前面的
var foo; // var定义在这里代码执行也结果也一样
function foo() {
console.log(1);
}
function foo() {
console.log(3);
}
foo = function () {
console.log(2);
};
foo() // 2 出现在后面的赋值还是可以覆盖前面的
作用域闭包
闭包概念
书中闭包的概念是这样的:
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包
我的理解如下:
首先,闭包通常在函数嵌套中发生,嵌套函数中有外层函数和内层函数。
闭包是在外层函数被调用时,内层函数被引用,且内层函数引用了外层函数的变量,内层函数不会被释放,同时内存函数的作用域链包含了外层函数的作用域对象,内层函数会保留着对外层函数的引用,这个时候闭包就形成了
闭包应用1——块级作用域
for (var i = 1; i <= 5; i++) {
(function () {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})();
}
timer函数是内层函数,匿名的立即执行函数是外层函数,每次for循环时,立即执行函数调用,间隔一秒延时调用timer,timer函数引用了外层函数的变量j,于是便形成了闭包环境,成功实现了间隔1s打印1~5,而以下代码只能间隔1s输出5个6
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
闭包应用2——模块模式
模块模式的两个条件
必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
动态作用域和词法作用域的区别
动态作用域和this很像,只关心函数是在哪里被调用的
而词法作用域只关心函数声明的位置(假如没有用eval和with)