如果有问题,请大家麻烦指出来。
javascript我们通常把它归类为"动态"或"解释执行"语言,但事实上它是一门编译语言。它与传统编译语言(C语言等)不同,它不是提前编译,而且并不会产生可移植的编译结果。
(传统编译语言)编译器工作流程
通常分为三个步骤:
1.分词/词法分析(Tokennizing/Lexing)
这个过程会将由字符组成的字符串分解成有意义的代码块(把输入的字符串分解为一些对编程语言有意义的代码块),这些代码块被称为"词法单元"(token)。
2.解析/语法分析(Parsing)
这个过程是将上一步的词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为"抽象语法树"(Abstract Syntax Tree,AST)。
3.代码生成
这个过程是将上一步的AST转换为可执行代码(机器指令)的过程被称为代码生成。
JavaScript编译器工作流程
1.分词/词法分析(Tokennizing/Lexing)解释如上文一致,例子:
var a = 2;
小提示:空格是否会被当作词法单元,取决于空格在这门语言是否有意义。
2.解析/语法分析(Parsing)解释如上文一致,例子:
var a = 2;
3.代码生成 解释如上文一致
简单的描述一下就是有某种方法可以将var a = 2; 的AST转化为一组机器指令,用来创建一个叫作a的变量(包括分配内存地址等),并将一个值存储在a中。
JavaScript编译器工作流程与(传统编译语言)编译器工作流程有什么区别?
javascript编程过程与(传统编译语言)编译过程都是分三个步骤。但是javascript引擎会在语法分析和代码生成阶段进行优化(例如针对冗余元素进行优化),还会对编译过程进行优化(如JIT,延迟编译或者重编译),目的是缩短编译过程,保证性能最佳。
JIT(Just In Time)是什么?
每当一个程序在运行时创建并运行一些新的可执行代码,而这些代码在存储于磁盘上时不属于该程序的一部分,它就是一个JIT。
JavaScript中的引擎、编译器、作用域是如何配合工作?
引擎:从头到尾负责整个javascript程序的编译及执行过程。浏览器不同,其引擎也不同,比如Chrome采用的是v8,Safari采用的是SquirrelFish Extreme。
编译器:负责词法分析、语法分析及代码生成。
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。通俗来讲就是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。在说明白一点:也就是变量的管家用一个事先定义好的规则(词法作用域),管理变量的查询与访问。
先讲讲作用域
作用域有两种模型:
- 词法作用域
- 动态作用域(javascript中没有)
词法作用域(静态作用域)和动态作用域
词法作用域(书写时定义):
我们将"作用域"定义了一套规则,javascript引擎根据这套规则来管理变量的查找与引用,词法作用域就是其使用的规则,在编译器进行词法化时,会根据你写代码时将,变量和块作用域写在哪里,来决定规则的内容。这其中又包含了块作用域这个概念,只要记住ES6之前没有块作用域,只有函数有作用域,即:函数内部是一个独立的块作用域。(有个特例:catch语句块内也是独立的作用域。) 通俗来讲:词法作用域是定义在词法阶段的作用域,换句话来说就是在执行之前就确定了它可以应用哪些地方的作用域(变量)。
javascript引擎可以通过eval和with来改变词法作用域,但这两种会导致引擎无法在编译时对作用域查找进行优化, 因此不要使用它们。
例子:
-
function foo(a) {
-
var b = a +
2;
-
function bar(c) {
-
console.log(a, b, c);
-
}
-
bar(b *
3);
-
}
-
foo(
2);
// 2, 4, 12
看图:
讲解:
1. a 会先在bar()作用域查找有没有a,有就返回,没有就继续往上查找
2. a往foo()作用域查找有没有a,有就返回,没有就继续往上查找直到全局作用域,还没有找到就抛出异常。
.....
动态作用域(运行时定义):
javascript例子(javascript是不支持的):
-
-
var value =
1;
-
-
function foo() {
-
console.log(value);
// 打印 1
-
}
-
-
function bar() {
-
var value =
2;
-
foo();
-
}
-
-
bar();
讲解:
如果javascript支持的话在foo()作用域查找value,查找不到会往bar()作用域查找 然后 打印 2。
但是javascript不支持它会在foo()作用域查找value,查找不到会往在当前函数上边查找(在例子中就是全局作用域) 然后 打印 1。
作用域范围
- 全局作用域
- 函数级作用域
- 块级作用域(es6)
先说一下var、let(es6)、const(es6)!
编译器将var 声明的变量提升至作用域顶部,将let或const声明的变量放到暂时性死区(temporal dead zone, TDZ)。访问时死区(TDZ)中的变量会触发运行时的错误,只有执行过变量声明语句后,变量才会从时暂时性死区(TDZ)中移出,这时才可访问。
全局作用域:
-
var a =
1;
// 全局变量
-
-
function foo() {
// 全局函数
-
-
b =
2;
// 未定义却赋值(LHS)会创建一个全局变量b,然后赋值
-
-
var name =
'an';
// 局部变量
-
-
function bar() {
// 局部函数
-
console.log(name);
-
}
-
}
-
-
console.log(name)
// ReferenceError: name is not defined
函数级作用域:
-
function foo() {
-
var name =
"an";
-
function sayName() {
-
console.log(
`hello, ${name}`);
-
}
-
sayName();
-
}
-
foo()
// hello, an
-
console.log(name)
// ReferenceError: name is not defined
-
sayName()
// ReferenceError: sayName is not defined
值得注意的是, if、switch、while、for 这些条件语句或者循环语句不会创建新的作用域, 虽然它也有一对{}包裹. 能不能访问的到内部变量取决于声明方式(var 还是 let/const),因为var 会提升到当前作用域顶部!
块级作用域
JavaScript 没有块级作用域的情况,在es6 let 和 const 的出现解决了这样的情况,所声明的变量在指定块的作用域外无法被访问。
-
function test() {
-
let name =
"an"
-
console.log(name)
// an
-
function bar() {
-
console.log(name)
// an
-
}
-
bar()
-
}
-
let name =
"name" ||
var name =
"name"
-
test()
-
console.log(name)
// name
-
{
-
let name =
""
-
}
-
console.log(name)
// ReferenceError: name is not defined
作用域链
通俗来讲:当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。
例子:
-
function foo(a) {
-
var b = a +
2;
-
function bar(c) {
-
console.log(a, b, c);
-
}
-
bar(b *
3);
-
}
-
foo(
2);
// 2, 4, 12
讲解:
1. a 会先在bar()作用域查找有没有a,有就返回,没有就继续往上查找
2. a往foo()作用域查找有没有a,有就返回,没有就继续往上查找直到全局作用域,还没有找到就抛出异常。
....
先讲讲引擎的提升
大部分编程语言都是先声明变量再使用,但是在javascript中,并不是这样。
因为javascript引擎拿到代码的时候,编译器已经做了一些转换,编译器为什么要作这个事情?
因为它要在第一步就找到所有的声明,并且用合适的作用域将他们关联起来,这也正是词法作用域的核心。表现为:包括变量和函数在内的所有声明都会在当前块作用域内被首先处理,即类似于提升到最前面声明,但是复制处理操作因为是在执行阶段,因此编译阶段他们原地待命等待执行。换句话说,就是编译阶段的一部分工作就是找到所有的声明,并且使用合适的作用域将它们串联起来,变量和函数在内的所有声明都会在代码执行前被处理。
变量提升
看下面代码:
-
console.log(a);
// undefined
-
var a =
1;
javascript引擎会拆分成
-
var a;
-
console.log(a);
-
a =
2;
javascript引擎会将var a = 2; 拆分成 var a; a = 2;当作两个单独的声明,第一个是在编译阶段的任务,第二个是在执行阶段的任务。
换句话说, 这个过程将会把变量和函数声明放到其作用域的顶部,这个过程就叫做提升。
可能你会有疑问,为什么 let 和 const 不存在变量提升呢?
这是因为在编译阶段, 当遇到变量声明时,编译器要么将它提升至作用域顶部(var 声明),要么将它放到暂时性死区(temporal dead zone, TDZ),也就是用 let 或 const 声明的变量。访问暂时性死区(TDZ)中的变量会触发运行时的错误,只有执行过变量声明语句后,变量才会从时死区(TDZ)中移出,这时才可访问。
例子:
-
console.log(a)
// ReferenceError: Cannot access 'a' before initialization
-
let a =
"a";
-
-
// 分割线
-
-
console.log(a)
// ReferenceError: Cannot access 'a' before initialization
-
const a =
"a";
函数提升
函数声明和变量声明都会被提升,但值得注意的是,函数首先被提升,然后才是变量。
定义一个函数有两种方式:函数声明(function definition/declaration/statement)和函数表达式( function expression)。
-
foo();
// 1
-
var foo;
-
function foo() {
-
console.log(
1);
-
}
-
foo =
function() {
-
console.log(
2);
-
}
javascript引擎会理解成
-
function foo() {
-
console.log(
1);
-
}
-
foo();
// 1
-
foo =
function() {
-
console.log(
2);
-
}
注意,var foo尽管出现在function foo() 声明之前,但是它是重复的声明(会忽略),因为函数声明会被提升到普通变量之前。
函数表达式不可提升
-
a();
// TypeError: a is not a function
-
var a =
function f1() {
-
console.log(
"a");
-
}
先讲讲引擎的LHS、RHS
LHS:赋值操作的目标是谁(对变量进行赋值所执行的查询)
RHS:谁是赋值操作的源头(找到并使用变量所执行的查询)
例子:
-
var a;
// LHS 寻找a,未找到,通知作用域声明一个新变量,命名为a
-
a =
2;
// LHS 找到a并给其赋值为2
-
console.log(a);
// RHS 找到a的值2,并将其输出
例子:
-
function test(a) {
-
// 这里隐式包含了 a = 2 这个赋值,所以对 a 进行了 LHS 查询
-
var b = a;
-
// 这里对 a 进行了 RHS 查询,找到 a 的值,然后对 b 进行 LHS 查询,把 2 赋值给 b
-
return a + b;
-
// 这里包含了对 a 和 b 进行的 RHS 查询
-
}
-
var a = test(
2)
-
// 这里首先对 test() 进行 RHS 查询,找到它是一个函数,然后对 a 进行 LHS 查询把 test 赋值给 a
区分LHS、RHS很重要!
因为在变量还没有声明(在任何作用域中都无法找到该变量)情况下,这两种查询行为是不一样的。
-
function test(a) {
-
console.log(a + b)
-
b = a
-
}
-
test(
2)
以上代码对b进行RHS的时候无法找到该变量的值,则会抛出ReferenceError异常。
如果是LHS找不到变量:
非严格模式下,会在全局作用域中,创建一个具有该名称的变量,
严格模式下,会抛出与RHS类似的异常。
看例子:
-
function test() {
-
a =
100
-
console.log(a)
// 100
-
}
-
test()
-
console.log(a)
// 100
-
console.log(c)
// ReferenceError: c is not defined
好了就讲这里。回到原文!
var a = 2;
这段程序的工作流程:
当你看见 var a = 2; 这段程序时,很可能认为这是一句声明,事实上javascript引擎认为(这个是提升)把它们拆成 var a; a = 2;有两个完全不同的声明,一个由编译器在编译时处理,另一个在引擎运行时处理。
因为javascript引擎拿到代码的时候,编译器已经做了一些转换,编译器为什么要作这个事情?
因为它要在第一步就找到所有的声明,并且用合适的作用域将他们关联起来,这也正是词法作用域的核心。
代码执行前会对其进行编译,首先编译器会词法分析,然后词法单元流(数组)解析成语法树,最后进行代码生成,别忘了代码生成就是将语法树转化为一组机器指令。
- 生成代码前编译器会询问作用域是否有该名称的变量(LHS),如果有,忽略该声明,如果没有,会要求作用域在当前作用域的集合中声明一个新变量,并命名为a。
- 接下来为引擎生成运行时所需要的代码,这些代码用来处理a = 2;这个赋值操作(LHS),运行时引擎首先会问作用域,在当前作用域集合中是否有一个叫作a的变量,如果找到就会对他赋值(如果没有就继续往上级作用域查找,如果到根作用域仍然找不到javascript引擎就会抛出一个异常)。
继续往下看!LHS、RHS版:
-
var a =
2;
-
console.log(a);
1 .编译器:作用域,我需要对a进行LHS查找,你见过么?
2 .作用域:我这找到根都没看到啊,要不咱声明一个吧!
3. 编译器:好,建好了,那我生成代码了,引擎,给你你要的代码。
4 .引擎:收到,咦,需要一个a啊,作用域,帮我LHS找一下有没有?
5. 作用域: 找到了,编译器已经帮忙声明了。
6. 引擎:好的,那我对它赋值。
7. 引擎:作用域,不要意思,我碰到一个console,需要RHS引用
8 .作用域: 找到了,是个内置对象,拿走不谢。
9 .引擎: 好的作用域,对了能在帮我确认一下a的RHS么?
10 .作用域:确认好了,没变,拿去用吧,他的值是2
11. 引擎:好咧,我把2传递给log(..)
谈谈闭包
我们上文以及讲了作用域以及作用域链,然后在来看看面试多数问到的闭包。
什么是闭包?
红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。
闭包产生的原因?
首先要明白作用域链的概念(上文也讲过),其实很简单,在ES5中只存在两种作用域————全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链,值得注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。 比如:
-
var a =
1;
-
function f1() {
-
var a =
2
-
function f2() {
-
var a =
3;
-
console.log(a);
//3
-
}
-
}
-
f1();
讲解:f2(),console.log(a)的时候会先查找自身作用域看看有没有a这个变量,如果没有就会查找f1()作用域,如果没有会查找全局作用域。不过f2查找自身的时候就找了 所以就 直接 输出 3。
闭包产生的本质就是,当前环境中存在指向父级作用域的引用。例子:
-
var name =
"an";
-
function f1() {
-
var a =
2
-
function f2() {
-
a++;
-
console.log(a);
-
console.log(name);
-
}
-
return f2;
-
}
-
var x = f1();
-
x();
// 3 an
-
x();
// 4 an
-
x();
// 5 an
-
// console.log(name); // an
-
// console.log(a); // ReferenceError: a is not defined
讲解:这里x()会拿到f1()作用域和全局作用域中的变量,输出 3 an ....。因为在当前环境中,含有对f2()作用域的引用,f2()恰恰引用了全局作用域、f1()作用域。因此f2()作用域可以访问到f1()的作用域和全局作用域的变量。
来个难一点的例子:
-
var f3;
-
function f1() {
-
var a =
2
-
console.log(
"---f1---")
-
f3 =
function() {
-
console.log(a);
-
}
-
}
-
f1();
// ---f1---
-
console.log(f3)
// [Function: f3]
-
f3();
// 2
-
-
// 如果直接访问
-
// console.log(f3) // undefined
-
讲解:让f1()执行,给f3赋值一个函数,f3拥有全局、f1()、本身作用域的访问权限,f3()打印a,会先查找自身作用域,没有就查找f1的作用域(还没有就查找全局作用域),找到之后返回打印 2。
经典的面试题
-
for (
var i=
1; i<=
5; i++) {
-
setTimeout(
function timer() {
-
console.log( i );
-
}, i*
1000 );
-
}
为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)
因为setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后setTimeout中的回调才依次执行,但输出i的时候当前作用域没有,往上一级再找,发现了i,此时循环已经结束,i变成了6。因此会全部输出6。
解决方法:
1、利用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到定时器中
-
for(
var i =
1;i <=
5;i++){
-
(
function(j){
-
setTimeout(
function timer(){
-
console.log(j)
-
},
0)
-
})(i)
-
}
2、给定时器传入第三个参数, 作为timer函数的第一个函数参数
-
for(
var i=
1;i<=
5;i++){
-
setTimeout(
function timer(j){
-
console.log(j)
-
},
0, i)
-
}
3.使用es6的let
-
for(
let i =
1; i <=
5; i++){
-
setTimeout(
function timer(){
-
console.log(i)
-
},
0)
-
}
参考:
《你不知道的javascript》上卷
http://47.98.159.95/my_blog/js-base/004.html
https://github.com/creeperyang/blog/issues/16
https://www.jianshu.com/p/5ebf2ad6def2
https://juejin.im/post/5ca995626fb9a05e1a7aabd8#heading-7
转载:https://blog.csdn.net/echo_Ae/article/details/106814089