本文首发于贝壳社区FE专栏,欢迎关注!javascript
- 分词/词法分析(Tokenizing/Lexing) 这个过程会将由字符组成的字符串分解成(对编程语言来讲)有意义的代码块,这些代 码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序一般会被分解成 为下面这些词法单元:var、a、=、2 、;。空格是否会被看成词法单元,取决于空格在 这门语言中是否具备意义。
- 解析/语法分析(Parsing) 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的表明了程序语法 结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。 var a = 2; 的抽象语法树中可能会有一个叫做 VariableDeclaration 的顶级节点,接下 来是一个叫做 Identifier(它的值是 a)的子节点,以及一个叫做 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫做 NumericLiteral(它的值是 2)的子 节点。
- 代码生成 将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息 息相关。 抛开具体细节,简单来讲就是有某种方法能够将 var a = 2; 的 AST 转化为一组机器指 令,用来建立一个叫做 a 的变量(包括分配内存等),并将一个值储存在 a 中。
简而言之:java
整个编译过程有三个角色须要登场:node
那么整个 var a = 2;
的编译过程以下:es6
var a = 2;
这段代码,进行语法分析。var a
,向做用域进行变量定义操做。
a = 2
这段代码编译为及其语言传给引擎。a = 2
向做用域中去查找 a 变量,准备赋值操做。
Refence Error
错误。我对于 LHS 和 RHS 的理解是:全部赋值操做都是 LHS,如 a = 2;
;而全部的取值操做都是 RHS,如 console.log(a);
。面试
当变量出如今赋值操做的左侧时进行 LHS 查询,出如今右侧时进行 RHS 查询。 —— 《你不知道的 JavaScript》编程
在非严格模式下,当变量 a 未被定义,像 console.log(a)
这样的RHS 查找会报 ReferenceError
的错误,而像 b = 2
这样的 LHS 查找会在全局做用域下建立变量并进行赋值。数组
console.log(a); // type: RHS, output: ReferenceError
b = 2; // type: LHS, output: 2
复制代码
而在严格模式下,LHS 和 RHS 的效果是相同的,都会报 ReferenceError
。安全
function foo(a){
console.log(a);
}
foo(2);
复制代码
在以上例子中有 3 次 RHS 和 1 次 LHSbash
foo(2)
查找 foo 函数。foo(2)
隐藏着 a = 2
赋值行为。console.log(a)
查找 console 对象console.log(a)
查找 a 变量function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
复制代码
找出 3 次 LHS 4 次 RHS。闭包
foo(2)
查找 foo 函数。foo(2)
隐藏有 a = 2
赋值行为。var c = foo(2)
是赋值行为。var b = a
查找 a 变量。var b = a
是赋值行为。return a + b
查找 a 变量。return a + b
查找 b 变量。词法做用域就是指咱们代码词法所表示的做用域。看下以下代码:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
复制代码
这段代码的词法做用域如图:
其实就是咱们在代码编写时所定义的做用域即词法做用域。
固然也有不按词法规则来的写法,称为欺骗词法。
相似于 eval()
方法会将字符串解析成 JS 语言的执行。它将破坏词法做用域的规则。如
function foo() {
eval('var a = 3')
console.log(a) // 3
}
var a = 2;
foo();
复制代码
with 这个冷门的关键词一般被看成重复引用同一个对象中的多个属性的快捷方式,能够不须要重复引用对象自己。
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
复制代码
两种赋值方式看似等价。但若是赋值目标是 obj 对象中没有的变量,两种赋值效果是不一样的。
var obj = {
a: 1,
b: 2,
c: 3
};
obj.d = 11;
console.log(obj) // { a: 1, b:2, c:3, d: 11 }
console.log(d) // ReferenceError
复制代码
var obj = {
a: 1,
b: 2,
c: 3
};
with (obj) {
d = 11;
}
console.log(obj) // { a: 1, b:2, c:3 }
console.log(d) // 11
复制代码
能够看到在 with 函数中的对于变量 d 的赋值行为(LHS)是定义在了 window 对象上的。
一般状况下,函数内的变量没法在函数外调用。即变量存在于函数做用域下,因此函数做用域起到了局部变量或者变量隐藏的做用。以下例子
var a = 2;
function foo() {
var a = 3;
console.log(a); // 3
}
foo();
console.log(a); // 2
复制代码
以上写法将 foo 方法中的 a 变量隐藏了起来。不过也产生了一个问题 —— 全局做用域下多了一个 foo 函数变量。解决这种污染的方式是当即执行函数(IIFE),咱们将上面的代码进行改造:
var a = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})();
console.log(a); // 2
console.log(foo) // ReferenceError
复制代码
这种写法就能够将 foo 函数变量也隐藏起来,避免对全局做用域的濡染。
块级做用域存在于 if
, for
, while
, {}
等语法中,这些做用域中使用 var 定义的变量是不在这个做用域内的。
块做用域和函数定义域的区别在于:函数定义域隐藏函数内的变量,而块做用域隐藏块中的变量。举个栗子:
// 函数做用域,隐藏变量a
function test() {
var a = 2
}
console.log(a) // ReferenceError
复制代码
// 块做用域,隐藏变量 i
// 不隐藏变量 a (不是函数做用域)
for (let i = 0; i < 10; i++) {
var a = 2;
}
console.log(i) // ReferenceError
复制代码
with 和 catch 关键字都会建立块级做用域,由于他们建立的做用域在外部做用域中无效。
var obj = {
a: 1
}
with(obj) {
a = 2
}
console.log(obj) // { a: 2 }
console.log(a) // ReferenceError
复制代码
try {
undefined(); // 执行一个非法操做来强制制造一个异常
} catch (err) {
console.log(err); // 可以正常执行!
}
console.log(err); // ReferenceError
复制代码
let 和 const 关键字能够将变量绑定到所在的任意做用域中。换句话说,let 和 const 为其声明的变量隐式地了所在的块做用域。
{
let a = 2;
}
console.log(a) // ReferenceError
复制代码
可见 const 和 let 可以保证变量隐藏在所在做用域中。
因为 ES5 只有全局做用域和函数做用域,没有块级做用域,这带来不少不合理的场景。
而 ES6 所提出的 let 和 const 为 JavaScript 带来了块做用域解决了这个问题。
下面列出4点 var 与 let 的差别之处:
console.log(foo); // undefined
var foo = 2;
console.log(bar); // ReferenceError
let bar = 2;
复制代码
var a = 3
{
console.log(a) // ReferenceError
let a
}
console.log(a) // 3
复制代码
在使用 var 定义变量和使用 function 定义函数时,会出现变量提高的状况。
看几个例子来理解下变量提高:
var a = 2;
console.log( a );
// JavaScript 的处理逻辑
var a;
a = 2;
console.log(a); // 2
复制代码
console.log( a );
var a = 2;
// JavaScript 的处理逻辑
var a;
console.log(a); // undefined
a = 2;
复制代码
foo();
function foo() {
console.log(a); // undefined
var a = 2;
}
// JavaScript 的处理逻辑
function foo() {
var a;
console.log(a); // undefined
a = 2;
}
foo();
复制代码
**为何呢?**回忆一下上文说到的编译过程就能理解了。看图!
能够看到编译器会将变量都定义到做用域中,而后再编译代码给引擎去执行代码命令。即 var a = 2;
是被拆开执行的且 var a
变量会提早被定义。
再来看一个不靠谱的函数定义方法:
foo(); // "b"
var a = true;
if (a) {
function foo() {
console.log("a");
}
} else {
function foo() {
console.log("b");
}
}
复制代码
输出结果与《你不知道的 JavaScript》中的有所不一样,在 node v10.5.0 中输出的是 TypeError
而非 b
。这个差别有待考证。
虽然函数和变量都会提高,可是编译器会先提高函数,再是变量。看以下例子:
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
复制代码
同时是函数定义,可是第二种是定义变量的形式,因此听从函数优先原则,以上代码会变为:
function foo() {
console.log(1);
}
var foo; // 无心义
foo(); // 1
foo = function () {
console.log(2);
};
复制代码
下面是人见人怕的闭包。
当函数能够记住并访问所在的词法做用域时,就产生了闭包。 当函数能够记住并访问所在的词法做用域时,就产生了闭包。 当函数能够记住并访问所在的词法做用域时,就产生了闭包。 重要的定义说三遍!
function foo() {
var a = 2;
function bar() {
return a;
}
return bar;
}
var baz = foo();
console.log(baz()); // 2 <-- 这就是闭包
复制代码
按照咱们对于函数做用域的理解,函数做用域外是没法获取函数做用域内的变量的。
可是经过闭包,函数做用域被持久保存,而且闭包函数能够访问到做用域下的变量。
下面再展现几个闭包便于理解:
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 将 baz 分配给全局变量
}
function bar() {
fn(); // <-- 闭包!
}
foo();
bar(); // 2
复制代码
function foo() {
var a = 2;
function baz() {
console.log(a); // 2
}
bar(baz);
}
function bar(fn) {
fn(); // <-- 闭包!
}
复制代码
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
// timer 持有 wait 函数做用域,因此是闭包。
复制代码
上面几个例子能够概括下闭包的特性:
就这么简单!按照这个定义其实全部的回调函数都属因而闭包。
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
复制代码
看看以上写法最终输出的是什么呢?因为 var i = 0 是在全局做用域下,且没有任何地方存 i 的变化值,因此最终输出是 5 个 6
。
解决方案有两种:
// 闭包写法
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
复制代码
// 块做用域写法
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
复制代码
本文旨在更方便和全面的理解做用域的相关知识,但愿能对你有所帮助 JavaScript 的做用域知识不论是在面试中仍是在实际工做中都是很是重要的。