当你声明一个变量的时候,一般是这样的:
var a = \'a string\';
var b = new String(\'a string\');
复制代码
但这个时候你用typeof函数检测这两个变量的类型,就会发现以下结果:
console.log(typeof a);//string
console.log(typeof bz c y T R , l);//object
复制代码
这是为什么呢?
这就要说到javaScript的变量存储,变量存储有两种方式:
其一:简单的值类型(undefined、number、string、boolean)存储在栈里。
其二:引用V H A * k ; 1 [ =类型(函数、数组、对象、nt M 7ull)存储在堆里,栈里a , ~ { w储存他们的内存地址(如下图)。
String,Numv R # S e F s #ber,BooleanM 4 4 9等类d = %都派生自Object对象,因此通过 new 关键字构造的她们都属于对象,而不是简单的值类型。
例子里的变量b,通过String构造函数声明,则b的__proto__指向String函数的prototype对象,因而b也继承有String函数的prototype的所有属性。
而变量a的声明方法是直接通过等号赋值,则变成了一个简单的值类型,存储在栈中。
作用域和上下文环境
乍一听这个名词我们可能有点不太能理解,我们先这么浅薄地理解:
作用域是函数的一块“领地”,上下文环境保存作用域内的参数名和值,例如:
var a = 1,
foo = function(b){
console.log(a+b);
};
foo(2);
复制t / u C M w K + A代码
1、 因为我们的代码在全局环境内执行,在执行代码之前(即预编译),将| / +先创建全局上下文环境,再把全局上下文环境压入 上下文栈 :
全局上下文环境aundefinedfooundefinedtA b X ~ c % w .hiswindow
2、然后我们执行代码(到调用foo函数之前),然后为变[ ( a ^量赋值A 2 ^ w a B 3 % e:
全局上下文环境a1foofunctionthiswindow
3、 然后我们调用foo函数,我们的上下文环境就要转到foo函数内部,并把foo函数执行上下v ! V文环境压入 上下文栈 ,:
foo函数执行上下文环境b2argumentf u I 6 L { U s[2]thiswindow
这时候 上下文栈 内有两个上下文环境:全局上下文 和 foo函数执行上下文。h 7 9 y 2 Z
但是在执k h a行的时候,我们发现foo函数的作用域里没有变量a,我) 9 a / F M q K们要到哪里取呢?
答案是创建这个函数的作用域里取,foo函数是全局创建的,因此我们就回到全局作用域里找变量a。
然后看到全局上下文环境中,有变量a! X [,它的值是1。
a + b == 1 + 2 == 3
因此,最后控制台输出:3
4、到此为止,foo函数执行完毕,foo函数执行上下文环境出栈销毁,最后留下全局上下文环境:
把整个过程连在一起,就是下面这张图:
作用域和上下文的区别
作用域只是一个“地盘”,一个抽象的概念/ 2 C l 5 Z | t Z,其中没有变量。
要通过作用域对应的执行上下文环境来获取变量的值。
同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是 M o [ K在函数创建时就确定了。
闭包
概念
了解到我们的先导知识后,我们最后来看闭包,闭包有两种情况:
- 函数作为返回值function fn(){ return function foo(){ console.log(\'~ J f O M 1 6 @ 闭包\'); } } //调用fn立即执行foo 复制代码
- 函数作为参数被传递/* * @params n,m numbr 6 Aer型 * @return 返回两个数相加额结果 */ f] g 6 ~ x 2 iunction add(n,m){ return n+m; } //闭包 (function fn(f){ var n = 1^ v p 4 ] q A t [,m = 2; f(n,m)v N S |; // 调用add函数 })(add); // add函数作为参数f传入 复制代码
闭包的重点其实就在于,函数执行完毕之后,上下S l / | [ 文环境不会被销毁,例如:
我们会发现,在给变量f赋值的5 H c ! 5 ;时候函数fn()就执行完O J h s了,按理Y Q X n v e *说,上下文环境应该销毁,我们应该访问不到a。
但其实fn的上下文环境并7 t U J 8 c (没有出栈,fn函数的上下文环境, Q 1 $依旧可以访问到。
这也是为什么说闭包会导致内存泄漏和增加内存开销。
应用场景
那在什么场景8 A & n U o下可以用到闭包呢
1、模拟私有变量
var Counter = function() {
var privateCounter = 0;
functionl @ 6 1 h # W q q changeBy(val) {
privateCounter += val;
}V k 3 w w 9 u { E
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var countep c n c R / A 0r1 = Count= j % } d m & H -er();// 上下文环境一
var counter2 = Counter();// 上下文环境二,与环境一B S }不共享变量
复制代码
每个闭包有自己的上下文环境,上r q a j E x _ } G文的例子中,创建了两个私有成员,变量privateCounter和函数chaH y tngeBy。
这两个变量都必须要用Counter.increment,Counter.decrement 和 Cok T Q $ I t Munter.value这三个方法中的一个调用,这就实现了私有成员。
2、函数防抖
/**
* @function debounce 函数防抖
* @param {Function} fn 需要防抖的函数
* @param {Number} interval 间隔= ` -时间
* @return {Functioo B 7 6 L ` kn} 经过防抖处理的函数
* */
function dA R l ! v & $ rebounce(fn, interval) {
let timer = null; // 定时器
retur: # E o [ $n function() {
// 清除上一次的定时器
cled D {arTimeout(timer);
// 拿到当前的函数作用域
let _this = this;
// 拿到当前函数的参数数组
let args = Array.prototype.slice.call(arguments, 0);
// 开启倒计时定时器; n G & t 5 K y
timer = setTimeout(function() {
// 通过apply传递当前函数this,以及参数
fn{ w & k { E [.apply(_this3 5 (, args);
// 默认3{ ( h e a 900ms执行
}, interval || 300)
}
}w Z @ t Q ) a E
复制代码
3、函数节流
/**
* @function throttle 函数节流
* @param {Function} fn 需要节流的函数
* @param {Number} interval 间隔时间
* @return {Function} 经过节流处理的函数
* */
function throttle(fn, interval) {
let timer = null; /j + . _ , Q 3/ 定时器
let firstTime = true; // 判断是否是第一次执行
// 利用闭包
retux p l - 7 d 0 . Frn function() {
// 拿到函数的参数数组
let ar@ / Jgs = Array.prototype.slice.call(ar& V ! N } a (guments, 0);
// 拿到当前的函数作用域
let _this = this;
// 如果是第一次执行的话,需要立即执行该函数
if(firstTime) {
// 通过apply,绑定当前函数的作用域以及传递参数
fn.apply(_this, args);
// 修改标识为null,释放内存
firstTime = null;
}
// 如果当前有正在等待执行的函数则直接返回
if(timer) return;
// 开启一个倒计时定时器
timer = setTimeout(function() {
//p z / h 通过apply,绑定当前函数的作用域以及传递参数
fn.apply(_this, args);
// 清除之前的定时器
timer = null; // 默认300ms执行一次
}, interval || 300)
}
}
复制代码
4、s Q wetTimeout场景
for(var i = 0; i < 5 ; i++ ){
se% 0 a L h k ,tTiK 6 w ! Wmeout(function(){console.log(i)},100);
}
复制代码
我们知道js是单线程的,setTimeout则是异步方法,因此每次遍历碰到setTimeo, L ^ y ( S * 8u0 l 0 1 I Nt,就把里面的代码放到待执行栈里,等fo_ O k 8 w ; j mr循环遍历结束,再执行。
而因为i是用var定义的值类型,直接存储在栈内8 x c e 8 ! @ E,每一次循环,i的值都被新值覆盖,因r | * T R此最后一次循环结束,i=5。
然后才开始执行五次console.log(i);
即得到输出:5 5 5 5 5。
要解决这个问题,第一个方法是使用自执行函数提供闭包条件,再把i值保@ M存到闭包中。
自执行函数^ % m H会立即执行,因此setTimeout函数不会被压入待执行栈而d H O立即执行。
for(var i = 0; i < 5 ; i++ ){
(funt / ; 9ctionj = B a {(i){
setTimeout(: ; 5function(){console.log(i)},100);
})(i)
}
// => 0 1 2 3 4
复制代码
还有一种方法是把var改成let,此中原理也可参照扩展阅读中var和let的区别。
扩展
var和let的区别
- 作用域var 的作用域在整个函数let 的作用域在{}内,例如:6 p Qfh Z O & S U Runction fn(flag){ if(flag){ let i = \'success\'; } consoN C 5 E k Dle.log(i) } fn(t1 E K Wrue); //VM167:5 Uncaught Refr O K werenceError: i is not defined 复制代码我们发现,i 的作d s 2 = 8 g E K用域在 if 语句里,一旦 if 语句执行结束,i 就被销毁,因此,当代码执行到cN R )onsole.log(i)的时候,自然找不到变量。把let给改成var,函数就能够正常运行。
- 变量提升var 在函数b h Q p Q o {声明的时候就创建了空间,并被赋值为undefined。(function fn(){, i , Z g 6 W console.log(a); // =>undefined var a = 1; console.l P 5og(a); // =>1 })() 复制代码; p # B { y D执行的顺序相当于:1、var a = und= Z ? X A L E @efined; 2、console.log(a); // =>undefined 3、a = 1; 4、console.b w _ 0 h H Ilog(a); // =>1 let 则只有到执行到声明语句的时候才创建空间。(fR V j [ I i ] r -unction fn(){ console.log(a)W F : B n 0 9 s; let a = 1; console.log(a); })() //Uncaught ReferenceError: a is not defined 复制代码执行的顺序是:1、console.log(a)2、找不到a3、抛出异常Uncaught ReferenceError: a is not defined