编译语言与脚本语言的区别
之所以要讲这个,是因为后文涉及到JavaScript的预编译,众所周知,语言分为:
- 标记语言:html xml
- 编程语言:C++ Java
- 脚本语言:JavaScript PHP
标记语言暂且不谈。
用编程语言所写的程序在执行前都要经过编写-编译-链接-运行
这四个步骤,编译就是把我们所看到的代码翻译成机器所理解的机器码,接着链接所需的库文件和资源文件,最后才能执行。以C++为例,编译前是一个.cpp文件,编译后的结果是个二进制目标文件.obj,link后生成可执行文件.exe。
而像JavaScript这类脚本语言,就是为了缩短编程语言的传统过程而创建的,所谓脚本,字面上的意思就像”剧本”,演员们按照剧本参演,演出几次都不变。类似的,脚本就是执行一系列操作,所以早期的脚本语言通常被称为批量处理语言或工作控制语言。
一个脚本通常是解释运行而非编译运行。
JavaScript的预编译
如上节所说,JS是一种脚本语言,JS的执行过程,是一种翻译执行的过程,但JS执行过程中,有着类似编译的过程:1
2console.log(a);//undefined
var a = 2;
1 | b = 3; |
1 | var name = "Mmear"; |
对于var a = 2
这个语句,我们会认为这是一个声明,但JS实际上将它看成两部分:
- var a //声明,预编译阶段执行,也就是提升
- a = 2 //赋值,运行阶段执行
PS:通常预编译与执行之间的间隔时间非常短,一般只有几微妙甚至更短,所以难免认为是类似编译
因此,经过预编译,实际上的代码应该是:1
2
3var a; //被提升
console.log(a); //undefined
a = 2;
1 | var b; //被提升 |
总之,先有声明,再有赋值
不仅是变量如此,函数声明也会被提升,因此第三部分的代码应该为:1
2
3
4
5
6
7
8
9
10
11
12
13
14function sayName(){
function fn(){ //规定函数声明先提升
var name; //函数内部的变量也会提升至函数顶部
console.log(name);
name = "Morr";
}
var name; //变量提升在函数之后
/**执行阶段**/
fn();
name = "Mirr"; //访问不到,因为fn函数内部已经找到了name变量
fn();
}
sayName(); //undefined undefined
但是只有函数声明会被提升funtion fn() {...}
,函数表达式并不会提升var func = function() {...}
:1
2
3
4func(); // TypeError,因为func是undefined,不能被调用
var func() = function() {
...
}
JavaScript中的执行环境(excution context)
“执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。每个执行环境中都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。”
——《JavaScript高级程序设计(第三版)》
这个概念和C++,Java等编程语言类似,只不过JavaScript中,全局执行环境是最外围的一个执行环境,在Web浏览器中,全局执行环境一般是window对象。每个函数也有自己的执行环境,当执行流进入一个函数时,该函数的执行环境被推入一个环境栈中,函数执行完后,栈将其环境弹出,将控制权交还给上一个执行环境。
当某个环境的代码执行完毕后,其中的所有变量和函数定义都会被销毁(闭包情况除外)
JavaScript中的作用域/作用域链(scope chain)
“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”
——《JavaScript权威指南》
当代码在一个环境执行时,都会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境所有变量和函数的有序访问(如果允许访问)。
明确几点:
1.变量对象保存着当前环境所的有变量和定义的函数
2.作用域链保存着各级变量对象,作用域链或许可称为”变量对象链”(指针链表)
3.作用域类似于栈,最前端永远是当前环境的变量对象
4.如果当前环境是函数,则将其活动对象(由js预编译时所创建)作为变量对象,并为这个对象创建argument属性、形参同名属性、内部提升的局部变量的同名属性
对于第四点还要强调:
- 当一个函数被定义时,会将它定义时刻环境的作用域链链接至函数对象的
[[scope]]
内部属性 - 当一个函数被调用时,会创建一个活动对象,然后相对于函数的每个形参,都命名为该对象的命名属性,并将这个活动对象作为此时作用域链的最前端,它本身的[[scope]]属性也会链接到当前作用域链
举个栗子:ο(=•ω<=)ρ⌒☆1
2
3
4
5function func(var1,var2) { //定义
var name = "Mmear";
...
}
func("hi"); //调用
- 在定义func时,会为这个函数对象添加一个[[scope]]属性(内部属性,只有js引擎能够访问),并将当前作用域链链接上去,因为此时func定义在全局环境,所以此时的[[scope]]指向全局活动对象window active object
- 在调用func时(其实是预编译时,因为间隔相当短),会创建一个活动对象,并添加argument属性,同时添加var1,var2属性,name属性,然后将调用参数赋值给形参数,对于缺少的调用参数,赋值为undefined。
- 然后将这个活动对象加入它函数对象作用域链的最顶端,并将其[[scope]]属性链接到作用域链
所以在刚进入func函数时,活动对象的情况如下:1
2
3
4
5
6func_active_object{
arguments: [],
var1: "hi",
var2: undefined, //未传值的形参
name: undefined //内部提升的变量
}
有了作用域链之后,就可以进行标识符解析了,标识符解析就是沿着作用域链一级级搜索标识符的过程。搜索过程始终从作用域链最前端开始,一直搜寻到全局环境活动对象,如果搜寻不到,便会发生RefrenceError错误。
因此下面这段代码:1
2
3
4
5
6
7
8
9var name = "Mirr"
function fun1() {
console.log(name);
}
function fun2() {
var name = "Not me";
fun1();
}
fun2(); //Mirr
因为fun1在定义在全局环境中,所以其[[scope]]属性只含有全局环境的活动对象,当调用fun1()时,它的活动对象将fun1的[[scope]]链接的作用域链附加在其后:1
2
3
4
5
6
7[[fun1_scope_chain]] = [ //作用域链本质是一个指针链表,引用着各级活动对象
{ //fun1 active object
},{ //window active object
name: "Mirr"
},
]
综合案例
1 | function middle() { |
一步步来看:
当调用big函数时,作用域链是 big_active_object –> window_active_object,详情如下:
1
2
3
4
5
6
7
8
9[[big_scope_chain]] = [
{ //big active object
param: "second",
name: undefined,
small: undefined
},{ //window active object
}
]当调用middle函数时,作用域链是 middle_active_object –> window_active_object,详情如下:(此时作用域链中并不包含big函数的活动对象)
1
2
3
4
5
6
7
8[[middle_scope_chain]] = [
{ //middle active object
name: undefined,
xsmall: undefined
},{ //window active object
}
]在middle函数中定义(赋值)xsmall函数时,作用域链是 middle_active_object –> window_active_object,详情如下:
1
2
3
4
5
6
7
8[[xsmall_func_scope_chain]] = [ //注意这个是xsmall函数对象[[scope]]属性链接的作用域,前文是活动对象的作用域
{ //middle active object
name: "Mirr",
xsmall: undefined
},{ //window active object
}
]被middle函数返回,并在big函数调用xsmall函数时,发生了标识符解析,此时的作用域链是:
1
2
3
4
5
6
7
8
9
10[[xsmall_scope_chain]] = [
{ //xsmall active object
},{ //middle active object
name: "Mirr",
xsmall: undefined
},{ //windwo active object
}
]
PS:因为middle函数已经执行完毕,其执行环境已经被销毁,所以其活动对象中没有被引用的变量全被置为undefined,而name属性被xsmall函数引用着,所以仍然保留着,middle函数的活动对象也随之保留在作用域中,这就是之后要谈到的闭包
因为作用域链中不包含big函数的执行对象,所以name标识符的解析应该是middle活动对象中的name属性,所以输出为:
I am first
变量查询
“变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值.”
——《你不知道的JavaScript(上卷)》 P7
简介
这里再记录一下两种不同的查询:
- 赋值左侧查询 LHS
- 赋值右侧查询 RHS(其实理解为普通的获取变量值比较好)
简单解释一下,当要查询的变量出现在赋值操作’=’左侧时,就是LHS查询,出现在右侧就是RHS查询;
以var a = b
为例,标识符a
出现在赋值操作符左边,所以是LHS查询,查找b
时则是RHS查询。更形象点来说,LHS查询是为了找到一个用来装东西(赋值)的容器,对变量原有的值并不关注;RHS查询则注重找到变量的值(retrieve his source value);
实例
1 | console.log(a); //这里发生了对`a`的RHS查询,另外查找console对象也算一个RHS引用 |
为什么要理解?
因为在变量没有声明的情况下,这两种查询得到结果是不同的:1
2
3
4
5function func(a){
console.log(a + b);
b = a;
}
foo(2);
第一次对b
进行RHS查询是找不到的,因为根本没有声明,此时引擎会抛出一个ReferenceError
异常;然而,当第3行对b
进行LHS查询时,一直沿着作用域链到全局对象都找不到这个变量,结果引擎会在全局作用环境创建一个b
变量,并将a
的值赋给它(前提是引擎运行在非严格模式下);
ES5中的严格模式中有一个行为是禁止自动或隐式创建全局变量,所以严格模式下LHS查询失败时,会抛出和RHS查询失败一样的ReferenceError
异常;
另外,即使RHS成功查询到了变量,如果尝试对变量进行不合理的操作,比如对一个非函数类型变量进行函数调用,就会抛出TypeError
异常;注意,ReferenceError
异常代表查询失败,TypeError
异常代表查询成功但操作非法。
总结
“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”
——《JavaScript权威指南》
言简意亥,没什么好说的,这句话足够概括全文内容了~
题外话
写这篇文章时偶然发现一个爆炸好用的Hexo插件hexo-admin,基本功能是提供一个UI界面让你写博客,然后post上去(支持MarkDown),还能预览部署完后的效果,有空再找找这些有趣又猛的插件。