这篇文章主要介绍JavaScript中的作用域、原型链、垃圾回收机制、闭包、事件执行顺序
执行环境及作用域
一、执行环境的定义
执行环境: 执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。
全局执行环境: 在Web浏览器中,全局执行环境被认为时window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。全局执行环境直到应用程序退出,例如关闭网页或浏览器时才会被销毁。
函数执行环境: 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回个之前的执行环境。
二、作用域链
1) 定义: 当代码在一个环境中执行时,会创建变量对象的一个作用域链。保证对执行环境有权访问的所有变量和函数的有序访问。
2) 用途: 保证对执行环境有权访问的所有变量和函数的有序访问。作用域的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象再最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
3) 查找过程: 标识符解析式沿着作用域链一级一级地搜索标识符的过程。搜索过程始终是从作用域的前端开始,然后逐级地向后回溯,直至找到标识符为止(如果找不到标识符,通常会导致错误)。
4) 注意事项: 内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。
参考来自: JavaScript高级程序设计(第3版) 4.2 执行环境及作用域 P73
原型继承与原型链
一、原型链的定义
原型链: 每一个实例对象都有一个私有属性(称之为__proto__)指向它的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到原型链的末端null。根据定义,null没有原型,并作为原型链中的最后一个环节(Object.prototype.__proto__),所以原型的终点是null,因为null没有__proto__属性,于是返回undefined。几乎所有JavaScript中的对象都是位于原型链顶端的Object的实例,除了Object.create(null)。在访问一个对象的属性或方法时,若是在当前对象上找不到,就会到它的原型上找,如果还是找不到就沿着它的原型向上查找,如果找到就返回结果,如果没有就一直查找直到原型链的末端null(Object.prototype.__proto__,其指向null),返回undefined。
二、原型链的详细描述
1) 若 A 通过 new 创建了 B,则 B.__proto__ = A.prototype;
2) 执行B.a,若在B中找不到a,则会在B.__proto__中,也就是A.prototype中查找,若A.prototype中仍然没有,则会继续向上查找,最终,一定会找到Object.prototype,倘若还找不到,因为Object.prototype.__proto__指向null,因此会返回undefined;
3) 原型链的顶端,一定有 Object.prototype.__proto__ ——> null。
三、原型分类
原型分类:
A、函数对象: 它的实例为Function的对象。typeof Obj === "function",如String、Number、Boolean、Object、Function、Array、Date、RegExp、Error。拥有 proto 和 prototype。
B、普通对象: 它的实例为Object的对象。 typeof fn === "object",如 const obj = {}; const fn = new fun1(){};
只有__proto__。
proto: 隐式原型。
1) 大多数情况下__proto__可以理解为"构造器的原型"。主动指向构造的prototype(这个属性会指向该对象的原型),通过Object.create()创建的对象不适用此等式
const obj = {}; obj.__proto__ === Object.prototype;
function fn() {}; fn.__proto__ === Function.prototype
2) 该属性是创建一个函数和对象时自动生成的,除了null
prototype: 显示原型
1)该属性是创建函数对象时,自动生成的
2)该属性被__proto__属性所指向
function Fun(){}
// 以下是 JavaScript隐式执行的; func.prototype.__proto__ === Object.prototype为true
Fun.__proto__ = Function.prototype;
Fun.prototype = {
constructor: Fun,
__proto__: Object.prototype
}
四、new关键字对一个普通函数做了什么隐式操作?:
function myNew(Cons,...args){
let obj = {};
obj.__proto__ = Cons.prototype; //执行原型链接
let res = Cons.call(obj,args);
return typeof res === 'object' ? res : obj;
}
参考来自:
你还没学会JavaScript原型和原型链吗?
三张图搞懂JavaScript的原型对象与原型链
垃圾收集机制
一、定义
垃圾收集: JavaScript具有自动垃圾收集机制,也就是说执行环境会负责管理代码执行过程中使用的内存。垃圾回收机制的原理就是找出那些不再继续使用的变量,然后释放其占用内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。
二、垃圾收集方式
1) 标记清除: 是目前主流的垃圾收集算法。它是当变量进入环境(例如,在环境中声明一个变量)时,就将这个变量标记为"进入环境";当变量离开环境时,则将其标记为"离开环境"。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加入标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
2) 引用计数: JavaScript引擎目前都不再使用这种算法。它是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
引用计数的缺陷: 出现循环引用时,会出现严重问题。循环引用是指对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。当循环引用出现在函数中时,当函数执行完之后,objectA和objectB它们还会继续存在,因为它们的引用计数永远不会是0。假使这个函数被重复多次调用,就会导致大量内存得不到回收。
三、内存优化
优化方式: 优化内存占用的最佳方式就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null来释放其引用,这种做法叫做解除引用。这一做法使用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动解除引用。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
参考: JavaScript高级程序设计(第3版) 4.3 垃圾收集 P78
闭包
一、 定义
定义: 闭包是指有权访问另一个函数作用域中的变量的函数。
二、闭包的优缺点
优点:
a. 可以读取函数内部的变量
b. 可以让变量的值始终保存在内存中
c. 进行数据隐藏和封装
缺点:
a. 过度使用闭包会导致内存泄露
b. 在闭包中使用 this 对象也可能会导致一些问题。在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。
三、创建闭包的常见方式
在一个函数内部创建另一个函数,然后将它返回出去。
function add() {
let num = 1;
return function () {
return ++num;
}
}
const getCount = add();
getCount(); // 2
getCount(); // 3
四、闭包为什么可以访问另外一个函数的作用域中的变量?
注:外部函数称为fnA,被返回出去的内部函数称为fnB
当fnB从fnA中被返回后,fnB的作用域链被初始化为包含fnA函数的活动对象和全局对象。因此fnB可以访问fnA中定义的所有变量。由于fnB的作用域链一直在引用fnA的活动对象,因此当fnA执行完后,虽然fnA的执行环境的作用域被销毁,但是它的活动对象仍然会留在内存中,直到fnB被销毁后,fnA的活动对象才会被销毁。
五、清除闭包
将对匿名函数引用的变量值设置为null解除对该函数的引用,等同于通知垃圾回收机制将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)都可以安全地销毁了。
参考: JavaScript高级程序设计(第3版) 7.2 闭包 P178
JavaScript中setTimeout/setInterval与promise的执行顺序
一、同步和异步任务的执行顺序(不考虑Promise)
1.同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
2.当指定的事情完成时,Event Table会将这个函数移入Event Queue。
3.主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行
4.上述过程会不断重复,也就是常说的Event Loop(事件循环)
注: 这里的异步指的是setTimeout和setInterval
二、宏任务与微任务
JavaScript中的任务广义上分为两类:同步任务、异步任务,在精细一点分为宏任务和微任务。
macro-task(宏任务/宏观任务): 宿主(浏览器、Node)发起的任务称为宏观任务。也就是宿主环境传递给JavaScript引擎一段代码,引擎把代码直接顺次执行了,这个任务也就是宿主发起的任务。宏任务包括整体代码script、setTimeout和setInterval。
micro-task(微任务/微观任务): JavaScript引擎发起的任务称为微观任务。也就是ES6的Promise,不需要浏览器的安排,JavaScript引擎本身也可以发起任务。微任务包括Promise、process.nextTick(node.js中的API)。
三、整体的事件循环顺序
整体的事件循环顺序: 进入整体代码(宏任务)后,开始第一次循环,先执行完所有的同步任务 ---> 接着执行所有满足条件的微任务 ----> 然后再次从宏任务开始,找到其中一个任务队列执行完毕,在执行所有的满足条件的微任务。
四、事件循环、宏任务、微任务案例分析
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
resolve();
}).then(function() {
console.log('then');
})
console.log('console');
步骤解析:
1) 这段代码作为宏任务进入主线程
2) 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue
3) 接下来遇到Promise,new Promise立即执行,输出'promise', then函数分发到微任务 Event Queue
4) 遇到console.log('console'), 立即执行。
5) 整段代码script作为第一个宏任务执行结束,然后查询满足条件的微任务,发现promise的then,执行
6) 第一轮时间循环结束了,然后开始第二轮,先从宏任务Event Queue开始,发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行
7) 结束
参考来源: 这一次,彻底弄懂 JavaScript 执行机制
以上内容如有不对,希望大家指出,谢谢。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。