06_JavaScript 高级

发布时间 2023-10-31 15:15:02作者: 城市炊烟

一、浏览器深入解析

https://www.cnblogs.com/1rookie/p/7832930.html

https://segmentfault.com/a/1190000018428170

1.1、浏览器的组成

​ 浏览器在不断的演变中,并没有被要求呈现出一种特定的形态,但基本包括了如用户地址栏输入框、网络请求、浏览器文档解析、渲染引擎渲染网页、 JavaScript 引擎执行 js 脚本、客户端存储等功能。

​ 从原理构成上分为七个模块,分别是User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI后端)、Date Persistence(数据持久化存储)。

结构如下所示:

​ 和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个渲染引擎实例。每个标签页都是一个独立的进程。

​ 其中,最重要的是渲染引擎(内核)和JavaScript解释器(JavaScript引擎)。浏览器内核主要负责 HTML、CSS 的解析,页面布局、渲染与复合层合成,主流的内核有:Blink、Webkit、Gecko、EdgeHTML、Trident。

​ JavaScript引擎负责 JavaScript 代码的解释与执行,主流的 JavaScript 引擎有:V8、SpiderMonkey、JavaScriptCore、Chakra。

​ 首先浏览器是一个软件,就和qq,wechat,没什么差别,只是功能不一样。qq,wechat是社交类,而浏览器就是专门用来访问和浏览万维网页面的客户端软件。

1. User Interface(用户界面):

​ 包括工具栏、地址栏、前进/后退按钮、书签菜单、可视化页面加载进度、智能下载处理、首选项、打印等。除了浏览器主窗口显示请求的页面之外,其他显示的部分都属于用户界面。

2. Browser engine(浏览器引擎):

​ 浏览器引擎是一个可嵌入的组件,其为渲染引擎提供高级接口。

​ 浏览器引擎可以加载一个给定的URI,并支持诸如:前进/后退/重新加载等浏览操作。

​ 浏览器引擎提供查看浏览会话的各个方面的挂钩,例如:当前页面加载进度。

​ 浏览器引擎还允许查询/修改渲染引擎设置。

3. Rendering Engine(渲染引擎):

​ 渲染引擎为指定的URI生成可视化的表示。

​ 渲染引擎能够显示HTML和XML文档,可选择CSS样式,以及嵌入式内容(如图片)。

​ 渲染引擎能够准确计算页面布局,可使用“回流”算法逐步调整页面元素的位置。

​ 渲染引擎内部包含HTML解析器。

​ Chrome为每个Tab分配了各自的渲染引擎实例,每个Tab就是一个独立的进程。

4. Networking(网络)

​ 网络系统实现HTTP和FTP等文件传输协议。

​ 网络系统可以在不同的字符集之间进行转换,为文件解析MIME媒体类型。

​ 网络系统可以实现最近检索资源的缓存功能。

5. JavaScript Interpreter(JS解释器)

​ JavaScript解释器能够解释并执行嵌入在网页中的JavaScript(又称ECMAScript)代码。 为了安全起见,浏览器引擎或渲染引擎可能会禁用某些JavaScript功能,如弹出窗口的打开。

6. XML Parser(XML解析器)

​ XML解析器可以将XML文档解析成文档对象模型(Document Object Model,DOM)树。 XML解析器是浏览器架构中复用最多的子系统之一,几乎所有的浏览器实现都利用现有的XML解析器,而不是从头开始创建自己的XML解析器

​ 功能相似的HTML解析器和XML解析器为什么前者划分在渲染引擎中,后者作为独立的系统?

​ XML解析器对于系统来说,其功能并不是关键性的,但是从复用角度来说,XML解析器是一个通用的,可重用的组件,具有标准的,定义明确的接口。相比之下,HTML解析器通常与渲染引擎紧耦合。

7. UI Backend(显示后端)

​ 用来绘制类似组合选择框及对话框等基本组件,具有不特定于某个平台的通用接口,底层使用操作系统的用户接口。

8. Data Persistence(数据持久层)

​ 数据持久层将与浏览会话相关联的各种数据存储在硬盘上。 这些数据可能是诸如:书签、工具栏设置等这样的高级数据,也可能是诸如:Cookie,安全证书、缓存等这样的低级数据。

1.2、浏览器内核

​ 浏览器内核负责对网页语法的解释(如标准通用标记语言下的一个应用HTML、JavaScript)并渲染(显示)网页。

​ 浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。

​ 内核分为两个部分:渲染引擎和js引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎。

​ 不同的浏览器内核对网页编写语法的解释也有不同,因此同一网页在不同的内核的浏览器里的渲染(显示)效果也可能不同,这也是网页编写者需要在不同内核的浏览器中测试网页显示效果的原因。

1. 渲染引擎

​ 负责请求网络页面资源加以解析排版并呈现给用户,渲染引擎可以显示html、xml文档及图片,它也可以借助插件显示其他类型数据,例如使用PDF阅读器插件,可以显示PDF格式。

2. js引擎

​ JS 引擎则是解析 Javascript 语言,执行 javascript 语言来实现网页的动态效果。

3. 当前主流内核

Trident ([‘traɪd(ə)nt]):普遍称作 “IE内核”

​ 该内核程序在 1997 年的 IE4 中首次被采用,是微软在 Mosaic(”马赛克”,这是人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览) 代码的基础之上修改而来的,并沿用到 IE11,也被普遍称作 “IE内核”。

​ IE 从版本 11 开始,初步支持 WebGL 技术。IE8 的 JavaScript 引擎是 Jscript,IE9 开始用 Chakra,这两个版本区别很大,Chakra 无论是速度和标准化方面都很出色。

​ Window10 发布后,IE 将其内置浏览器命名为 Edge,Edge 最显著的特点就是新内核 EdgeHTML。

Gecko ([‘gekəʊ]):Firefox 内核

​ Gecko 内核的浏览器Firefox (火狐) 用户最多,所以有时也会被称为 Firefox 内核,此外 Gecko 也是一个跨平台内核,可以在Windows、 BSD、Linux 和 Mac OS X 中使用。

Webkit:

​ webkit内核 可以说是以硬件盈利为主的苹果公司给软件行业的最大贡献之一。随后,2008 年谷歌公司发布 chrome 浏览器,采用的 chromium 内核便 fork 了 Webkit。

Chromium/Blink:

​ 2008 年,谷歌公司发布了 chrome 浏览器,浏览器使用的内核被命名为 chromium。

​ chromium fork 自开源引擎 webkit,却把 WebKit 的代码梳理得可读性提高很多,所以以前可能需要一天进行编译的代码,现在只要两个小时就能搞定。因此 Chromium 引擎和其它基于 WebKit 的引擎所渲染页面的效果也是有出入的。所以有些地方会把 chromium 引擎和 webkit 区分开来单独介绍,而有的文章把 chromium 归入 webkit 引擎中,都是有一定道理的。

​ 谷歌公司还研发了自己的 Javascript 引擎,V8,极大地提高了 Javascript 的运算速度。

​ Blink 引擎问世后,国产各种 chrome 系的浏览器也纷纷投入 Blink 的怀抱。

​ Blink是一个由Google和Opera Software开发的浏览器排版引擎,Opera表示将会跟随谷歌采用其Blink浏览器核心,同时参与了Blink的开发。

Presto ([‘prestəʊ]):opera 的 “前任” 内核

​ 为何说是 “前任”,因为最新的 opera 浏览器早已将之抛弃从而投入到了谷歌大本营。

1.3、JavaScript引擎

JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中。

Mozilla

● SpiderMonkey,第一款JavaScript引擎,由Brendan Eich在Netscape Communications时编写,用于Mozilla Firefox 1.0~3.0版本。

● Rhino,由Mozilla基金会管理,开放源代码,完全以Java编写。

● TraceMonkey,基于实时编译的引擎,其中部份代码取自Tamarin引擎,用于Mozilla Firefox 3.5~3.6版本。

● JaegerMonkey,结合追踪和组合码技术大幅提高性能,部分技术借凿了V8、JavaScriptCore、WebKit,用于Mozilla Firefox 4.0以上版本。

Google

● V8,开放源代码,由Google丹麦开发,是Chrome浏览器的一部分。

V8是被设计用来提高网页浏览器内部JavaScript执行的性能,那么如何提高性能呢?

​ 为了提高性能,v8会把js代码转换为高效的机器码,而不在是依赖于解释器去执行。v8引入了JIT在运行时把js代码进行转换为机器码。这里的主要区别在于V8不生成字节码或任何中间代码。

V8介绍:https://www.jianshu.com/p/81f6ded64ab2

JIT:https://www.zhihu.com/question/19672491

微软

● Chakra (JScript引擎),中文译名为查克拉,用于Internet Explorer 9的32位版本。

Opera

● Linear A,用于Opera 4.0~6.1版本。

● Linear B,用于Opera 7.0~9.2版本。

● Futhark,用于Opera 9.5~10.2版本。

● Carakan,由Opera软件公司编写,自Opera10.50版本开始使用。

其它

● KJS,KDE的ECMAScript/JavaScript引擎,最初由Harri Porten开发,用于KDE项目的Konqueror网页浏览器中。

● Narcissus,开放源代码,由Brendan Eich编写(他也参与编写了第一个SpiderMonkey)。

● Tamarin,由Adobe Labs编写,Flash Player 9所使用的引擎。

● Nitro(原名SquirrelFish),为Safari 4编写。

1.4、浏览器的渲染过程

1.4.1、渲染过程

从上面这个图上,我们可以看到,浏览器渲染过程如下:

  1. 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  2. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  3. Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  4. Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  5. Display:将像素发送给GPU,展示在页面上。

1.4.2、生成渲染树

渲染过程看起来很简单,让我们来具体了解下每一步具体做了什么。

  • 为了构建渲染树,浏览器主要完成了以下工作:
    1. 从DOM树的根节点开始遍历每个可见节点。
    2. 对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们。
    3. 根据每个可见节点以及其对应的样式,组合生成渲染树
  • 什么是不可见节点
    1. 一些不会渲染输出的节点,比如script、meta、link等。
    2. 一些通过css进行隐藏的节点。比如display:none。注意,利用visibility和opacity隐藏的节点,还是会显示在渲染树上的(因为还占据文档空间)。只有display:none的节点才不会显示在渲染树上。
  • 注意:渲染树只包含可见的节点

1.5、回流和重绘

1.5.1、回流

​ 前面我们通过构造渲染树,我们将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。

​ 为了弄清每个对象在网站上的确切大小和位置,浏览器从渲染树的根节点开始遍历,我们可以以下面这个实例来表示:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Critial Path: Hello world!</title>
  </head>
  <body>
    <div style="width: 50%">
      <div style="width: 50%">Hello world!</div>
    </div>
  </body>
</html>

​ 我们可以看到,第一个div将节点的显示尺寸设置为视口宽度的50%,第二个div将其尺寸设置为父节点的50%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值(如下图)

1.5.2、重绘

​ 最终,我们通过构造渲染树和回流阶段,我们知道了哪些节点是可见的,以及可见节点的样式和具体的几何信息(位置、大小),那么我们就可以将渲染树的每个节点都转换为屏幕上的实际像素,这个阶段就叫做重绘节点。

实际像素值和屏幕像素:

https://www.zhihu.com/question/21149600

1.5.3、何时发生回流重绘

​ 既然知道了浏览器的渲染过程后,我们就来探讨下,何时会发生回流重绘。

​ 我们前面知道了,回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

注意:回流一定会触发重绘,而重绘不一定会回流.

​ 根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。

1.5.4、减少回流和重绘

1.5.4.1、最小化重绘和重排

​ 由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉

考虑这个例子:

var el = document.getElementById('test');
el.style.padding = '5px';
el.style.borderLeft = '1px';
el.style.borderRight = '2px'

​ 例子中,有三个样式属性被修改了,每一个都会影响元素的几何结构,引起回流。当然,大部分现代浏览器都对其做了优化,因此,只会触发一次重排。但是如果在旧版的浏览器或者在上面代码执行的时候,有其他代码访问了布局信息(上文中的会触发回流的布局信息),那么就会导致三次重排。

​ 因此,我们可以合并所有的改变然后依次处理,比如我们可以采取以下的方式:

  • 使用cssText

    var el = document.getElementById('test');
    el.style.cssText += 'border-left: 1px; border-right: 2px;   padding: 5px;
    
  • 修改CSS的class

    var el = document.getElementById('test');
    // 在使用 += 操作class的时候,一定要记得添加一个空格
    el.className += ' active';
    

1.5.4.2、批量修改DOM

当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少回流重绘次数:

  1. 使元素脱离文档流,不是指的定位脱离文档流
  2. 对其进行多次修改
  3. 将元素带回到文档中。

该过程的第二步和第三步可能会引起回流,但是经过第一步之后,对DOM的所有修改都不会引起回流,因为它已经不在渲染树了。

有三种方式可以让DOM脱离文档流:

  1. 隐藏元素,应用修改,重新显示
  2. 使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档。
  3. 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

考虑我们要执行一段批量插入节点的代码:

function appendDataToElement(appendToElement, data) {
    var li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}

var ul = document.getElementById('list');
appendDataToElement(ul, data);

如果我们直接这样执行的话,由于每次循环都会插入一个新的节点,会导致浏览器回流一次。

我们可以使用这三种方式进行优化:

方式1: 隐藏元素,应用修改,重新显示

这个会在展示和隐藏节点的时候,产生两次重绘

function appendDataToElement(appendToElement, data) {
    var li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
var ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

方式2:使用文档片段(document fragment)在当前DOM之外构建一个子树,再把它拷贝回文档

createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。

var ul = document.getElementById('list');

var fragment = document.createDocumentFragment();

appendDataToElement(fragment, data);

ul.appendChild(fragment);

方式3:将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。

var ul = document.getElementById('list');

var clone = ul.cloneNode(true);

appendDataToElement(clone, data);

ul.parentNode.replaceChild(clone, ul);

1.5.4.3、对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起回流重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的回流。

1.5.4.4、css3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘。这个时候,css3硬件加速就闪亮登场啦!!

划重点:使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

常见的触发硬件加速的css属性:

  • transform
  • opacity
  • filters
  • Will-change

如果你为太多元素使用css3硬件加速,会导致内存占用较大,会有性能问题。

Will-change:
	https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change
	https://www.zhangxinxu.com/wordpress/2015/11/css3-will-change-improve-paint/

1.6、浏览器静态资源缓存机制

1.6.1、为什么需要缓存?

缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求(强缓存),或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据(协商缓存)。

1.6.2、缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

1.Service Worker

​ Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

​ Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

​ 当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

2.Memory Cache

​ Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

​ 那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?

​ 这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。

​ 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

​ 内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。

​ 需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

3.Disk Cache

​ Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储持久性上。

​ 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Header 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache,关于 HTTP 的协议头中的缓存字段,我们会在下文进行详细介绍。

​ 浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?

​ 关于这点,网上说法不一,不过以下观点比较靠得住:

​ 对于大文件来说,大概率是不存储在内存中的,反之优先

​ 当前系统内存使用率高的话,文件优先存储进硬盘

4.Push Cache

​ Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

​ Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake Archibald的 HTTP/2 push is tougher than I thought 这篇文章,文章中的几个结论:

  • 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差

  • 可以推送 no-cache 和 no-store 的资源

  • 一旦连接被关闭,Push Cache 就被释放

  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。

  • Push Cache 中的缓存只能被使用一次

  • 浏览器可以拒绝接受已经存在的资源推送

  • 你可以给其他域名推送资源

    如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

More:
	https://blog.csdn.net/Fly_as_tadpole/article/details/86629288

二、正则表达式

2.1、正则表达式简介

什么是正则表达式

​ 正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早期研究,现在在编程语言中有广泛的应用。正则表通常被用来检索、替换那些符合某个模式(规则)的文本。

​ 正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符")。

​ 正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。

​ 正则表达式是繁琐的,但它是强大的,学会之后的应用会让你除了提高效率外,会给你带来绝对的成就感。

​ 许多程序设计语言都支持利用正则表达式进行字符串操作。

正则表达式的作用

通过使用正则表达式,可以:

  • 测试字符串内的模式(匹配)。
    例如,可以测试输入字符串,以查看字符串内是否出现电话号码模式或信用卡号码模式。这称为数据验证。
  • 替换文本(替换)。
    可以使用正则表达式来识别文档中的特定文本,完全删除该文本或者用其他文本替换它。
  • 基于模式匹配从字符串中提取子字符串(提取)。
    可以查找文档内或输入域内特定的文本

正则表达式的特点

​ 灵活性、逻辑性和功能性非常的强

​ 可以迅速地用极简单的方式达到字符串的复杂控制

​ 对于刚接触的人来说,比较晦涩难懂

发展历史

​ 正则表达式的"祖先"可以一直上溯至对人类神经系统如何工作的早期研究。Warren McCulloch 和 Walter Pitts 这两位神经生理学家研究出一种数学方式来描述这些神经网络。

​ 1956 年, 一位叫 Stephen Kleene 的数学家在 McCulloch 和 Pitts 早期工作的基础上,发表了一篇标题为"神经网事件的表示法"的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为"正则集的代数"的表达式,因此采用"正则表达式"这个术语。

​ 随后,发现可以将这一工作应用于使用 Ken Thompson 的计算搜索算法的一些早期研究,Ken Thompson 是 Unix 的主要发明人。正则表达式的第一个实用应用程序就是 Unix 中的 qed 编辑器。

​ 如他们所说,剩下的就是众所周知的历史了。从那时起直至现在正则表达式都是基于文本的编辑器和搜索工具中的一个重要部分。

应用领域

​ 目前,正则表达式已经在很多软件中得到广泛的应用,包括 *nix(Linux, Unix等)、HP 等操作系统,PHP、C#、Java 等开发环境,以及很多的应用软件中,都可以看到正则表达式的影子。

2.2、正则表达式的组成

  • 普通字符abc 123
  • 特殊字符(元字符):正则表达式中有特殊意义的字符\d \w

2.2.1、字符类:

字符类 说明
[abc] 只能是a b c中的任意一个,也就是说[]里面的内容只能选择任意一个
[^abc] 任何字符,除了 a、b 或 c(否定)
[a-z] a 到 z 中的任意一个
[^a-z] 非a 到 z 中的任意一个
[a-zA-Z] a 到 z 或 A 到 Z,两头的字母包括在内(范围)
[0-9] 0-9之间的任意一个
[\u4e00-\u9fa5] 表示汉字的取值范围

2.2.2、元字符:

元字符 说明
\d 匹配数字
\D 匹配任意非数字的字符
\w 匹配字母或数字或下划线
\W 匹配任意不是字母,数字,下划线
\s 匹配任意的空白符
\S 匹配任意不是空白符的字符
. 匹配除换行符以外的任意单个字符
^ 表示匹配行首的文本(以谁开始)
$ 表示匹配行尾的文本(以谁结束)

2.2.3、限定符:

限定符 说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
重复n次
重复n次或更多次
重复n到m次

2.2.4、条件:

条件 说明
(?![0-9]+$) 不能全是数字
(?![a-zA-Z]+$) 不能全是字母

2.2.5、其他

其他 说明
[] 字符串用中括号括起来,表示匹配其中的任一字符
[^] 匹配除中括号以内的内容
\ 转义符
| 或者,选择两者中的一个。注意 | 将左右两边分为两部分,而不管左右两边有多长多乱
() 从两个直接量中选择一个,分组

2.3、简单案例

验证手机号:

^\d{11}$

验证邮编:

^\d{6}$

验证日期 2012-5-01

^\d{4}-\d{1,2}-\d{1,2}$

验证邮箱 xxx@newcapec.com.cn:

^\w+@\w+\.\w+$

验证IP地址 192.168.1.10

^\d{1,3}(.\d{1,3}){3}$

2.4、使用正则表达式

2.4.1、创建正则对象

方式1:

var reg = new RegExp('\d', 'i');
var reg = new RegExp('\d', 'gi');

方式2:

var reg = /\d/i;
var reg = /\d/gi;

参数

标志 说明
i 忽略大小写
g 全局匹配
gi 全局匹配+忽略大小写

2.4.2、正则匹配

 /*
            正则的作用: 匹配、替换、提取
            1、作为正则对象来用,用来调用匹配方法  (匹配)
                正则对象.test(字符串)
                var reg = /^正则规则$/

                new RegExp(pattern, attributes);
                    pattern:一个字符串,指定了正则表达式的模式或其他正则表达式。
                    attributes:一个可选的字符串,包含属性“g”、“i”和“m”,分别用于指定全局匹配、区分大小写的匹配和多行匹配

                regexp.test(string)
                    string:要检测的字符串。
                    如果字符串string中含有与regexp匹配的文本,就返回true,否则返回false。

            2、作为正则参数来用,用来作为方法的参数  (替换、提取)
                字符串.replace(正则,字符串)
                字符串.replace(/正则规则/,字符串)
        */

// 电话号码
var tel_reg = /^\d{11}$/;
var tel = "12311111111";
console.log("电话号码是否符合规则:" + tel_reg.test(tel));

// 邮编
var postcode_reg = /^\d{6}$/;

// 验证日期 2012-5-01   
var date_reg = /^\d{4}(-\d{1,2}){1,2}$/;
// var date_reg = /^\d{4}-\d{1,2}-\d{1,2}$/;
var date1 = "2020-05";
var date2 = "2020-09-1";
var date3 = "2020";
console.log("日期是否符合规则:" + date_reg.test(date1));
console.log("日期是否符合规则:" + date_reg.test(date2));
console.log("日期是否符合规则:" + date_reg.test(date3));

// 邮箱 .com.cn
var email_reg = /^[A-Za-z0-9]{5,}@[a-z0-9]{2,5}(\.[a-z]{2,3}){1,2}$/;
// var email_reg = /^\w+@\w+\.\w+$/;
var email1 = "2@q.com";
var email2 = "zhangsan@163.com";
var email3 = "32132@qq.com";
var email4 = "zhangsan123@yahoo.com.cn";
console.log("邮箱是否符合规则:" + email_reg.test(email1));
console.log("邮箱是否符合规则:" + email_reg.test(email2));
console.log("邮箱是否符合规则:" + email_reg.test(email3));
console.log("邮箱是否符合规则:" + email_reg.test(email4));

// ip校验
var ip_reg = /^\d{1,3}(.\d{1,3}){3}$/;

// 用户名,8-20为字母、数子、下划线,不能全是数字
var userName_reg = /^(?![0-9]+$)\w{8,20}$/;
var userName = "12345678";
console.log("用户名是否符合规则:" + userName_reg.test(userName))

2.4.3、正则提取

<!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>
        // 1. 提取工资
        var str = "张三:1000,李四:5000,王五:8000。";
        var array = str.match(/\d+/g);
        console.log(array);

        // 2. 提取email地址
        var str =
            "123123@xx.com,fangfang@valuedopinions.cn 286669312@qq.com 2、emailenglish@emailenglish.englishtown.com 286669312@qq.com...";
        var array = str.match(/\w+@\w+\.\w+(\.\w+)?/g);
        console.log(array);

        // 3. 分组提取  
        // 3. 提取日期中的年部分  2015-5-10
        var dateStr = '2016-1-5';
        // 正则表达式中的()作为分组来使用,获取分组匹配到的结果用Regex.$1 $2 $3....来获取
        var reg = /^(\d{4})-\d{1,2}-\d{1,2}$/;
        if (reg.test(dateStr)) {
            console.log(RegExp.$1);
        }

        // 4. 提取邮件中的每一部分
        var reg = /^(\w+)@(\w+)\.(\w+)(\.\w+)?$/;
        var str = "123123@xx.com";
        if (reg.test(str)) {
            console.log(RegExp.$1);
            console.log(RegExp.$2);
            console.log(RegExp.$3);
        }
    </script>
</body>

</html>

2.4.4、正则替换

// 1. 替换所有空白
var str = "   123AD  asadf   asadfasf  adf ";
str = str.replace(/\s/g,"xx");
console.log(str);

// 2. 替换所有,|,
var str = "abc,efg,123,abc,123,a";
str = str.replace(/,|,/g, ".");
console.log(str);

2.4.5、案例表单验证

2.4.5.1、常规方式

QQ号:<input type="text" id="txtQQ"><span></span><br>
邮箱:<input type="text" id="txtEMail"><span></span><br>
手机:<input type="text" id="txtPhone"><span></span><br>
生日:<input type="text" id="txtBirthday"><span></span><br>
姓名:<input type="text" id="txtName"><span></span><br>
//获取文本框
var txtQQ = document.getElementById("txtQQ");
var txtEMail = document.getElementById("txtEMail");
var txtPhone = document.getElementById("txtPhone");
var txtBirthday = document.getElementById("txtBirthday");
var txtName = document.getElementById("txtName");

//
txtQQ.onblur = function () {
  //获取当前文本框对应的span
  var span = this.nextElementSibling;
  var reg = /^\d{5,12}$/;
  //判断验证是否成功
  if(!reg.test(this.value) ){
    //验证不成功
    span.innerText = "请输入正确的QQ号";
    span.style.color = "red";
  }else{
    //验证成功
    span.innerText = "";
    span.style.color = "";
  }
};

//txtEMail
txtEMail.onblur = function () {
  //获取当前文本框对应的span
  var span = this.nextElementSibling;
  var reg = /^\w+@\w+\.\w+(\.\w+)?$/;
  //判断验证是否成功
  if(!reg.test(this.value) ){
    //验证不成功
    span.innerText = "请输入正确的EMail地址";
    span.style.color = "red";
  }else{
    //验证成功
    span.innerText = "";
    span.style.color = "";
  }
};

2.4.5.2、封装成函数

 //获取文本框
var txtQQ = document.getElementById("txtQQ");
var txtEMail = document.getElementById("txtEMail");
var txtPhone = document.getElementById("txtPhone");
var txtBirthday = document.getElementById("txtBirthday");
var txtName = document.getElementById("txtName");

var qq_flag = false;
var email_flag = false;

// 封装共有函数
function check(element, reg, tips) {
    element.onfocus = function () {
        var nextSpan = this.nextElementSibling;
        nextSpan.innerHTML = "请输入您的" + tips;
        nextSpan.className = "";
    }

    element.onblur = function () {
        var val = this.value;
        var nextSpan = this.nextElementSibling;
        if (val.replace(/\s/g, "") === "") {
            nextSpan.innerHTML = tips + "不能为空";
            nextSpan.className = "error";
        } else {
            var reg_obj = reg;
            if (reg_obj.test(val)) {
                nextSpan.innerHTML = "格式正确";
                nextSpan.className = "correct";
                element.correct = true;
            } else {
                nextSpan.innerHTML = tips + "格式错误";
                nextSpan.className = "error";
            }
        }
    }
}

// 验证qq
var QQ_REG = /^\d{5,}$/;
check(txtQQ, QQ_REG, "QQ号码")

// 验证email
var email_reg = /^[A-Za-z0-9]{5,}@[a-z0-9]{2,5}(\.[a-z]{2,3}){1,2}$/;
check(txtEMail, email_reg, "邮箱地址")

// 验证手机号
var tel_reg = /^\d{11}$/;
check(txtPhone, tel_reg, "手机号码")



var f1 = document.getElementById("f1");
f1.onsubmit = function () {
    console.log(txtQQ.correct);
    if (!(txtQQ.correct && txtEMail.correct && txtPhone.correct)) {
        return false;
    }
}

2.4.5.3、添加自定义验证属性对表单进行验证

<form id="frm">
  QQ号:<input type="text" name="txtQQ" data-rule="qq"><span></span><br>
  邮箱:<input type="text" name="txtEMail" data-rule="email"><span></span><br>
  手机:<input type="text" name="txtPhone" data-rule="phone"><span></span><br>
  生日:<input type="text" name="txtBirthday" data-rule="date"><span></span><br>
  姓名:<input type="text" name="txtName" data-rule="cn"><span></span><br>
</form>
// 所有的验证规则
var rules = [
  {
    name: 'qq',
    reg: /^\d{5,12}$/,
    tip: "请输入正确的QQ"
  },
  {
    name: 'email',
    reg: /^\w+@\w+\.\w+(\.\w+)?$/,
    tip: "请输入正确的邮箱地址"
  },
  {
    name: 'phone',
    reg: /^\d{11}$/,
    tip: "请输入正确的手机号码"
  },
  {
    name: 'date',
    reg: /^\d{4}-\d{1,2}-\d{1,2}$/,
    tip: "请输入正确的出生日期"
  },
  {
    name: 'cn',
    reg: /^[\u4e00-\u9fa5]{2,4}$/,
    tip: "请输入正确的姓名"
  }];

addCheck('frm');


//给文本框添加验证
function addCheck(formId) {
  var i = 0,
      len = 0,
      frm =document.getElementById(formId);
  len = frm.children.length;
  for (; i < len; i++) {
    var element = frm.children[i];
    // 表单元素中有name属性的元素添加验证
    if (element.name) {
      element.onblur = function () {
        // 使用dataset获取data-自定义属性的值
        var ruleName = this.dataset.rule;
        var rule =getRuleByRuleName(rules, ruleName);

        var span = this.nextElementSibling;
        //判断验证是否成功
        if(!rule.reg.test(this.value) ){
          //验证不成功
          span.innerText = rule.tip;
          span.style.color = "red";
        }else{
          //验证成功
          span.innerText = "";
          span.style.color = "";
        }
      }
    }
  }
}

// 根据规则的名称获取规则对象
function getRuleByRuleName(rules, ruleName) {
  var i = 0,
      len = rules.length;
  var rule = null;
  for (; i < len; i++) {
    if (rules[i].name == ruleName) {
      rule = rules[i];
      break;
    }
  }
  return rule;
}

2.5、常用正则汇总

2.5.1、校验数字的表达式

数字:^[0-9]*$
n位的数字:^\d{n}$
至少n位的数字:^\d{n,}$
m-n位的数字:^\d{m,n}$
零和非零开头的数字:^(0|[1-9][0-9]*)$
非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(\.[0-9]{1,2})?$
带1-2位小数的正数或负数:^(\-)?\d+(\.\d{1,2})$
正数、负数、和小数:^(\-|\+)?\d+(\.\d+)?$
有两位小数的正实数:^[0-9]+(\.[0-9]{2})?$
有1~3位小数的正实数:^[0-9]+(\.[0-9]{1,3})?$
非零的正整数:^[1-9]\d*$ 或 ^([1-9][0-9]*){1,3}$ 或 ^\+?[1-9][0-9]*$
非零的负整数:^\-[1-9][]0-9"*$ 或 ^-[1-9]\d*$
非负整数:^\d+$ 或 ^[1-9]\d*|0$
非正整数:^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$
非负浮点数:^\d+(\.\d+)?$ 或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$
非正浮点数:^((-\d+(\.\d+)?)|(0+(\.0+)?))$ 或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$
正浮点数:^[1-9]\d*\.\d*|0\.\d*[1-9]\d*$ 或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$
负浮点数:^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$ 或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$
浮点数:^(-?\d+)(\.\d+)?$ 或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$

2.5.2、校验字符的表达式

汉字:^[\u4e00-\u9fa5]{0,}$
英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
长度为3-20的所有字符:^.{3,20}$
由26个英文字母组成的字符串:^[A-Za-z]+$
由26个大写英文字母组成的字符串:^[A-Z]+$
由26个小写英文字母组成的字符串:^[a-z]+$
由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}$
中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
禁止输入含有~的字符:[^~\x22]+

2.5.3、特殊需求表达式

Email地址:^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?
InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$
国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
电话号码正则表达式(支持手机号码,3-4位区号,7-8位直播号码,1-4位分机号): ((\d{11})|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)
身份证号(15位、18位数字),最后一位是校验位,可能为数字或字符X:(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)
帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在 8-10 之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z0-9]{8,10}$
强密码(必须包含大小写字母和数字的组合,可以使用特殊字符,长度在8-10之间):^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
日期格式:^\d{4}-\d{1,2}-\d{1,2}
一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$

钱的输入格式:
    有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$
    这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
    一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
    这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧。下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
    必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$
    这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
    这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
    1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
    备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
    
xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\\.[x|X][m|M][l|L]$
中文字符的正则表达式:[\u4e00-\u9fa5]
双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
空白行的正则表达式:\n\s*\r (可以用来删除空白行)
HTML标记的正则表达式:<(\S*?)[^>]*>.*?|<.*? /> ( 首尾空白字符的正则表达式:^\s*|\s*$或(^\s*)|(\s*$) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
IP地址:((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))

正则表达式在线测试:https://c.runoob.com/front-end/854

三、JavaScript面向对象编程

3.1、面向对象概述

3.1.1、什么是对象

Everything is object (万物皆对象)

对象到底是什么,我们可以从两次层次来理解。

(1) 对象是单个事物的抽象。

一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。

(2) 对象是一个容器,封装了属性(property)和方法(method)。

属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

在实际开发中,对象是一个抽象的概念,可以将其简单理解为:数据集或功能集

ECMAScript-262 把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数

严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。

提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。

3.1.2、什么是面向对象

面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维 护性。

面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。

它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。

在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。

因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。

面向对象与面向过程:

  • 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
  • 面向对象就是找一个对象,指挥得结果
  • 面向对象将执行者转变成指挥者
  • 面向对象不是面向过程的替代,而是面向过程的封装

面向对象的特性:

  • 封装性
  • 继承性
  • [多态性]抽象

扩展阅读:

3.1.3、程序中面向对象的基本体现

在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。

我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。

假设我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个对象表示:

var stu1 = {name: 'zs', subject: '语文', score: 90};
var stu2 = {name: 'ls', subject: '语文', score: 80};

而处理学生成绩可以通过函数实现,比如打印学生的成绩:

console.log(stu1.name, stu1.subject, stu1.score);

console.log(stu2.name, stu2.subject, stu2.score);

如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是 Student 这种数据类型应该被视为一个对象,这个对象拥有 namescore 这两个属性(Property)。

如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个 printScore 消息,让对象自己把自己的数据打印出来。

抽象数据行为模板(Class):

function Student(name, score) {
  this.name = name;
  this.score = score;
  this.printScore = function() {
    console.log('姓名:' + this.name + '  ' + '成绩:' + this.score);
  }
}

根据模板创建具体实例对象(Instance):

var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)

实例对象具有自己的具体行为(给对象发消息):

std1.printScore() // => 姓名:Michael  成绩:98
std2.printScore() // => 姓名:Bob  成绩 81

面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。

Class 是一种抽象概念,比如我们定义的 Class——Student ,是指学生这个概念,而实例(Instance)则是一个个具体的 Student ,比如, Michael 和 Bob 是两个具体的 Student 。

所以,面向对象的设计思想是:

  • 抽象出 Class(构造函数)
  • 根据 Class(构造函数) 创建 Instance
  • 指挥 Instance 得结果

面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。

3.2、创建对象

3.2.1、new Object()创建对象

var person = new Object()
person.name = 'Jack'
person.age = 18

person.sayName = function () {
  console.log(this.name)
}

每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:

3.2.2、对象字面量

var person = {
  name: 'Jack',
  age: 18,
  sayName: function () {
    console.log(this.name)
  }
}

对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?代码太过冗余,重复性太高。

3.2.3、工厂函数创建对象

我们可以写一个函数,解决代码重复问题:

function createPerson (name, age) {
  return {
    name: name,
    age: age,
    sayName: function () {
      console.log(this.name)
    }
  }
}

然后生成实例对象:

var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)

这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,
但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

3.2.4、构造函数

3.2.4.1、构造函数语法

一种更优雅的工厂函数就是下面这样,构造函数:

function Person (name, age) {
  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }
}

var p1 = new Person('Jack', 18)
p1.sayName() // => Jack

var p2 = new Person('Mike', 23)
p2.sayName() // => Mike

3.2.4.2、构造函数分析

在上面的示例中,Person() 函数取代了 createPerson() 函数,但是实现效果是一样的。
这是为什么呢?

我们注意到,Person() 中的代码与 createPerson() 有以下几点不同之处:

  • 没有显示的创建对象
  • 直接将属性和方法赋给了 this 对象
  • 没有 return 语句
  • 函数名使用的是大写的 Person

而要创建 Person 实例,则必须使用 new 操作符。

以这种方式调用构造函数会经历以下 4 个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
  3. 执行构造函数中的代码
  4. 返回新对象

下面是具体的伪代码:

function Person (name, age) {
  // 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
  // var instance = {}
  // 然后让内部的 this 指向 instance 对象
  // this = instance
  // 接下来所有针对 this 的操作实际上操作的就是 instance

  this.name = name
  this.age = age
  this.sayName = function () {
    console.log(this.name)
  }

  // 在函数的结尾处会将 this 返回,也就是 instance
  // return this
}

3.2.4.3、构造函数和实例对象的关系

使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。

在每一个实例对象中同时有一个 constructor 属性,该属性指向创建该实例的构造函数:

console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true

对象的 constructor 属性最初是用来标识对象类型的,
但是,如果要检测对象的类型,还是使用 instanceof 操作符更可靠一些:

console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true

总结:

  • 构造函数是根据具体的事物抽象出来的抽象模板
  • 实例对象是根据抽象的构造函数模板得到的具体实例对象
  • 每一个实例对象都具有一个 constructor 属性,指向创建该实例的构造函数
    • 注意: constructor 是实例的属性的说法不严谨,具体后面的原型会讲到
  • 可以通过实例的 constructor 属性判断实例和构造函数之间的关系
    • 注意:这种方式不严谨,推荐使用 instanceof 操作符,后面学原型会解释为什么

3.2.4.4、构造函数的问题

使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = function () {
    console.log('hello ' + this.name)
  }
}

var p1 = new Person('Tom', 18)
var p2 = new Person('Jack', 16)

在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。
那就是对于每一个实例对象,typesayHello 都是一模一样的内容,
每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。

console.log(p1.sayHello === p2.sayHello) // => false

对于这种问题我们可以把需要共享的函数定义到构造函数外部:

function sayHello () {
  console.log('hello ' + this.name)
}

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = sayHello
}

var p1 = new Person('Top', 18)
var p2 = new Person('Jack', 16)

console.log(p1.sayHello === p2.sayHello) // => true

这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。

你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:

var fns = {
  sayHello: function () {
    console.log('hello ' + this.name)
  },
  sayAge: function () {
    console.log(this.age)
  }
}

function Person (name, age) {
  this.name = name
  this.age = age
  this.type = 'human'
  this.sayHello = fns.sayHello
  this.sayAge = fns.sayAge
}

var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)

console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true

至此,我们利用自己的方式基本上解决了构造函数的内存浪费问题。

但是代码看起来还是那么的格格不入,那有没有更好的方式呢?

3.3、原型

3.3.1、更好的解决方案: prototype

JavaScript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。

这个对象的所有属性和方法,都会被构造函数的所拥有。

这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。

function Person (name, age) {
  this.name = name
  this.age = age
}

console.log(Person.prototype)

Person.prototype.type = 'human'

Person.prototype.sayName = function () {
  console.log(this.name)
}

var p1 = new Person(...)
var p2 = new Person(...)

console.log(p1.sayName === p2.sayName) // => true

这时所有实例的 type 属性和 sayName() 方法,其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。

3.3.2、构造函数、实例、原型三者之间的关系

任何函数都具有一个 prototype 属性,该属性是一个对象。

function F () {}
console.log(F.prototype) // => object

F.prototype.sayHi = function () {
  console.log('hi!')
}

构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。

console.log(F.prototype.constructor === F) // => false

通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__

var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true

`__proto__` 是非标准属性。

实例对象可以直接访问原型对象成员。

instance.sayHi() // => hi!

总结:

  • 任何函数都具有一个 prototype 属性,该属性是一个对象
  • 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
  • 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__
  • 所有实例都直接或间接继承了原型对象的成员

3.3.3、属性成员的搜索原则:原型链

了解了 构造函数-实例-原型对象 三者之间的关系后,接下来我们来解释一下为什么实例对象可以访问原型对象中的成员。

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性

  • 搜索首先从对象实例本身开始
  • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
  • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
  • 如果在原型对象中找到了这个属性,则返回该属性的值

也就是说,在我们调用 person1.sayName() 的时候,会先后执行两次搜索:

  • 首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。
  • ”然后,它继续搜索,再问:“ person1 的原型有 sayName 属性吗?”答:“有。
  • ”于是,它就读取那个保存在原型对象中的函数。
  • 当我们调用 person2.sayName() 时,将会重现相同的搜索过程,得到相同的结果。

而这正是多个对象实例共享原型所保存的属性和方法的基本原理。

总结:

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined

3.3.4、实例对象读写原型对象成员

读取:

  • 先在自己身上找,找到即返回
  • 自己身上找不到,则沿着原型链向上查找,找到即返回
  • 如果一直到原型链的末端还没有找到,则返回 undefined

值类型成员写入(实例对象.值类型成员 = xx):

  • 当实例期望重写原型对象中的某个普通数据成员时实际上会把该成员添加到自己身上
  • 也就是说该行为实际上会屏蔽掉对原型对象成员的访问

引用类型成员写入(实例对象.引用类型成员 = xx):

  • 同上

复杂类型修改(实例对象.成员.xx = xx):

  • 同样会先在自己身上找该成员,如果自己身上找到则直接修改
  • 如果自己身上找不到,则沿着原型链继续查找,如果找到则修改
  • 如果一直到原型链的末端还没有找到该成员,则报错(实例对象.undefined.xx = xx
function Person(name, age) {
    this.name = name
    this.age = age
}

console.log(Person.prototype)

Person.prototype.type = 'human'
Person.prototype.evr = {
    breathe: "氧气",
    drink: "水"
}

Person.prototype.sayName = function () {
    console.log(this.name)
}

var p1 = new Person("zhangsan", 12);
var p2 = new Person("lis", 11);

p1.type = "cat"
console.log(p1.type); // cat
console.log(p1.__proto__.type) // human

// p1.evr = "xx";
// console.log(p1.evr); // xx
// console.log(p1.__proto__.evr) // {breathe: "氧气", drink: "水"}

p1.evr.drink = "纯净水";
console.log(p1.evr); // {breathe: "氧气", drink: "纯净水"}
console.log(p1.__proto__.evr) // {breathe: "氧气", drink: "纯净水"}

3.3.5、更简单的原型语法

我们注意到,前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}

在该示例中,我们将 Person.prototype 重置到了一个新的对象。

这样做的好处就是为 Person.prototype 添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了 constructor 成员。

所以,我们为了保持 constructor 的指向正确,建议的写法是:

function Person (name, age) {
  this.name = name
  this.age = age
}

Person.prototype = {
  constructor: Person, // => 手动将 constructor 指向正确的构造函数
  type: 'human',
  sayHello: function () {
    console.log('我叫' + this.name + ',我今年' + this.age + '岁了')
  }
}

3.3.5、原生对象的原型

所有函数都有 prototype 属性对象。

  • Object.prototype
  • Function.prototype
  • Array.prototype
  • String.prototype
  • Number.prototype
  • Date.prototype
  • ...

练习:为数组对象和字符串对象扩展原型方法。

var array = [5, 4, 1, 8];

Array.prototype.getSum = function () {
    // 求数组中所有偶数的和
    var sum = 0;
    for (var i = 0; i < this.length; i++) {
        if (this[i] % 2 === 0) {
            sum += this[i];
        }
    }
    return sum;
}
// 数组或者String 中的prototype是不可以修改的
// Array.prototype = {
//   getSum: function () {
//      // 求数组中所有偶数的和
//     var sum = 0;
//     for (var i = 0; i < this.length; i++) {
//       if (this[i] % 2 === 0) {
//         sum += this[i];
//       }
//     }
//     return sum;
//   }
// }

console.log(array.getSum());

3.3.6、原型对象使用建议

  • 私有成员(一般就是非函数成员)放到构造函数中
  • 共享成员(一般就是函数)放到原型对象中
  • 如果重置了 prototype 记得修正 constructor 的指向

3.4、继承

3.4.1、什么是继承

​ 继承是面向对象编程中的一个重要概念,通过继承可以使子类的实例使用在父类中定义的属性和方法。

3.4.2、继承的实现方式

对象字面量继承

var wjl = {
    name: '王健林',
    money: 10000000,
    cars: ['玛莎拉蒂', '特斯拉'],
    houses: ['别墅', '大别墅'],
    play: function () {
        console.log('打高尔夫');
    }
}

var wsc = {
    name: '王思聪'
}

// // 复制对象的成员给另一个对象
// for (var key in wjl) {
//   // 不给wsc复制同名的属性
//   if (wsc[key]) {
//     continue;
//   }
//   wsc[key] = wjl[key];
// }

// console.log(wsc);

// 对象的拷贝
// 复制对象的成员给另一个对象
function extend(parent, child) {
    for (var key in parent) {
        // 不给wsc复制同名的属性
        if (child[key]) {
            continue;
        }
        child[key] = parent[key];
    }
}

extend(wjl, wsc);

console.log(wsc);

构造函数的属性继承:借用构造函数

function Person (name, age) {
  this.type = 'human'
  this.name = name
  this.age = age
}

function Student (name, age) {
  // 借用构造函数继承属性成员 
  // call()是改变函数中的this,直接调用函数
  Person.call(this, name, age)
}

var s1 = new Student('张三', 18)
console.log(s1.type, s1.name, s1.age) // => human 张三 18

构造函数的原型方法继承:拷贝继承(for-in)

function Person(name, age) {
    this.type = 'human'
    this.name = name
    this.age = age
}

Person.prototype.sayName = function () {
    console.log('hello ' + this.name)
}

function Student(name, age) {
    Person.call(this, name, age)
}

// 原型对象拷贝继承原型对象成员
for (var key in Person.prototype) {
    Student.prototype[key] = Person.prototype[key]
}

var s1 = new Student('张三', 18)

s1.sayName() // => hello 张三

另一种继承方式:原型继承

 function Person(name, age) {
     this.type = 'human'
     this.name = name
     this.age = age
 }

Person.prototype.sayName = function () {
    console.log('hello ' + this.name)
}

function Student(name, age) {
    Person.call(this, name, age)
}

// 利用原型的特性实现继承
Student.prototype = new Person();
Student.prototype.constructor = Student;

var s1 = new Student('张三', 18)

console.log(s1.type) // => human

s1.sayName() // => hello 张三

四、函数进阶

4.1、函数回顾

4.1.1、函数的定义方式

  • 函数声明
function foo () {

}
  • 函数表达式
var foo = function () {

}

函数声明与函数表达式的区别

  • 函数声明必须有名字
  • 函数声明会函数提升,在预解析阶段就已创建,声明前后都可以调用
  • 函数表达式类似于变量赋值
  • 函数表达式可以没有名字,例如匿名函数
  • 函数表达式没有变量提升,在执行阶段创建,必须在表达式执行之后才可以调用

4.1.2、函数的调用方式

  • 普通函数
function foo () {

}

foo()
  • 构造函数
function Person (name,age) {
	thia.name = name;
	this.age = age
	...
}

new Person();
  • 对象方法
var obj = {
    name:"zhangsan",
    age:12,
    eat:function(){
        ..
    }
}

obj.eat();

4.2、函数内 this 指向的不同场景

函数的调用方式决定了 this 指向的不同:

调用方式 非严格模式 备注
普通函数调用 window 严格模式下是 undefined
构造函数调用 实例对象 原型方法中 this 也是实例对象
对象方法调用 该方法所属对象 紧挨着的对象
事件绑定方法 绑定事件对象
定时器函数 window

这就是对函数内部 this 指向的基本整理,写代码写多了自然而然就熟悉了。

<ul id="ul1">
    <li>我是第1个li</li>
    <li>我是第2个li</li>
    <li>我是第3个li</li>
    <li>我是第4个li</li>
    <li>我是第5个li</li>
</ul>
<script>
    // 普通函数内部this指向
    // 这是全局函数,挂载在window上
    var a = 100;

    function fn1() {
        // 所以在这个函数内部使用this,指向的就是window
        console.log(this, this.a);
    }
    fn1();

    console.log("-----------------------------");
    // 构造函数调用,构造函数里面的this,是实例对象
    // 当我们使用new来调用构造函数的时候,会隐式创建一个对象,然后把这个对象给this,最后把this反馈出去
    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayHello = function () {
            console.log(this.name + "说:Hello");
        }
    }

    var p1 = new Person("张三", 18);
    p1.sayHello();

    console.log("-----------------------------");
    // 对象方法调用,this就是该方法所属对象
    var obj = {
        name: "李四",
        // this代表的是window
        age: a,
        // 对象方法里面的this,代表的就是方法所属对象
        sayHello: function () {
            console.log(this.name + "说:Hello");
        }
    }

    obj.sayHello();
    console.log(obj.age);

    console.log("-----------------------------");
    // 事件绑定方法
    // document.onclick = function () {
    //     // 事件函数里面的this,代表事件的当前对象
    //     console.log(this)
    // }

    // 定时器函数
    setTimeout(function () {
        console.log(this);
    }, 1000)


    // 获取li的下标
    var lis = document.querySelectorAll("#ul1>li");
    for (var i = 0; i < lis.length; i++) {
        var li = lis[i];
        li.index = i;

        //在鼠标函数里面,this是点击的li
        li.onclick = function () {
            // 通过变量来保存this
            var _this = this;

            // 1s之后,输出li的下标
            setTimeout(function () {
                // 因为在定时器里面,this是window
                console.log(_this.index);
            }, 1000)
        }
    }
</script>

4.3、call、apply、bind

先看明白下面:

例子1:

// window下面定义的两个全局变量
var name = "小王",
    age = 17;

var obj = {
    name: "小张",
    // 这里的this是window
    objAge: this.age,
    myFun: function () {
        // 对象函数里面的this就是函数所在的对象,obj
        console.log(this.name + "年龄" + this.age);
    }
}

console.log(obj.objAge); // 17
obj.myFun(); // 小张年龄 undefined

例子2:

var fav = "小张";

function shows() {
    console.log(this.fav)
}

shows(); // 小张

​ 那了解了函数 this 指向的不同场景之后,我们知道有些情况下我们为了使用某种特定环境的 this 引用,
这时候时候我们就需要采用一些特殊手段来处理了,例如我们经常在定时器外部备份 this 引用,然后在定时器函数内部使用外部 this 的引用。

​ 然而实际上对于这种做法我们的 JavaScript 为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部 this 指向问题。

​ 这就是接下来我们要学习的 call、apply、bind 三个函数方法。

4.3.1、语法

call讲解:

call() 方法调用一个函数, 其具有一个指定的 this 值和分别地提供的参数(参数的列表)。

注意:该方法的作用和 apply() 方法类似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。

语法:

fun.call(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg
    • 在 fun 函数运行时指定的 this 值
    • 如果指定了 null 或者 undefined 则内部 this 指向 window
  • arg1, arg2, ...
    • 指定的参数列表

apply

apply() 方法调用一个函数, 其具有一个指定的 this 值,以及作为一个数组(或类似数组的对象)提供的参数。

注意:该方法的作用和 `call()` 方法类似,只有一个区别,就是 `call()` 方法接受的是若干个参数的列表,而 `apply()` 方法接受的是一个包含多个参数的数组。

语法:

fun.apply(thisArg, [argsArray])

参数:

  • thisArg
  • argsArray

apply()call() 非常相似,不同之处在于提供参数的方式。
apply() 使用参数数组而不是一组参数列表。例如:

fun.apply(this, ['eat', 'bananas'])

bind

bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。
当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

语法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

参数:

  • thisArg
    • 当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用new 操作符调用绑定函数时,该参数无效。
  • arg1, arg2, ...
    • 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

返回值:

​ 返回由指定的this值和初始化参数改造的原函数拷贝。

4.3.2、测试

1、call()、apply()、bind() 都是用来重定义 this 这个对象的!

var name = "小王",
    age = 17;

var obj = {
    name: "小张",
    objAge: this.age,
    myFun: function () {
        console.log(this.name + "年龄" + this.age);
    }
}

var db = {
    name: "德邦总管",
    age: 99
}

obj.myFun.call(db); // 德邦总管年龄 99
obj.myFun.apply(db); // 德邦总管年龄 99
obj.myFun.bind(db)(); // 德邦总管年龄 99

以上出了 bind 方法后面多了个 () 外 ,结果返回都一致!

由此得出结论,bind 返回的是一个新的函数,你必须调用它才会被执行。

2、对比call 、bind 、 apply 传参情况下

var name = "小王",
    age = 17;

var obj = {
    name: "小张",
    objAge: this.age,
    myFun: function (fm, to) {
        console.log(this.name + " 年龄 " + this.age, " 来自 " + fm + " 去往 " + to);
    }
}

var db = {
    name: "德邦总管",
    age: 99
}

obj.myFun.call(db, '成都', '上海'); // 德邦总管 年龄 99  来自 成都去往上海
obj.myFun.apply(db, ['成都', '上海']); // 德邦总管 年龄 99  来自 成都去往上海  
obj.myFun.bind(db, '成都', '上海')(); // 德邦总管 年龄 99  来自 成都去往上海
obj.myFun.bind(db, ['成都', '上海'])(); // 德邦总管 年龄 99  来自 成都, 上海去往 undefined

4.3.3、应用

call的应用

<!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>
    <span>1</span>
    <span>2</span>
    <span>3</span>
    <script>
        // 获取到页面的所有span,得到HTMLCollection,无论是HTMLCollection还是NodeList,我们都称之为伪数组
        // 伪数组本质是一个对象,只是模拟的数组的特性
        // 用下标作为对象的key,并且也提供的length
        var spans = document.getElementsByTagName("span");
        console.log(spans)
        // spans.shift(); // spans.shift is not a function

        var arr = ["zhangsan", "lis", "wangwu"]
        console.log(arr)

        // getElementsByTagName()
        // 伪数组
        var obj = {
            0: 100,
            1: 10,
            2: 11,
            3: 20,
            length: 4
        };

        console.log("伪数组下标2的值:" + obj['2']);

        // 向伪数组添加一个值
        // obj.push(30); // obj.push is not a function

        // obj['4'] = 30;
        // obj.length++;

        // console.log(obj);

        // push(参数)   向当前数组的末尾添加一个参数
        // push()   this[最后下标+1] = 值;  length++;
        Array.prototype.push.call(obj, 330); // this[最后下标+1] = 值;
        // Array.prototype.splice.call(obj, 0, 3);

        console.log(obj);

        var obj = {
            name: 'zs'
        };

        console.log(obj.toString()); // [object object]

        var arr = [5, 9];
        console.log(arr.toString()); // 5,9

        console.log(Object.prototype.toString.call(arr));
    </script>
</body>

</html>

apply应用

// fn.apply(,[])

// Math.max(3, 5, 6);

var arr = [5, 10, 1, 3, 6];
// Math.max不能求数组中的最大值
// console.log(Math.max(arr));

console.log(Math.max.apply(null, arr));
console.log(Math.max.apply(Math, arr));


// console.log(1, 2, 3);
// console.log(arr);

bind应用

// 3 bind的应用
function Bloomer() {
    this.petalCount = Math.ceil(Math.random() * 12) + 1;
}

// 1秒后调用declare函数
Bloomer.prototype.bloom = function () {
    window.setTimeout(this.declare.bind(this), 1000);
};

Bloomer.prototype.declare = function () {
    console.log('我有 ' + this.petalCount + ' 朵花瓣!');
};

var test = new Bloomer();

test.bloom();
<ul id="ul1">
    <li>我是第1个li</li>
    <li>我是第2个li</li>
    <li>我是第3个li</li>
    <li>我是第4个li</li>
    <li>我是第5个li</li>
</ul>

<script>
	// 获取li的下标
    var lis = document.querySelectorAll("#ul1>li");
    for (var i = 0; i < lis.length; i++) {
        var li = lis[i];
        li.index = i;

        //在鼠标函数里面,this是点击的li
        li.onclick = function () {
            // // 通过变量来保存this
            // var _this = this;

            // 1s之后,输出li的下标
            setTimeout(function () {
                // 因为在定时器里面,this是window
                console.log(this.index, this.innerHTML);
            }.bind(this), 1000)
        }
    }
</script>

4.3.4、小结

  • call 和 apply 特性一样
    • 都是用来调用函数,而且是立即调用
    • 但是可以在调用函数的同时,通过第一个参数指定函数内部 this 的指向
    • call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递即可
    • apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递
    • 如果第一个参数指定了 null 或者 undefined 则内部 this 指向 window
  • bind
    • 可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
    • 它和 call、apply 最大的区别是:bind 不会调用
    • bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
      • 在 bind 的同时,以参数列表的形式进行传递
      • 在调用的时候,以参数列表的形式进行传递
      • 那到底以谁 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准
      • 两者合并:bind 的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部

4.4、函数的其它成员

  • arguments
    • 实参集合
  • caller
    • 函数的调用者
  • length
    • 形参的个数
  • name
    • 函数的名称
function fn(x, y, z) {
  console.log(fn.length) // => 形参的个数
  console.log(arguments) // 伪数组实参参数集合
  console.log(arguments.callee === fn) // 函数本身
  console.log(fn.caller) // 函数的调用者
  console.log(fn.name) // => 函数的名字
}

function f() {
  fn(10, 20, 30)
}

f()

4.5、高阶函数

  • 函数可以作为参数
  • 函数可以作为返回值

作为参数

function eat (callback) {
  setTimeout(function () {
    console.log('吃完了')
    callback()
  }, 1000)
}

eat(function () {
  console.log('去唱歌')
})

作为返回值

function genFun (type) {
  return function (obj) {
    return Object.prototype.toString.call(obj) === type
  }
}

var isArray = genFun('[object Array]')
var isObject = genFun('[object Object]')

console.log(isArray([])) // => true
console.log(isArray({})) // => true

八、附录

8.1、代码规范

8.1.1、代码风格

8.1.2、校验工具

8.2、文档相关工具