文章目录
- 一. 什么是作用域?
- 二. `var a = 2`是如何赋值并添加到作用域中的?
- 三. 作用域链
- 四. js中的各种作用域
- 五. 闭包?
- 六. 参考
今天开始读了《你所不知道的JavaScript(上卷)》的一部分, 自己对于 JS 的理解还是非常浅薄的; 本着学习与分享的目的, 对这本书的第一章的内容进行理解与总结; 在浏览本文后, 如果发现有什么错误的地方,或者不到位的地方,欢迎指正!!!
一. 什么是作用域?
存储变量的值,并且之后可以对这个值进行访问或修改, 是几乎所有变成语言最基本的功能之一. 但是这些变量存在哪里, 如何快速找到他们, 这需要设计一套良好的规则来存储变量, 这套规则被称为作用域。(— 《你所不知道的JavaScript(上卷) 》)
在 MDN 对于js的作用域给出了这样的一个概念:
作用域是当前的执行上下文,在其中的值和表达式“可见”(可被访问)。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。
简单来说: js 的作用域是变量或表达式的作用范围
二. var a = 2
是如何赋值并添加到作用域中的?
当我们编写了 var a = 2
这段程序是如何执行的呢? 又是怎么添加到作用域中的呢?
要理解这个问题, 先介绍一下js的三个好朋友
- 引擎: 负责整个js程序的编译及执行过程
- 编译器: 负责语法分析及代码生成
- 作用域: 负责收集并维护所有标识符(变量)组成的而查询,并实施一套非常严格的规则
当我们在运行var a = 2
这段程序, 会按照如下顺序去执行
- 编译器会将这段程序进行分词, 将词法单元解析成一个树状结构,
- 遇到
var a
, 编译器判断同一作用域集合中是否存在同名称的变量, 存在就会忽略该声明, 否则他会要求在作用域中声明一个新的命名为a的变量 - 编译器为引擎生成运行时的代码, 用来处理
a = 2
这个操作; - 引擎运行生成的代码, 现在同一作用域中去找名为a的变量, 如果存在就会使用这个变量, 如果没有找到就会在上一级作用域去寻找
- 引擎找到名为a的变量,将2赋值给他
三. 作用域链
作用域链是 JavaScript 中用于查找变量和函数的机制。它是由当前执行环境的一系列作用域对象组成的一个链式结构。
当在js中使用到一个变量时, 引擎会在当前作用域下去寻找该变量, 若没有找到, 就会到他的上层作用域去找, 若还没有找到会以此类推, 直到找到最外层作用域(全局作用域)为止;
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
var a = 1function foo() {function bar() {console.log(a) // 1}bar()
}foo()
上段代码中使用到了a变量, 首先会在当前的作用域 (bar) 中找 a, 当前作用域中没有 a 变量, 再向上一级作用域( foo )中寻找, foo中的作用域也没有, 最后到全局作用域中找到了 a 变量;
四. js中的各种作用域
JavaScript 的作用域分以下几种:
- 全局作用域:脚本模式运行所有代码的默认作用域
全局作用域中的变量和函数可以在代码的任何位置被访问(只要在同一个 JavaScript 环境中)。
var a = 1;
console.log(a); // 1
- 模块作用域:模块模式中运行代码的作用域
模块可以包含变量、函数、类等各种定义,并且这些定义在模块外部是不可直接访问的,除非通过模块暴露的接口。这种作用域有助于防止命名冲突,同时也使得代码的组织更加清晰和模块化。
// 定义一个模块(myModule.js)
const myVariable = 10;
function myFunction() {console.log(myVariable);
}
export { myFunction };
- 函数作用域:由函数创建的作用域
函数内部定义的变量和函数具有函数作用域。这些变量和函数只能在函数内部被访问,外部无法直接访问函数内部的变量(除非通过闭包等特殊机制)。
function foo() {var a = 10;function bar() {var b = 20;console.log(a + b); // 30}bar();
}
foo();
console.log(a); // 会报错, a is not defined
- 块级作用域:用一对花括号(一个代码块)创建出来的作用域
使用let和catch语句块(try - catch结构中的catch块)等方式可以创建块级作用域。在一个块(由{}包围的代码区域)内部定义的变量,其作用域被限制在这个块内。
{let a = 10;console.log(a);
}
console.log(a); // 会报错,a is not defined
五. 闭包?
闭包是指有权访问另一个函数作用域中变量的函数。简单来说,当一个函数内部返回了一个新的函数,并且这个新函数可以访问到外部函数的变量,就形成了一个闭包。
闭包形成的条件
- 函数嵌套:必须有内部函数和外部函数的嵌套结构。外部函数定义了变量,内部函数能够访问这些变量。
- 内部函数被返回或传递到外部:内部函数需要以某种方式(如返回值、作为参数传递等)离开它原本的外部函数作用域,这样它才能在外部环境中被调用,并且依然能够访问外部函数中的变量。
function foo() {var a = 10;return function bar() {console.log(a);};
}
var baz = foo();
baz(); // 输出10, 没错这就是闭包
闭包的作用
- 数据隐藏和封装:闭包可以用于隐藏数据,外部无法直接访问内部函数所依赖的变量,只能通过返回的函数接口来操作。例如,在一个模块系统中,可以使用闭包来封装模块内部的状态变量,只暴露一些特定的方法来修改和获取这些变量的值。
- 函数式编程中的柯里化(Currying):闭包在函数式编程中用于实现柯里化。柯里化是把一个多参数的函数转换为一系列单参数的函数。
- 实现私有变量和方法:在 JavaScript 中没有像其他语言一样的私有成员访问修饰符,但是可以利用闭包来模拟私有变量和方法。
下面是使用闭包实现似有变量的例子:
var counter = (function() {var count = 0;return {increment: function() {count++;return count;},getCount: function() {return count;}};
})();
console.log(counter.increment()); // 输出1
console.log(counter.getCount()); // 输出1
闭包的潜在问题(内存泄漏)
由于闭包会使得内部函数引用外部函数的变量,在某些情况下,如果内部函数一直存在并且引用了外部函数中的大量变量,这些变量所占用的内存就无法被释放,从而导致内存泄漏。
例如,在浏览器的 DOM 事件处理中,如果一个闭包函数引用了一个包含大量数据的对象,并且这个闭包函数一直被添加到 DOM 元素的事件监听器中,那么这个对象就可能无法被垃圾回收,导致内存泄漏。解决这个问题通常需要在适当的时候移除事件监听器或者手动释放对闭包函数所引用对象的引用。
六. 参考
- mdn-作用域
- 《你所不知道的JavaScript(上卷)》
- 理解Javascript的作用域和作用域链
- 彻底弄懂JavaScript作用域问题