js中的作用域
1、作用域
js作用域可以隔离变量,减少命名冲突。js作用域分为两种:全局作用域和局部作用域,局部作用域:函数作用域+块级作用域
1.1全局作用域
全局作用域在代码任何地方被访问到。
属于全局作用域有以下三种:
最外层的函数、最外层的变量
任何未经声明就赋值的变量
window对象的属性
function test(a) {
var e = (f = 0);
let g = (h = 1);
function test2() {
let j = (k = 2);
}
test2();
}
console.log(f, h, k); // 函数内部的变量只有函数被调用时才会初始化,详细参考预编译
test(2);
console.log(e, g, j); // 在严格模式下会抛出错误,非严格模式下undefined
console.log(f, h, k); // 0,1,2 任何未经声明就赋值的变量归为全局对象
1.2局部作用域
局部作用域定义的变量外部不能访问。
在ES6之前,局部作用域是只有函数作用域,ES6时新增let和const命令可以实现块级作用域。
1.2.1函数作用域
函数每次调用都会创建一个不同的作用域。所以函数被多次调用时,作用域尽管变量名相同,但是互不干扰。
1.2.2块级作用域
ES6块级作用域由 一组大括号{}组成。
let实现块级作用域
{
let a = 9;
// var a = 8; // 不可以重复声明let声明的变量,var可以
}
console.log(a) // Uncaught ReferenceError: a is not defined
if(){}实现块级作用域
var b = true
if(b){
function aaa(){
}
}
console.log(aaa) // b = true时:function aa(){}、 b = false时:函数声明提升,undefined
还有for实现块级作用域等
块级作用域应用场景:
当for循环碰上异步操作时:var声明的变量没有块级作用域,属于全局,每次循环 i 值都会被覆盖。let声明的变量,在每个块级作用域中 i 值不一样。
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 几乎同时输出3个3(i++会在执行完+1)
}, 5000);
}
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2
}, 5000);
}
2、作用域中变量和函数提升
2.1 提升
作用域中的基础函数和变量在赋值之前,会将声明提升到作用域头部,函数优先提升。变量赋值为undefined,函数赋值为函数体。
提升允许赋值之前使用变量和函数。
console.log(a, b); // undefined , function b(){console.log('test')}
var a = 1;
function b(){console.log('test')}
console.log(a, b); // 1, function b(){console.log('test')}
但是通过let/const声明的变量和函数却不可以提升
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 1;
这是因为即使let/const声明的是全局变量,它也存在在一个看不见的块级作用域,不可以提前使用。
2.2 暂存性死区
上述let声明的变量不能提前使用就是暂存性死区。
暂存性死区是针对变量和表达式声明的函数的,变量所在作用域开始到 let/const 声明的变量结束之间的区域为改变量暂存性死区。
let i = 1;
{
//死区开始
console.log(i); // 报错,死区里边拿不到外边的i,也拿不到本代码块内的i。该块作用域let声明了i就不能用外部的i
//死区结束
let i = 2;
console.log(i); //2, 直到这里才能正常使用 i
}
3 作用域链
作用域链由多级作用域连续引用形成的链式结果,作用域链保存的是各级作用域对象的引用,最近的排在最前面,当要获取某个变量或函数时,将一层层往上查找,直到找到全局对象的引用结束。函数每当执行结束时,都会把该函数作用域对象从该函数中移除。
var a = 9;
{
let b = 10;
console.log(a); // 9
}
4 词法作用域和动态作用域
词法作用域(也称静态作用域)、动态作用域不是对js作用域的又一个划分。实际上js属于词法作用域,js没有动态作用域的概念。
比如js是通过作用域链方式进行查找的,而js作用域链就是词法作用域。
词法作用域
在函数声明时就确定的,无论函数在哪里被调用,它的词法作用域都只由函数被声明时所处位置决定。
var a = 10;
function c1() {
console.log("c1", a);
}
function b() {
var a = 5;
function c2() {
console.log("c2", a);
}
c1() // 10
c2() // 5
}
调用c1时,先在c1查找a,没找到按照作用域链往上找到全局作用域的a。
动态作用域
在函数调用时才确定,会先在调用环境b中找a,就会输出两个5。
5 作用域链应用场景(闭包)
闭包是一种作用域链独特场景,前面说过,js函数的会在定义的时候创建一个拥有各级作用域链引用对象的作用域链,使得可以访问外部变量。
闭包就是利用这一特性,比如一个外部函数返回一个引用该外部函数变量的函数,这就形成了一个闭包,调用该函数的时候,外部可以间接访问函数的内部变量。
一般情况下,函数执行完毕后,占用的内存空间会被回收,由于返回的函数中引用了外部函数的变量/函数,所以外部函数被引用的变量/函数并没有被回收,可能会造成内存泄漏。
var fn = null;
function test(){
var a = 9;
function fn2(){
console.log(a)
}
fn = fn2
}
test()
fn() // 9
闭包的应用场景
- 解决索引值问题
如当有多个dom元素循环绑定onclick事件时。由于点击事件并不会在循环的时候触发,var声明的 i 没有独立作用域会被后面的值覆盖,可以利用闭包保存每次循环的值。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title></title></head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</body>
<script type="text/javascript">
var oDiv = document.getElementsByTagName("div");
for (var i = 0; i < oDiv.length; i++) {
// console.log(oDiv[i])
// oDiv[i].οnclick=function(){
// console.log(i)
// } // 5 5 5 5 5
function clickfn () { // 每次循环得到新的函数作用域
var x = i;
return function () {
console.log(x)
}
}
oDiv[i].onclick= clickfn() // 0 1 2 3 4
}
</script>
- 防抖、节流
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title></title></head>
<body>
<button>防抖开启按钮</div>
</body>
<script type="text/javascript">
var oDiv = document.getElementsByTagName("button")[0];
// 防抖,停止点击前的一次生效
var debounce = (fn, delay) => {
var time = null
return (...args) => {
if(time){
clearTimeout(time)
}
time = setTimeout(() => fn.apply(this,args), delay) // 如果不绑定this,fn内部的this指向window
}
}
oDiv.onclick = debounce(() => console.log('执行了'), 1000)
</script>
闭包优缺点:
优点:防止全局变量污染,实现局部变量/函数私有化
缺点:被引用的变量和函数长期驻扎内存,浪费内存,还可能造成内存泄漏
内存占用解决方案:将js运行结束后,将形成引用的对象手动置空,切断引用就可以被释放。
var fn = null;
function test(){
let arr = new Array(10000000)
function fn2(){
return arr[0]
console.log(a)
}
fn = fn2
}
test()
fn()
// fn = null
不重置fn的占用内存
重置fn后的占用内存
验证学习成果,自测一下:
let x = 5;
function fn(x) {
return function (y) {
console.log(y + ++x);
};
}
let obj = {a:1,b:6};
let f = fn(obj.b);
f(7);
console.log(x, b);