做用域、做用域链及闭包(一)

正文

做用域是什么

做用域是一套规则,用于肯定在何处以及如何查找变量。前端

了解做用域以前还要简单了解一下编译原理: 书中(你不知道的js)说道JavaScript是一门编译语言。在传统编译语言的流程中,程序中一段源代码在执行以前会经历三个步骤,统称为“编译”。编程

  • 分词/词法分析将字符串分解成有意义的代码块,代码块又称词法单元。
  • 解析/语法分析将词法单元流转换成一个由元素逐级嵌套所组成的表明了程序语法接口的数,又称“抽象语法树”。
  • 代码生成:将抽象语法树转换为机器可以识别的指令。

理解做用域

做用域与编译器、引擎进行配合完成代码的解析bash

书中举了个例子 对于 var a = 2 这条语句,首先编译器会将其分为两部分,一部分是 var a,一部分是 a = 2。编译器会在编译期间执行 var a,而后到做用域中去查找 a 变量,若是 a 变量在做用域中尚未声明,那么就在做用域中声明 a 变量,若是 a 变量已经存在,那就忽略 var a 语句。而后编译器会为 a = 2 这条语句生成执行代码,以供引擎执行该赋值操做。因此咱们平时所提到的变量提高,其实就是利用这个先声明后赋值的原理。架构

做用域的工做模式

做用域共有两种主要的工做模型。第一种是最为广泛的,被大多数编程语言所采用的词法做用域( JavaScript中的做用域就是词法做用域)。另一种是动态做用域,仍有一些编程语言在使用(好比Bash脚本、Perl中的一些模式等)。编程语言

异常状况

对于 var a = 10 这条赋值语句,其实是为了查找变量 a, 而且将 10 这个数值赋予它,这就是 LHS 查询。 对于 console.log(a) 这条语句,其实是为了查找 a 的值并将其打印出来,这是 RHS 查询。函数

为何区分 LHS 和 RHS 是一件重要的事情?工具

在非严格模式下,LHS 调用查找不到变量时会建立一个全局变量,RHS 查找不到变量时会抛出 ReferenceError。 在严格模式下,LHS 和 RHS 查找不到变量时都会抛出 ReferenceError。学习

词法做用域

词法做用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。词法做用域最重要的特征是它的定义过程发生在代码的书写阶段(假设没有使用 eval() 或 with )ui

举个🌰spa

function testA() {
  console.log(a);  // 2
}

function testB() {
  var a = 3;
  testA();
}

var a = 2;

testB()
词法做用域让testA()中的a经过RHS引用到了全局做用域中的a,所以会输出2。
复制代码

动态做用域

动态做用域只关心它们从何处调用。换句话说,做用域链是基于调用栈的,而不是代码中的做用域嵌套。

// 所以,若是 JavaScript 具备动态做用域,理论上,下面代码中的 testA() 在执行时将会输出3。
function testA() {
  console.log(a);  // 3
}

function testB() {
  var a = 3;
  testA();
}

var a = 2;

testB()
复制代码

函数做用域

具名与匿名

书中举了个例子->回调函数

setTimeout( function() {
  console.log("我等了很久!")
}, 1000 )
复制代码

其实这个叫函数匿名表达式,函数表达式能够匿名,而函数声明则不能够省略函数名。匿名函数表达式书写起来简单快捷,不少库和工具也倾向鼓励使用这种风格的代码。但它也有几个缺点须要考虑。

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  • 若是没有函数名,当函数须要引用自身时只能使用已通过期的 arguments.callee 引用,好比在递归中。另外一个函数须要引用自身的例子,是在事件触发后事件监听器须要解绑自身。
  • 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可让代码不言自明。

针对于这种缺点,书中给出了建议:给函数表达式命名

setTimeout( function timeoutHandler() {
  console.log("我等了很久!")
}, 1000 )
复制代码

提高

先提出个问题,现有赋值仍是先有声明

a = 2;

var a;

console.log(a); // 2
复制代码

等价于

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //undefined
复制代码

咱们习惯将 var a = 2; 看做一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 看成两个单独的声明,第一个是编译阶段的任务,而第二个是执行阶段的任务。

这意味着不管做用域中的声明出如今什么地方,都将在代码自己被执行前首先进行处理。能够将这个过程形象地想象成全部的声明(变量和函数)都会被“移动”到各自做用域的最顶端,这个过程称为提高。

因此能够看出先有声明后有赋值

再看个小🌰

foo();  // TypeError
bar();  // ReferenceError

var foo = function bar() {
  // ...
};
复制代码

这个代码片断通过提高后,实际上会被理解为如下形式:

var foo;
foo();  // TypeError
bar();  // ReferenceError

foo = function() {
  var bar = ...self...
  // ...
};
复制代码

这段程序中的变量标识符 foo() 被提高并分配给全局做用域,所以 foo() 不会致使 ReferenceError。可是foo此时并无赋值(若是它是一个函数声明而不是函数表达式就会赋值)。foo()因为对 undefined 值进行函数调用而致使非法操做,所以抛出 TypeError 异常。另外即时是具名的函数表达式,名称标识符(这里是 bar )在赋值以前也没法在所在做用域中使用。

结语

但愿你们都能找到适合本身的学习方法重学前端,完善本身的知识体系架构~

相关文章
相关标签/搜索