整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

JavaScript 函数执行方式

数的各种执行方式

① 普通函数调用(包括匿名函数自调用)

② 构造函数执行 new 函数

③ 作为对象的成员方法执行

④ 通过call和apply执行

call和apply可以明显控制变量污染的风险。

实例1:

<script type="text/javascript">
//call方式执行函数
function say(){
document.write("在"+this.addr+'演讲');
}
var dog = {addr:'德国',color:'black'};
//dog对象去调用say()函数执行
//通过"call()"实现
//函数.call(函数内部this的指引,形参1, 形参2, 形参3...);
say.call(dog);//在德国演讲
var cat = {addr:'波斯湾'}
say.call(cat);//在波斯湾演讲
//dog.express = say;
//dog.express = "abc";//同名的express把同名的方法污染了
//dog.express();
</script>

实例2:

<script type="text/javascript">
//call方式执行函数
function say(){
document.write("在"+this.addr+'演讲');
}
var dog = {addr:'德国',color:'black'};
//dog对象去调用say()函数执行
//通过"call()"实现
//函数.call(函数内部this的指引, 形参1, 形参2, 形参3...);
say.call(dog);//在德国演讲
var cat = {addr:'波斯湾'}
say.call(cat);//在波斯湾演讲
dog.express = say;
dog.express(); //显示:在德国演讲
dog.express = "abc";//同名的express把同名的方法污染了
document.write(dog.express);//显示:abc
dog.express(); //没有结果
</script>

实例3:

<script type="text/javascript">
//call方式执行函数
function run(time,addr){
document.write(this.name+"时间:"+time+",在"+addr+"跑步");
}

var tiger = {name:'东北虎',weight:220}
run.call(tiger); //东北虎时间:undefined,在undefined跑步
run.call(tiger,'早上','深山老林');//东北虎时间:早上,在深山老林跑步

var cat = {name:'kitty'}
run.apply(cat,['傍晚','屋后']);//kitty时间:傍晚,在屋后跑步
</script>

注意:call和apply的区别在于语法格式区别

实例3:

JavaScript在我们日常开发中扮演着至关重要的角色,大部分时间都与它打交道,因此对这门语言的了解再多也不为过。

虽然有很多关于JavaScript的书籍和资料,但绝大多数都从JavaScript语言本身的角度去分析其语法和语义,很少有与JavaScript具体执行过程相关的分析资料。因此,我邀请大家一起从浏览器的角度来看一下一段JavaScript代码到底是如何执行的。

以 Google 浏览器为例,JavaScript 代码由 V8 引擎负责解释执行。代码的执行总体上经历以下三个步骤:

  1. 将代码解析为抽象语法树(AST)并创建执行上下文
  2. 根据 AST 生成字节码作为中间代码
  3. 解释器 Ignition 会逐条解释执行代码

整个流程图如下:

生成AST

为什么要将代码解析成AST呢,直接用原代码不行吗?

答案肯定是不行,因为浏览器并不能理解我们所写的代码。这个理由就像将html需要解析为DOM一样,浏览器并不能理解我们所编写的代码,需要将我们写的代码编译成浏览器能够理解的结构,也就是AST。

AST是一种非常重要的数据结构,许多著名的项目都在使用它。

比如Babel,Babel被用于代码转换,将es6的代码转换为es5的代码,用于解决兼容性问题,Babel的工作原理就是将es6源码解析为AST,再将es6的AST转化为es5的AST,最后利用es5的AST生成源码。

类似的还有ESlint,ESlint是用来检查js代码规范的插件,它的检查流程也是将源码解析成AST,再利用AST来检查代码的规范问题。

将源码解析成AST通常需要两个步骤:

一、词法分析

词法分析又称为分词(tokenize),是将一行行代码拆分为一个个token。如下图:

二、语法分析

语法分析又称解析,将上一步生成的token数据根据语法规则转化为AST。在转化过程中,如果不符合语法规则,会终止转化并抛出一个语法错误。转化成的AST如下图:

可以通过 AST可视化网站:https://astexplorer.net/ 来体验成生成AST。

除了生成AST,该阶段还会创建代码块的执行上下文。其实上述的编译过程都发生在v8引擎内部,对于我们开发者来说完全是黑盒的,但是执行上下文这个概念却是至关重要的,因为它是代码运行的基本环境。

生成字节码

生成AST后,接着解释器 Ignition 就会将AST转化为字节码并解释执行字节码。

为什么又要将AST解析成字节码呢,上文不是说过浏览器可以理解AST吗?答案是解决内存占用的问题。

其实一开始v8引擎并不会将AST转化为字节码,而是直接将AST转化为机器码并执行。虽然执行效率特别高,但是同时暴露出来了严重的内存占用问题。因为v8引擎要消耗大量的内存来存放转化后的机器码。为了解决内存占用的问题,v8团队引入了字节码,花了大量的时间,完成了当前的这套架构。

解释下字节码是什么,字节码是介于AST和机器码之间的一种代码,字节码需要解释器将其转化为机器码之后才能执行。为了更好的去理解字节码,下面是源码、字节码、机器码的一个对比图:

可以看出,字节码对比机器码占用的空间要小得多。所以使用字节码可以减少系统的内存使用。

执行代码

对于一段第一次被执行的字节码,解释器 Ignition 会逐条解释并执行。在执行的过程中,如果发现热点代码(Hotspot),比如说一段被重复执行多次的代码,后台的 编译器 TurboFan 就会将该段热点字节码编译成机器码并保存起,当下次在执行到该段热点代码时,只需要执行编译后的机器码就行,这样就节提升了大量的执行效率。执行流程如下图所示:

这种字节码配合解释器和编译器的技术正是最近很火的技术————即时编译(JIT)

总结

至此本文分析完了 JavaScript 代码执行的整个阶段。在宏观角度上 JavaScript 代码会经历如下步骤:

  1. 解析器将 JavaScript 代码解析(词法分析、语法分析)成AST并创建执行上下文
  2. 解释器 Ignition 将AST转化为字节码
  3. 解释器 Ignition 对字节码逐条解释执行
  4. 如果发现热点代码(HotSpot),后台编译器 TurboFan 会将热点代码编译成机器码并保存,进而提升执行效率。

整个JavaScript代码执行是慢启动,越执行越快。这种字节码配合解释器和编译器的技术叫做即时编译(JIT)。v8引擎也是基于这种技术来实现对内存占用和执行效率的调控

最后在JavaScript代码执行过程中,最重要的部分就是执行上下文了,它是代码运行时的基本环境。

参考链接

  • 浏览器工作原理与实践:https://blog.poetries.top/browser-working-principle/guide/part2/lesson07.html#%E5%8F%98%E9%87%8F%E6%8F%90%E5%8D%87-hoisting
  • How JavaScript Works:https://www.freecodecamp.org/news/javascript-under-the-hood-v8/

来源:微信公众号:大转转FE

出处:https://mp.weixin.qq.com/s/ZFtxVyERBkCgSadQqYxijQ

了执行Javascript,需要在HTML文件内以特定的方式书写JavaScript的代码,JavaScript的书写方法有多种,其执行的流程也各不相同:

1 <script>标签嵌入

此种嵌入方法无法操作<script>之后的DOM元素。因为<script>之后的DOM元素还未构造,因此在<script>标签内就无法取得位于其后的DOM元素。

2 读取外部JavaScript文件

此种嵌入方法可以指定defer、async属性。defer可以推迟执行,async可以异步执行。

3 onload嵌入

此种嵌入方法在页面读取完后再对其执行,所以可以对所有的DOM元素操作。

<body onload="alert('hello')">
window.onload = function(){alert('hello');};

当window.onload事件触发时,页面上所有的DOM、样式表、脚本、图片、flash都已经加载完成了。

//window.onload不能同时编写多个。
//以下代码无法正确执行,结果只输出第二个。
window.onload = function(){
  alert("test1");
};

window.onload = function(){
  alert("test2");
};

//$(document).ready()能同时编写多个
//结果两次都输出
$(document).ready(function(){ 
   alert("Hello World"); 
}); 
$(document).ready(function(){ 
   alert("Hello again"); 
}); 

window.onload和body中onload也有些许区别:

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.0.js"></script>
    <script language="javascript">
        window.onload = haha;
        function haha(){console.log("window.onload");}

        if(document.addEventListener){
            function DOMContentLoaded(){
                console.log("DOMContentLoaded");
            }
            document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
        }</script>
</head>
<body onload="console.log('bodyonload');">
        <div id="div1">a</div>
</body>
</html>

在IE10和FireFox下,结果为 :

"DOMContentLoaded"
"bodyonload"

说明body中的onload会覆盖window.onload

在chrome下,结果为:

DOMContentLoaded
window.onload
bodyonload

然后,如果把javascript代码移到最下面,结果又会是什么样呢?

chrome和IE10、FireFox的结果竟然是一样的:

DOMContentLoaded
window.onload

IE 10、Fire Fox可以理解,window.on load和body中的 on load 谁在下面就是谁覆盖谁,只会执行后面的那个。

4 DOM ContentLoaded嵌入

onload方法可能需要等待时间,而本方法可以在完成HTML解析后发生的事件,减少等待时间。

在chrome、IE10和FireFox中,执行结果是:DOMContentLoaded然后才是onload的输出。所以说一般情况下,DOMContentLoaded事件要在window.onload之前执行,当DOM树构建完成的时候就会执行DOMContentLoaded事件。

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title></title>
    <script type="text/javascript" src="jquery2.js"></script>
    <script language="javascript">
        window.onload = haha;
        function haha(){console.log(document.getElementById("div1"));}
        if(document.addEventListener){
            function DOMContentLoaded(){
                console.log("DOMContentLoaded");
            }
            document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
        }
    </script>
</head>
<body>
    <div id="div1">a</div>
</body>
</html>

如果你是个jQuery使用者,你可能会经常使用$(document).ready();或者$(function(){}),这都是使用了DOMContentLoaded事件

5 动态载入JavaScript文件

5.1 使用原生js方法

动态创建script标签,并指定script的src属性

function loadJs(url, callback) {
    var script = document.createElement('script');
    script.type = "text/javascript";
    if (typeof(callback) != "undefined") {
        if (script.readyState) {
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" || script.readyState == "complete") {
                    script.onreadystatechange = null;
                    callback();
                }
            }
        } else {
            script.onload = function() {
                callback();
            }
        }
    }
    script.src = url;
    document.body.appendChild(script);
}
loadJs("test.js", function() {
    alert('done');
});

还可以使用同样的原理动态加载css文件,只不过插入的的父节点是head标签。

5.2 使用document.write/writeln()方式

该种方式可以实现js文件的动态加载,原理就是在重写文档流,这种方式会导致整个页面重绘。

document.writeln("<script src=\"http://lib.sinaapp.com/js/jquery/1.6/jquery.min.js\"></script>");

需要注意的是特殊字符的转义。

5.3 使用jQuery

使用getScript(url,callback)方法实现动态加载js文件

$.getScript('test.js',function(){
    alert('done');
});

-End-