# 作用域与闭包
# 作用域 🥗
作用域就是变量与函数的可访问范围(规则),即作用域控制着变量与函数的可见性和生命周期。
# 全局作用域
- 程序最外层定义的函数或者变量(script 里、单独 js 文件)
- 所有末定义直接赋值的变量(包括你在函数内直接赋值的变量)
- 所有 window 对象的属性和方法(window.location、window.navigator 等)
# 函数作用域(局部作用域)
局部作用域在函数内创建,在函数内可访问,函数外不可访问。
function test() {
var str = "juncaihe.com";
alert(str);
}
test(); // 函数内可访问到str
console.log(str); // ReferenceError: str is not defined
# 块级作用域(ES6)
- 块作用域由 { } 包括,if 语句和 for 语句里面的{ }也属于块作用域、函数内部
- let/const 声明的变量不会提升到代码块顶部
# var、let、const 的区别?
TIP
- var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
- let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
- const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改。
- 同一个变量只能使用一种方式声明,不然会报错
# 变量对象&活动对象
# 变量对象(VO)
VO 对应的是函数创建阶段,JS 解析引擎进行预解析时,所有的变量和函数的声明,统称为 Variable Object。该变量与执行上下文相关,知道自己的数据存储在哪里,并且知道如何访问。VO 是一个与执行上下文相关的特殊对象,它存储着在上下文中声明的以下内容:
- 变量 (var, 变量声明)
- 函数声明
- 函数的形参
function add(a, b) {
var sum = a + b;
function say() {
alert(sum);
}
return sum;
}
// sum,say,a,b 组合的对象就是VO,不过该对象的值基本上都是undefined
# 活动对象(AO)
AO 对应的是函数执行阶段,当函数被调用执行时,会建立一个执行上下文,该执行上下文包含了函数所需的所有变量,该变量共同组成了一个新的对象就是 Activetion Object。该对象包含了:
- 函数的所有局部变量
- 函数的所有命名参数
- 函数的参数集合
- 函数的 this 指向
function add(a, b) {
var sum = a + b;
function say() {
alert(sum);
}
return sum;
}
add(4, 5);
// JS对象来表示AO
// AO = {
// this : window,
// arguments : [4,5],
// a : 4,
// b : 5,
// say : ,
// sum : undefined
// }
# 作用域链
TIP
作用域链是由当前执行环境与上层执行环境的一系列变量对象组成的,它保证了当前执行环境对符合访问权限的变量和函数的有序访问
作用域链作用域普通函数上,操作的是全局变量和局部变量
- 当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)来保证对执行环境有权访问的变量和函数的有序访问。
- 作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)
- scopeChain 其实是在创建函数的时候确定的
# 作用域链的定义以及形成
- 定义1、当所需要的变量在所在的作用域中查找不到的时候,它会一层一层向上查找,直到找到全局作用域还没有找到的时候,就会放弃查找。这种一层一层的关系,就是作用域链。
- 定义2、作用域根据代码层次分层,以便子作用域能访问父级作用域,而不能从父级作用域访问到子级作用域中的变量和引用。
var a = 1;
function check() {
return function() {
console.log(a); // 当前作用域内找不到a,会向上一层一层查找,最后找到了全局下的a,输出结果为1
console.log(b); // 同理,所以输出"Uncaught ReferenceError: b is not defined"
};
}
var func = check(); // 此时返回匿名函数
func(); // 执行匿名函数
# 代码示意
function one() {
var a = 1;
function two() {
var b = 2;
function three() {
var c = 3;
console.log(a, b, c);
}
three();
}
two();
}
one();
// 1.创建全局上下文
var globalExecuteContextVO = { one: `()=>{var a = 1;}` };
var globalExecuteContext = {
VO: globalExecuteContextVO,
scopeChain: [globalExecuteContextVO],
};
var executeContextStack = [globalExecuteContext];
//2.执行one,创建one执行上下文
var oneExecuteContextVO = {
a: 1,
two: `()=>{var b = 2 ;}`,
};
var oneExecuteContext = {
VO: oneExecuteContextVO,
scopeChain: [oneExecuteContextVO, globalExecuteContext.VO],
};
//2.执行two,创建two执行上下文
var twoExecuteContextVO = {
b: 2,
three: `()=>{var c = 3 ;}`,
};
var twoExecuteContext = {
VO: twoExecuteContextVO,
scopeChain: [
twoExecuteContextVO,
oneExecuteContext.VO,
globalExecuteContext.VO,
],
};
//3.执行three,创建three执行上下文
var threeExecuteContextVO = {
c: 3,
};
var threeExecuteContext = {
VO: threeExecuteContextVO,
scopeChain: [
threeExecuteContextVO,
twoExecuteContext.VO,
oneExecuteContext.VO,
globalExecuteContext.VO,
],
};
function getValue(varName) {
for (let i = 0; i < threeExecuteContext.scopeChain.length; i++) {
if (varName in threeExecuteContext.scopeChain[i]) {
return threeExecuteContext.scopeChain[i][varName];
}
}
}
//console.log(a, b, c);
console.log(getValue("a"), getValue("b"), getValue("c"));
scopeChain 其实是在创建函数的时候确定的
function one() {
var a = 1;
function two() {
console.log(a);
}
return two;
}
var a = 2;
var two = one();
two();
// ----------------------------------------
// 1.创建全局上下文
var globalExecuteContextVO = {
one: `()=>{var a = 1;}`,
a: undefined,
two: undefined,
};
var globalExecuteContext = {
VO: globalExecuteContextVO,
scopeChain: [globalExecuteContextVO],
};
//2.开始执行
globalExecuteContextVO.a = 2;
//3.开始执行one
var oneExecuteContextVO = { a: undefined, two: `()=>{console.log(a)}` };
var oneExecuteContext = {
VO: oneExecuteContextVO,
scopeChain: [oneExecuteContextVO, globalExecuteContextVO],
};
oneExecuteContextVO.a = 1;
//4.给two赋值
globalExecuteContextVO.two = oneExecuteContextVO.two;
//5.执行two
var twoExecuteContextVO = {};
var twoExecuteContext = {
VO: twoExecuteContextVO,
//scopeChain是在创建此函数据的时候就决定了,跟在哪里执行无关
scopeChain: [
twoExecuteContextVO,
oneExecuteContextVO,
globalExecuteContextVO,
],
};
# 使用规范
# 闭包
# 闭包的定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包。即使函数是在当前词法作用域之外执行。
# 闭包的形成
function foo() {
var a = 1;
return function() {
console.log(a);
};
}
var bar = foo();
bar();
以上代码分析如下:
foo()
函数的执行结果返回给 bar
,而此时由于变量 a
还在使用,因而没有被销毁,然后执行 bar()
函数。这样,我们就能在外部作用域访问到函数内部作用域的变量。这个就是闭包。
闭包的形成条件:
- 函数嵌套
- 内部函数引用外部函数的局部变量
# 闭包的作用
- 可以读取函数内部的变量
- 可以使变量的值长期保存在内存中,生命周期比较长。
- 可用来实现 JS 模块(JQuery 库等)
(function() {
var a = 1;
function test() {
return a;
}
window.module = {a, test}; // 向外暴露
})()
<script>
console.log(module.a); // 1
console.log(module.test()); // 1
</script>
# 闭包的特性
- 每个函数都是闭包,函数能够记住自己定义时所处的作用域,函数走到了哪,定义时的作用域就到了哪。
- 内存泄漏
内存泄漏就是一个对象在你不需要它的时候仍然存在。所以不能滥用闭包。当我们使用完闭包后,应该将引用变量置为 null
function outer() {
var num = 0;
return function add() {
num++;
console.log(num);
};
}
var func1 = outer();
func1(); // 1
func1(); // 2 [没有被释放,一直被占用]
var func2 = outer();
func2(); // 1 [重新引用函数时,闭包是新的]
func2(); // 2
# 闭包的应用
实现点击第几个 button 就输出几
<button>1</button> <button>2</button>
let buttons = document.getElementsByTagName("button");
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
console.log(i + 1);
};
}
// 无论点击哪个都会是3 因为此时的i是全局变量,当执行点击事件时,i已经变成了3
解决方式
- let 声明
var i = 0
=>
let i = 0
- 闭包
for (var i = 0; i < buttons.length; i++) {
(function(k) {
buttons[k].onclick = function() {
console.log(k + 1);
};
})(i);
}