1.函数作用域
1.1 函数作用域的含义
属于这个函数的全部变量都可以在这个函数的范围内使用及复用(包括嵌套的作用域)。题外提一点:这充分利用了JS变量能够根据需要改变值类型的“动态”特性。
2.函数存在的意义——隐藏内部实现
2.1 隐藏内部实现的意义
- 遵循软件设计中“最小暴露”原则,保持变量原有的私有特性
- 规避同名标识符之间的冲突,避免其导致的值错误覆盖等问题
2.2 隐藏内部实现的常用方法
- 全局命名空间: 在导入第三方库时,这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴露在顶级的词法作用域中。
- 模块管理: 借由模块管理器等类似的模块管理工具。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入另外一个特定的作用域中。其本质就是强制所有标识符都不能注入到共享的作用域中,继续保持在私有、无冲突的作用域中。
3.函数声明和函数表达式
3.1 概念及区别
- 函数声明的名称标识符被绑定在所在的作用域中,而函数表达式的名称标识符被绑定在函数表达式自身所处的函数中。
- 函数声明不能省略函数名(在JS中是非法的),但函数表达式可 以(匿名)。
- 函数声明会被解析器率先读取(声明提前),而函数表达式则需等待解析器执行到它所在的代码时才会真正被解释执行。==> 函数表达式不会被声明提前,而函数声明会。
!! 注意: 将函数声明用圆括号括起来之后,在当前作用域下就不能访问到该函数了,eg:
(function foo() {
// do something...
})
foo() // ReferenceError: foo is not defined
3.2 匿名函数表达式
优点:
- 不用给函数命名,这既节省了空间也避免了函数名称污染所在作用域
- 不用通过函数名显示地调用该段代码执行
- 写起来简便快捷
缺点:
- 没有有意义的函数名,导致调试困难
- 代码可读性大大降低
- 除了使用过时的arguments.callee以外,无法调用自身(无法进行递归)
应用场景:
- IIFE(见下文)
- 作为回调函数使用(作为某函数的参数传入)
最佳实践: 始终保持给函数命名
3.3 立即执行函数表达式(IIFE)
函数声明即调用:用圆括号将函数声明括起来(成为一个函数表达式),而后紧接一个圆括号立即调用该函数表达式
用法:
- 上文中提及的匿名函数表达式声明完立即调用,是最常见的一种用法,eg:
(function IIFE() {
// do something...
})()
- 当作函数调用并传参进去,eg:
var a = 2
(function IIFE(global) {
var a = 3
console.log(a) // 3
console.log(global.a) // 2
})(window)
console.log(a) // 2
- 解决undefined标识符默认值被错误覆盖导致的异常,eg(下面的代码中undefined是一个变量名,而非保留字):
var undefined = true // 千万不要这样做!挖了个深坑
(function IIFE(undefined) {
var a
if(a === undefined) {
console.log("undefined is safe here!")
}
})()
上述代码中的IIFE第二个括号内没有任何参数,即调用IIFE时没有参数传入,那么形参undefined的值就是其默认值undefined,if条件正确,程序会打印输入“undefined is safe here!”这句话。
自己写代码的时候千万千万不要搞什么给变量命名为undefined、null这种骚操作,很容易出bug!!!
- 倒置代码执行顺序:即将要运行的函数放在第二位,在IIFE执行之后当做参数传进去,eg:
var a = 2; // 注意此处的“;”不可少
(function IIFE(def) {
// do something...
def(window)
})(function def(global) {
var a = 3
console.log(a) // 3
console.log(global.a) // w
})
def声明可以写在括号内,也可以在外部定义,然后传一个函数名到第二个括号内,功能上一致。但是,如果将def声明写在外面,它的可访问范围就变大了,这不一定是我们想要的,故最好是写在括号内,保证最小暴露原则。
【题外话】 浏览器中的全局对象是window,但node中的全局对象是global,故上述代码如果直接在node中执行会报错:ReferenceError: window is not defined
4.块作用域
4.1 JS中不存在块作用域?非也
我们编程常常会有如下代码段:
for(var i = 0;i < 10;i++) {
console.log(i)
}
console.log(i) // 依然有效,且i=10
在JS中,即使将变量i声明在for循环的头部,但其并非会就此成为for循环的局部变量,而是会被声明提前,即被声明在for循环所在的作用域中,故此第四行打印也能打印出变量i的值,且i最后的值是for循环完毕之后的值。
由此,很多人可能以为JS中并不存在块作用域一说,但细细深究,其实是有的。
4.2 JS中的作用域
- with
如笔记02中所述,with会在运行时在当前作用域下凭空创建出一个新的作用域,即将一个对象的引用当做作用域来处理,对象中的变量和方法都属于这个作用域,并仅在这个作用域下可访问 - try/catch
catch(err)语句内部接受从try语句中throw出来的数据,并且err参数的值在catch语句外部是不可访问的
ES3规范中规定:try/catch的catch分句会创建出一个块作用域
一个try语句可对应多个catch分句,但每个catch分句都会创建各自的块作用域,互不可访问
- let
ES6规范中引入。let关键字可以将变量绑定在任意作用域中,也就是说,let可以为声明的变量隐式地劫持当前所在的作用域。被let声明的变量不会存在变量的提升
使用let声明的变量不会在块作用域小红进行提升。声明的代码被运行前,声明并不“存在”。
【tips】 至于let的原理,待后文整理闭包的时候再进行总结
- const
const同let,但区别在于用const声明的变量属于常量,声明后不可更改,否则会报错
4.3 块作用域的作用(简述)
- 垃圾回收
在块作用域中如果不存在闭包(详见笔记05),则在该块作用域的代码执行完毕之后,即可被完全销毁,将内存释放。 - let在循环中的作用
循环中用let声明的变量,在每次迭代时,都会被重新绑定,eg:
for(let i = 0;i < 10;i++) {
console.log(i)
}
等价于
{
let j;
for(j = 0;j < 10;j++) {
let i = j // 每次迭代重新绑定
console.log(i)
}
}
【注意】 移动含有let声明的变量的语句时,要连该变量的声明一块儿移动,否则该语句中的该变量将会不可访问。