作用域与闭包
LHS 引用与 RHS 引用
LHS 引用类似于左值引用,即我们要赋值给哪一个变量,此时值是主动的,他寻找变量!
如 const a = 2
其中值 2 主动赋值给左侧的 a 变量
RHS 引用类似于右值引用,即寻找赋值操作的源头,此时值是被动的,他需要找到发起赋值请求的主动变量;
如 fn(2)
其中 2 作为实参传入函数,他需要找到被赋值的形参
不能简单地理解为:等号左侧是变量就是 LHS,等号右侧时变量就是 RHS
ReferenceError 异常
一般的,无论 LHS 还是 RHS,在当前作用域找不到对应变量时就会不断往外层作用域查询,直到找到为止;
非严格模式下
,若最外层仍然找不到,系统自动创建对应名称全局变量,赋予未定值并返回;
严格模式下
,若最外层找不到,直接抛出 ReferenceError 异常
试图对一个非函数类型的值进行函数调用,或者引用 null 或 undefined 类型的值中的属性,那么引擎会抛出 TypeError 异常
欺骗词法作用域
尽量不要使用欺骗,有证明将会导致性能下降
词法作用域可以简单的理解为上一节讲述的作用域概念;
使用 eval()方法,在代码中的某处再插入一行代码,让编译器认为该行代码是原本存在的,即可达到欺骗效果;
如下代码,通过 eval 插入定义变量 b,让下一行代码中的 b 只在本层中找到变量 b,而不向外层查找,此即为欺骗
function foo(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
foo("var b = 3", 1); // 1,3
作用域变量冲突
如下方代码将会导致死循环,因为每次执行 bar 函数,都会设置 i=3,且这个 i 会被默认指定为 for 循环中的 i,那么永远存在 i==3<10,所以必定无限循环!!!
function foo() {
function bar(a) {
i = 3; // 此处的i会被误指定为for中的i
console.log(a + i);
}
for (var i = 0; i < 10; i++) {
bar(i * 2); // 无限循环
}
}
函数术语释疑
函数声明:即一个正常的函数 function foo(){}
函数表达式:开头关键词不是 function 的任意一个函数 (function(){})()
匿名函数表达式:function(){} 或者 ()=>{}
立即执行函数表达式 IIFE:(function(){})()
立即执行函数详解
有效避免变量污染,IIFE 函数内部的表达式访问范围仅限于该函数,而不会外泄:
var a = 2;
(function () {
var a = 3;
console.log(a); // 3
})();
可以为 IIFE 函数传入一个参数,以便调用外部的变量
下面将 window 作为实参传给了内部函数 IIFE 的形参 global
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})(window);
可以倒置函数的运行过程;
将小括号里面的 def 函数作为参数传递给内部 foo 函数,然后 foo 再次调用并执行 def
var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
});
可以把 IIFE 函数末尾的小括号写在外面
(function(){})()
或者写在里面(function(){}())
二者的运行结果一致,但大家普遍喜欢写在里面
块作用域
一个普通的块内的函数、变量,都会在执行完毕后自行垃圾回收掉
{
let a = 1;
function(){}
}
作用域提升
对于 var 声明的变量,它具有作用域提升,但提升的只是声明,而真正的赋值操作仍然保留在原地;
故以下代码输出 undefined,因为真正的赋值在我们使用后
console.log(a); // undefined
var a = 10;
函数声明优先于变量声明;
顾名思义,同名的函数/变量,会优先把函数提升到顶层,然后再到变量声明
foo(); // 1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
};
作用域闭包
所谓闭包,即能持有对内部作用域的访问,且该引用不会被垃圾回收掉!
如下代码就展示了闭包的使用效果
- 通过执行 foo()得到一个 bar()函数作为返回值
- 然后执行 baz()来运行对应的 bar()
- 此刻我们可以看到 bar()函数能轻松地访问到内部函数作用域中的 a 值,并且因为变量 baz 的存在,永远不会 GC 掉这个作用域
这就是闭包的基本原理,其他的代码也可以实现,只要记住这个基本原则即可!
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2
因为 settimeout 倒计时完毕,是吧事件添加到事件触发线程的任务队列中去,等到整个 JS 执行线程空了才会执行事件触发线程;
所以我们需要在 for 中使用一个 IIFE 函数来保证最终的结果是正确的;
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(() => {
console.log(j);
}, j * 1000);
})(i);
}
如果不使用 IIFE 函数,直接写 settimeout,那么最终结果输出 5 个 6,而不是 1-5 逐一输出!
模块
对于一个模块,在 return 中使用对象式写法,将内部对函数暴露出来,使得外部函数可以调用内部函数的词法作用域!
但是不可以暴露模块内的变量;
function CustomModule() {
var something = "none";
function doSomething() {
console.log(something);
}
return {
doSomething: doSomething,
};
}
var module = CustomModule();
module.doSomething(); // none
模块模式必备的两个条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态