之前记在word里的学习笔记太乱了,整理到掘金上来复习一遍。
- 执行上下文
- 作用域
- 闭包
执行上下文
执行上下文也就是Javascript代码的执行环境。
执行上下文的类型分有三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象
- 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。(会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问)
- Eval 函数执行上下文:指的是运行在 eval 函数中的代码,很少用而且不建议使用
生命周期 创建阶段 → 执行阶段 → 回收阶段
- 创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
- 执行阶段:变量赋值、函数表达式赋值,使变量对象变成活跃对象
执行栈 执行栈,也叫调用栈,具有栈的特点( LIFO后进先出),用于存储在代码执行期间创建的所有执行上下文。
- 当Javascript引擎开始执行第一行脚本代码的时候,它就会创建一个
全局执行上下文然后将它压到执行栈中 - 每当引擎碰到一个函数的时候,它就会创建一个
函数执行上下文,然后将这个执行上下文压到执行栈中。 - 引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文
- 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
- 只有浏览器关闭的时候全局执行上下文才会弹出
作用域
- 作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期
- 代码执行过程中会创建变量对象的一个作用域链,作用域链的前端是当前执行环境的变量对象,末端是全局执行环境的变量对象。
- 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
- 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的
作用域链针对函数作用域来说的,比如创建了一个函数,函数里面又包含了一个函数,那么就会有全局作用域、函数1的作用域、函数2的作用域。
闭包
闭包就是能够读取其他函数内部变量的函数,其实就是利用了作用域链向上查找的特点。
闭包让你可以在一个内层函数中访问到其外层函数的作用域。每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁。
写一个简单的闭包 创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域。
function fun() {
var a = 10; //a是被fun函数创建的局部变量
return function () { //内部函数,一个闭包
console.log(a); //使用父函数中声明的变量
};
}
var a = 20;
var f = fun();
f(); //10
内部函数没有自己的局部变量。由于闭包的特性,它可以访问到外部函数的变量。
注意闭包利用作用域链向上查找的特点,这里查找的是包裹它的父级中声明的a,不是全局作用域中的a。依据的是函数定义时的作用域链,而不是函数执行时。
作用
- 内部函数可以引用外层的参数和变量
- 让这些变量始终保持在内存中(闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在的词法环境依然存在,以达到延长变量的生命周期的目的。)
- 封装对象的私有属性和私有方法
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
举例说明闭包使函数内部的变量无法进行销毁,始终保持在内存中
function myMethod(){
var num=6;
return function(){
var n=0;
console.log(++n);
console.log(++num);
}
}
myMethod();
var myFn=myMethod();//
myFn();//n=1,num=7
myFn();//n=1,num=8
myMethod()执行后,返回的是个方法(就是所谓的闭包),因为这个方法中还用到了myMethod里面的变量num,会导致num无法释放。
将返回的方法用变量保存起来,执行发现num没有被销毁,仍然保存着值。
注意:无法销毁变量,使用不当,容易造成内存泄漏!
应用场景 ①for循环中使用闭包解决 var 定义的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
因为 setTimeout 是个异步函数,会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。
使用闭包后,保留变量i
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
②函数柯里化
柯里化的目的:避免频繁调用具有相同参数函数的同时,又能够轻松的重用
// 假设有一个求长方形面积的函数
function getArea(width, height) {
return width * height
}
// 如果碰到的长方形的宽总是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)
// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)
③使用闭包模拟私有方法
私有方法只能被同一个类中的其它方法所调用。JavaScript 不提供原生的支持,但是可以使用闭包模拟私有方法。
下面使用闭包来定义公共函数,且其可以访问私有函数和变量。这也叫模块方式。
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); //0
Counter.increment();
Counter.increment();
console.log(Counter.value()); //2
Counter.decrement();
console.log(Counter.value()); //1
这次只创建了一个环境,为三个函数所共享:Counter.increment,Counter.decrement 和 Counter.value。
该共享环境创建于一个匿名函数体内,该函数一经定义立刻执行。环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。 这两项都无法在匿名函数外部直接访问。必须通过匿名包装器返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法范围的作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。
上述定义了一个匿名函数用于创建计数器,然后直接调用该函数,并将返回值赋给 Counter 变量。也可以将这个函数保存到另一个变量中,以便创建多个计数器。
var makeCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); //0
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); //2
Counter1.decrement();
console.log(Counter1.value()); //1
console.log(Counter2.value()); //0
两个计数器 Counter1 和 Counter2 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量。
参考链接: JavaScript—用闭包模拟私有方法
③防抖和节流
防抖:你尽管触发事件,我只在最后一次触发后的一段时间内执行。
例如搜索框,如果每输入一个字符就触发一次,服务器压力巨大。利用防抖,在输入完之后停顿某个时间后才触发。
function debounce2(fn,wait) {
let timer = null; //创建一个标记用来存放定时器的返回值
return () => {
clearTimeout(timer); //清除定时器
timer = setTimeout(() => { //创建新的 setTimeout
fn.apply(this, arguments);
}, wait);
}
}
节流:持续触发事件,但一段时间内只执行一次。
比如打开一个网页,当我们向下滚动页面的时候,onscroll事件触发的频率太高太高,稍微向下滚动一丢丢,就已经触发了很多次,而且其中很多的触发并没有实在的意义,如何减少触发的频率,减少那么多的计算操作呢?这就用到节流,让这个onscroll事件触发后周期性执行。
function throttle(func, wait) {
var timer; //闭包保存这个变量
return function () {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function () {
func.apply(context, args);
timer = null; //消除
}, wait);
}
}
}
将setTimeout返回的标记当做判断条件:判断当前定时器是否存在,如果存在表示还在冷却,并且在执行func之后消除定时器表示激活。
④设计模式中的单例模式
var SingleTon = function(){
var instance;
class CreateSingleTon {
constructor (name) {
if(instance) return instance;
this.name = name;
this.getName();
return instance = this;
}
getName() {
return this.name;
}
}
return CreateSingleTon;
}();
var a = new SingleTon('instance1');
console.log(a.getName()); //输出instance1
var b = new SingleTon('instance2');
console.log(b.getName()); //输出instance1
console.log(a === b); //输出true
未完待续...