精读《你不知道的javascript》上卷

深入 JavaScript

Posted by Shiyanping on October 26, 2018

一、作用域及闭包

1.1 作用域是什么

查询分为 LHS 和 RHS,如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询,如果是获取变量的值,就会使用 RHS 查询。

赋值操作符会导致 LHS 查询。=操作符和调用函数时传入的参数操作都会导致关联作用域的赋值操作。

1
2
console.log(a); // RHS引用
var a = 2; // LHS引用

用书中一个稍微复杂的例子进行一个更加完善的解释。

1
2
3
4
5
6
7
function foo(a) {
  // 将形参2传入函数foo中,其中包括一个LHS引用,var a = 2;
  var b = a; // 给b赋值属于一个LHS引用,通过查询a属于RHS引用
  return a + b; // 使用a和b属于两次RHS引用
}

var c = foo(2); // 给c赋值属于LHS引用,执行foo函数属于RHS引用

LHS 和 RHS 查询都会在当前执行作用域中开始,如果没有找到所需要的标识符,就会向上级作用域继续查找目标标识符,逐层往上找,最后找到全局作用域,无论有没有找到都会停止。

不成功的 RHS 引用会抛出ReferenceError异常,不成功的 LHS 引用在非严格模式下会创建一个全局变量,该变量使用 LHS 引用的目标作为标识符,如果在严格模式下,会抛出和 RHS 同样的异常。

1.2 词法作用域

作用域是由书写代码时函数声明的位置决定的。eval(..)with能够欺骗词法作用域,前者可以对一段包含一个或者多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上通过将一个对象的引用当做作用域来处理,将对象的属性当做作用域中的标识符来处理,从而创建一个新的词法作用域(同样在运行时)

谨慎使用上面两种方法,会导致引擎优化无效,都会讲代码变慢,最好不要使用。

1.3 函数作用域和块作用域

1.3.1 函数作用域

函数作用域是指,属于这个函数的全部变量都可以在整个函数范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

函数分为函数表达式和函数声明,区分函数表达式和函数声明的最简单的方法就是看function关键字出现的位置(不是一行,是整个声明的位置),如果function是声明中的第一个词,就是函数声明,否则就是函数表达式。

  • 立即执行函数表达式(IIFE)
1
2
3
4
5
6
var a = 2;
(function() {
  var a = 3;
  console.log(a); // 3
})();
console.log(a); // 2

由于函数被包含在一对()括号内部,因此成为了一个表达式,通过在末尾加上另外一个()可以立即执行这个函数,比如(function(){...})()。第一个()将函数变成表达式,第二个()执行了这个函数。

IIFE 还可以传递参数,例:

1
2
3
4
5
6
7
var a = 2;
(function IIFE(global) {
  var a = 3;
  console.log(a); // 3
  console.log(global.a); // 2
})(window);
console.log(a); // 2

1.3.2 隐藏内部实现

尽量不要将过多的函数或者变量暴露,应该遵循最小授权活最小暴露原则(最小限度地暴露必要内容,将其他内容都“隐藏”起来,类似设计一个 API 或者单独模块)。

将私有化的内容具体私有化,无法从外部访问,不受外部影响。隐藏作用域中的变量和函数,可以有效的避免同名标识符之间的冲突。

解决冲突的方案:

  1. 全局命名空间,定义一个大对象,在对象下添加属性和方法,传统的 jquery 插件实现方式。
  2. 模块管理

1.3.3 块级作用域

块级作用域是对最小授权原则的一种扩展,将隐藏信息扩展为块中隐藏信息。

ES6 之前只有with语句和try/catch中的catch存在块级作用域。ES6 中引入了letconst关键字,可以生成块级作用域,只在指定的代码块内访问,外部不可以访问,并且不会进行变量提升。

1.4 提升

举个例子:

1
2
3
4
5
a = 2;

var a;

console.log(a); // 2

a = 2 会进行变量提升,会被引擎解析成如下代码:

1
2
3
4
5
var a;

a = 2;

console.log(a); // 2

首先会声明var a,然后给a进行赋值,最后输出 2。引擎对变量声明进行了变量提升。

var a = 2,javascript 引擎其实会分解成var aa = 2两个单独的声明,第一个是编译阶段,第二个是执行阶段。

函数声明和变量声明在被执行前都会进行“移动”处理,“移动”到各自作用域的最顶端,也就是所谓的提升。

虽然函数声明和变量声明都会进行提升,但是函数提升优于变量提升,例:

1
2
3
4
5
6
7
8
9
10
11
foo(); // 2

var foo;

function foo() {
  console.log(1);
}

foo = function() {
  console.log(2);
};

会先输出 1 而不是 2,因为引擎会解析成一下代码:

1
2
3
4
5
6
7
8
9
function foo() {
  console.log(1);
}

foo(); // 2

foo = function() {
  console.log(2);
};

var foo 是重复声明,所以被忽略了,因为函数声明优于变量声明。

1.5 作用域闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,不论函数在哪儿执行。

其实在 JS 开发中处处都会用到闭包,只不过我们没有仔细去发现。

闭包可以用来实现模块,模块的两个主要特征:

  • 为了创建内部作用域而调用一个包装函数
  • 包装函数返回值必须至少包括一个对内部函数的引用