说明

《你不知道的JavaScript》学习笔记。

函数中的作用域

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

举例说明:

function foo(a) {
var b = 2;
// 一些代码
function bar() {
// ...
}
// 更多的代码
var c = 3;
}

上述代码分析:

  • 1、​​foo(..)​​​ 的​​作用域气泡​​​中包含了标识符​​a、b、c 和 bar​
  • 2、全局作用域也有自己的作用域气泡,它只包含了一个标识符:foo。
  • 3、无法从​​foo(..)​​​ 的外部对标识符​​a、b、c 和 bar​​进行访问。
  • 4、标识符(​​a、b、c、foo 和 bar​​​)在​​foo(..)​​ 的内部都是可以被访问的。
  • 5、标识符(​​a、b、c、foo 和 bar​​​)在​​bar(..)​​​ 内部也可以被访问(假设​​bar(..)​​ 内部没有同名的标识符声明)。

隐藏内部实现

1、先看一个例子:

function doSomething(a) {
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething( 2 ); // 15

上面代码中:

  • 1、变量 b 和函数​​doSomethingElse(..)​​​ 应该是​​doSomething(..)​​ 内部具体实现的“私有”内容。
  • 2、给予外部作用域对 b 和​​doSomethingElse(..)​​ 的“访问权限”没有必要
  • 3、这种​​访问权限​​可能导致它们被有意或无意地以非预期的方式使用,具有一定的“危险性”。

2、另一个例子:

function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse( a * 2 );
console.log( b * 3 );
}
doSomething( 2 ); // 15

上面代码中:

  • b 和​​doSomethingElse(..)​​​ 都无法从外部被访问,而只能被​​doSomething(..)​​ 所控制。

(最小授权或最小暴露原则)在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来。

规避冲突

“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。

例如:

function foo() {
function bar(a) {
i = 3; // 修改 for 循环所属作用域中的 i
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); // 糟糕,无限循环了!
}
}
foo();

在上面代码中:

  • 1、​​bar(..)​​​ 内部的赋值表达式​​i = 3​​​ 意外地覆盖了声明在​​foo(..)​​ 内部 for 循环中的 i。
  • 2、i 被固定设置为 3,永远满足小于 10 这个条件,导致了无限循环

规避冲突两种方法

1、全局命名空间

在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的​​命名空间​​。

2、模块管理

通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。

函数作用域

区分函数声明和表达式最简单的:

看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

匿名和具名

例如:

setTimeout( function() {
console.log("I waited 1 second!");
}, 1000 );

上面这种就叫做​​匿名函数表达式​​,因为 function()… 没有名称标识符。

  • 1、函数表达式可以是匿名的
  • 2、函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的。

匿名函数表达式书写起来简单快捷,但是有几个缺点

  • 1、调试很困难:匿名函数在栈追踪中不会显示出有意义的函数名
  • 2、可读性 / 可理解性很差…

立即执行函数表达式

术语:​​IIFE​​​,代表​​立即执行函数表达式​​(Immediately Invoked Function Expression)

例如下面代码就是:

var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2

IIFE 用于倒置代码的运行顺序

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

块作用域

with

用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效。

try/catch

JavaScript 的 ES3 规范中规定 ​​try/catch​​ 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。

例如:

try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found

let

es6 引入关键字 let ,它可以将变量绑定到所在的任意作用域中(通常是 { … } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。

例子:

var foo = true;
if (foo) {
let bar = foo * 2;
console.log( bar ); // 2
}
console.log( bar ); // Uncaught ReferenceError: bar is not defined

let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。

{
console.log( bar ); // Uncaught ReferenceError: Cannot access 'bar' before initialization
let bar = 2;
}

1、垃圾收集

块作用域和闭包及回收内存垃圾的回收机制相关。

2、let 循环

例子:

for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // Uncaught ReferenceError: i is not defined

for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环
的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。

const

ES6 还引入了 const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误。

例子:如果在if里面写了​​b = 4​​这个赋值,就会导致报错,程序不会往下执行,去掉这个赋值就会往下执行。

var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量
a = 3; // 正常 !
b = 4; // Uncaught TypeError: Assignment to constant variable.
}
console.log( a ); // 3
console.log( b ); // Uncaught ReferenceError: b is not defined.