JS高级入门教程

JS高级入门教程

目录

  1. 本文章定位及介绍php

  2. JavaScript与ECMAScript的关系html

  3. DOM的本质及DOM级介绍前端

  4. JS代码特性java

  5. 基本类型与引用类型node

  6. JS的垃圾回收机制程序员

  7. 做用域链介绍及其实现原理web

  8. 闭包面试

  9. this指针ajax

  10. 自执行函数的介绍及应用chrome

  11. 声明提早

  12. JS线程问题

本培训的定位及相关介绍

内容特色:

目标对象定位:

  • 主要面向对象:对于有一年左右工做经验的前端工程师。提升JS的认识,突破JS学习瓶颈。

  • 对于没有经验的小伙伴,能够经过本文章对JS有初步认识,了解JS的相关特性。

  • 除了系统性的JS内容,还会穿插介绍一些笔者在学习过程当中遇到的认识上的困惑与小问题。但愿能减小小伙伴在学习过程当中遇到的障碍。

  • 对了,里面有些内容是本人以为有趣的,好玩的概念。可能除了能帮你们理清楚概念之外,并没什么卵用。但愿你们不要见怪。

JavaScript与ECMAScript的关系

前言:

可能你们在阅读JS相关书籍或浏览网上资料的时候会多多少少看过ECMAScript这个词汇,它与JS有什么关系呢?还有为何JS最新的语法不是叫JS6而是叫ES6呢?在这里给你们介绍一下JS与ES的关系。
ES6

解析:

  • 首先ECMAScript(简称ES)是由欧洲计算机制造商协会ECMA(European Computer Manufacturers Association)制定的标准化脚本程序设计语言。

  • 其次ES只是JS的其中一部分。一个完整的JS主要包括这几部分(ES,DOM,BOM)。他们的结构图是这样的。JS与ES的关系

  • 其中ES负责脚本的基本语法及逻辑的实现。

  • DOM负责HTML标签的解析及渲染实现。

  • BOM负责提供浏览器的API接口。

结论:

  • ES是一个与应用场景无关的纯语言,只提供基本逻辑操做的实现,原则上不实现视觉上的功能。

  • WEB只是ES实现的宿主环境之一。ES在WEB中结合DOM和BOM造成了JS。

  • 这也是为何最新的JS语法称为ES6而不是JS6。(实际上,像前面所说的,ES只是一个纯语言,因此ES6上更新的只是JS语法上的内容。并不会提供其余新的WEB功能。新的WEB功能的内容属于HTML或CSS的内容,如HTML5和CSS3)

  • 小插曲,有的小伙伴可能看到过JScript这个词。JScript是在未制定出ES标准的混乱时代中,微软出的自家使用脚本语言,至关于IE版本的JS。但如今JS已经获得了统一。JScript已经不复存在了。

DOM的本质及DOM级介绍

前言:

在前端学习过程当中,咱们一听到DOM会天然地联想到HTML的标签。可是DOM真的只是和HTML有关系吗?到底什么才是DOM呢?另外时常遇到的DOM0123级又是指什么东西呢?

解析(DOM的本质)

  • 咱们先来看看DOM的字面意思是什么:DOM(Document Object Model),文档对象模型。是将基于某文档结构(如XML结构)的字符串转化为一棵在常驻内容的树状数据结构的模型。对于XML来讲,就是对XML标签进行解析后的数据结构体(咱们称之为DOM树)。

  • 它的理念是:开发者经过该数据结构得到文档结构(即有特定字符串组成的文档)的控制权。经过DOM提供的接口,对该文档结构进行增,删,改等操做,即对数据结构进行操做。

  • DOM自己是独立于平台和语言的。是进行脚本解析的解决思路(将文档转化为对象,并以树状结构组织起来)。不一样的语言能够针对自身的特色制做本身的DOM实现。(而咱们理解中的DOM只是针对HTML语言的是其中一种实现而已)

  • 除了HTML DOM外,其余的语言也发布了只针对本身的DOM标准。如SVG(可伸缩矢量图),MathML(数学标记语言),SMIL(同步多媒体集成语言)

解析(DOM级别介绍)

解释完DOM的本质之后,接下来的DOM全都特指HTML DOM

  • 一句话说完,DOM级别只是DOM的版本。不一样的DOM级实现了不一样的功能。

  • DOM0是指在W3C进行标准化以前,还处于未造成标准的初期阶段的版本。即还未造成标准的东西。严格意义上并不在DOM版本的范畴,只是为版本出来前的DOM起个名字,因此才说0版本。

  • DOM1级在1998年10月份成为W3C的提议,由DOM核心与DOM HTML两个模块组成。DOM核心能映射以XML为基础的文档结构,容许获取和操做文档的任意部分(即对某标签的get和set)。DOM HTML经过添加HTML专用的对象与函数对DOM核心进行了扩展(即封装了一些函数和对象,更方便地get和set)。

  • DOM2经过对象接口增长了对鼠标和用户界面事件、范围、遍历(重复执行DOM文档)和层叠样式表(CSS)的支持。

  • DOM3经过引入统一方式载入和保存文档和文档验证方法对DOM进行进一步扩展。

  • 并没什么卵用,只是学习几个名词的意思。

JS代码特性介绍

前言

这部份内容适合没有实际经验的同窗,能够经过这一部分了解JS的特性。有经验的同窗也能够了解一下,由于接下来的内容都是围绕这几大特性进行讲解的。

特性

  • 弱类型语言:在JS中,变量没有固定的数据类型。不一样数据类型的变量是能够相互转换的,如:var a = 0; a = "a";(前面赋值为数值类型,后面变成了字符串类型) 而C++,PHP则是强类型语言,不能直接进行数据类型的转换。

  • 解析性语言:不一样于java或C#等编译性语言,js是不须要进行编译的,而是由浏览器进行动态地解析与执行。能够理解为浏览器是一个大型的函数,而JS代码是函数的参数。由浏览器去解析JS代码。

  • 跨平台性:因为JS只依赖浏览器自己。底层实现无关,使得其他操做环境无关。实现了跨平台。

  • 一切皆对象:JS是一项面向对象的语言。所看到的一切都是一个对象。

  • 单线程:JS是单线程的。这牵扯到JS的代码执行顺序,下面章节会进行介绍。

  • 垃圾自动回收:JS不须要主动回收内存。JS引擎内部会周期性地检查内容,定时回收无用的内存。

基本类型与引用类型

前言

本小节将介绍JS的基本类型和引用类型。固然咱们不会讲哪些无聊的基本语法,而是深刻内部,介绍一些有趣的东西。

解析

  • ECMAScript中有5种基本数据类型:Undefined,Null,Boolean,Number和String。以及一种引用数据类型Object。

  • 其中Undefined和Null最为特殊,由于他们是只有一个值的数据类型(undefined和null)。并且他们几乎是同义的,他们之间有什么区别呢?(虽然并没什么卵用,但面试却最喜欢出这个题)。在这里作一下介绍。示例代码

    • 其实Underfined表示“缺失值”。表示有一个变量存在,即进行了定义,但该变量没有被赋值。

    • 而Null表示一个空对象指针,根本不存在这个变量。Null更多地是起到语义做用。强调不存在这个变量。并且在实际编程过程当中,除非主动为变量赋值为null,不然不多出现变量为null的状况。另外null的做用是提早标示该变量已无用,让GC回收机制能早点回收该资源。

    • 还有一个常见的面试题:请写出typeof null的值。如示例代码所示typeof null的值为object。有没有小伙伴会好奇为何其余4中基本数据类型的类型值都是其自身。而null的类型却为object

    • 其实这与JS的设计有关。JS类型值是存在32 BIT空间里面的,在这32位中有1-3位表示其类型值,其它位表示真实数值。其中表示object的标记位正好是低三位都是0。000: object. The data is a reference to an object.
      而JS里的Null是机器码NULL空指针(空指针以全0标示)故其标示为也是0,最终体现的类型仍是object。曾经有提案 typeof null === 'null'。但提案被拒绝了。

  • 基本数据类型和引用数据类型的区别:基本数据类型是按值访问的。即该变量就存在了实际值。而引用数据类型保存的是则是对实际值的引用(即指向实际值的指针)。而咱们知道这个有什么用呢?固然有用,这涉及到对象复制(浅复制与深复制)的问题。

    • 咱们来看一个例子示例代码。能够看到,到直接使用b=a进行赋值时。这时b获取到是a变量的指针值,即此时b和a指向的是同一个地址值。因此当修改b对象中的值时,a对象中的值也会发生改变。这种只复制指针地址值的行为称为浅复制。相应的,若是能返回独立对象的值,咱们称为深复制。这也是为何array的复制须要用到concat函数而不是直接用“=”进行复制。

    • 另外,插播一个知识点。在进行函数参数传递时。是经过按值传递的。即传递的是变量自己的值,而不是变量的引用。一样咱们看一下示例代码。即在函数在进行传递参数时,会新建一个形参变量elem,并为其赋予实参变量的值。也许有同窗会认为,在修改objA的时候明明会影响函数外部值,为何还能叫作按值传递呢?其实这偏偏是按值专递的结果。由于这里传递的是objA这个指针对象的值,即对象的地址。那么elem指向的就行该对象。即objA和elem指向的是同一个值(浅复制)。因此外部会发生变化

    • 引用类型和按值传递的概念很重要,后面讲函数特性及this指针的时候会用到。

  • 另外,还有一个有趣的知识点。var a = "aaaa";这里定义的a明明只是一个基本数据类型。而不是object,为何会有a.substr这样的函数呢?示例代码。其实JS引擎在读取基本数据类型时,会在后台建立一个对应的类对象(称为基本包装类型)。从而使咱们更加方便地对该变量进行操做。但这个基本操做类型的生存时间很是短,在相应的函数调用完成后就会自动销毁,变回基本数据类型。因此对其添加变量的操做是无效的。

JS的垃圾回收机制

  • 首先科普一下GC是什么意思。GC是垃圾回收的英文缩写GC(Gabage collection)。(额,本人以前一直觉得什么是高大上的东西....)

  • JavaScript有自动垃圾回收机制,也就是说执行环境会负责管理代码执行过程当中使用的内存,在开发过程当中就无需考虑内存回收问题。

  • JavaScript垃圾回收的机制很简单:找出再也不使用的变量,而后释放掉其占用的内存,可是这个过程不是实时的,由于其开销比较大,因此垃圾回收器会按照固定的时间间隔周期性的执行。

  • JS的内存回收机制通常有两种实现方式,分别是"标记清除""和"引用计数""方式。

  • 其中"标记清除"是最多见的回收方式,当某变量进入到执行上下文(做用域)时。JS引擎会将该变量标记为“进入环境”状态,当该环境所在的执行上下文(做用域)执行完毕时。里面的变量将被标记为“离开环境”状态。而后当JS引擎周期性地检测内存时。会将标记为“离开环境”的变量所占内存清空。以起到回收内存的目的。

  • 另一种垃圾回收实现方式是"引用计数"。即JS引擎会跟踪每个变量的被引用次数。当被其余变量引用时,计数就加一。但引用结束后,就将计数减一。当引用次数为0时,进行内存回收。但这种方法有可能会致使内存的泄露。示例代码

  • 在平常开发中,咱们能够经过将变量的值设置为null的方法,将该变量的引用计数清为0。主动回收内存资源

做用域链介绍及其实现原理

前言的前言

做用域是JS中至关重要的一部份内容。是理解JS其余高级内容(如:闭包,this指针,自执行函数)的基础。若是能把JS做用域的原理及其应用理解透,那至关于已经拿下半个JS了。

前言

首先,咱们先来看这么一段代码。示例代码。这段代码很简单,相信你们都能猜到预期的结果,外部函数不能访问内部函数定义的变量,因此最后一个输出elem2 is not defined。可是,有同窗好奇过为何外部函数就不能访问内部函数的变量吗?其底层的实现原理是什么?

做用域及做用域链介绍

在介绍上面的问题以前,咱们先来介绍做用域这个概念。

  • 做用域的定义

    • 定义什么的咱们就直接略过了,咱们用人话来讲:是指函数中定义过的内容的集合,即定义了的所有内容加在一块儿就是做用域(除了程序员主动定义的,还有程序内部帮咱们定义的变量,例如函数中arguments变量)。

  • 做用域有这么几个特色

    • 每定义一个函数,都会产生一个对应的做用域

    • 做用域的层级关系与函数定义时所处的层级关系相同

    • 各层做用域间经过单向链式结构组织起来,造成做用域链,连接方向为由内向外。

    • 变量的访问是从当前所在做用域起,沿着不断向外访问,直到访问到为止,因此做用域间的访问范围是单向的,是内部做用域能够访问外部做用域。

  • 结论:因此在上面的例子中,外部函数不能访问内部函数的变量

  • 彩蛋

    • 相信你们在看书的时候,常常会看到“做用域链”,“执行环境”,“执行上下文”这样的字眼。其实这三个词汇的意思是同样的。都是指在执行这段代码(一般以函数的形式存在)的时候,所能访问的资源集合。

  • 很简单,对吧。但这个概念后面会反复用到。

闭包

前言

讲闭包以前,咱们先来看一段示例代码。你们以为这段代码的运行结果是什么呢?

解析

  • 如控制台所示,log出来的结果是“elem a”。funA函数已经执行完了。elemA这个变量应该会被回收释放啊?那么为何还会输出a呢?咱们使用JS的其中一种回收机制“引用计数”发来分析一下。

  • 咱们把变量elemA的内存记为memoryA。memoryA首先会被elemA引用,引用计数为1。

  • 在funB函数中使用了elemA,memoryA引用计数加一,为2

  • funA执行完毕。回收funA中的elemA。memoryA引用计数减一。还剩1,不为零。因此JS回收机制不会回收memoryA的内存空间。因此在执行c()实际是执行funB的时候。还能访问到memoryA的内存。因此输出为"elem a"。

  • 像这种函数自己已执行完,但因为其内部变量仍被使用。而得不到释放的现象。咱们就称做为“闭包”。那么怎么释放该内存呢?继续减小其引用计数便可。就如代码中所示。执行funB函数。memoryA的引用计数再减一。为0。回收机制便可回收该内存。

  • 咱们能够经过做用域链的角度,使用“标记清除”法再分析一遍。在定义elemA的时候,elemA标记为“进入环境”(即进入其自己的做用域)。在funA执行完成后,本来其做用域须要回收的,可是因为funB使用到elemA变量。因此funA的做用域没能回收,因此elemA仍在“做用域链中”,没能被标记为“离开环境”。因此没能被释放。

  • 接下来,咱们经过一些小练习强化一下对闭包的理解。示例代码

  • 为何输出的两遍都是2?咱们来分析一下。

  • setTimeout函数里面的i是在那个做用域里面?是在setTimeout函数外部的test函数里面。因此这里造成了闭包。即便test执行完毕,i变量也不会被释放。

  • setTimeout函数和test函数那个函数先被执行?很明显setTimeout中的函数是后于test函数执行的。因此当setTimeout函数执行的时候。i以及被自增为2了。因此两边的输出为2。

  • 再来一个更难的。咱们结合做用域和闭包来看一道题示例代码

  • 这不是造成了闭包吗?为何输出的是2而不是3呢?

  • 咱们回过头看一下做用域链的定义。函数的做用域层级是在定义函数的时候就被固定下来的,与函数定义时的层级关系相同。因此funA和funB的做用域是处于同一级的。不存在闭包关系。因此funA中的输出的A的值是外部定义的A的值。因此是2而不是3。

this指针

this指针的介绍

  • 什么是this指针。

    • this指针是指向某个对象空间的指针,通常状况下是指向(和强类型语言同样)定义当前做用域所在的对象示例代码。如示例代码所示,logName函数中的this指针指向了objA对象(logName的做用域是在objA对象中定义的)。因此输出了aaa。

    • 接着看上面的示例代码,咱们在后面定义了一个没有logName函数的对象objB,但没有定义logName函数。那须要输出objB中的name要这么办呢?咱们能不能向objA对象借用一下logName函数?让objA里面的this指针指向objB对象呢?

  • JS中this指针的特色

    • 不一样于强语言的是,JS中的this指针是能够经过修改,动态变化的。咱们能够经过修改this指针来实现上面的需求。如示例代码

    • 比较两个例子,能够发现,第二个代码多加了bind(objB)。是这里改变了this指针的指向吗?是的。其实在JS中。有三个函数能够改变this指针的指向。分别是bind,call,和apply函数。

  • bind,call,apply的介绍和比较

    • 先来看看这三个函数的使用示例示例代码

    • 首先,三个里面,最突出的函数就是bind。为何惟独bind函数不是直接使用。还须要在外面添加一个setTimeout呢?若是不加setTimeout会怎么样呢?示例代码。在不加setTimeout的时候没有输出任何东西。而加了setTimeout才会输出。其实那是由于使用bind的时候,是仅仅修改this指针,并不会执行函数。在setTimeout中,计时后,才由setTimeout去调用执行这个函数。

    • 接着,call和apply直接执行了函数。他们的区别是什么呢?他们惟一的区别是传参方式的不一样,call函数以枚举(即直接列出来)的方式传参。而apply是以数组的方式传参的。咱们试一下调转过来传参示例代码

    • 彩蛋,除了使用bind来修改this指针之外。咱们还可使用它来返回一个函数,日后再去执行。示例代码。其实这个实例中并没什么卵用,只是这个实例说明两个问题。

      • 当bind函数的第一个参数传null时,表示不改变函数中this的指向。

      • 当函数前面的参数已经被赋值时,再使用bind时,是从剩余没有赋值的函数参数开始赋值的。

this指针的应用

  • 将this的指针指向正确的值。这个内容后面再讲。

  • 利用this指针借用某对象的函数,前面的几个例子就是利用了this指针借用函数。在介绍更多例子以前,咱们先插入一个伪数组的概念

    • 伪数组是指哪些只有length和中括索引功能,没有数组相应功能函数的数据结构。例如示例代码。如这个例子中pList1就没有slice的函数。

    • 在前端中最常常接触到的伪数组有函数中的隐藏变量 arguments 和用 getElementsByTagName得到到的元素集合。

    • 这时咱们就能够利用到this指针去借用数组中的函数,实现咱们想要的目的。示例代码

    • 另外怎么把伪数组转化为真数组呢?其实只要在上一个例子上再修改一下就能够了。示例代码

  • 在此基础上,咱们还能够利用this指针实现一部分类功能。示例代码

this指针与做用域的关系(主要是window)

其实若是没有该死的window对象的话,本来this指针和做用域是没有太大关系的

  • 先看代码,示例代码。!!!?竟然输出的是HanMeiMei而不是LiLei?为何会输出HanMeiMei?这是否是意味着this指针发生变化了?咱们log一下this指针的值.

  • 示例代码。果真,this指针真的是指向了window??WTF?咱们明明没有修改过this指针的值啊?为何this的指向改变了?

  • 在这里就要补充一下的this指针的定义了。上面讲到(通常状况下是指向定义当前做用域时所在的对象,即在那个对象内定义就是指向谁。)。但实际上,this是指向经过点操做符(如objA.funA())调用本函数的那个对象。即谁调用我,我就指向谁。示例代码如在这里,objB并无定义logName函数。只是定义了一个变量并赋值为函数的引用。这时使用objB.logName实际上调用的是objA对象里面的函数。而log出来的对象就是objB。

  • 再回到setTimeout函数。咱们前面log过this,发现this是指向window的。这证实在setTimeout中的是window对象去调用logName的。即至关于window.logName();发现问题了吗?HanMeiMei既是做用域中的变量,也是window对象中的变量。

  • 实际上,全部在全局做用域(注意仅仅是全局做用域)中定义的变量都是window对象下的变量。均可以经过window对象进行访问。因此一旦没有经过对象去调用某函数,而是直接运行的话(如不是objA.fun()而是直接fun()),等价于在window下调用(fun()等价于window.fun())。this指针的值都指向window。

  • 再因为全部在全局做用域(注意仅仅是全局做用域)中定义的变量都是window对象下的变量因此logName中的值指指向了window.name。也就是做用域中的name值。做用域就是经过window与this指针挂上了关系。

this指针和闭包的比较

  • 这是一个性能优化的探讨问题。

  • 有时候咱们会遇到这样的状况。使用闭包和this指针均可以实现一样的功能。例如示例代码。使用这两种方式都可以成功log出name的值。那这时候咱们使用哪一个好呢?

  • 因为log函数会输出到控制台,执行速度慢,咱们经过修改name的值来模拟内部操做示例代码能够看到。直接使用闭包的性能更佳。性能是使用bind的5倍以上。因为时间缘由。原理我就再也不这里介绍了。有兴趣的同窗能够经过这个连接看看.为何闭包比this更快

  • 至此,本教程中,最困难的两个点(this指针与JS线程)中的其中一个已经介绍完了。接下来休息一下,穿插几个比较简单的概念。

自执行函数

什么是自执行函数

  • 这个术语看起来很高大上。其实说到底就是一个定义完了立刻就执行的函数。其实这不是JS的新特性。而是利用JS特性作出来的效果。即JS特性的巧妙应用。它的表现形式是这样的。示例代码.其中第一个()是用于隔离第二个括号,使其function(){}函数定义完,避免语法错误。而第二个()是为了执行刚刚定义好的函数。

自执行函数的做用

  • 第一个,也是百度答案最多的一个避免污染全局做用域

    • 究竟是怎么污染全局做用域呢?示例代码。这里假设调用了两个JS的状况,本来LaoWang要向HanMeiMei表白的,可是因为第二个文件中,name的值被修改为了LiLei,致使LaoWang表错白。恩,小明是爽了。隔壁老王就糟了。

    • 那怎么用自执行函数来解决呢?很简单,用自执行函数把每一个人本身写的JS代码包裹起来就能够了;示例代码.

    • 原理:利用到了做用域的概念。因为每自执行函数自己造成了一个做用域。而这两个做用域是处于同一级的。互不影响,从而避免了污染全局做用域。这个技巧在多人协做的项目里面颇有用。

  • 第二个,也是用得比较少的一个构建私有属性

    • 使用强类型语言的同窗应该都知道私有的概念。而在JS中,没有类的概念(在ES5以前是没有的,但ES6新增了类的概念),从而也没有了私有属性。但咱们能够利用自执行函数构建一个。示例代码。这样就避免了使用直接调用name变量。实现了私有属性的功能。

    • 原理:前面对闭包及做用域理解了的同窗,相信大家已经能够猜想背后的原理了。这里使用到了闭包(使得name不被回收),做用域(使得返回的对象中的函数能访问name变量),已经自执行函数(没有留下函数的引用,使得外界不能访问)的特色。

声明提早

前言

人们常说JS是解释性语言,语句都是执行到哪里才进行解析的。但实际上真的是这样吗?

什么是声明提早

  • JS声明提早是指,在做用域中(即在函数中啦),变量的声明老是优先于其余语句被执行的。(即老是先执行声明语句,再执行其余语句)。示例代码

  • 但若是把var name;改为var name = "LiLei"又会怎么样呢?示例代码。说好的提早呢?其实JS引擎在解析这段代码的时候会把var name = "LiLei"分红两条语句来执行。一个是变量定义,一个是变量赋值。因此实际上登记于这样示例代码

  • 声明对于函数一样适用示例代码。并且注意点也是同样的。示例代码

何时容易出现错误

讲这个点以前,先插入两个待会要用到的概念

  • 没有块级做用域

    • 在JS中是没有块级做用域的概念的。for,while等控制语句不构成块级做用域。示例代码

  • 没有经过var定义的变量均是定义全局变量

    • 如标题所说。 没有经过var定义的变量都是全局变量,都在全局做用域中找获得。示例代码。当这两个概念在加载声明提早上就很恶心了。

  • 例子示例代码。这个例子比较特殊,在chrome中可能回报错。建议你们用其余浏览器打开。我这里使用safari打开。执行结果是HanMeiMei。给你们一点时间整理一下思路。其实它等价于

  • 固然这些例子都比较极端。并且看起来很傻。其实只是想告诉你们。当你们在见解中遇到很奇怪的bug的时候。能够考虑是否是因为声明提早引发的了。

JS线程问题

前言

这是前面提到过在本教程中,最复杂的两个知识点(this指针与JS线程)中的JS线程问题。虽然咱们平常开发中可能不会主动提到这个概念,但其实咱们常常会用到。譬如ajax异步请求,事件处理,计时器延迟执行等都涉及到JS线程的概念。学习好本概念虽然不会像this指针那样解决不少问题,但有助于加深咱们对JS底层的理解。有助于理清逻辑关系。

什么是线程

  • 首先,咱们先来了解一下什么是线程。百度一下。恩,第一句进程什么的咱们就略过,先无论啦。重点是后面那句。线程是程序执行流的最小单元。用人话翻译就是说,一次只能执行一段代码的执行器。同一时间内只能完成一项任务。后一项任务必须等待前面的任务执行完成才能被执行。

  • 单线程语言,顾名思义,是指一次只能执行一项任务的语言。

  • 多线程语言,是指一次能执行多项任务的语言。典型的多线程语言有C,C++等强类型语言。

  • JS是单线语言。介绍到这里的时候,不知道小伙伴们会不会有这样的疑惑。不对啊,JS能执行异步操做(例如AJAX操做)的啊,异步操做不就是容许后面的代码先执行吗?这和单线程的概念相冲突啊。JS怎么会是单线程的呢?恩,咱们带着这个问题往下看。

页面加载流程解析

为何js文件要放在body标签的底部,而不建议放在头部。由于当JS文件加载过程太慢的时候,会阻碍后面标签的执行。恩,这个知识点相信你们都知道。但为何JS文件的加载会影响HTML标签的解析呢?它底层的原理是什么呢?

先问你们一个小问题,你知道在HTML页面里面,怎么样作到不引人script标签去执行JS代码?

  • 先抛出两个概念,浏览器的核心是两部分:渲染引擎和JavaScript解释器(又称JavaScript引擎)。

    • 其中渲染引擎负责解析HTML标签,将标签转化为HTML视图。渲染引擎处理页面,一般分红四个阶段

      • 解析代码:HTML代码解析为DOM,CSS代码解析为CSSOM(CSS Object Model)

      • 对象合成:将DOM和CSSOM合成一棵渲染树(render tree)

      • 布局:计算出渲染树的布局(layout)

      • 绘制:将渲染树绘制到屏幕

    • JavaScript引擎的主要做用是,读取网页中的JavaScript代码,对其处理后运行。

    • 渲染引擎和JS引擎分别使用不一样的线程

  • 其实整个HTML页面的加载过程是这样的。

    • 浏览器一边下载HTML网页,一边开始解析。

    • 解析过程当中,发现script标签。

    • 暂停解析,网页渲染的控制权转交给JavaScript引擎。

    • 若是script标签引用了外部脚本,就下载外部脚本,不然就直接执行脚本。

    • 执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页

  • 也就是说,加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。既然渲染引擎和JS引擎是用不一样的线程去执行的。那应该能够并行执行啊。例如渲染线程继续解析标签,JS引擎去执行JS语句。为何不这样作呢?

  • 缘由是JavaScript能够修改DOM(好比使用document.write方法),因此必须把控制权让给它,不然会致使复杂的线程竞赛的问题。(例如同时对同一个DIV进行修改,那以那个为准?)

  • 彩蛋,恩,回答前面问的那个问题。其实在html页面中,在标签里面设置的每个事件。都是一个JS执行段。都会以事件回调的方式执行里面的代码。示例代码

JS事件循环(Event Loop)

注意,这里讲的是JS的事件循环机制,不是JS的事件传递机制,经过本节的学习,你将明白JS异步是怎么实现的.

  • 首先,须要明确一个概念:JS是单线程的,但并不意味着浏览器也是单线程的。实际上浏览器是多线程的(例如前面讲到的渲染引擎就是其中一个线程),JS经过和浏览器后台线程的配合,实现了JS的事件机制。

  • 咱们以示例代码为例。JS的执行流程是这样的。

    • 首先JS在底层维护了一个是消息队列的队列。

    • JS执行到addEventListener时,将回调函数的地址交给监听页面操做的其中一个浏览器后台线程(假设叫作监听线程)。

    • 当监听的事件被触发的时候,监听线程将以前JS交代的回调函数地址放入到消息队列当中。

    • 重点来了,JS引擎是怎么读取消息列表的呢?JS引擎是先把JS同步操做所有执行完毕(即JS文件中的代码所有执行完毕,咱们先用“主逻辑操做”),才会去按顺序调用消息队列的回调函数地址。并且这个从消息队列中调用回调函数的过程是循环执行的。

  • 从上面的分析中,咱们能够获得如下结论:

    • 所有回调操做都在主逻辑操做完成后才被执行的.

    • 因为消息队列是以队列的形式保存起来的。而队列自己是一个先进先出的数据结构,因此会优先调用队列排在前面的回调函数,(因为JS的执行是单线程的,一次只能执行一个代码段)因此只有前一个回调函数被执行完了,第二个回调函数才能被执行。因此回调函数的执行顺序是在被加入到消息队列的那一刻决定的。

    • 另外,除了前面提到的监听线程,浏览器还有处理定时器的进程(如setTimeout)、处理用户输入的进程(input)、处理网络通讯的进程(AJAX)等等

setTimeout问题

  • 一样的setTimeout函数也可使用上面的过程进行分析。只不过把上面的监听线程换成处理定时器的浏览器线程(假设叫作定时器线程)。即

    • JS执行到setTimeout时,将回调函数的地址交给定时器线程。

    • 定时间线程进行计时,当计时结束后。定时器线程将所携带的回调函数地址放入消息队列中。

    • JS引擎把主逻辑执行完成后,调用消息队列中的回调函数。

  • 前面两步都没有问题,但到最后一步就可能会出现问题了。

    • 因为定时器线程和JS引擎是使用不一样线程,同时进行的。而JS引擎去读取消息队列以前须要先将主操做执行完。那么一旦主操做的执行时间大于定时器的计时时间。那么回调函数的时间等待时间将大于程序所设置的计时时间。示例代码

    • 另外,除了主操做会延迟计时器的回调函数执行。因为JS引擎在读取消息队列的时候是按顺序读取的。这意味着排在前面的回调函数也可能会推迟排在后面的计时器回调函数的执行示例代码

    • 插播一个知识HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,若是低于这个值,就会自动增长。在此以前,老版本的浏览器都将最短间隔设为10毫秒。示例代码。固然了,实际运行时间还须要结合电脑自己的运行状况。再加上console.log自己须要消耗必定的时间。因此每次的运行时间可能都会有变化。在这是也只是告诉你们会有这个规定。

  • setTimeout的妙用。

    • 除了日常的计时做用,咱们还能够利用消息队列必须在主逻辑以后被执行的特色,结合setTimeout把某段代码放在最后执行(即主逻辑以后)。示例代码setTimeout(fn,0)的含义是,指定某个任务在主线程最先可得的空闲时间执行。

HTML5新内容:worker介绍

web Worker这是H5新出的一个内容,使用这个API,咱们能够简单地建立后台运行的线程。但JS本质上仍是单线程的。

  • 咱们先来看看他的基本使用方法示例代码。注意,worker的调用是异步的。因此after post优先执行。

  • 须要注意的是worker只能进行数据操做,不能调用DOM,和BOM。它里面连window对象都没有。这个我就不进行深刻讲解的。这些均可以在网上找获得。我想把哪些不怎么容易找获得的内容给你们讲解一下。

  • worker实现的不是真正意义上的线程,它彻底受主线程所控制的。示例代码注意,这里执行将执行死循环,小伙伴们根据本身的状况考虑要不要运行】。能够看到,一开始worker能够被正常执行,但当JS主线程被的死循环执行的时候,worker立刻中止了工做,貌似JS和worker同一时间只能执行一个。而真正的多线程运行结果应该是script和worker随机交替出现的。(其实这里的分析是不够全面的)

  • 前面只给到JS主线程貌似阻塞了worker的线程。那么反过来。worker会不会阻塞JS主线程的操做呢?咱们来看示例代码。【注意,这里也是死循环的】(其实你们应该已经猜到了运行的结果,若是worker会阻塞JS主逻辑的操做,那要worker还有什么用?)能够看到,worker线程并不会阻塞JS主线程的执行。这意味着worker颇有用。咱们能够将一些很耗时的操做放到worker中执行。保证主页面的流畅运行。譬如对大型的图片或附件进行压缩,加密操做等。参考网站,这是一个分解质因数的网站,以前高等数学的老师告诉咱们。分解质因数的难度复杂度是O(n1/4)。这意味着整个分解过程是很耗时间的。而这个页面使用了worker在后台进行分解。因此在等待的过程当中,页面是不卡的。

worker的本质分析

  • 咱们来分析一下worker的本质是什么,其实worker本质上和浏览器的计时器线程等是没有区别的。是浏览器新创建的一个线程,用于执行特定的任务。

  • 而worker实际上和JS主线程是能够同步执行的,是能够组成多线段的。以前貌似JS阻塞了worker的缘由,只是由于console对象不能同时被多个线程所拥有。而JS主线程的优先级比worker高,因此从worker中抢走了console对象的控制器。形成了这个现象发生。

  • 而第二个例子则证实了worker和JS主线程的并行性。由于在作修改数字的操做时。console的输出没有被打断。

  • 因此证实worker是能够实如今数据计算上的多线程。但因为它自己不彻底具有JS的所有功能,如不能操做DOM,BOM。因此广泛被认为worker实现的是ECMAScript的多线程,JS从本质上是单线程的