作用域
JavaScript 中大部分情况下,只有两种作用域类型
- 全局作用域: 全局作用域为程序的最外层作用域,一直存在
- 函数作用域: 函数作用域只有函数被定义时才会创建,包含在父级函数作用域/全局作用域内
由于作用域的限制,每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。
/* 全局作用域开始 */
var a = 1;
function func () { /* func 函数作用域开始 */
var a = 2;
console.log(a);
} /* func 函数作用域结束 */
func(); // => 2
console.log(a); // => 1
/* 全局作用域结束 */
作用域链
当可执行代码内部访问变量时,会先查找本地作用域,如果找到目标变量即返回,否则会去父级作用域继续查找…一直找到全局作用域。我们把这种作用域的嵌套机制,称为 作用域链。
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo(2); // 2 4 12
词法作用域(静态作用域)
**词法作用域**是JavaScript中使用的作用域类型
词法作用域,就意味着函数被定义的时候,它的作用域就已经确定了,和拿到哪里执行没有关系,因此词法作用域也被称为 “静态作用域”。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 打印1
块级作用域
if (true) {
var a = 1
}
console.log(a) // 打印1
ES6
标准提出了使用 let
和 const
代替 var
关键字,来“创建块级作用域”。也就是说,上述代码改成如下方式,块级作用域是有效的:
if (true) {
let a = 1
}
console.log(a) // ReferenceError
创建作用域
创建/改变作用域的手段
- 定义函数,创建函数作用
function foo () {
// 创建了一个 foo 的函数作用域
}
- 使用
let
和const
创建块级作用域(推荐):
for (let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i); // ReferenceError
闭包
能够访问其他函数内部变量的函数,被称为 闭包。
上面这个定义比较难理解,简单来说,闭包就是函数内部定义的函数,被返回了出去并在外部调用。我们可以用代码来表述一下:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo(); // 2
baz(); // 这就形成了一个闭包
我们可以简单剖析一下上面代码的运行流程:
- 编译阶段,变量和函数被声明,作用域即被确定。
- 运行函数
foo()
,此时会创建一个foo
函数的执行上下文,执行上下文内部存储了foo
中声明的所有变量函数信息。 - 函数
foo
运行完毕,将内部函数bar
的引用赋值给外部的变量baz
,此时baz
指针指向的还是bar
,因此哪怕它位于foo
作用域之外,它还是能够获取到foo
的内部变量。 baz
在外部被执行,baz
的内部可执行代码console.log
向作用域请求获取a
变量,本地作用域没有找到,继续请求父级作用域,找到了foo
中的a
变量,返回给console.log
,打印出2
。
闭包的执行看起来像是开发者使用的一个小小的 “作弊手段” ——绕过了作用域的监管机制,从外部也能获取到内部作用域的信息。闭包的这一特性极大地丰富了开发人员的编码方式,也提供了很多有效的运用场景。
闭包的应用场景
单例模式
单例模式是一种常见的设计模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销:
// 单例模式
function Singleton(){
this.data = 'singleton';
}
Singleton.getInstance = (function () {
var instance;
return function(){
if (instance) {
return instance;
} else {
instance = new Singleton();
return instance;
}
}
})();
var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
模拟私有属性
javascript
没有 java
中那种 public
private
的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:
// 模拟私有属性
function getGeneratorFunc () {
var _name = 'John';
var _age = 22;
return function () {
return {
getName: function () {return _name;},
getAge: function() {return _age;}
};
};
}
var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined
柯里化
柯里化的优势之一就是 参数的复用,它可以在传入参数的基础上生成另一个全新的函数,来看下面这个类型判断函数:
实现一个add函数,完成函数柯里化
add(1)(2)(3) = 6
add(1,2,3)(4) = 10
add(1)(2)(3)(4)(5) = 15
function add () {
var args = Array.prototype.slice.call(arguments);
var fn = function () {
// 把参数都放在一个相当于全局变量的 args 里面
args.push(...arguments)
return fn;
}
fn.valueOf = function () {
return args.reduce(function(a, b) {
return a + b;
})
}
return fn;
}
console.log(add(1,2)) // 3
console.log(add(1)(2)) // 3
console.log(add(1)(2)(3)) // 6
console.log(add(1,2,3)(4)) // 10
闭包的问题
极容易导致内存泄露。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 这就形成了一个闭包
我们知道,javascript
内部的垃圾回收机制用的是引用计数收集:即当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为 0
的变量标记为失效变量并将之清除从而释放内存。
上述代码中,理论上来说, foo
函数作用域隔绝了外部环境,所有变量引用都在函数内部完成,foo
运行完成以后,内部的变量就应该被销毁,内存被回收。然而闭包导致了全局作用域始终存在一个 baz
的变量在引用着 foo
内部的 bar
函数,这就意味着 foo
内部定义的 bar
函数引用数始终为 1
,垃圾运行机制就无法把它销毁。更糟糕的是,bar
有可能还要使用到父作用域 foo
中的变量信息,那它们自然也不能被销毁… JS 引擎无法判断你什么时候还会调用闭包函数,只能一直让这些数据占用着内存。
这种由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露。
内存泄露
内存泄露 是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。
-
意外的全局变量
function foo(){ bar=2 console.log('bar没有被声明!') }
bar 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.
-
被遗忘的计时器或回调函数
如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 但是 setTimeout ,它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.
-
脱离 DOM 的引用
很多时候,为了方便存取,经常会将 DOM 结点暂时存储到数据结构中.但是在不需要该DOM节点时,忘记解除对它的引用,则会造成内存泄露.
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用
-
闭包
相互循环引用.这是经常容易犯的错误,并且有时也不容易发现.
function foo() {
var a = {};
function bar() {
console.log(a);
};
a.fn = bar;
return bar;
};
避免
避免策略:
-
使用严格模式
-
减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收(即赋值为null);
-
注意程序逻辑,避免“死循环”之类的 ;
-
避免创建过多的对象 原则:不用了的东西要记得及时归还。
-
减少层级过多的引用
总结、摘抄自
作者:不写bug的米公子
链接:https://juejin.cn/post/6844904165672484871
来源:稀土掘金