上卷-作用域和闭包

103 阅读8分钟

第一章-作用域

编译

分词/词法分析

将字符串分解成计算机认识的代码块

解析/语法分析

形成抽象语法树

代码生成

AST(抽象语法树)转化为可执行代码的过程

作用域

演员表

引擎

负责整个程序的编译及执行过程

编译器

负责语法分析及代码生成等

作用域

负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套很严格的规则,确定当前执行的代码对这些标识符的访问权限

总结:

变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在该作用域中查找该变量,如果能够找到就会对它进行赋值

关于LHS和RHS

LHS:等式的左边——赋值操作的左侧——给变量赋值

RHS:等式的右边——赋值操作的右侧——取出变量值

注意:不要只想着变量定义,赋值什么的,还有函数啊

小结

作用域是一套规则,用于确定 在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,,那么就会使用LHS查询,如果目的是获取变量的值,会使用RHS查询。赋值操作会导致LHS查询,=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作

不成功的RHS会导致抛出ReferenceReeor异常,不成功的LHS会导致自动隐式创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)

第二章-词法作用域

查找

作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这叫做“遮蔽效应”(内部的标识符遮蔽了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外/向上进行,直到遇到第一个匹配的标识符为止

此法作用域查找只会查找一级标识符,比如a,b,c,如果代码中引用了foo.bar.baz,词法作用域查找只会试图查找foo标识符,找到这个变量后,对象属性访问规则会分别接管对bar和baz属性的访问

欺骗词法

欺骗词法作用域会导致性能下降

eval

eval(...) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。

举个例子:

function foo(str, a) {
    eval(str);
    console.log(a, b);
}
var b = 2;
foo("var b = 3;", 1);// 1,3
// 上面的代码转化为正常理解相当于
function foo(a) {
    var b = 3;
    console.log(a, b);
}
var b = 2;
foo(1);// 1,3
// 严格模式下,eval有自己的作用域,其中的声明无法修改所在的作用域
function foo(str) {
    "use strict";
    eval(str);
    console.log(b); // ReferencError
}
foo("var b = 3;");// 1,3

with

通常被当作重复引用同一个对象中多个属性的快捷方式。

举个例子

var obj = { a: 1, b: 2, c: 3 }; // 单调重复调用obj obj.a = 2; obj.b = 3; obj.c = 4; // 使用with with(obj) { a = 3; b = 4; c = 5; }

但是观察以下代码

fuction 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, a被泄露到全局了

with块内部,我们看起来只是对变量a进行简单的词法引用,实际上就是一个LHS引用,并将2赋值给它

第三章-函数作用域和块作用域

函数作用域

属于这个函数的全部变量都可以在整个函数的范围内使用及复用

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在各处

隐藏内部实现

最小限度的暴露必要内容,将其他内容都隐藏起来

作用之一:规避冲突

隐藏作用域的好处之一,可避免同标识符之间的冲突

匿名函数

缺点

1、栈追踪中不会显示出有意义的函数名,调试困难

2、调用自身只能使用过期的arguments.callee引用

3、代码可读性不好

立即执行函数(IIFE)

函数被包含在一对()内,因此成为表达式(函数名对函数来说不是必须的)

写法

(function foo() { ... })() // 或 (function foo() { ... }())

以上两种写法都可以,功能一致

为什么有立即执行函数

当函数在当前作用域定义,因为随时会调用,所以调用完并不会进行垃圾回收,多余只执行一次的函数怎么办呢,所以有了立即执行函数(自我理解)

还说可以undefined标识符的默认值被错误覆盖问题

还有一种变换的用途:倒置代码的运行顺序,将运行的函数放在第二位

var a = 2;
(function IIFE(def) {
    def(window);
})(function def(global) {
    var a = 3;
    console.log(a);// 3
    console.log(global.a);// 2
})

块作用域

if/for

用{}包起来的块

with也是块作用域的例子

try/catch

let-当前的块作用域生效

let不会进行变量提升

关于垃圾回收

function foo(data) { ... }
var something = {...};
foo(something);
var btn = document.getElementById("my\_button"); btn.addEventListener("click", function click(evt) {
    console.log("button clicked");
})

以上代码,当something执行完毕之后,就不在需要了,可以进行垃圾回收了,但是click函数形成整个作用域的闭包,js引擎很可能保留这个结构,所以用到块作用域

function foo(data) { ... } // 在这个块作用域的内容完事可以进行销毁
{ 
    var something = {...};
    foo(something);
}
var btn = document.getElementById("my\_button"); btn.addEventListener("click", function click(evt) {
    console.log("button clicked");
})

let对于循环非常nice

const

总结

函数不是唯一的作用域单元,块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块,{}内部

提升

先有声明,后有赋值

关于变量提升

console.log(a);
var a = 2;

以上代码输出结果为undefined

为什么?

以上代码被机器编译为以下内容

var a;
console.log(a);
a = 2;

包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理

但是,函数的声明会被提升,但是函数表达式不会被提升

举个例子

foo();
function foo() {
    console.log(a);// undefined
    var a = 2;
}
// 以上的函数声明被提升了,即便在foo声明之前调用foo,是成功的
// 但是,观察如下代码
foo();// 不是ReferenceError,而是TypeError
var foo = function bar() { ... }

这段程序中,foo被提升并分配给所在的作用域,因此不会导致ReferenceError,但是此时foo并灭有被赋值,foo()由于对undefined值进行函数调用而导致非法操作,因此会抛出TypeError异常

函数优先

函数声明和变量声明都会被提升,但是记住函数首先被提升,然后才是变量

举个例子

foo();// 1
var foo;
function foo() {
    console.log(1);
}
foo = function() {
    console.log(2)
} // 被引擎理解为如下模式
function foo() {
    console.log(1);
}
foo();
foo = function() {
    console.log(2)
} // 虽然var foo出现在function foo()之前,但是因为是重复的声明,所以被忽略了

但是出现在后面的函数声明还是可以覆盖前面的

总结

我们习惯将 var a = 2; 看作一个声明,实际上JS引擎将var a;和a = 2;当作两个单独的声明,第一个编译阶段的任务,第二个是执行阶段的任务

所以说,无论作用域中的声明出现在什么地方,豆浆在代码本身被执行前首先进行处理,可以将这个过程形象的想象成所有的的声明都会被移动到各自作用域的最顶端。

作用域和闭包

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包

闭包定义

当函数可以记住并访问所在词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包

模块模式

模块模式的两个必要条件

1、必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块)

2、封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包

——只有数据属性,没有闭包函数的对象,不能真正的模块

附录

动态作用域

动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用——换句话说,作用域是基于调用栈的,而不是代码中的作用域嵌套

JS并不具有动态作用域,它只有词法作用域,但是this机制某种程度上很像动态作用域

主要区别

词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的(this也是)

词法作用域关注函数在何处声明,动态作用域关注函数啊在何处调用