BY 张建成(prettyEcho@github)
除非另行注明, 页面上所有内容采用知识共享 - 署名 (CC BY 2.5 AU) 协议共享
我们常说, 万物都有其存在的价值, 这话的确不错, 但是深思一下, 是不是需要有个前提, 万物都在某些领域或多或少的存在某些价值
举个例子, 汽车, 绝对是个非常有价值的 stuff, 它给我们的日常出行, 货物运输等带来了极大的便利; 筷子, 同样也是个非常有价值的 stuff, 它给我们吃饭带来了极大的方便但是, 汽车能帮我们把菜送到嘴里吗? 筷子能载着我们出行吗?
那么, 我上面所说的某些领域, 我们是不是可以称其为作用域, 我想是可以的
说到这, 那么我就想问了: 在 JS 里, 作用域是不是也是类似的概念呢?
首先, 我可以肯定的说这是一个在 JavaScript 中灰常灰常重要的概念, 关系着 JS 里很多核心的机制, 理解它, 很多问题都迎刃而解了
那么, 问问自己, 在 JS 里, 作用域是什么?
心里大概知道是什么, 但是细细一想又好像说不太清
没关系, 下面我们就细细品味这个有意思的东东
先 throw 概念吧:
作用域负责收集并维护由所有声明的标识符 (变量) 组成的一系列查询, 并实施一套非常严格的规则, 确定当前执行的代码对这些标识符的访问权限
通俗来说, 作用域相当于一个管理员(有自己的一套规则), 他负责管理所有声明的标识符的有序查询
我们来讲个故事, 说说作用域到底干了啥
三兄弟齐上阵
long long ago, 有 3 个关系很好的基友, 老大叫引擎, 老二叫编辑器, 老三叫作用域三兄弟眼看年岁已长, 可手上还是没有几个银子个个都很着急, 于是三兄弟谋划一同做个事
求职过程: 此粗略去数万个字
最终他们做的工作是: 负责 JS 的编译和运行
他们的工作内容是这样的:
老板甩给他们一项任务编译并执行下面代码:
- var a = 1;
- console.log( a );
开始工作:
编译器: 作用域, 帮我看看你那有没有储存变量 a
作用域: 二哥, 还没有
编译器: 那好, 帮我储存一个
引擎: 老三, 你那有没有一个叫做 a 的变量
编译器: 大哥, 还真有, 刚二哥让我存储了一个
引擎: 真是太好了, 帮我拿出来, 它的值是几, 我需要给它复制
编译器: 大哥, 它的值是 2
引擎: 谢谢你, 三弟, 这样我就能打印它的值了
上面讲了一个不恰当的小故事, 但是三者之间的关系大概就是这样
词法作用域 VS 动态作用域
词法作用域
彻底搞懂 JavaScript 作用域里介绍过, 大部分标准语言编译器的第一个工作阶段叫作词法化 (也叫单词化) 回忆一下, 词法化的过程会对源代码中的字符进行检查, 如果是有状态的解析过程, 还会赋予单词语义
在 JS 里, 使用的作用域就是词法作用域
简单地说, 词法作用域就是定义在词法阶段的作用域换句话说, 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的, 因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)
动态作用域
在 JS 里, 动态作用域和 this 机制息息相关它的作用域诗是在运行的过程中确定的
- var a = 1;
- function foo() {
- var a = 2;
- console.log( this.a );
- }
- foo(); // 1
从上面的代码, 我们可以看出: foo 中打印 a 的值不是由写代码的位置确定的, 而是取决于 foo 执行的位置
区别
词法作用域是在写代码或者说定义时确定的, 而动态作用域是在运行时确定的(this 也是!)
词法作用域关注函数在何处声明, 而动态作用域关注函数从何处调用
函数作用域
JS 里, 生成作用域的方式:
函数
witheval (不建议使用, 影响性能)
由此, 我们知道 JS 里, 绝大多数的作用域都是基于函数生成的
每个函数都会为自身生成一个作用域气泡这个气泡内所有的标识符都可以在这个气泡中使用
- function bar() {
- var a = 1;
- function foo() {
- var b = 2;
- console.log(b);
- }
- foo();
- console.log(a);
- }
- bar();
上面代码, bar 气泡有标识符 afoo, 因此在 bar 气泡中可以访问到 afoo; foo 气泡有标识符 b, 因此在 bar 气泡中可以访问到 b; 当然还有一个全局气泡, 全局气泡中有 bar 标识符, 因此在全局气泡中可以访问到 bar
最小授权原则
最小授权原则是指在软件设计中, 应该最小限度地暴露必要内容, 而将其他内容都隐藏起来, 比如某个模块或对象的 API 设计
这个原则可以延伸到如何选择作用域来包含变量和函数如果所有变量和函数都在全局作 用域中, 当然可以在所有的内部嵌套作用域中访问到它们但这样会破坏前面提到的最小 特权原则, 因为可能会暴漏过多的变量或函数, 而这些变量或函数本应该是私有的, 正确 的代码应该是可以阻止对这些变量或函数进行访问的
例如:
- function doSomething(a) {
- b = a + doSomethingElse( a * 2 );
- console.log( b * 3 );
- }
- function doSomethingElse(a) {
- return a - 1;
- }
- var b;
- doSomething( 2 ); // 15
在这个代码片段中, 变量 b 和函数 doSomethingElse(..) 应该是 doSomething(..) 内部具体 实现的私有内容给予外部作用域对 b 和 doSomethingElse(..) 的访问权限不仅 没有必要, 而且可能是危险的, 因为它们可能被有意或无意地以非预期的方式使用, 从而导致超出了 doSomething(..) 的适用条件更合理的设计会将这些私有的具体内容隐藏在 doSomething(..) 内部,
例如:
- 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() {
- var a = 1;
- }
- function bar() {
- var a = 2;
- }
foo 和 bar 中定义了相同的变量 a, 但是却不会相互造成影响因为函数可以很好的把标识符 "隐藏" 起来
全局命名空间
变量冲突的一个典型例子存在于全局作用域中当程序中加载了多个第三方库时, 如果它 们没有妥善地将内部私有的函数或变量隐藏起来, 就会很容易引发冲突 这些库通常会在全局作用域中声明一个名字足够独特的变量, 通常是一个对象这个对象 被用作库的命名空间, 所有需要暴露给外界的功能都会成为这个对象 (命名空间) 的属 性, 而不是将自己的标识符暴漏在顶级的词法作用域中
例如:
- var myLibrary = {
- name: 'echo',
- getName: function() {
- console.log( this.name );
- }
- }
函数声明 VS 函数表达式
函数声明和函数表达式判别的依据是: 函数的生命是否以 function 关键词开始 以关键词 function 开始的声明是函数声明, 其余的函数声明全部是函数表达式
- // 函数声明
- function foo() {
- }
- // 函数表达式
- var foo = function () {
- };
- (function() {
- })();
具名函数 VS 匿名函数
具名函数 拥有名字的函数
- function foo() {
- }
- var foo = function bar() {
- }
- setTimeout( function foo() {
- } )
- +function foo() {
- }();
需要注意: 函数声明一定要是具名函数
匿名函数 没有名字的函数
- var foo = function () {
- }
- setTimeout( function foo() {
- } )
- -function foo() {
- }();
立即执行函数(IIFE)
- vara=2;
- (function foo() {
- var a=3;
- console.log( a ); // 3
- })();
- console.log( a ); // 2
该函数是以 () 开始, 不是以关键词 function 开始, 因此 IIFE 是函数表达式
函数名对 IIFE 当然不是必须的, IIFE 最常见的用法是使用一个匿名函数表达式虽然使 用具名函数的 IIFE 并不常见, 但它具有以下优势:
匿名函数在栈追踪中不会显示出有意义的函数名, 使得调试很困难
如果没有函数名, 当函数需要引用自身时只能使用已经过期的 arguments.callee 引用, 比如在递归中另一个函数需要引用自身的例子, 是在事件触发后事件监听器需要解绑 自身
匿名函数省略了对于代码可读性 / 可理解性很重要的函数名一个描述性的名称可以让 代码不言自明
因此具名函数的 IIFE 也是一个值得推广的实践
另一种表达形式
- (function() {
- }())
这也是 IIFE 的一种表达方式, 功能上和上面那种方式是一致的选择哪种全凭个人爱好
参数传递
IIFE 也可以和其他形式的函数一样实现参数的传递(多说一句: 参数传递是按值传递)
- (function foo(a) {
- console.log(a);
- })(3);
这个模式的另外一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常 (虽 然不常见) 将一个参数命名为 undefined, 但是在对应的位置不传入任何值, 这样就可以 保证在代码块中 undefined 标识符的值真的是 undefined:
- undefined = true; // 给其他代码挖了一个大坑! 绝对不要这样做!
- (function IIFE( undefined ) {
- var a;
- if (a === undefined) {
- console.log( "Undefined is safe here!" );
- }
- })();
- UMD (Universal Module Definition)
IIFE 还有一种变化的用途是倒置代码的运行顺序, 将需要运行的函数放在第二位, 在 IIFE 执行之后当作参数传递进去尽管这种模式略显冗长, 但有些人认为它更易理解
- var a=2;
- (function IIFE( def ) {
- // 参数的处理
- def( window );
- })(function def( global ) {
- // 逻辑运算
- var a=3;
- console.log( a ); // 3
- console.log( global.a ); // 2
- });
块作用域
尽管函数作用域是最常见的作用域单元, 当然也是现行大多数 JavaScript 中最普遍的设计 方法, 但其他类型的作用域单元也是存在的, 并且通过使用其他类型的作用域单元甚至可 以实现维护起来更加优秀简洁的代码
try...catch 非常少有人会注意到 JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域, catch 的参数变量仅在 catch 内部有效
- try{
- throw undefined;
- }catch(a){
- a = 2;
- console.log(a); // 2
- }
- console.log(a); // ReferenceError
- let
ES6 的标准使我们能够简单的创建块作用域, 其中一个变量定义方式是 let 关键词定义
let 定义的变量具有以下的特点:
let 隐形的创建块作用域({...})
let 声明的变量不能进行变量提升, 因此只能先定义, 后使用
- {
- let a = 1;
- console.log(a); // 1
- }
- console.log(a); // ReferenceError
let 一个典型的应用就是在 for 循环里
我们看下面两个例子:
- // 每秒输出一个 5
- for( var i = 0; i <5 ; i++ ) {
- setTimeout(() => {
- console.log( i );
- }, i *1000)
- }
- // 依次输出 0,1,2,3,4, 时间间隔位 1 秒
- for( let i = 0; i <5 ; i++ ) {
- setTimeout(() => {
- console.log( i );
- }, i *1000)
- }
其原因就是 let 形成了 5 个块作用域, 使每次输出的变量都从本次循环的块作用域中获取
当然我们还可以有其他方式做到第二种效果, 我们将在 闭包, 是真的美中说道
const
除了 let 以外, ES6 还引入了 const, 同样可以用来创建块作用域变量, 但其值是固定的 (常量)之后任何试图修改值的操作都会引起错误
- var foo = true;
- if (foo) {
- var a=2;
- const b = 3; // 包含在 if 中的块作用域常量
- a=3;// 正常!
- b=4;// 错误!
- }
- console.log( a ); // 3
- console.log( b ); // ReferenceError!
作用域链
作用域链是由当前作用域与上层一系列父级作用域组成, 作用域的头部永远是当前作用域, 尾部永远是全局作用域作用域链保证了当前上下文对其有权访问的变量的有序访问
- var a = 2;
- function bar() {
- function foo() {
- console.log(a);
- }
- }
- bar(); // 2
上面代码是由 3 层作用域气泡组成, foo 气泡中试图打印变量 a, 引擎在 foo 气泡中未找到 a 变量, 于是去其父作用域气泡 bar 中寻找... 以此类推直到找到全局作用域气泡, 发现有变量 a, 将其值打印出来如若没找到, 报 ReferenceError 错误
来源: https://juejin.im/post/5ac307285188255c313aef90