Yuan的博客
EN

变数、作用域、闭包

var, let, 以及 const 有什么差别

  1. 在作用域上,var 可以是全域、也可以是以函式作为范围;let 与 const 则是以区块作为范围。
  2. 在宣告上,var 可以被重复宣告,但是 let 与 const 则不行。
  3. 在提升上,var 宣告的变数会自动初始化值为undefined,因此在宣告前就使用变数,不会出现错误,而会是undefined ;但是let 与const 宣告的变数则不会自动初始化,而是会进到暂时死区(TDZ),因此在let 与const 宣告变数前使用该变数,会出现错误。
  4. let 与 const 在绝多数面向都是一样,两者的一大区别在于,用 let 宣告的变数可以重新赋值,但是用 const 的不行。

什么是提升 (Hoisting)?

  • Hoisting :JavaScript 编译阶段将变数和函式的宣告存入记忆体的概念。
  • var 提升 (hoisting):提早呼叫的结果会是 undefined
  • let / const :let 在提升变数到区块作用域(block scope) 范围时,并不会初始化此变数,这个状态可以称之为uninitialized,也有另一个常见的说法是,let 和const 定义的变数目前存在于暂时死区(TDZ,Temporal dead zone
  • function declarations
    • 用 var 宣告的foo 函式,在宣告前使用时,当时值会是 undefined,因此呼叫undefined 会报错。
    • 用 let 宣告的 foo 函式,在宣告前使用时,此时 foo 在暂时死区,因此呼叫 foo 会报错。

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

foo(); // Uncaught TypeError: foo is not a function
var foo = function () {};

foo(); // Uncaught ReferenceError: foo is not defined
let foo = function () {};

Javascript 的作用域 (Scope) 与作用域链 (Scope Chain) 是什么?

  • 作用域指的是当前正在执行的上下文,也就是程式目前所处的情境。在这个情境内,我们可以轻松地存取各种值(value)或表达式(expression)。

    • 全域(Global Scope):
    • 块级作用域 (Block Scope):只有 let 和 const 定义的变数会属于块级作用域,如果是 var 定义的变数会是只有函式作用域。
    • 函式作用域 (Function Scope):由函式所创建的作用域
  • 什么是作用域链 (Scope Chain):当 JavaScript 使用每一个变数的时候,会先尝试在当前作用域中寻找该变数,若在当前的作用域找不到该变数,会一直往父层作用域寻找,直到全局作用域还是没找到,就会直接报错,这一层一层的关系,就是作用域链。

什么是闭包 (Closure)?

  • 什么是闭包:闭包就是内部函式能够取得函式外部的变数,并且记住这个变数。因为能够记住这个外部变数,闭包很常被用来做状态保存。

  • 闭包的应用 1 — 状态保存

function useState(initialState) {
  let state = initialState;

  function getState() {
    return state;
  }

  function setState(updatedState) {
    state = updatedState;
  }
  return [getState, setState];
}

const [count, setCount] = useState(0);

count(); // 0
setCount(1);
count(); // 1
setCount(500);
count(); // 500
  • 闭包的应用 2 — 快取
function memoize(fn) {
  // 声明一个 cache 物件,透过 cache 来放快取的东西
  // 因为闭包的缘故,下面回传的函式可以存取到这个 cache 变数
  const cache = {};

  // 透过扩展运算符,拿到引数
  return (...args) => {
    // 将引数当作快取的 key
    const key = JSON.stringify(args);
    // 查看现在的快取有没有这个 key,有的话就不用再次运算,直接回传
    if (key in cache) {
      return cache[key];
    } else {
      // 没有的话,就把收到引数带入,运算出结果
      const val = fn(...args);
      // 把结果放入快取,下次有同样的 key 就不用重新运算
      cache[key] = val;
      return val;
    }
  };
}
  • 闭包的应用 3 — 模拟外部无法直接访问的变量。
var counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    },
  };
})();

console.log(counter.value()); // logs 0
counter.increment();
counter.increment();
console.log(counter.value()); // logs 2
counter.decrement();
console.log(counter.value()); // logs 1