Mmear's 😘.

JavaScript作用域

字数统计: 2.9k阅读时长: 11 min
2018/03/25 Share

编译语言与脚本语言的区别

之所以要讲这个,是因为后文涉及到JavaScript的预编译,众所周知,语言分为:

  • 标记语言:html xml
  • 编程语言:C++ Java
  • 脚本语言:JavaScript PHP

标记语言暂且不谈。

编程语言所写的程序在执行前都要经过编写-编译-链接-运行这四个步骤,编译就是把我们所看到的代码翻译成机器所理解的机器码,接着链接所需的库文件和资源文件,最后才能执行。以C++为例,编译前是一个.cpp文件,编译后的结果是个二进制目标文件.obj,link后生成可执行文件.exe

而像JavaScript这类脚本语言,就是为了缩短编程语言的传统过程而创建的,所谓脚本,字面上的意思就像”剧本”,演员们按照剧本参演,演出几次都不变。类似的,脚本就是执行一系列操作,所以早期的脚本语言通常被称为批量处理语言或工作控制语言。

一个脚本通常是解释运行而非编译运行。

JavaScript的预编译

如上节所说,JS是一种脚本语言,JS的执行过程,是一种翻译执行的过程,但JS执行过程中,有着类似编译的过程:

1
2
console.log(a);//undefined
var a = 2;

1
2
3
b = 3;
var b;
console.log(b);//3而不是 undefined
1
2
3
4
5
6
7
8
9
10
11
var name = "Mmear";
function sayName(){
fn();
var name = "Mirr";
function fn(){
console.log(name);
var name = "Morr";
};
fn();
}
sayName(); //undefined undefined

对于var a = 2这个语句,我们会认为这是一个声明,但JS实际上将它看成两部分:

  1. var a //声明,预编译阶段执行,也就是提升
  2. a = 2 //赋值,运行阶段执行

PS:通常预编译与执行之间的间隔时间非常短,一般只有几微妙甚至更短,所以难免认为是类似编译
因此,经过预编译,实际上的代码应该是:

1
2
3
var a; //被提升
console.log(a); //undefined
a = 2;

1
2
3
var b; //被提升
b = 3;
console.log(b); //3

总之,先有声明,再有赋值
不仅是变量如此,函数声明也会被提升,因此第三部分的代码应该为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function 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
4
func(); // 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
5
function 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
    6
    func_active_object{
    arguments: [],
    var1: "hi",
    var2: undefined, //未传值的形参
    name: undefined //内部提升的变量
    }

有了作用域链之后,就可以进行标识符解析了,标识符解析就是沿着作用域链一级级搜索标识符的过程。搜索过程始终从作用域链最前端开始,一直搜寻到全局环境活动对象,如果搜寻不到,便会发生RefrenceError错误。

因此下面这段代码:

1
2
3
4
5
6
7
8
9
var 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
2
3
4
5
6
7
8
9
10
11
12
13
function middle() {
var name = "first";
var xsmall = function() {
console.log("I am " + name);
}
return xsmall;
}
function big(param) {
var name = param;
var small = middle();
small();
}
big("second"); //I am first;

一步步来看:

  • 当调用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
2
3
4
5
6
7
8
console.log(a); //这里发生了对`a`的RHS查询,另外查找console对象也算一个RHS引用
a = 2; //这里发生了LHS查询,我现在什么都不想做,只想把2赋值给`a`这个容器
function foo(a){
...
}

//发生了RHS查询,查找foo这个变量,并将其作为函数执行;还发生了隐式的LHS查询,将2赋值给a这个形参
foo(2);

为什么要理解?

因为在变量没有声明的情况下,这两种查询得到结果是不同的:

1
2
3
4
5
function 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),还能预览部署完后的效果,有空再找找这些有趣又猛的插件。

CATALOG
  1. 1. 编译语言与脚本语言的区别
  2. 2. JavaScript的预编译
  3. 3. JavaScript中的执行环境(excution context)
  4. 4. JavaScript中的作用域/作用域链(scope chain)
    1. 4.1. 综合案例
    2. 4.2. 变量查询
      1. 4.2.1. 简介
      2. 4.2.2. 实例
      3. 4.2.3. 为什么要理解?
  5. 5. 总结
    1. 5.1. 题外话