函数作用域及块作用域

Author Avatar
Roojay 9月 16, 2017
  • 在其它设备中阅读本文章

最小特权原则

最小特权原则也叫最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计。

函数封装

将函数和变量包裹在一个外部包装函数的作用域中,使用这个作用域将这些具体的细节隐藏起来,实现函数的封装。

  1. 可以避免同名标识符之间的冲突,避免全局变量污染。
  2. 将具体内容私有化,防止外部以非预期的方式使用内部私有的内容。

函数作用域

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

函数声明:

function foo() {
    for (let i = 0; i < 10; i++) {
        console.log(i);
    }
}

foo();

立即执行函数表达式:

(function foo() {
    for (let i = 0; i < 10; i++) {
        console.log(i);
    }
})();

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

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

  1. 函数声明中,foo 被绑定在所在的作用域中,可以在外部通过 foo() 来调用它。
  2. 函数表达式中,foo 被绑定在函数表达式自身的函数中,外部不能访问调用,只能在其(function foo(){…}) 中的...位置被访问。

匿名函数表达式

函数表达式可以匿名,而函数声明则不可以省略函数名。

立即执行函数表达式(IIFE)

立即执行函数表达式(Immediately Invoked Function Expression),将函数包含在一对()中,成为一个函数表达式,在末尾加上另一对()可以立即执行这个函数。
第一个()将函数变成表达式,第二个() 执行了这个函数。

IIFE 的两种写法

  1. 立即执行函数表达式:
(function foo() {
    for (let i = 0; i < 10; i++) {
        console.log(i);
    }
})();
  1. 另一种改进形式写法将调用的()移到了用来包装的()里面。
(function foo() {
    for (let i = 0; i < 10; i++) {
        console.log(i);
    }
}());

两种写法功能上一样,凭个人喜好。

IIFE 表达式的用法

  1. 匿名函数表达式
(() => {
    const a = 6;
    console.log(a); // a
})();
  1. 将它当作函数调用,并传递参数进去
const name = 'haha';
(function IIFE(name) {
    console.log(name); // haha
})(name);
  1. 倒置代码的运行顺序,将需要运行的函数放在第二位, IIFE 执行之后,放在第二位的函数被当作参数传递进去。
const objA = {
    name: 'haha',
};

(function IIFE(foo) {
    foo(objA);
}((obj) => {
    const name = '3';
    console.log(name); // 3
    console.log(obj.name); // haha
}));

第二部分为一个匿名函数,被当作一个参数 foo 传入到 IIFE 函数的第一部分。最后匿名函数调用执行,将一个全局对象 objA 传入当作 obj 的参数值。

块作用域

块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。

块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { .. } 内部)。

只想在 for 循环内部的上下文中使用 i,所以在 for 循环的头部使用 var 声明一个变量 i,但实际上 i 会被绑定在外部作用域(函数或全局)中,在全局范围内都有效。

var arr = [];
for (var i = 0; i < 10; i++) {
  arr[i] = function() {
    console.log(i);
  };
}
arr[6](); // 10

console.log(i) 内部的 i 指向全局的的 i,最后循环完毕后在执行 arr6,这时全局的 i 的值为 10.

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

Let 变量

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

var arr = [];
for (let i = 0; i < 10; i++) {
  arr[i] = function () {
    console.log(i);
  };
}
arr[6](); // 6

由 let 声明的 i,每次循环都在循环的内部重新生成了一次变量,

var arr = [];
for (let i = 0; i < 10; i++) {
    let _i = i;
  arr[i] = function () {
    console.log(_i);
  };
}
arr[6](); // 6

for 循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

let 声明的变量只在它所在的代码块有效。

{
    var a = 6;
    let b = 9;
}

console.log(a); // 6
console.log(b); // b is not defined

暂时性死区

在代码块内,使用 let 命令声明变量,在声明之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ).

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 6;

if(true) {
  tmp = 3; // ReferenceError
  let tmp;
}

The MIT License (MIT)
Copyright (c) 2019, Roojay.

本文链接:https://roojay.com/pages/70cbbc9f/