asnyc、defer
< script >标签属性设置
无async和defer的< script >
标签
- 立即加载脚本
- 会阻塞页面其他操作(暂停解析HTML)
- 脚本加载完成之后立即执行
- 当代码执行完毕后恢复解析
有async的< script >标签
代码用法:
<script>alert("hello world1")</script> <script src="Eureka.js" async></script> <script>alert("hello world2")</script> <script>alert("hello world3")</script>
页面加载的过程&script脚本的输出顺序:
- 立即加载脚本
- 不阻塞页面其他操作(异步)
- 脚本加载完成之后
- 如果HTML未解析完,则暂停解析先让JS执行代码
- 如果HTML解析完,则JS立即执行代码
- 如果存在多个 async 的时候则不能保证执行顺序(谁先到执行谁)
有defer的< script >标签
代码用法:
<body> <h1>Hello</h1> <script defer type="text/javascript"src="/script2. js"></script> <script defer type="text/javascript"src="/script1. js"></script> <p>after script tags</p> <script type="text/javascript"> window. addEventListener('DOMContentLoaded', function(){ console.1og('DOMContentLoaded') }) </script> </body>
页面加载的过程&script脚本的输出顺序:
- 立即加载脚本
- 不阻塞页面其他操作(异步)
- 加载完成之后JS不会立即执行
- html5会按文档中顺序执行
🤔 如果同时存在async,defer属性呢?
😁 则相当于只有 async
怎么使用
- async:
- 脚本是模块化的,并且不依赖于任何脚本
- 你的脚本并不关心页面中的DOM元素
- Defer:
- 脚本依赖于或者被另一个脚本所依赖
- 你的脚本代码依赖于页面中的DOM元素(文档是否解析完毕)
- 不放置任何属性:
- 脚本很小,并且异步脚本依赖于它
执行上下文、作用域
简单代码
🤔我们先来看一段代码,思考一下他是怎么运行的?
console.log(a); //undefined var a = 1; var b = 2; var result = a + b; console.log(result); //3
😅为什么a是undefined呢?下面不是已经定义了a=1了吗?
😁 这时候我们就要了解一下JS在执行代码前做了什么导致 a = undefined。
解析过程
在代码被执行之前有一个解析的过程,而在这个过程中JS引擎会创建一个对象(Global Object)下面简称GO
Global Object
- Global Object里面一般包含当前浏览器环境或node环境所拥有的全局变量
- 还有一个window指向自己
var globalObject = { Date、Array、String、Number、setTimeout、setInterval、window:globalObject.... }
而在开头那段代码里创建的GO是什么样的呢?
var globalObject = { Data Array String window: globalObject, //自己定义的变量 a: undefined, b: undefined, result: undefined }
运行代码前的工作
为了有层次的运行代码,就要依靠JS引擎内部的执行上下文栈(Execution Context Stack,简称ECS)用来执行全局代码块的调用栈
而ECS这个栈里面会有全局执行上下文(Global Execution Context,简称GEC)(全局代码需要执行时才创建)被放进里面,如图:(图中的VO指的是变量对象,在全局执行上下文中VO是GO)
执行代码
从上到下依次执行代码,往GO里面的变量赋值
所以开头console.log(a) 执行的结果才会是 undefined
有函数的代码
🤔当遇到有函数的代码怎么办呢?
var a = 1; foo(); function foo(){ var b = 2; console.log("foo"); // foo } console.log(a)
解析过程
var globalObject = { ... //自己定义的变量 a: undefined, foo: 0xa00 //表示JS引擎为foo函数开辟的空间的地址 }
运行代码前的工作
ECS这个栈里面会有全局执行上下文(Global Execution Context,简称GEC)(全局代码需要执行时才创建)被放进里面,如图:(图中的VO指的是变量对象,在全局执行上下文中VO是GO)
执行代码
从上到下依次执行代码,往GO里面的变量赋值
给 a 赋值之后,我们遇到了foo()
,函数的执行不是在全局执行上下文中执行,而是在函数执行上下文(Functional Execution Context, 简称FEC)中执行(FEC由JS引擎遇到函数自行创建)。
函数执行上下文中也有一个VO,不同于全局上下文中的VO,函数中的VO执行完是会被销毁的。
所以函数执行上下文中的VO不对应GO,而是对应AO(Activation Object)
如图(还未执行函数里面的内容):
接着就是执行函数里面的内容(赋值,console.log("foo"))执行完毕就被弹出执行上下文栈并且销毁,接着执行GEC剩下的内容
作用域
简单来说,作用域 指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。
🤔如果在函数中我们找不到变量怎么办?
var a = 1; function foo() { console.log(a) } function bar() { var a = 2 foo() } bar()
这段代码中console.log(a)输出的会是什么?
首先我们要知道在AO中找不到要输出的变量就要到父级作用域去找。
如果答案是 2 的话就说明你混淆了作用域和执行上下文的概念,正确答案是 1
在这段代码中,函数foo的父级作用域是全局作用域
函数能够访问到的上层作用域,是在函数声明时候就已经确定了的。
函数声明在哪里,上层作用域就在哪里,和拿到哪里执行没有关系。
而对于执行上下文,从上面就可以知道,遇到一个函数就创建一个函数执行上下文,所以执行上下文是不确定的
扩:变量环境、词法环境
上面的讲解是基于ES3版本规范,ES5 规范又对 ES3 中执行上下文的部分概念做了调整,最主要的调整,就是去除了 ES3 中AO和VO,以 词法环境组件(LexicalEnvironment component) 和 变量环境组件(VariableEnvironment component) 替代。
变更前后的本质其实差不多,变更前的只适用于var变量,而变更后则可以适用于let、const和var变量
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储var变量绑定。
内存管理和垃圾回收
认识内存管理
不管什么样的编程语言,在代码的执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存(如:比如C、C++都是需要手动来管理内存的申请和释放的(malloc和free函数)),某些编程语言会可以自动帮助我们管理内存(如:JavaScript通常情况下不需要手动管理)
内存管理的生命周期
- 申请内存
- 使用内存
- 释放内存
JS的内存分配
JavaScript会在定义变量时为我们分配内存
而对于变量,它有两种类型:简单数据类型(内存直接存的值),分别是Undefined,Null,Boolean,Number,String,Symbol,还有一种复杂数据类型(内存存的是引用地址):Object(对象、数组、函数)。
这两种类型在内存管理中分配内存的方式是不一样的:
- 简单数据类型(基本类型)的值直接在栈空间进行分配,值与值之间是独立存在,修改一个变量的值不会影响其他变量的值
- 复杂数据类型(引用类型)存在堆内存中,每创建一个对象,就会在堆内存中开辟出一个新的空间,而变量保存的对象的内存地址(对象的引用,如果两个变量保存的同一个对象,当一个变量改变属性值时,另一个变量的属性值也会变 )
简单数据类型的内存分配
// 定义变量 var a = 1; var b = 2;
复杂数据类型的内存分配
// 定义变量 const a = 1; const b = 2; const info { name: 'eureka', age: 100 } function foo() { var c = 3; var d = 4; console.log(a+b); }
JS的内存释放
因为内存的大小是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间。
因为JS不用手动去释放内存,它有自己的垃圾回收机制(Garbage Collection,简称:GC),那JavaScript是怎么判断这个内存不再使用,进而被当成垃圾回收呢?
- 当对象不再被引用时,JavaScript就认为它不再使用 (通过引用计数来确认)
- 对象不能从根上访问到时,JavaScript也会把它认定为是垃圾 (通过标记清除来确认)
常见的GC算法——引用计数
当一个对象有一个引用指向它时,那么这个对象的引用就+1,当一个对象的引用为0时,这个对象就可以被销毁掉;
使用引用计数会有一个很严重的问题:循环引用。
循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用,并且不能通过代码访问它们,这样它们的引用永远不会为0,垃圾回收机制不能将这两个对象释放,进而会造成内存泄露
let a = { name: 'a', dad: b }; let b = { name: 'b', son: a } a = null; b = null;
常见的GC算法——标记清除
- 这个算法是假定一个根对象(root object),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于哪些没有引用到的对象,就认为是不可用的对象;
- 根对象在浏览器中是 window 对象,在 NodeJS 中是 global 对象,只有当程序退出或者关闭网页或者关闭浏览器的时候才会销毁。
- 这个算法可以很好的解决循环引用的问题;