JaveScript的实现
javaScript的实现应该包括三个部分ECMAScript、DOM、BOM
核心-ECMAScript
ECMAScript即为ECMA-262定义的语言,并不局限于Web浏览器,Web浏览器只是ECMAScript实现可能存在的一种宿主环境,宿主环境提供了ECMAScript的基准实现和扩展,扩展也是使用ECMAScript的核心类型、语法,但提供特定于环境的额外功能,其他宿主环境还有node.js、adobe flash等
DOM(文档对象模型)
DOM是一个应用编程接口(API),这也与之前提到的扩展的定义相同,DOM也是使用ECMAScript实现。DOM将整个页面抽象成为一组分组节点。比如我们常见的html页面,就可以通过dom表示成为一组分组节点。
BOM(浏览器对象模型)
BOM让开发者可以操控浏览器显示页面之外的部分,比如浏览器的窗口,但BOM并没有一个javaScript的标准实现,这导致BOM是出现问题最多的地方,在h5出现后,html以正式规范的形式涵盖了尽可能多的BOM特性。 BOM主要包括两部分:浏览器窗口和子窗口,我们将特定于浏览器的扩展都归为BOM的部分,比如:
- 弹出浏览器新窗口,比如可以使用window.open打开新的浏览器窗口
- 操作浏览器窗口大小、位置
- navigator对象,提供了浏览器的信息
- location对象,提供了浏览器加载页面的信息
- screen对象,提供了用户浏览器的屏幕相关信息
- performance对象,提供了浏览器内存占用、导航、使用时间等信息
- cookie
- ...
JavaScript的使用
首先我们需要明白,页面的主体语言是HTML,那么我们要使用JS,就得处理好HTML与JS的关系,那么我们该如何在页面中使用JavaScript呢?
JavaScript的引入
在页面中使用JS,我们通常在html中使用<script>标签引入js代码,包含在<script>标签中的代码按照从上到下的顺序解释,并且在<script>元素中的代码被计算完成之前,页面的其余内容不会被加载和显示,即<script>脚本的加载和执行的过程中,会阻塞后续DOM的渲染。过去,所有<script>元素都放在<head>中,但页面是在浏览器解析到<body>的时候才开始渲染的,这意味这如果有JS代码没有下载、解析、解释完成,浏览器窗口会保持空白,因此,现在的浏览器往往把<script>放在<body>中的页面元素之后,这样页面在处理JS代码之前完全渲染页面,用户会感觉页面加载更快。
使用script标签引入js主要包括行内脚本和外部脚本两种方式,但我们更加推荐尽可能将JavaScript代码维护在外部文件中,这不仅更加方便开发者的维护,且更能提高复用性,并且如果几个页面同时使用到了一个js文件,用户只需要下载一次,能够更快加载页面,并且如果考虑到不同文本标记语言,比如html和xhtml,内部js使用可能不同,但如果外部引入,就不需要考虑这个差异,有更好的兼容性。
1.行内脚本
行内脚本是指直接在<script>标签中编写js代码。
2.外部脚本
外部脚本是指通过<script>标签的src属性引入外部js文件。
script标签
普通<script>标签内的脚本执行有如下的特点:
- 文档解析的过程中,如果遇到script脚本,会停止页面的解析渲染,下载script脚本
- 如果存在多个script脚本,会近似于并行下载script脚本,尽管之前提到,遇到script脚本会停止后续标签的解析渲染,但chrome做了优化,遇到script脚本,会快速的查看后面有没有需要下载的资源,一起并行下载。
- 尽管脚本是近似并行开始下载,脚本下载完成时间并不一致,脚本的执行顺序总是按照html中的先后顺序一次执行的,如果后面的脚本先下载完了,也必须要等前面的脚本下载好并执行完成才能开始执行
- 执行script脚本时,会停止页面的解析渲染
- 执行完script脚本后,继续页面的解析渲染
- 执行完script脚本,页面渲染完成之后,才会依次触发
DOMContentLoaded(页面渲染完成,但可能还有外部资源没有加载)、loaded(外部资源加载完成)等事件
以一段代码演示上述内容效果
<!DOCTYPE html>
<html lang="en">
<script>
function test1() {
console.log("test1执行了");
}
test1();
</script>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script>
function test2() {
console.log("test2执行了");
}
test2();
</script>
</head>
<script>
function test3() {
console.log("test3执行了");
}
test3();
</script>
<body>
<script>
function test4() {
console.log("test4执行了");
setTimeout(() => {
console.log("test4延迟执行");
}, 2000);
}
test4();
</script>
<div style="width: 100%; height: 20px; background-color: blue">测试DOM</div>
</body>
<script>
function test5() {
console.log("test5执行了");
}
test5();
</script>
</html>
这段代码在不同的地方使用了script元素,最终的执行顺序如下:
最终可以看到script是按html文档中的顺序执行的,至于为什么test4延迟执行在最后打印,这与js的事件循环机制有关,后面再详细讨论。
接着我们在代码中test4函数中增加一个死循环来验证script脚本的执行是否会影响页面的渲染:
<!DOCTYPE html>
<html lang="en">
<script>
function test1() {
console.log("test1执行了");
}
test1();
</script>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script>
function test2() {
console.log("test2执行了");
}
test2();
</script>
</head>
<script>
function test3() {
console.log("test3执行了");
}
test3();
</script>
<body>
<script>
function test4() {
console.log("test4执行了");
var i = 0;
while (true) {
i += 1;
}
setTimeout(() => {
console.log("test4延迟执行");
}, 2000);
}
test4();
</script>
<div style="width: 100%; height: 20px; background-color: blue">测试DOM</div>
</body>
<script>
function test5() {
console.log("test5执行了");
}
test5();
</script>
</html>
输出效果如下:
且页面保持加载状态,文档没有渲染出来,页面为空白,验证了我们的想法
script标签的属性
但如果js文件太大太多,每次都阻碍了我们页面的渲染,这会给用户带来很差的体验,这不是我们希望得到的效果,那我们有没有办法解决这个问题呢?
这个问题的解决可以使用script标签的async属性或defer属性,首先我们来看一下script标签有哪些属性
1. async: 可选属性,表示应该立即开始下载脚本,但不会阻止页面的其他动作,比如下载资源、等待其他脚本下载,只对外部脚本生效。
2. charset: 可选属性,使用src属性指定的代码字符集,这个属性很少使用,因为大多数浏览器不在乎他的值。
3. crossorigin: 可选属性,配置相关请求的CORS设置,默认不使用CORS。如果配置了这个属性,会让浏览器启用CORS检查,浏览器会检查响应头中的
Access-Control-Allow-Origin是否与当前文档同源,开启 CORS 校验之后,跨域的 script 资源在运行出错的时候,window.onerror 可以捕获到完整的错误信息。crossorigin有两种不同的值:anonymous(默认)和user-credentials,只有指定use-credentials时才表现为use-credentials,其他任何不合法的值都视为anonymous。值为anonymous时,获取资源时不会发送cookie,收到请求后响应头中的Access-Control-Allow-Origin与请求头中的origin相同时浏览器才会执行脚本;值为user-credentials时,发送请求是否携带cookie取决于cookie的同站策略,且响应头中的Access-Control-Allow-Origin与请求头中的origin相同并且Access-Control-Allow-Credentials为 true时,浏览器才会执行脚本。4. defer: 可选属性,表示脚本可以延迟到文档完全解析和显示之后再执行,只对外部脚本有效。
5. integrity: 可选属性,允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI, Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错, 脚本不会执行。这个属性可以用于确保内容分发网络(CDN,Content Delivery Network)不会提 供恶意内容。
6. language: 废弃,最初用于表示代码块中的脚本语言(如
JavaScript、JavaScript 1.2或VBScript)。大多数浏览器都会忽略这个属性,不应该再使用它。7. src: 可选属性,表示包含要执行的代码的外部文件。
8. type: 可选属性,代替 language,表示代码块中脚本语言的内容类型(也称 MIME 类型)。按照惯 例,这个值始终都是
text/javascript,尽管text/javascript和text/ecmascript都已经废弃了。JavaScript 文件的 MIME 类型通常是application/x-javascript,不过给 type 属性这个值有可能导致脚本被忽略。在非 IE 的浏览器中有效的其他值还有application/javascript和application/ecmascript。如果这个值是module,则代码会被当成 ES6 模块,而且只有这时候代码中才能出现 import 和 export 关键字。
接下来我们使用async属性和defer属性来验证一下效果,我们准备三个文件来验证这两个属性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./test1.js" defer></script>
<script src="./test2.js" async></script>
<div>111</div>
</body>
</html>
function test1() {
console.log("test1执行了");
var i = 0;
while (true) {
i++;
}
}
test1();
function test2() {
console.log("test2执行了");
var i = 0;
while (true) {
i++;
}
}
test2();
页面效果如下
可以看到,页面渲染没有被阻塞,设置了defer属性的srcipt运行了,设置了async的脚本没有运行,因为async属性的script一直在等待前面的脚本运行完,这说明脚本仍然按照顺序执行。如果调整<script>顺序,相应的执行的就是test2函数,那么async属性和defer属性有什么区别呢?
答案是async属性设置的script无法保证执行顺序,添加async的目的是告诉浏览器,不需要等脚本下载和执行完成再加载页面,也不需要等异步脚本下载和执行后再加载其他脚本,即异步脚本一定会在loaded事件之前执行,但不能确保是在DOMContentLoaded之前或是之后,这意味着执行异步脚本的时候,页面可能没有渲染完成,因此最好不要在异步脚本中操作dom,在实际开发中我们也不推荐使用异步脚本;defer属性设置的脚本按照规范遵循执行的顺序以出现的先后顺序执行,并一定会在DOMContentLoaded之前执行,但实际可能并非如此,因此最好只有一个defer属性设置的脚本。
动态加载脚本
<script>标签加载是一种加载js的方式,但我们还可以使用DOM API动态添加script元素,比如:
let script = document.createElement('script');
script.src = 'test.js';
document.head.appendChild(script);
默认情况下,这种方式创建的<script>在DOM创建并执行脚本之前不会引入test.js,即默认是以异步脚本的形式引入的,如果不希望以异步的形式,可以显式指定async的值为false。
以这种方式引入外部资源,浏览器预处理器不知道资源文件的存在,这样可能会严重影响性能,我们可以在文档头部使用link标签让预处理器加载文件,即
<link rel="preload" href="test.js">
noscript标签
当一个浏览器不支持或者禁用了JavaScript的支持,<noscript>中的内容就会被渲染,标签中的内容可以是<body>中能出现的任何内容(除<script>),这样就可以给不支持JS的浏览器提供一个替代内容,实现页面的优雅降级。
小结
javaScript并不只是包括ECMAScript的相关内容,还有DOM、BOM的存在,javaScript的引入主要通过<script>标签,<script>的执行顺序是按脚本出现的顺序决定(特殊属性除外),脚本的加载和执行影响页面渲染的效率,这对我们提高前端性能有很大的帮助。如今前端开发框架层出不穷,但了解HTML如何加载js文件对我们理解前端的相关开发内容很有帮助。