目录
# 1.作用域
回顾下 jQuery 源码中,代码是包在
(function(){//代码})()
当中。主要的目的是希望里面的所有变量,不会暴露到外面,以防止变量全局污染,这就是函数作用域。
在 JS 中,变量的作用域有两种:全局作用域和函数作用域。ES6 中有了块级作用域的概念,使用 let、const 定义变量即可。
# 1.1 全局作用域
(1)没有用
var
声明的变量(除去函数的参数)都具有全局作用域,成为全局变量;(2)
window
的所有属性都具有全局作用域;(3)最外层函数体外声明的变量也具有全局作用域
最外层的作用域,具有全局作用域的变量可以被任何函数访问。
var data = { name: 'peter' };
(function f1() {
console.log('data', data); //{name:"peter"}
})();
这样的坏处就是变量间很容易产生冲突。比如开发者 A 定义一个变量,开发者 B 又定义了一个同名变量:
var data = { name: 'peter' }; //A定义
//省去100行代码
var data = { name: 'lily' }; //B定义
这样变量就很容易冲突。
# 1.2 函数作用域
在函数作用域中定义的变量,在函数外部是无法访问的,会报错:
function f1() {
var data = { name: 'peter' };
}
console.log('data', data); //Uncaught ReferenceError: data is not defined
# 1.3ES6 中的块级作用域
什么是块级作用域呢?
任何一个对花括号
({})
中的语句都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,称之为块级作用域
。
let
所声明的变量,只在使用let
所在的代码块内有效,在代码块外调用let
声明的变量则会报错:
示例:
{
var a = { name: 'peter' };
let b = { name: 'lily' };
console.log('inner a', a); //{name:"peter"}
console.log('inner b', b); //{name:"lily"}
}
console.log('a', a); //a{name:"peter"}
console.log('b', b); //Uncaught ReferenceError: b is not defined
说明:
(1)不存在变量提升,使用 let 命令声明的变量,只能在声明后使用,语法上称为“
暂时性死区
”(2)使用 const 声明的变量不能再次赋值。
# 2.变量提升、函数提升
示例:
console.log('f1', f1);
function f1() {
console.log('enter f1');
}
var f1 = 1;
从结果可以看出打印的 f1 是函数,不是undefined
,也不是后面变量声明的 1。这说明:函数提升优先于变量提升,会优先处理函数声明,再处理变量声明。
若声明的变量和声明的函数重名,则变量的声明不会影响函数的声明。
小结:
(1)使用
var
声明的变量存在变量提升
,即可在声明变量之前使用变量。使用let
、const
声明的变量不能在声明变量之前使用变量。(2)
函数提升
优先于变量提升
,函数提升会把整个函数挪到作用域顶部,变量提升会把变量声明挪到作用域顶部。
# 3.作用域链
要得到一个变量的值,若当前作用域没有定义,就到父级作用域寻找。如果父级作用域中也没找到,就再向一层去寻找,直到找到全局作用域。这种一层一层的关系,就是作用域链。
示例:
var data = { name: 'peter' };
(function f1() {
var a = 1;
(function f2() {
var b = 2;
console.log('data', data); //{name:"peter"},data向上顺着作用域寻找
console.log('a', a); //a向上顺着作用域寻找
console.log('b', b); //b是当前作用域的变量
console.log('c', c);
})(); //立即执行f2
})(); //立即执行f1
# 4.闭包
闭包:有权访问另一个作用域中的变量的函数。
换个直观的说法,在函数 A 内部有个函数 B,函数 B 可以访问函数 A 中的变量,函数 B 就是闭包:
function A() {
var a = 1;
B = function() {
console.log('a in function B', a);
};
}
A();
B(); //执行B,结果是:1
优点:
可以避免全局变量的污染 缺点:
参数和变量不会被垃圾回收机制回收,闭包会常驻内存,增大内存使用率,使用不当容易造成内存泄漏。 关于闭包有个经典问题,就是循环中使用闭包解决用 var 定义变量的问题,下面有两个示例:
示例 1:
for (var i = 1; i <= 8; i++) {
setTimeout(function() {
console.log(i); //结果:每秒输出一个9,共输出8个9
}, i * 1000);
}
结果:
控制台打印的结果并不是原本以为的,按秒输出 1,2,3,…,7,8,。而是按秒输出 9,9,9,…,9,9,共输出 8 个 9。
原因是 setTimeout()是一个异步函数,会先把循环执行完,所以 i 的值是 9,然后再按 1 秒一个,共输出 8 个 9。
类似的问题还有给下面的每个<li>
加个点击事件,点击对应的<li>
就弹出对应<li>
中的内容,示例二:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
js:
var aLi = document.getElementsByTagName('li');
for (var i = 0; i < aLi.length; i++) {
aLi[i].addEventListener('click', function() {
alert(i + 1);
});
}
其实执行后会发现,不管点击哪个<li>
,弹出的都是7
。
上述两个例子的结果都不符合预期,解决的方法是可以通过闭包来解决:
针对示例一的解决方法:
for (var i = 1; i <= 8; i++) {
(function(j) {
setTimeout(function() {
console.log(j); //结果:按秒输出1,2,3,...,8
}, j * 1000);
})(i);
}
当然除了闭包的解决方法,最简单的解决方法是使用 let 替代 var 来定义 i:
for (let i = 1; i <= 8; i++) {
setTimeout(function() {
console.log(i); //结果:按秒输出1,2,3,...,8
}, i * 1000);
}
针对示例二的解决方法:
var aLi = document.getElementsByTagName('li');
for (var i = 0; i < aLi.length; i++) {
(function(j) {
aLi[j].addEventListener('click', function() {
alert(j + 1);
});
})(i);
}
# 5.小结
本文主要分别介绍了作用域、作用域链、变量提升和闭包的内容,作为 JS 基础知识总结的最后一篇。如有问题,欢迎指正。