作用域与闭包

一、什么是作用域?

作用域 :定义如何在某些位置存储变量,及如何在稍后找到这些变量的规则称为作用域。作用域是通过标识符名称查询变量的一组规则。
嵌套的作用域遍历简单规则:引擎从当前执行的作用域开始查找变量,如果没找到,就向上走一级继续查找,如此类推,如果到了最外层的全局作用域,就或停止查找,无论是否找到变量。

作用域是一组规则,它决定了一个变量(标识符)在哪里和如何被找到。这种查询也许是为了向某个变量赋值,这时变量是一个LHS(Left-hand Side左手边)引用,或者为了取得他的值,这时变量是一个RHS(Right-hand Side右手边)的引用。
LHS 引用得自赋值操作。作用域 相关的赋值可以通过 = 操作符发生,也可以通过向函数参数传递(赋予)参数值发生。

1.1编译器理论

在传统的编译型语言处理中,一块儿源代码在执行之前通常会经历三个步骤,被称为”编译“;
1、分词\词法分析:将一连串字符打断成有意义的片段,称为token(记号),例如 var a = 2; 可能被打断成如下token: var , a, =, 2,和 ; 。
2、解析:将一个token的流(数组)转换为一个嵌套元素的树,它综合的表示了程序的语法结构,这棵树称为“抽象语法树”。
3、代码生成:这个处理将抽象语法树转换为可执行的代码。

JavaScript 引擎 在执行代码之前首先会编译它,因此,它将 var a = 2; 这样的语句分割为两个分离的步骤:
首先,var a 在当前 作用域 中声明。这是在最开始,代码执行之前实施的。
稍后,a = 2 查找这个变量(LHS 引用),并且如果找到就向它赋值。

LHS 和 RHS 引用查询都从当前执行中的 作用域 开始,如果有需要(也就是,它们在这里没能找到它们要找的东西),它们会在嵌套的 作用域 中一路向上,一次一个作用域(层)地查找这个标识符,直到它们到达全局作用域(顶层)并停止,既可能找到也可能没找到。

未被满足的 RHS 引用会导致 ReferenceError 被抛出。未被满足的 LHS 引用会导致一个自动的隐含地创建的同名全局变量(如果不是“Strict 模式”),或者一个 ReferenceError(如果是“Strict 模式”)

二、词法作用域

作用域的工作方式与两种主要模式,分别是 词法作用域动态作用域;绝大多数编程语言使用的是词法作用域,Bash脚本等使用的是 动态作用域。
词法作用域意味着作用域是由编写时函数被声明的位置的决策定义的。编译器的词法分析阶段实质上可以知道所有的标识符是在哪里和如何声明的,并如此在执行期间预测它们将如何被查询。词法作用域是一组关于 引擎 如何查询变量和它在何处能够找到变量的规则。 词法作用域的关键性质是它是在代码编写时被定义的
动态作用域:动态作用域本身不关心函数和作用域是在哪里和如何被声明的。而是关心 它们是从何处被调用的。换句话说,它的作用域链条是基于调用栈的,而不是代码中作用域的嵌套。JavaScript实际上是没有动态作用域的,但是this机制有些像动态作用域

词法分析
处理是检查一串源代码字符,并给token赋予语法含义作为某种有状态解析的输出。
词法作用域是在词法分析时被定义的作用域。词法作用域是基于变量和作用域块儿在何处被编写决定的,因此词法作用域在词法分析器处理代码时(基本上)是固定不变的。

查询
一旦找到第一个匹配,作用域查询就停止了。相同的标识符名称可以在嵌套作用域的多个层中被指定,这称为“遮蔽(shadowing)”(内部的标识符“遮蔽”了外部的标识符)。无论如何遮蔽,作用域查询总是从当前被执行的最内侧的作用域开始,向外/向上不断查找,直到第一个匹配才停止。

全局变量也自动地是全局对象(在浏览器中是window,等等)的属性,所以不直接通过全局变量的词法名称,而通过将它作为全局对象的一个属性引用来间接地引用,是可能的。

window.a;

这种技术给出了访问全局变量的方法,没有它全局变量将因为被遮蔽而不可访问。然而,被遮蔽的非全局变量是无法访问的。不管函数是从哪里被调用的,也不论它是如何被调用的,它的词法作用域是由这个函数被声明的位置 唯一 定义的。词法作用域查询 仅仅 在处理头等标识符时实施。

欺骗词法作用域
如果词法作用域是由函数被声明的位置唯一定义的,而且这个位置完全是一个编写时的决定,那么怎么可能有办法在运行时“修改”(也就是,作弊欺骗)词法作用域呢?JavaScript 有两种这样的机制,但是 欺骗词法作用域会导致更低下的性能
1、eval
JavaScript 中的 eval(…) 函数接收一个字符串作为参数值,并将这个字符串的内容看作是好像它已经被实际编写在程序的那个位置上。

function foo(str, a) {
  eval(str); // 作弊!var b = 3  相当于在这里定义变量 b 并赋值
  console.log(a, b);
}

var b = 2;

foo("var b = 3;", 1); // 1 3

注意: 在这个例子中,为了简单起见,我们传入的“代码”字符串是固定的文字。但是它可以通过根据你的程序逻辑将字符拼接在一起,很容易地以编程方式创建。eval(…) 通常被用于执行动态创建的代码,因为动态地对一段实质上源自字符串字面值的静态代码进行求值,并不会比直接编写这样的代码带来更多真正的好处。
注意:当 eval(…) 被用于一个操作它自己的词法作用域的 strict 模式程序时,在 eval(…) 内部做出的声明不会实际上修改包围它的作用域。

2、with(现在被废弃了)
with 的常见方式是作为一种缩写,来引用一个对象的多个属性,而不必每次都重复对象引用本身。with 在运行时将一个对象和它的属性转换为一个带有“标识符”的“作用域”。

var obj = {
  a: 1,
  b: 2,
  c: 3,
};

//  重复“obj”显得更“繁冗”
obj.a = 2;
obj.b = 3;
obj.c = 4;

// “更简单”的缩写
with (obj) {
  a = 3;
  b = 4;
  c = 5;
}

然而,这里发生的事情要比只是一个对象属性访问的便捷缩写要多得多。考虑如下代码:

function foo(obj) {
  with (obj) {
    a = 2;
  }
}

var o1 = {
  a: 3,
};

var o2 = {
  b: 3,
};

foo(o1);
console.log(o1.a); // 2

foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2 -- 哦,全局作用域被泄漏了!

注意: 除了使用它们是个坏主意以外,eval(…) 和 with 都受 Strict 模式的影响(制约)。with 干脆就不允许使用,而虽然 eval(…) 还保有其核心功能,但各种间接形式的或不安全的 eval(…) 是不允许的。

性能:
通过在运行时修改,或创建新的词法作用域,eval(…) 和 with 都可以欺骗编写时定义的词法作用域。
JavaScript 引擎在编译阶段期行许多性能优化工作。其中的一些优化原理都归结为实质上在进行词法分析时可以静态地分析代码,并提前决定所有的变量和函数声明都在什么位置,这样在执行期间就可以少花些力气来解析标识符。
但如果引擎在代码中发现一个 eval(…) 或 with,它实质上就不得不 假定 自己知道的所有的标识符的位置可能是无效的,因为它不可能在词法分析时就知道你将会向eval(…)传递什么样的代码来修改词法作用域,或者你可能会向with传递的对象有什么样的内容来创建一个新的将被查询的词法作用域。

欺骗词法作用域机制(eval()和with)压制了引擎在作用域查询上进行编译优化的能力,因此不要使用eval()和with

三、函数与块儿作用域

函数中的作用域
声明的每一个函数都为自己创建一个作用域,函数作用域中所有变量都属于函数,而且贯穿整个函数始终都可以使用和重用(而且甚至可以在嵌套的作用域中访问)。
块儿作为作用域
虽然函数是最常见的作用域单位,但是其他的单位作用域单位也是存在的。块儿作用域指的是这样一种想法:变量和函数可以属于任意代码块儿(一般来说,就是任意的 { … }),而不是仅属于外围的函数。

for (var i = 0; i < 10; i++) {
  console.log(i);
}
//我们在 for 循环头的内部直接声明了变量 i,
//因为我们意图很可能是仅在这个 for 循环内部的上下文环境中使用 i,
//而实质上忽略了这个变量实际上将自己划入了外围作用域中(函数或全局)的事实


for (let i = 0; i < 10; i++) {
  console.log(i);
}
console.log(i); // ReferenceError
//在 for 循环头部的 let 不仅将 i 绑定在 for 循环体中,而且实际上,它会对每一次循环的 迭代 重新绑定 i,确保它被赋予来自上一次循环迭代末尾的值
var foo = true;

if (foo) {
  var bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

我们仅在 if 语句的上下文环境中使用变量 bar,所以我们将它声明在 if 块儿的内部是有些道理的。然而,当使用 var 时,我们在何处声明变量是无关紧要的,因为它们将总是属于外围作用域。这个代码段实质上为了代码风格的原因“假冒”了块儿作用域,并依赖于我们要管好自己,不要在这个作用域的其他地方意外地使用 bar。
块作用域列子:
1、with:它从对象中创建的作用域仅存在与这个with语句的声明周期中,而不存在外围作用域中。
2、try/catch: ES3中明确指出 catch字句中声明的变量是属于catch块儿的块作用域的。

try {
  undefined(); //用非法的操作强制产生一个异常!
} catch (err) {
  console.log(err); // 好用!
}

console.log(err); // ReferenceError: `err` not found

注意: 虽然这种行为已经被明确规定,而且对于几乎所有的标准 JS 环境(也许除了老 IE)来说都是成立的,但是如果你在同一个作用域中有两个或多个 catch 子句,而它们又各自用相同的标识符名称声明了它们表示错误的变量时,许多 linter 依然会报警。实际上这不是重定义,因为这些变量都安全地位于块儿作用域中,但是 linter 看起来依然会恼人地抱怨这个事实。

3、let:let关键字将变量声明附着在它所在的任何块儿(通常是一个{…})的作用域中。换句话说,let为它的变量声明隐含地劫持了任意块的作用域。

var foo = true;

if (foo) {
  let bar = foo * 2;
  bar = something(bar);
  console.log(bar);
}

console.log(bar); // ReferenceError

4.const:ES6还引入了const,它也创建一个块儿作用域变量,但是它的值是固定的(常量)。

四、提升

引擎在执行代码时会编译它,编译的过程就是找到所有声明,并将它们关联在合适的作用域上。所以所有的声明、变量、函数都会被首先处理,赋值为了执行阶段而留在原处。

注意:只是声明本身被提升了,而任何赋值或其他的执行逻辑被留在原处。提升是以作用域为单位的。
注意:函数声明会被提升,但是函数表达式不会
注意:函数声明和变量声明都会提升。但是函数会首先被提升,然后才是变量。

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

var foo = function bar() {
  // ...
};

代码被提升为:

var foo;

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

foo = function() {
	var bar = ...self...
	// ...
}

五、作用域闭包

闭包就是函数能够记住并访问他的词法作用域,即使当这个函数在它的词法作用域之外执行时。闭包就是能读取其他函数内部变量的函数,可以理解成 定义在一个函数内部的函数。本质上,闭包是将函数内部和函数外部连接的桥梁。

function foo() {
  var a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz(); // 2 --闭包

函数作为值传递的闭包:

function foo() {
  var a = 2;
  function baz() {
    console.log(a); // 2
  }
  bar(baz);
}
function bar(fn) {
  fn(); // 闭包
}

函数传递也可以是间接的:

var fn;

function foo() {
  var a = 2;
  function baz() {
    console.log(a);
  }
  fn = baz; // 将`baz`赋值给一个全局变量
}
function bar() {
  fn(); // 闭包
}
foo();
bar(); // 2

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值