JS探险之作用域(二)(变量提升,闭包,this指向,执行上下文,垃圾回收)

1、提升

首先,先来回顾一下作用域的概念,作用域是指一套良好的规则,以便更加方便地寻找到变量。

函数作用域与块作用域的行为是一样的,总结为,任何声明在某个作用域内的变量,都将附属于这个作用域。

思考一下代码:

var a;
 a = 2;
 console.log(a);//2
var a;
console.log(a);//undefined
a = 2;

上文说到,var a = 2;在JS眼中实际上是两个过程,首先,编译器先找到这些变量 var a;,然后引擎运行到赋值的位置时 a = 2,才进行赋值,也就是说,包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理

这个过程就如同变量和函数声明从它们在代码中出现的位置被“提升”到了最上面,这个过程———“变量提升”

note:
1、每个作用域都会进行提升操作(所在作用域最上方),尽管并不都是提升到全局。
2、函数会被提升,函数表达式并不会提升。
3、只有声明本身会被提升,而赋值或其他运行逻辑会留在原地,如果提升改变了代码执行顺序,将会造成非常严重的后果。
4、函数提升的优先级 > 变量提升的优先级。

2、 作用域与闭包

接下探讨更加深入一些的内容——“闭包”

2.1 什么是闭包

什么是闭包?
闭包并不是JS设计者故意设计出的内容,而是基于词法作用域书写代码时,所产生的自然结果。

闭包的产生:

funcion foo () {
	var a = 2;
	function bar () {
		console.log(a);
	}
	bar();
}
foo();
//2

从以上代码的运行结果可以看出,函数bar(),访问到了a变量,也就是说,当函数可以记住并访问所在的词法作用域时,就产生了闭包。

可以简单地这么理解。

2.2 理解闭包

观察闭包如何工作

function foo() {
	var a = 2;
	function bar() {
		console.log(a);
	}
	return bar;
}
var baz = foo();
baz();//2

在这里,我们将bar()放在自己定义的词法作用域以外的地方执行。

通常,在foo(),执行以后,会将foo()整个内部作用域销毁(垃圾回收),来释放空间,但是,上述代码中,这件事好像并没有发生。

这就是闭包的神奇之处,它阻止了这件事,看起来,内部作用域依然存在,没有被回收,正是bar()这个函数在foo()内部起作用。因此,bar()拥有foo()的内部作用域,使得这个作用域依旧存活,以供bar()在之后的任何时间引用。

bar()依然保持对该作用域的引用,这个引用就叫做闭包。

note:
1、在这个过程中,词法作用域保持完整。
2、只要使用回调函数,基本是在使用闭包。
3、闭包使函数可以访问定义时的词法作用域。
4、无论通过何种手段将内部的函数传递到所在的词法作用域外都会保持对原始作用域的引用。

2.3 循环与闭包

for (let i = 1; i <= 5; i++) {
	(function (j) {
		setTimeout(function timer () {
			console.log(j)
		},j*1000);
	})(i)
}

观察以上代码,每次循环,IIFE都会产生一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确的变量提供访问。

注意:let i= 1,会在for循环中形成一个块作用域,作为闭包的块作用域。

3、this指向问题

接下来进入一个最精彩的环节——this指向,有了前面的知识铺垫,理解this问题,应该可能大概不会很难。

首先,this指向哪里?指向本身还是指向函数的词法作用域?no,都不。

this是在运行时绑定,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

那么this到底是什么?

当一个函数被创建时,会创建一个活动记录(有时候也称为执行上下文),这个记录会包包含函数在哪里被调用(调用栈),函数的调用方式,传入的参数等信息,this就是这个记录的一个属性,会在函数执行的过程中用到。

3.1 调用位置

调用位置就是函数在代码中被调用的位置(而不是声明的位置),this的绑定也只与它的调用位置有关,通常来说,寻找调用位置就是寻找“函数被调用的位置”

3.2 绑定规则

1、默认绑定:当进行独立函数调用时,this指向全局对象。
2、隐式绑定:把函数调用中的this绑定到这个上下文对象(obj.a/this.a),对象属性引用链中,只有上一层或者说最后一层在调用位置中起作用

note:setTimeout()等定时器函数,中的this,指向全局,如何解决? self = this 或 ()=> {},下面的文章会介绍,这里暂时不讨论。

3、显式绑定:call(),apply(),bind()

function foo() {
	console.log(this.a);
}
var obj = {
	a:2;
}
foo.call(obj);//2

调用foo()时,强制绑定到this上。

4、new 绑定
在JS中,构造函数只是一些使用new操作符时被调用的函数,它们并不会属于某个类,也不会实例化一个类,甚至都不能说时一种特殊的函数类型,只是被new操作符调用的普通函数。

所以,包括内置对象函数在内的所有函数都可以使用new来调用,这种函数调用被称为构造函数调用
,实际上并不存在所谓的“构造函数”,只有对函数的”构造调用“。

使用new来构造调用,会发生四个步骤:
1、创建一个全新的对象{};
2、这个新对象执行prototype连接;
3、新对象绑定到函数调用的this;
4、如果函数没有返回其他对象,那么new表达式中的函数调用,会自动返回这个新对象。

简单说,new,一共完成了三个步骤:

var obj = {};//创建变量
obj.__proto__ = Object.prototype;//改变指向
Object.call(obj);//将Object(),中this指向obj变量

5、优先级:显式 >隐式

3.3 this词法

箭头函数:

箭头函数并不是使用function定义,而是使用”=>“定义,箭头函数抛开了this的四种绑定规则,而是根据全局作用域定义this。

箭头函数可以像bind()一样,确保函数this绑定到指定对象,此外,最重要的是,它用更常见的词法作用域取代了传统的this机制。

function foo() {
	setTimeout(()=>{
		console.log(this.a);//2
	},100);
}
var obj = {
	a:2
}
foo.call(obj);

或者

function foo() {
	var self = this;
	setTimeout(()=>{
		console.log(this.a);//2
	},100);
}
var obj = {
	a:2
}
foo.call(obj);

note:如果想知道this是什么,必须检查相关的函数是如何被调用。

4、执行上下文与作用域

变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每一个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上,这个对象无法通过代码访问,但后台数据处理会用到它,你也可以认为这个上下文对象是一个虚拟对象

全局上下文是最外层上下文,根据ECMAScript实现的宿主环境,表示全局上下文的对象可能不一样。在浏览器中,全局上下文就是我们常说的window对象,因此,所有通过var定义的全局变量和函数都会成为window对象的属性和方法。使用。let,const的顶级声明不会定义在全局上下文中,但是在作用域链上的解析效果是一样的。

上下文在其所有代码都执行完毕后会被销毁掉,包括定义在上面的所有变量和函数,全局上下文会在应用程序退出前才会被销毁。

每个函数都有自己的执行上下文,当代码执行流进入函数时,函数的上下文被推到一个上下文栈上,在函执行完毕后,上下文栈会弹出该执行上下文。将控制权返回给之前的执行上下文,ECMAScript执行流就是通过这个上下文栈控制的。

上下文代码在执行过程中,会创建变量对象的一个作用域链。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序,代码正在执行的上下文的变量对象始终位于作用域链的最前端

var color = "blue";
function changeFn() {
	console.log(color)
}
 changeFn();

观察以上代码有几个上下文,思考输出是什么,为什么?

note:内部上下文可以通过作用域链访问外部上下文中的一切,但是外部上下文无法访问内部上下文。

总结:
1、全局执行上下文:
在执行全局代码前将window确定为全局执行上下文;
对全局数据进行处理;
2、函数执行上下文:
在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象;
对局部函数进行预处理;
3、上下文栈:
在全局代码执行前,JS引擎就会创建一个栈来储存管理所有的执行上下文对象;
在全局执行上下文确定后,压栈;
在函数执行上下文创建后,压栈;
在当前函数执行完毕后,出栈;
所有代码执行完毕后,栈中只有window

可以粗浅地理解为:

书写代码,创建相应的词法作用域等 —>>> 交给编译器,实现提升,预处理,代码分析,AST,代码生成等 -->>> 交给引擎,确定作用域与作用域链,执行上下文(this就是在这里的一个属性),执行代码等

4、垃圾回收

首先要明确的是,JS是使用垃圾回收机制的语言,也就是说,执行环境负责在代码执行过程中管理内存,通过自动内存管理实现内存分配和闲置资源的回收。

基本思路为:

确定哪个变量不会使用,然后释放它的内存。这个过程是周期性的,即,垃圾回收程序每隔一段时间,或者说在代码执行过程中某个预定的收集时间,就会自动执行。

在浏览器发展史上,主要用到两种标记策略:标记清理和引用计数

4.1 性能

垃圾回收程序会周期运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限度的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。开发者不知道什么时候运行时会收集垃圾,因此,最好的方法是在写代码时就要:无论什么时候使用垃圾回收,让它尽快结束。

4.2 内存管理

在垃圾回收的编程环境中,开发者无需关心内存管理,不过,JS运行在一个内存管理与垃圾回收都和很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的就更少了。这更多出于安全考虑而不是别的,就是避免运行大量的JS的网页耗尽系统内存而导致操作系统奔溃。

将内存占用量保持在一个较小的值,可以让页面的性能更好。

优化内存占用的最佳手段是,保持在执行代码时,只保存必要的数据。如果数据不再必要,那么把它设置成null,从而释放引用,这也叫解除引用

function creatPerson (name) {
	let localPerson = new Object();
	localPerson.name = name;
	return localPerson;
}
let globalPerson = createPerson("张三");
//解除引用
globalPerson = null;

note:解除引用后并不会立刻触发垃圾回收机制,解除引用的关键在于确保相关的值已经不在上下文里,因此它在下次垃圾回收时会被回收。

4.3 内存泄漏

写得不好的JS可能会出现难以察觉的内存泄露问题。在内存有限的设备上,或者在函数会被调用很多次的情况下,内存泄漏可能是个很大的问题,JS中的内存泄漏大部分是由不合理的引用导致的。

4.4 静态分配与对象池

为了提升JS性能,最后要考虑的一点往往就是压榨浏览器,此时,一个关键的问题就是,如何减少浏览器执行垃圾回收的次数,开发者无法直接控制什么时候开始回收垃圾,但可以间接控制触发垃圾回收机制的条件。

理论上,如果合理使用内存分配,同时避免多余的垃圾回收,就可以保住因释放内存而损失的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值