编译过程简单来说是编译器把程序分解成词法单元(token),然后把词法单元解析成语法树(AST),再把语法树变成机器指令等待执行的过程。
【1】分词(tokenizing)
把由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。
var a = 2;被分解成为下面这些词法单元:var、a、=、2、;。这些词法单元组成了一个词法单元流数组。
// 词法分析后的结果
[
"var" : "keyword",
"a" : "identifier",
"=" : "assignment",
"2" : "integer",
";" : "eos" // (end of statement)
]
【2】解析(parsing)
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称为“抽象语法树” (Abstract Syntax Tree, AST)。
var a = 2;的抽象语法树中有一个叫VariableDeclaration的顶级节点,接下来是一个叫Identifier(它的值是a)的子节点,以及一个叫AssignmentExpression的子节点,且该节点有一个叫Numericliteral(它的值是2)的子节点。
{
operation: "=",
left: {
keyword: "var",
right: "a"
}
right: "2"
}
【3】代码生成
将AST转换为可执行代码的过程被称为代码生成。
var a=2;的抽象语法树转为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将值2储存在a中。
实际上,javascript引擎的编译过程要复杂得多,包括大量优化操作,上面的三个步骤是编译过程的基本概述。
【4】执行
1、引擎运行时会首先查询作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量
2、如果引擎最终找到了变量a,就会将2赋值给它。否则引擎会抛出一个异常。
词法作用域
编译器的第一个工作阶段叫作分词,就是把由字符组成的字符串分解成词法单元。简单地说,词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
换句话说,无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
动态作用域
JS使用的是词法作用域,它最重要的特征是它的定义过程发生在代码的书写阶段,那为什么要介绍动态作用域呢?
实际上,动态作用域是JS另一个重要机制this的表亲。作用域混乱多数是因为词法作用域和this机制相混淆,傻傻分不清楚。
动态作用域并不关心函数和作用域是如何声明以及在任何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar();
【1】如果处于词法作用域,也就是现在的JS环境。变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为2。所以控制台输出2。
【2】如果处于动态作用域,同样地,变量a首先在foo()中查找,没有找到。这里会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,找到并赋值为3。所以控制台输出3。
两种作用域的区别,简而言之,词法作用域是在定义时确定的,而动态作用域是在运行时确定的。
执行环境栈
每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
闭包应用
img对象经常用于数据上报。
var report = function(src){
var img = new Image();
img.src = src;
}
report('http://xx.com/getUserInfo');
但是,在一些低版本浏览器中,使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功地发起了HTTP请求。
原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出HTTP请求,所以此次请求就会丢失掉。
现在把img变量用闭包封闭起来,就能解决请求丢失的问题
var report = (function(){
var imgs = [];
return function(src){
var img = new Image();
imgs.push(img);
img.src = src;
}
})()
report('http://xx.com/getUserInfo');
隐式丢失
隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这种情况容易出错却又常见。
【函数别名】
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar();//0
【参数传递】
var a = 0;
function foo(){
console.log(this.a);
};
function bar(fn){
fn();
}
var obj = {
a : 2,
foo:foo
}
//把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo。与上例类似,只是把foo函数赋给了fn,而fn与obj对象则毫无关系
bar(obj.foo);//0
【间接引用】
函数的"间接引用"一般都在无意间创建,最容易在赋值时发生,会造成隐式丢失。
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即执行
(p.foo = o.foo)(); // 2
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//将o.foo函数赋值给p.foo函数,之后p.foo函数再执行,是属于p对象的foo函数的执行
p.foo = o.foo;
p.foo();//4
箭头函数
箭头函数根据当前的词法作用域,而不是根据this机制顺序来决定this,所以,箭头函数会继承外层函数调用的this绑定,而无论this绑定到什么。
箭头函数不可以当作构造函数,也就是不可以使用new命令,否则会报错。
箭头函数中不存在arguments对象。