JS作用域、变量提升和闭包
作用域
作用域可以理解为JS引擎执行代码的时候,查找变量的规则。
从确定变量访问范围的阶段的角度,可以分为2类,词法作用域和动态作用域。js是词法作用域。
从变量查找的范围的角度,可以分为3类,全局作用域、函数作用域和块级作用域。
词法作用域
词法作用域是在词法分析阶段就确定的作用域,变量的访问访问仅由声明时候的区域决定。
动态作用域则是在调用的而时候决定,它是基于调用栈的。
var a = 2;
function foo() {
console.log( a );
}
function bar() {
var a = 3;
foo();
}
bar(); // 2
如果处于词法作用域,变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,于是输出2。
如果处于动态作用域,变量a首先在foo()函数中查找,没有找到。于是会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,于是输出3。
但上述代码输出2,由此可以证明js是词法作用域。
全局作用域、函数作用域和块级作用域
全局作用域:
直接编写在script标签中的JS代码,或者一个单独的JS文件中的,都是全局作用域。
全局作用域在页面打开时创建,页面关闭时销毁。
在全局作用域中有一个全局对象window,代表一个浏览器的窗口,由浏览器创建,可以直接使用。
函数作用域:
JS函数作用域是指在函数内部声明的变量,在函数内部和函数内部声明的函数中都可以访问到。
访问规则:访问变量时,现在函数内部找,找不到则在外层函数找,直到最外层的全局作用于,这个查找的过程就是‘作用域链’。
块级作用域(es6)
使用let/const关键字创建的变量都具有块级作用域。
块级作用域的变量只有在语句块内可以访问。所谓语句块就是用{ }包起来的区域。
块级作用域有几个特性:不存在变量提升、暂时性死区、不允许重复声明。
不存在变量提升和不允许重复声明很好理解,那什么是暂时性死区呢?
答:只要块级作用域内存在let命令,它所声明的变量就绑定了这个区域,不再受外部影响。在代码块内,使用let命令声明函数之前,该变量都是不可用的,这在语法上称为“暂时性死区”。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
变量提升
JS在执行之前,会先进行预编译,主要做两个工作:
- 1、将全局作用域或者函数作用域内的所有函数声明提前。
- 2、将全局作用域或者函数作用域内的所有var声明的变量提前声明,并且复制undefined
这就是变量提升。
// 将全局作用域或者函数作用域内的所有函数声明提前。
function test() {
exec();
function exec() {
console.log('exec');
}
}
// 等价于
function test() {
function exec() {
console.log('exec');
}
exec();
}
// 将全局作用域或者函数作用域内的所有var声明的变量提前声明,并且复制undefined
function test1() {
console.log(name);
var name = 'test';
}
// 等价于
function test1() {
var name;
console.log(name);
name = 'test';
}
注意:
- 函数声明可以提升,但是函数表达式不提升,具名的函数表达式的标识符也不会提升。
- 同名的函数声明,后面的覆盖前面的。
- 函数声明的提升,不受逻辑判断的控制。
// 函数表达式和具名函数表达式标识符都不会提升
test(); // TypeError test is not a function
log(); // TypeError log is not a function
var test = function log() { console.log('test') };
// 同名函数声明,后面的覆盖前面的
function test() {
console.log(1);
}
function test() {
console.log(2);
}
test(); // 2
// 函数声明的提升,不受逻辑判断的控制
// 注意这是在ES5环境中的规则,在ES6中会报错
function test() {
log();
if (false) {
function log() {
console.log('test');
}
}
}
test(); // 'test'
在块级作用域中声明函数会是什么效果呢?
ES6环境中,如果在语句块中声明函数,按照正常的规范,函数声明应该被封闭在语句块里面,但是为了兼容老代码,因此语法标准允许其他的实现:
- 允许在块级作用域内声明函数。
- 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f(); // Uncaught TypeError: f is not a function
}());
// 等价于
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f(); // Uncaught TypeError: f is not a function
}());
闭包
函数和函数内部能访问到的变量的总和,就是一个闭包。
如何生成闭包?函数内嵌套函数,并且函数执行完后,内部函数会被引用,这样内部函数可以访问外部函数中定义的变量,于是就生成了一个闭包。(函数嵌套 + 内部函数被引用)
闭包的作用是什么?可以让内部的函数访问到外部函数的变量,避免变量在全局作用域中存在被修改的风险。
注意事项:不用的时候解除引用,避免不必要的内存占用。
缺点:使用时候不注意的话,容易产生内存泄漏。
闭包实现一个计数器
var count = 0;
function createCounter() {
function increase() {
count++;
}
function getCount() {
return count;
}
return {
increase: increase,
getCount: getCount
};
}
var counter = createCounter();
counter.increase();
console.log(counter.getCount());
console.log(count);
上述实现方法,变量count放在全局,很容易被其他模块修改从而导致不可预知的问题。因此我们希望count变量不会被其他模块访问到,于是需要把count放在函数作用域中:
function createCounter() {
var count = 0;
function increase() {
count++;
}
function getCount() {
return count;
}
return {
increase: increase,
getCount: getCount
};
}
var counter = createCounter();
counter.increase();
console.log(counter.getCount());
console.log(count);
这样函数createCounter中的increate和getCount两个函数可以访问到createCounter内部定义的count,这样就形成了闭包。而count只能被createCounter内部定义的函数访问到,因此不会有被随意修改的风险。
通常情况下函数中定义的变量在函数执行完成后会被销毁,例如:
function createCounter() {
var count = 0;
function increase() {
count++;
}
function getCount() {
return count;
}
return {
increase: increase,
getCount: getCount
};
}
createCounter();
通常执行完createCounter()方法之后,内部的所有变量都被从内存中销毁(因为没有其他地方使用了)。但是如果生成了闭包(即有对内部嵌套函数的引用),则内部变量不会被销毁(因为还有其他地方在用,嵌套的内部函数还在使用)。
还是以上面createCounter闭包为例,由于createCounter返回的方法们被引用,因此形成闭包,所以内部变量count不会被销毁,而是会继续被increase和getCount使用。
生成闭包之后,如果我们不再需要使用counter可以执行counter = null;这样失去了对内部嵌套函数的引用,浏览器就会将方法内资源都销毁调了。因此当我们使用完闭包之后如果后续不再需要使用,最好通过取消引用来释放闭包的资源。
学习参考:https://www.yuque.com/baiyueguang-rfnbu/tr4d0i/gu0blp#WW3FR