JS作用域与闭包
2023-10-16 16:15:28 #JS

作用域

  • 作用域是当前的执行上下文,值和表达式在其中“可见”或可被访问
  • 如果一个变量或表达式不在当前的作用域中,那么它是不可用的
  • 作用域可以堆叠成层次结构,子作用域可以访问父作用域,反之父作用域不可以访问子作用域

全局作用域

  • 任何不在函数中或是大括号中声明的变量,都是在全局作用域中
  • 全局作用域中声明的变量可以在程序的任意位置访问

函数作用域

  • 函数作用域也叫局部作用域,如果一个变量是在函数内部声明的,那么它就是在一个函数作用域中
  • 函数作用域中的变量只能在函数内部访问,不能在函数以外访问

块级作用域

  • ES6 引入了块级作用域,块级作用域是使用 letconst 声明的变量所在的由一对花括号(一个代码块)所创建出来的作用域
  • 块级作用域只对 letconst 声明有效,对 var 声明无效
  • 块级作用域解决了“内层变量可能会覆盖外层变量”、“用来计数的循环变量泄露为全局变量”等不合理问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ES6
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}

function f2() {
let s = 'hello';
for(let i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // ReferenceError: i is not defined
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ES5
function f1() {
var n = 5;
if (true) {
var n = 10;
}
console.log(n); // 10
}

function f2() {
var s = 'hello';
for(var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
}
  • 允许块级作用域的任意嵌套
  • 内层作用域可以定义外层作用域的同名变量
1
2
3
4
5
6
7
8
{{{
{
let word = 'hello';
{let word = 'world'}
console.log(word); // 'hello'
}
console.log(word); // ReferenceError: word is not defined
}}};
  • 立即执行匿名函数(IIFE)不必要了
1
2
3
4
5
6
7
8
9
10
11
// 块级作用域写法
{
let tmp = ...;
...
}

// 等同于 IIFE 写法
(function() {
var tmp = ...;
...
}());

块级作用域与函数声明

  • ES5 规定函数只能在全局作用域和函数作用域中声明,不能在块级作用域中声明
  • ES6 允许在块级作用域中声明函数,在浏览器的 ES6 环境中,块级作用域内声明函数的行为类似于 var 声明,会提升到所在的块级作用域的头部
  • 考虑到兼容性,应该避免在块级作用域内声明函数,如有需要,应该写成函数表达式的形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 浏览器的 ES6 环境
function f() {
console.log('outside');
}
(function () {
if(false) {
function f() {
console.log('inside');
}
}
f(); // TypeError: f is not a function
}());

// 实际运行等同于
function f() {
console.log('outside');
}
(function () {
var f = undefined;
if(false) {
function f() {
console.log('inside');
}
}
f(); // TypeError: f is not a function
}());
1
2
3
4
5
6
7
8
9
10
11
function f() {
console.log('outside');
}
(function () {
if(false) {
let f = function() { // 改写为函数表达式形式
console.log('inside');
}
}
f(); // 'outside'
}());

作用域链

  • 作用域链是 JavaScript 中作用域的嵌套关系,由当前函数的变量对象和所有父级函数的变量对象构成
  • 当访问一个变量时,JavaScript 引擎会首先搜索当前函数的变量对象,如果找不到,则继续向上查找父级函数的变量对象,直到找到该变量或查找到全局作用域为止,这个查找的过程就是作用域链的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn() {
var a = 10;
function fn1() {
var b = 20;
console.log(a); // 10
function fn2() {
console.log(b); // 20
console.log(a); // 10
}
fn2();
}
fn1();
}
fn();
console.log(b); // ReferenceError: b is not defined

词法作用域

  • 词法作用域(也叫静态作用域),指变量的作用域是在代码编写阶段确定的,而不是在代码运行阶段确定的
  • JavaScript 是一种基于词法作用域的语言
1
2
3
4
5
6
7
8
9
10
11
12
13
// 无论 printNumber() 在哪里被调用,console.log(number)都会打印 10
let number = 10;

function printNumber() {
console.log(number);
}

function log() {
let number = 20;
printNumber();
}

log(); // 10

自由变量

  • 自由变量是指,在当前作用域没有定义但被使用了的变量,即跨越了自己的作用域的变量
  • 自由变量会向上级作用域,一层一层依次寻找,直至找到为止,如果直到全局作用域都没有找到该变量,则报错 xx is not defined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let a = 1;
function fn1() {
let a1 = 100;
function fn2() {
let a2 = 200;
function fn3() {
let a3 = 300;
console.log(a + a1 + a2 + a3); // 601
}
fn3();
}
fn2();
}
fn1();

闭包

闭包是什么

  • 简单地说,闭包是指能够访问自由变量的函数

  • 通过闭包可以访问创建闭包时所处环境中的所有变量

创建闭包的方法

  • 在一个函数的内部创建另一个函数,且在内部函数中引用了外部的变量,则创建了闭包
  • 闭包不仅包含了函数的声明,还包含了在函数声明时该作用域中的所有变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let n = 1;
function fn1() {
let n = 999;
add = function () { // add 为全局变量,其值是一个匿名函数,为一个闭包
n += 1;
console.log(n);
}
function fn2() { // fn2 是 fn1 的子函数,且作为 fn1 的返回值被返回,为一个闭包
console.log(n);
}
return fn2;
}

let result = fn1(); // 将 fn1 的返回结果赋值给全局变量
result(); // 999
add(); // 1000
result(); // 1000

result 一共执行了两次,第一次的值是 999,第二次的值是 1000,说明了函数 fn1 中的局部变量 n 一直维持在内存中,并没有在 fn1 调用执行完之后被自动清除。

因为 fn1 是 fn2 的父函数,而 fn2 通过 fn1 的 return 语句赋值给全局变量 result,因此 fn2 始终维持在内存中,而 fn2 依赖于 fn1,因此 fn1 也维持在内存中,不会在调用 fn1 结束后,被垃圾回收机制回收。

函数作为返回值被返回

1
2
3
4
5
6
7
8
9
10
function create() {
const a = 100;
return function () {
console.log(a);
}
}

const fn = create();
const a = 200;
fn(); // 100

函数作为参数被传递

1
2
3
4
5
6
7
8
9
10
11
12
function print(fn) {
const a = 100;
fn();
}

const a = 200;

function fn() {
console.log(a);
}

print(fn); // 200

注意!所有自由变量的查找,是在函数定义的地方(而不是函数执行的地方),向上级作用域查找。

闭包的作用

  • 将代码封装成一个闭包环境,用特定的方法管理私有变量,将变量的变化封装在安全的环境中
  • 闭包中的变量常驻在内存中,用作缓存

闭包的使用场景

  • 函数中的作用域仅供自己所有,外部无法直接去访问函数中的变量或方法,且函数执行后,会将其中的变量进行销毁
  • 通过作用域链从内部函数到外部函数向上层作用域逐一查找的方式获取外部函数中的变量,外部函数再将其进行返回,赋值给外部变量,这样就可以进行对函数内部私有变量和方法的管理和操作
  • 闭包的最大特性是,如果内部函数引用或访问了外部函数的某个变量,那么这个变量将会和内部函数一同存在,不会被销毁,直至被访问的这个函数被销毁时,这个变量才会被释放

封装私有变量

  • 构造函数内部所声明的变量的作用域局限于构造函数内,所以是“私有变量”
  • 可以通过闭包内部的方法(getter)获取私有变量的值,但是不能直接访问私有变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用闭包模拟私有变量
function Fn() {
let counts = 0; // “私有”变量
this.getCounts = function () { // getter 方法用于只读私有变量
return counts;
};
this.count = function () { // 一些业务逻辑的处理方法
counts++;
};
}

let fn1 = new Fn();
fn1.count();
console.log(fn1.counts); // undefined(不能直接访问私有变量)
fn1.getCounts(); // 1(可通过闭包内部方法获取私有变量的值)

let fn2 = new Fn();
fn2.getCounts(); // 0

处理回调函数

  • 回调函数指的是需要在将来不确定的某一时刻异步调用的函数
  • 在回调函数中,需要频繁地访问外部数据
  • 闭包内的函数不仅可以在闭包创建的时刻访问这些变量,而且当闭包内部的函数执行时,还可以更新这些变量的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在 interval 的回调函数中使用闭包
function animateIt(elemId) {
let elem = document.getElementById(elemId);
let tick = 0;
let timer = setInterval(function () {
if (tick < 100) {
elem.style.top = elem.style.left = tick + 'px';
tick++;
} else {
clearInterval(timer);
}
}, 10);
}
animateIt('box1');

手写 bind 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Function.prototype.myBind = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments);
// 获取 this(数组第一项)
const _this = args.shift();
// 获取当前调用的对象
const self = this;
// 返回一个函数
return function () {
return self.apply(_this, args);
}
}


function fn1(a, b, c) {
console.log(this); // {x: 100}
console.log(a, b, c); // 10 20 30
return 'this is fn1';
}

const fn2 = fn1.myBind({x: 100}, 10, 20, 30);
fn2();

实现简单的 cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {};
return {
set: function (key, val) {
data[key] = val;
},
get: function (key) {
return data[key];
}
}
}

const data_cache = createCache();
data_cache.set('a', 100);
const result = data_cache.get('a');
console.log(result); // 100