思维导图
函数中的作用域
函数作用域定义
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)
var x = -1; function foo() { var a = 0; function fxx() { var b = 1 } let c = 2; } 复制代码
- 全局作用域内包含 foo函数,变量x。
- foo函数的函数作用域包含变量a,c和函数fxx。但是还可以访问外部作用域的变量(全局作用域)
- fxx函数的函数作用域包含变量b,但是还可以访问外部作用域的变量(foo函数作用域和全局作用域)
总结 被包含的“小作用域”可以访问外侧“大作用域”。
最小授权
创建一个函数就会产生一个函数作用域,那么外部作用域就无法访问函数作用域内部的变量和函数,这样就达到了:“私有化变量”的目的。
规避冲突
1. 全局命名空间
变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果它 们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。
这些库通常会在全局作用域中声明一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。
2. 模块管理
通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中。
核心 就是为变量和函数都创建各自的作用域,这样“各扫门前雪”,不会共享作用域,避免命名冲突。
函数作用域
函数名污染作用域
创建一个函数就会产生一个函数作用域,内部的变量和函数就会被“私有化”,但是这样也会在外部作用域增加一个函数名,使外部作用域改变(多控制了一个刚刚声明的函数名)。
立即函数调用(IIFO)
(function foo() { var a = 0; function fxx() { var b = 1 } let c = 2; })() (function foo() { var a = 0; function fxx() { var b = 1 } let c = 2; }()) 复制代码
上面这2种写法都是立即调用函数。立即调用函数是一个函数表达式,不是函数声明(代码最左侧function开头的才是),这样函数名就不是外部作用域所包含的,而是函数内部包含的。
如果将一段代码中的任意一部分拿出来用函数进行包裹,会改变这段代码的含义,其中的 this、return、break 和 contine 都会 发生变化。所以不用随便使用IIFO去切割代码。
匿名函数表达式
函数表达式可以是匿名的, 而函数声明则不可以省略函数名,不然你是不是声明个寂寞。
setTimeout(function () { console.log("I waited 1 second!"); }, 1000); 复制代码
- 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
- 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee 引用, 比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
- 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让 代码不言自明。
行内函数表达式
行内函数表达式有不有名字都无所谓,但是始终给函数表达式命名是一个最佳实践。
setTimeout(function myFunction() { console.log("I waited 1 second!"); }, 1000); 复制代码
undefined标识符
这点你肯定觉得神奇,undefined
是一个标识符,也就是说它可以作为一个变量(容器),让我们给他赋新值。
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做! (function IIFE(undefined) { var a; if (a === undefined) { console.log("Undefined is safe here!"); } })(); 复制代码
块作用域
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息 扩展为在块中隐藏信息。
for (var i = 0; i < 10; i++) { setTimeout(function () { console.log(i) // 全是10 }, 0) } 复制代码
这是为什么,每次循环我们都声明了一个匿名函数到事件队列,但是这些声明的函数都是共享的一个作用域,等到for循环执行完毕,i = 10,那么这些函数就该执行了,自然都会去找这个i = 10
的麻烦。
但是在我们使用了块作用域就不一样了,先卖个关子,在let那讲。
with
用 with
从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效,但是在这个创建的作用域中也可以声明全局变量(就是漏掉var 去声明变量a = 1
这个a 就跑到全局作用域去了)。
try/catch
JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作 用域,其中声明的变量仅在 catch 内部有效。
try { undefined(); // 执行一个非法操作来强制制造一个异常 } catch (err) { console.log(err); // 能够正常执行! } console.log(err); // ReferenceError: err not found 复制代码
let
let 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。换句话说,let 为其声明的变量隐式地了所在的块作用域。
let循环
for (let i = 0; i < 10; i++) { setTimeout(function () { console.log(i) // 1-9 }, 0) } 复制代码
每次循环都会产生一个块级作用域,和变量i的当前值进行绑定。这样每个块级作用域绑定的变量就是不一样的值,最后每个匿名函数会去对应绑定的块级作用域中找到对应的变量i。
垃圾回收
了解闭包的都应该知道,闭包保存着对外部函数作用域的变量引用。这样即使外部函数销毁了,但是变量被闭包引用着,就不会被回收销毁。使用块级作用域就可以避免这种情况。
function process(data) { // 在这里做点有趣的事情 } // 在这个块中定义的内容可以销毁了! { let someReallyBigData = { .. }; process(someReallyBigData); } var btn = document.getElementById("my_button"); btn.addEventListener("click", function click(evt) { console.log("button clicked"); }, /*capturingPhase=*/false); 复制代码
const
声明的变量是常量,但是如果是对象,那么这个对象的引用是常量,但是对象不是常量。还是可以添加删除属性,方法什么的。
块级作用域的替代方案
try/catch的catch分句就会产生一个块作用域。当你讲代码转换为ES6之前就可能看到它了。
隐式和显式作用域
let 作用域或 let 声明
let(a = 2) { console.log(a); // 2 } console.log(a); // ReferenceError 复制代码
隐式使用会劫持一个已经存在的作用域 let声明(显示使用)会创建一个显示的作用域并与其进行绑定。
注意 let 声明并不包含在 ES6 中。官方的 Traceur 编译器也不接受这种形式的代码。