本篇文章主要讨论了:
- JavaScript引擎
- 全局对象
- 闭包
- 循环 + 闭包
- IIFE + 闭包
1.JavaScript引擎
-
在执行代码前JS引擎就已经读完了全部代码,如果找到语法错误会直接提示有syntax error
- 所有函数声明会存储在内存中
- 变量初始化不会被运行,但是在词法作用域(lexically-scoped)内声明的变量(用var声明的)名字(赋值不会)会被存储于内存中
2.全局对象
-
所有的变量和函数实际上都是全局对象的属性和方法
- 浏览器的全局对象是’window’对象
- node.js的全局对象是'global'对象
3.闭包
- 函数能引用到母函数(parent function)变量的行为,即能访问到母函数的作用域,即使是在母函数结束运行后。
function makeHelloFunction() {
const message = 'Hello!';
function sayHello() {
console.log(message);
} return sayHello;
}
const sayHello = makeHelloFunction();
console.log('typeof message,', typeof message);
//console.log(message); //会出现ReferenceError
console.log(sayHello.toString());
sayHello();
结果如图:
sayHello()
被执行了,但是是在它被声明的词法作用域外部被执行的。sayHello()
拥有一个词法作用域覆盖着makeHelloFunction()
的内部作用域,闭包为了能使sayHello()
在以后任意的时刻还可以引用这个作用域而保持它的存在。sayHello()
依然拥有对母函数makeHelloFunction()作用域的引用,而这个引用称为闭包。
这个函数在它被编写时的词法作用域之外被调用。闭包使这个函数可以继续访问它在编写时被定义的词法作用域。
加法器是最直观的能观察到闭包的例子:
function makeAddThree() {
var starter = 3;
function add(num) {
return starter + num;
}
return add;
}
const addThree = makeAddThree();
console.log(addThree(0)); // 3
addThree()
就是利用makeAddThree()
作用域对add()
形成的闭包来生成的。
4.循环 + 闭包
最老实巴交的for循环:
for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
}
我们一般会期待这段代码的输出结果是分别打印数字“1”,“2“,”3“,...”5”,一次一个,一秒一个,但实际上我们得到的是“6”被打印五次,1秒1个。因为for
循环的终结条件式i
不<=5
,第一次满足这个条件时的i
是6,所以输出结果反映的是i在循环终结后的最终值。超时的回调函数都将在循环完成之后立即运行。虽然所有这5个函数在每次循环迭代中分离地定义,由于作用域的工作方式,他们都闭包在同一个共享的全局作用域上,而它事实上只有一个i
。
举一反三:
function makeFunctionArray(){
const arr = [];
for(var i = 0; i < 5; i++) {
arr.push(function() {console.log(i);
});
}
return arr;
}
const arr = makeFunctionArray();
arr[0]();
结果为5,因为当调用arr0时循环已经结束,i=5
, i
有作用于函数makeFunctionArray()
闭包,所以被内部函数arr.push()
添加进arr[]
的五个i
全为5。
同理,如果加上console.log
,打印出的结果也为5。
function makeFunctionArray(){
const arr = [];
for(var i = 0; i < 5; i++) {
arr.push(function() {console.log(i);
});
}
console.log(i);
return arr;
}
const arr = makeFunctionArray();
arr[0]();
//结果是5 5
因为i是用var
声明的,var
的有词法作用域作用到for
循环块的}
截止,而此处i
被调用的位置已经结束了循环,所以console
打印5的结果
但是注意!如果把for
循环里的var
换成let
,我们会得到ReferenceError
,因为let
的块级作用域只作用到for
循环的 )
,因此在外部作用域被调用时JS引擎会当其不存在,而显示ReferenceError
。
但是同时,arr[0]();
的结果也变了:
function makeFunctionArray(){
const arr = [];
for(let i = 0; i < 5; i++) {
arr.push(function() {console.log(i);
});
}
//console.log(i);
return arr;
}
const arr = makeFunctionArray();
arr[0](); //结果为0
5.立即执行函数表达式IIFE + 闭包
IIFE(Immediately Invoked Function Expression)
- 会产生闭包
- 不会写入或者修改全局对象
const sayHello = (function makeHelloFunction() {
const message = 'Hello!';
function sayHello() {
console.log(message);
} return sayHello;
})()
sayHello(); // Hello!
也可以用IIFE来做加法器:
var add = (function () {
var counter = 0;
return function () {counter += 1; return counter}
})();
add();
add();
add();
// the counter is now 3
IIFE的好处在于它能创建出一个新的作用域,并且不会写入或者修改全局对象。因为JavaScript中只有全局作用域和函数作用域。为了避免变量污染,应该尽可能地少设置全局变量。
立即执行函数能配合闭包保存状态。
像普通的函数传参一样,立即执行函数也能传参数。如果在函数内部再定义一个函数,而里面的那个函数能引用外部的变量和参数(闭包),利用这一点,我们能使用立即执行函数锁住变量保存状态。
function makeFunctionArray(){
const arr = [];
for(var i = 0; i < 5; i++) {
arr.push((function(a) {
return function(){console.log(a);}
})(i));
}
console.log(i); //5
return arr;
}
const arr = makeFunctionArray();
arr[0](); //0
arr[1](); //1