作用域(scope)的概念
JS中作用域就是栈内存,包括三种:
- 全局作用域:window
- 私有作用域:函数执行
- 块级作用域:使用let创建变量存在块级作用域
作用域链
函数执行形成一个私有作用域(保护私有变量),进入到私有作用域中,首先变量提升(声明过的变量是私有的),接下来执行代码:
- 执行的时候遇到一个变量,若为私有变量,则按私有处理即可。
- 若当前这个变量不是私有的,则需要向它的上级作用域查找,上级作用域如果也没有,则继续向上查找,一直找到window全局作用域为止,我们
把这种向上一级查找的机制叫做
作用域链。
(1)若上级作用域有这个变量,我们当前操作的都是上级作用域中的变量(加入我们在当前作用域把值改了,相当于把上级作用域中的这个值给修改了)。
(2)若上级作用域中也没有这个变量(一直找到window也没有):
【设置】变量 = 值
(例如a = 12;
):相当于给window设置了一个属性a,以后再操作window下就有了。
【获取】alert(变量)
:想输出该变量,此时找不到,所以报错。
注意下面两种赋值的区别:
var x = 10, y = 20; //相当于var x = 10; var y = 20;所以有两个变量提升
var a = b = 100; //相当于var a = 100; b = 100; 只对a进行变量提升
私有变量
JS中的私有变量只有两种:
- 在私有作用域变量提升阶段,声明过的变量(或函数)。
- 形参也是私有变量。
function fn(n1, n2){
var total = n1 + n2;
return total;
}
var res = fn(100, 200);
console.log(res);
函数执行形成一个新的私有作用域:
- 形参赋值
- 变量提升
- 代码自上而下执行
- 当前栈内存(私有作用域)销毁或不销毁
// 变量提升:var x; var y; var z; f=function
var x = 10, y = 20, z = 30;
// 赋值: x=10; y=20; z=30;
function f(x, y) {
//形参赋值:x=10; y=20;
//变量提升:var x;(忽略,已经存在x了)
console.log(x,y,z); //=>10 20 30
var x = 100; // 形参x=100
y = 200; // 形参y=200;
z = 300; //全局作用域中的z=300
console.log(x,y,z); //=>100 200 300
}
f(x,y,z);
console.log(x,y,z); //=>10 20 300
function fn(b) {
// 形参赋值:b=1;
// 变量提升:b = function
console.log(b); //=>[Function: b]
function b() {
// 没有形参赋值和变量提升
// 所以这里b得从上级作用域中查找
console.log(b); //=>[Function: b]
}
b();
}
fn(1);
如何查找上级作用域
先看一个栗子:
var n = 10;
function sum() {
console.log(n);
}
sum(); //=>10
~function () {
var n = 100;
sum(); //=>10
//此时sum的宿主环境是当前自执行函数形成的私有作用域
//但是sum的上级作用域还是定义sum的地方,即全局作用域
}();
函数执行形成一个私有作用域(A),A的上级作用域是谁,和它在哪执行的没关系,主要是看它在哪定义的,在哪个作用域下定义的,当前A的上级作用域就是谁
。
var n = 10;
var obj = { //是个堆内存,不是作用域
n: 20,
fn: (function () { //上级作用域:window
var n = 30; //若注释掉这行代码,输出10
return function () { //上级作用于:自执行函数
console.log(n);
}
})()
};
obj.fn(); //=>30
将上面的代码改一下:
var n = 20;
var obj = { //是个堆内存,不是作用域
n: 20,
fn: (function (n) { //上级作用域:window
return function () { //上级作用于:自执行函数
console.log(n);
}
})(obj.n) //这里报错,TypeError: Cannot read property 'n' of undefined
};
obj.fn();
闭包的作用
1. 保护
形成私有作用域,保护里面的私有变量不受外界干扰。
在真是项目中,我们利用这种保护机制实现团队协作开发(避免了多人同一个命名导致的代码冲突问题)。
2. 保存
函数执行形成一个私有作用域,函数执行完成,形成的这个栈内存一般情况下都会自动释放。
但是还有二般情况:函数执行完成,当前私有作用域(栈内存)中的某一部分内容被栈内存意外的其他东西(变量/元素的事件)占用了,当前的栈内存就不能释放掉,也就形成了不被销毁的私有作用域(里面的私有变量也不会销毁)。
举个栗子:
function fn() {
var i = 1;
return function (n) {
console.log(n + i++);
}
}
var f = fn();
f(10); //=>11
fn()(10); //=>11
f(20); //=>22
fn()(20); //=>21
注:函数执行形成一个私有作用域,如果私有作用域中的部分内容被外面的变量占用了,当前作用域不被销毁,通过以下形式实现【保存】
:
- 函数执行返回了一个引用类型数据(堆内存)的地址(并且堆内存隶属于这个作用域),在外面有一个变量接收了这个返回值,此时当前作用域就不能被销毁(想要销毁,只需让外面的变量赋值为null,也就是不占用当前作用域中的内容了)。
综合练习:
// 变量提升:var num; var obj; var fn;
var num=1, // num = 1;
obj={
num: 2,
fn: (function (num) {
// 形参赋值:num=1;
// 变量提升:没有
this.num *= 2; // 自执行函数的this为window, 所以window.num=2;
num += 2; // 形参num=3;
return function () {
this.num = num;
num++;
console.log(num);
}
})(num) //实参num=1;是全局作用域中的num
};
var fn = obj.fn;
fn(); //=>4
/*执行fn,此时
fn = function () {
// 形参赋值和变量提升都没有
this.num = num; // fn前面没有点,所以this是window,window.num=num,
// 当前作用域没有num,从上级作用域(自执行函数的作用域)找num,num=3
// 所以window.num=3;
num++; // 还是上级作用域的num,num=4;
console.log(num); //=>4
}*/
obj.fn(); //=>5
/*obj.fn = function () {
this.num = num; // this-->obj,所以obj.num=num,
// 此时上级作用域的num=4
// 所以obj.num=4;
num++; // 还是上级作用域的num,num=5;
console.log(num); //=>5
}*/
console.log(num, obj.num); //=>3 4