前端面试必不可少的基础知识,虽然少但是你不能不知道

13,225 阅读49分钟

这个是小浪学习前端的时候对于一些面试的基础知识的总结(这里面没涉及框架),不是很全,一方面是和大家一起分享,有错误的地方请大家多多指教我这个新人,另一方面方便自己复习,自己总结做笔记也是加强印象的一种方式,加油喔!小浪努力学前端

HTML基础

1.如何理解HTML语义化

为什么要语义化

  1. 页面不止是给人看的,机器也要看爬虫也要看

  2. 如果全部都是div+css布局,有的时候页面因为一些原因加载不出来样式,全部都是div页面对用户不友好

特点

  1. 让人更加容易读懂,有利于构建清晰的结构(增加代码的可读性)
  2. 让搜索引擎更加容易读懂(方便SEO)

2.语义化的标签有哪些

html5之前的常用的语义化标签

h1~h6 p br ul ol li dl dt dd em strong table thead tobdy tfoot td th caption

注意的点:

  • bfontu等纯样式标签不要使用
  • strong是为了强调重要而加粗(不要用b b是为了加粗而加粗),em是斜体是强调(不用i i就是斜体)
  • 每个input标签对应的说明文本都需要使用label标签
  • 表单域要使用fieldset包起来,并使用legend说明表单的用途

html5新增的常用的语义化标签

header footer nav aside section artice

3.块级元素 和 内联元素

块级元素 :

ul li ol dl dd dt table h1-h6 form p 等等 display : block

内联元素 :

a span b img input button 等等 display : inline-block

样式转换:

  • display:block 行内元素转换为块级元素
  • display:inline 块级元素转换为行内元素
  • display:inline-block 转为内联元素

4.DOM怎么优化

  • 可以使用伪元素,阴影实现的内容尽量不使用

  • DOM实现,如清除浮动、样式实现等;按需加载,减少不必要的渲染;

  • 结构合理,语义化标签

  • 使用文档片段

  • DOM缓存

  • innerHTML代替appendChild

  • 虚拟DOM

CSS基础

1.盒子模型的宽度计算

标准盒子模型:

默认 box-sizing : content-box

offsetWidth : 包括 width + border + padding (不包括margin)

比如这个例子

#box {
	width : 20px;
	padding : 20px;
	margin : 20px;
	border : 2px solid #ccc;
}

这里计算就是 offsetWidth :

document.getElementById('box').offsetWidth 64

弹性盒子的计算

加上 box-sizing : border-box

总的宽度设置多少就是多少(width 设置多少就是多少)

然后内容的宽度是自己计算的

#box {
	width : 80px;
	padding : 20px;
	margin : 20px;
	border : 2px solid #ccc;
	box-sizing : border-box;
}

这里计算就是 offsetWidth :

document.getElementById('box').offsetWidth 80

如果padding + border 的宽度大于 width

这个时候的offsetWidth 就是 padding + border了

2.marign纵向重叠的问题

margin重叠是指两个或多个盒子(可能相邻也可能嵌套)的相邻边界(其间没有任何非空内容、补白、边框)重合在一起而形成一个单一边界

比如就有以下的问题:

计算第一行和最后一行之间的距离

<style>
      p {
        font-size: 16px;
        margin-top: 20px;
        margin-bottom: 10px;
        line-height: 1;
      }
    </style>
  </head>
  <body>
    <p>第一行</p>
    <p></p>
    <p></p>
    <p></p>
    <p></p>
    <p></p>
    <p>末行</p>
  </body>

中间的空着的p都没了 ,因为没有高度

最后一行和 第一行之间的距离是 20px

因为在margin纵向会有重叠

计算方法:

  • 全部都为正值,取最大者;

  • 不全是正值,则都取绝对值,然后用正值的最大值减去绝对值的最大值;

3.margin负值

  • margin-top margin-left负值,元素向上、向左移动
  • margin-right 负值 ,右侧元素左移(“后续元素”会被拉向指定方向),元素自身不变
  • margin-bottom 负值,右侧元素上移动(“后续元素”会被拉向指定方向), 元素自身不变

4.BFC

全称:Block Formatting Context, 名为 "块级格式化上下文"。

BFC是一个完全独立的空间(一块独立的渲染区域),让空间里的子元素不会影响到外面的布局

BFC规则

  • BFC就是一个块级元素,块级元素会在垂直方向一个接一个的排列
  • BFC就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签
  • 垂直方向的距离由margin决定, 属于同一个BFC的两个相邻的标签外边距会发生重叠
  • 计算BFC的高度时,浮动元素也参与计算

形成BFC常见条件

  • float 不为 none
  • positionabsolute fixed
  • overflow 不是 visible
  • displayflex inline-block table-cell

常用的情景 清除浮动 margin重叠

5.float布局

双飞翼布局 和 圣杯布局是PC端的经典布局了

两侧的宽度写死,中间的宽度自适应

公共结构如下

<body>
    <div class="container">
      <div class="main"></div>
      <div class="left"></div>
      <div class="right"></div>
    </div>
</body>

公共样式

main设置width: 100%,让它始终占满窗口,这样才有自适应的效果。

<style type="text/css">
    body {
        margin: 0;
        width: 700px;
        min-width: 550px;
    }
    .container {
        width: 100%;
        height: 30vh;
    }
    .container > div {
        height: 100%;
        float: left;
    }
    .main {
        width: 100%;
        background-color: aqua;
    }
    .left {
        width: 100px;
        background-color: red;
    }
    .right {
        width: 200px;
        background-color: green;
    }
</style>

image-20210707110318546.png

圣杯布局

为三个元素的父元素加上padding属性,腾开位置

 .container {
        width: 100%;
        height: 30vh;
        padding-left: 100px;
        padding-right: 200px;
}

left要放到main的左边,设置margin-left: -100%,因为margin的百分比是相对与父元素的,所以需要整整一行的宽度才能补偿这个margin的值,所以left就能到main的位置。

然后再通过相对定位 right自身的宽度就到了最左边

.left {
        position: relative;
        margin-left: -100%;
        right: 100px;
        width: 100px;
        background-color: red;
}

right到main的右边,比如margin-right: -200px;,正好使main重叠right的宽度,因为设置了浮动所以right就会到main的右边了。

.right {
        margin-right: -200px;
        width: 200px;
        background-color: green;
}

image-20210707113141261.png

双飞翼布局

双飞翼布局需要更改下布局,因为main设置了100% ,不能直接给main设置margin

样式

<style type="text/css">
      body {
        margin: 0;
        width: 550px;
        min-width: 500px;
      }
      .container {
        width: 100%;
        height: 30vh;
      }
      .container > div {
        height: 100%;
        float: left;
      }
      .wrap {
        background-color: aqua;
        height: 100%;
        margin: 0 200px 0 100px;
      }
      .main {
        width: 100%;
      }
      .left {
        width: 100px;
        background-color: red;
      }
      .right {
        width: 200px;
        background-color: green;
      }
</style>

结构

<body>
    <div class="container">
      <div class="main">
        <div class="wrap"></div>
      </div>
      <div class="left"></div>
      <div class="right"></div>
    </div>
</body>

image-20210707114727855.png

再创造一个内容层,将所有要显示的内容放到wrap中,给wrap设置margin就可以了

left要放到main的左边,设置margin-left: -100%,因为margin的百分比是相对与父元素的,所以需要整整一行的宽度才能补偿这个margin的值,所以left就能到main的左边。

接着让right到main的右边,只需要设置margin-left的值为负的right的宽,比如margin-left: -200px;,正好使main重叠right的宽度,因为设置了浮动所以right就会到main的右边了。

.left {
        margin-left: -100%;
        width: 100px;
        background-color: red;
      }
.right {
        margin-left: -200px;
        width: 200px;
        background-color: green;
      }

image-20210707115153416.png

使用 flex也可以达到效果

css样式

html,
body {
    margin: 0;
    padding: 0;
}
.box {
    display: flex;
    /* 左中右排列 */
    justify-content: space-between;
    height: 100px;
}
.center {
    /* 自己填充剩下的区域 */
    flex: 1;
    height: 100px;
    background-color: red;
}
.left,
.right {
    /* 缩小放大的比例 所占空间的大小 */
    flex: 0 0 200px;
    height: 100px;
    background-color: skyblue;
}

html结构

<body>
    <div class="box">
        <div class="left"></div>
        <div class="center"></div>
        <div class="right"></div>
    </div>
</body>

image-20210712104451124

使用定位来实现

最常使用的是定位来实现

css样式

html,
body {
    margin: 0;
    padding: 0;
}
.box {
	height: 100px;
}
.center {
    margin: 0 200px;
    height: 100px;
    background-color: red;
}
.left,
.right {
    position: absolute;
    top: 0;
    width: 200px;
    height: 100px;
    background-color: skyblue;
}
.left {
	left: 0;
}
.right {
	right: 0;
}

结构和上面一样

<body>
    <div class="box">
        <div class="left"></div>
        <div class="center"></div>
        <div class="right"></div>
    </div>
</body>

image-20210712111146802

6.手写clearfix

一般的话有的时候会要求10s左右 手写clearfix ,写的时候最好熟练不要犹豫

.clearfix:after {
    content : '';
    display : table;
    clear : both;
}
/*兼容IE低版本*/
.clearfix {
    *zoom : 1;
}

7.flex实现一个三点的色子

flex基础的语法

介绍几个常用的

flex-direction属性决定主轴的方向(即项目的排列方向)。

  • row 表示从左向右排列
  • row-reverse 表示从右向左排列
  • column 表示从上向下排列
  • column-reverse 表示从下向上排列

flex-wrap可以让Flex项目换行排列。

  • nowrap(缺省):所有Flex项目单行排列
  • wrap:所有Flex项目多行排列,按从上到下的顺序
  • wrap-reverse:所有Flex项目多行排列,按从下到上的顺序

flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap

justify-content属性定义了项目在主轴上的对齐方式及额外空间的分配方式。

  • flex-start(缺省):从启点线开始顺序排列
  • flex-end:相对终点线顺序排列
  • center:居中排列
  • space-between:项目均匀分布,第一项在启点线,最后一项在终点线
  • space-around:项目均匀分布,每一个项目两侧有相同的留白空间,相邻项目之间的距离是两个项目之间留白的和
  • space-evenly:项目均匀分布,所有项目之间及项目与边框之间距离相等

align-items属性定义项目在交叉轴上的对齐方式。

  • stretch(缺省):交叉轴方向拉伸显示
  • flex-start:项目按交叉轴起点线对齐
  • flex-end:项目按交叉轴终点线对齐
  • center:交叉轴方向项目中间对齐
  • baseline:交叉轴方向按第一行文字基线对齐

align-content属性定义了在交叉轴方向的对齐方式及额外空间分配,类似于主轴上justify-content的作用。

  • stretch (缺省):拉伸显示
  • flex-start:从启点线开始顺序排列
  • flex-end:相对终点线顺序排列
  • center:居中排列
  • space-between:项目均匀分布,第一项在启点线,最后一项在终点线
  • space-around:项目均匀分布,每一个项目两侧有相同的留白空间,相邻项目之间的距离是两个项目之间留白

来实现一个三点的色了(简单)

实现的思路就是使用flex布局。主轴是space-between

第2个点交叉轴方向是居中对齐。 第3个点项目按交叉轴终点线对齐。

结构

<div class="box">
      <div class="item"></div>
      <div class="item"></div>
      <div class="item"></div>
</div>

样式

<style type="text/css">
      .box {
        width: 200px;
        height: 200px;
        border: 2px solid #ccc;
        border-radius: 10px;
        padding: 20px;

        display: flex;
        justify-content: space-between;
      }
      .item {
        display: block;
        width: 40px;
        height: 40px;
        border-radius: 50%;
        background-color: rgb(210, 225, 228);
      }
      .item:nth-child(2) {
        align-self: center;
      }
      .item:nth-child(3) {
        align-self: flex-end;
      }
</style>

image-20210707122433924.png

8.定位

absolute relative fixed 相对什么定位

  • static默认的position值,无特殊定位,遵循标准文档流
  • relative相对于自身定位 ,但是还是占据原来的空间,同时可通过z-index定义层叠关系。
  • absolute 相对于该元素最近的已定位的祖先元素,如果没有一个祖先元素设置定位,那么参照物是body层。不占据原来的空间,同时可通过z-index定义层叠关系。
  • fixed相对于浏览器窗口进行固定定位,同时可通过z-index定义层叠关系。如果他的祖先元素有transform这个属性,它会参考他做固定定位

9.居中对齐的实现方式

水平居中

  • inline 元素 : text-align : center
  • block 元素 : margin : auto
  • absolute 元素 : left : 50% + margin-left 负自身的一半

垂直居中

  • inline 元素 :line-heigth 等于 height
  • absolute 元素 : top : 50% + margin-top 负自身的一半 或 transform (-50%,-50%)
  • absolute 元素 : top, buttom, left, right = 0 + margin : auto

flex 父盒子设置

display: flex;
justify-content: center;
align-items: center;

table-cell 这个是针对 不是盒子的元素居中,但是可以把里面的盒子display:inline-block

.father{
 display: table-cell;
 vertical-align: middle;
 text-align: center;
 /*要求是固定的宽高不能是百分比*/
}
.son{
    display: inline-block
}

10.line-height继承

  • 写具体数值,如30px,则继承该值(比较好理解)
  • 写比例,如2/ 1.5 ,则继承该比例(比较好理解)
  • 写百分比, 如200% ,则继承计算出来的值(考点)

比如这么一道题 求p的行高

<style type="text/css">
      body {
        font-size: 16px;
        line-height: 200%;
      }
      p {
        font-size: 18;
      }
</style>

结构

<body>
   <p>A</p>
</body>

如果是百分比的话,他会先乘起来再继承 16*200%

如果是数字。直接继承,用自己的fontsize*数字

11.rem 是什么

rem是一个长度单位

  • px,绝对长度单位,最常用
  • em ,相对长度单位,相对于父元素,不常用
  • rem ,相对长度单位,相对于根元素,常用于响应式布局

12.响应式布局的常用方案

  • media-query ,根据不同的屏幕宽度设置根元素font-size
  • rem,基于根元素的相对单位

13.rem的弊端:“阶梯”性

像下面的媒体查询 ,就比如中间的375~413 ,当屏幕的宽度达到376,378font-size还是100px,不会实时变化

除非超过了这个范围

@media only screen and (max-width: 374px) {
        /* iphone5 或者更小的尺寸,以iphone5 的宽度(320px)比例设置font-size */
        html {
          font-size: 86px;
        }
}
@media only screen and (min-width: 375px) and (max-width: 413px) {
        /* iphone6/7/8和iphone X */
        html {
          font-size: 100px;
}
      }
@media only screen and (min-width: 414px) {
        /* iphone6p 或者更大的尺寸,以iphone6p 的宽度(414px) 比例设置font-size */
        html {
          font-size: 110px;
        }
}

14.网页视口尺寸

网页视口尺寸

  • window.screen.height // 屏幕高度
  • window.innerHeight // 网页视口高度
  • document.body.clientHeight // body高度

vw/vh

  • vh网页视口高度的1/100
  • vw网页视口宽度的1/100
  • vmax取两者最大值; vmin取两者最小值

15.重绘和回流

重绘:指的是当页面中的元素不脱离文档流,而简单地进行样式的变化,比如修改颜色、背景等, 浏览器重新绘制样式 回流:指的是处于文档流中 DOM 的尺寸大小、位置或者某些属性发生变化时,导致浏览器重新渲 染部分或全部文档的情况

相比之下,回流要比重绘消耗性能开支更大。另外,一些属性的读取也会引起回流,比如读取某个 DOM 的高度和宽度,或者使用 getComputedStyle 方法。

JS基础

JS基础知识,规定语法(ECMA262标准)

JSWebAPI,网页操作的API(W3C标准)

前者是后者的基础,两者结合才能真正实际应用

1.class 和 继承

简单的回顾下class constructor 属性 方法 注:class是ES6语法

class Student {
    constructor(name, age) {
        this.name = name
        this.number = number
    }
    skill() {
        console.log(`我是${this.name}今年${this.age}我会学习`)
    }
}
var zhangsan = new Student('张三', 18)
console.log(zhangsan.name) //张三
console.log(zhangsan.number) //18
zhangsan.skill() //我是张三今年18我会学习

继承 extends super 扩展或重写方法

class People {
    constructor(name) {
        this.name = name
    }
    eat() {
        console.log(`${this.name} 特能吃`)
    }
}
class Student extends People {
    constructor(name, number) {
        super(name)
        this.name = name + '【法外狂徒】'
        this.number = number
    }
    skill() {
        console.log(`我是${this.name}今年${this.age}我会学习`)
    }
}
var zhangsan = new Student('张三', 18)
console.log(zhangsan.name) //张三【法外狂徒】
console.log(zhangsan.number) //18
zhangsan.skill() //我是张三【法外狂徒】今年18我会学习
zhangsan.eat() //张三【法外狂徒】 特能吃

typeof Student "function" typeof People "function"

由此可见 class 也是 function

2.变量类型

原始类型

注意:原始类型不包含 Object。

ECMAScript 中定义了 6 种原始类型:

  • Boolean
  • String
  • Number
  • Null
  • Undefined
  • Symbol(ES6 新定义)

typeof

typeof 可以判断以下几种类型:

  • undefined
  • boolean
  • number
  • string
  • object
  • function
  • symbol

注意:

typeof null 结果是 object ,实际这是 typeof 的一个bug,null是原始值,非引用类型

typeof 数组 结果是 object ,结果中没有 array 这一项,引用类型除了 function 其他的全 部都是 object

typeof Symbol()typeof 获取 symbol 类型的值得到的是 symbol ,这是 ES6 新增的知识点

类型判断 - instanceof

instanceof 是基于原型链查找

zhangsan instanceof Student
//true
zhangsan instanceof People
//true
zhangsan instanceof Object
//true
[] instanceof Array
//true
[] instanceof Object
//true
{} instanceof Object
//true

3.原型

原型

原型就是一个对象,实例“继承”那个对象的属性。在原型上定义的属性,通过“继承”,实例也拥有了这个属性。“继承”这个行为是在 new 操作符内部实现的。

构造函数内部有一个名为 prototype 的属性,通过这个属性就能访问到原型

隐式原型 显示原型

Student就是构造函数,Student.prototype 就是原型

实例通过 __proto__ 访问到原型

所以这两者是等价的:

  • zhangsan.prototype 显式原型
  • zhangsan.__proto__隐式原型
  • zhangsan.prototype === zhangsan.__proto__

image-20210707135253242.png

4.原型链

原型链

原型里面也有个__proto__属性,原型可以通过这个属性访问到原型它自己的原型

举个栗子

Student 的一个实例 zhangsan ,当调用它的一个属性或者方法时,他自身没有这个方法或者属性,它就通过__proto__来进行访问原型看看原型上有无该属性和方法,有的话就能直接使用,没有就查找原型的__proto__也就是原型的原型继续找有没有该属性或者方法直到最顶层为null为止,没有查找到就是不存在

这种不断向上搜索形成的链状关系我们就称为原型链

image-20210707165624028

5.如何准确的判断一个变量是数组

显然通过 typeof []判断不能得到想要的结果 "object"

常用的方法就是instanceof 基于原型链查找的方式进行判断

还有就是通过Object.prototype.toString进行判断

  • [] instanceof Array 结果 :true
  • Object.prototype.toString.call([]) 结果: "[object Array]"

6.手写简易的jQuery 考虑插件和扩展性

写个简单的html等下使用

<ul>
    <li>点我</li>
    <li>点我</li>
    <li>点我</li>
    <li>点我</li>
    <li>点我</li>
</ul>

首先先简单的写个jQuery

class jQuery {
    // 获取元素进行保存
    constructor(selector) {
        const result = document.querySelectorAll(selector)
        const lenght = result.length
        for (let i = 0; i < result.length; i++) {
            this[i] = result[i]
        }
        this.lenght = lenght
        this.selector = result
    }
    // 实现get方法,把jQ变成Dom
    get(index) {
        return this[index]
    }
    // 遍历
    each(fn) {
        for (let i = 0; i < this.lenght; i++) {
            const ele = this[i]
            fn(ele)
        }
    }
    // 绑定事件
    on(type, fn) {
        this.each((ele) => {
            ele.addEventListener(type, fn, false)
        })
        return this
    }
}
// 添加一个插件
jQuery.prototype.dialog = function () {
    alert('这是一个dialog')
    return this
}
// 以后使用就是直接使用 $
const $ = (selector) => new jQuery(selector)
// 使用
$('ul li').on('click', function () {
    console.log(this)
})
// 调用 添加的方法
$().dialog()

也可以继续造轮子

// 造轮子
class myJQuery extends jQuery {
    constructor(selector) {
        super(selector)
    }
    // 在这里扩展自己方法
    css(json) {
        for (let key in json) {
            this.each((ele) => {
                ele.style[key] = json[key]
            })
        }
        return this
    }
}
// 使用
new myJQuery('ul li').css({
    background: 'red',
})

7.作用域和闭包

简单来说,作用域 指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。

由于作用域的限制,每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。

image-20210707195902417

javascript 中大部分情况下,只有两种作用域类型:

  • 全局作用域:全局作用域为程序的最外层作用域,一直存在。
  • 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。

自由变量

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域,一层一层依次寻找,直至找到为止
  • 如果到全局作用域都没找到,则报错Xx is not defined

作用域链

如果当前作用域中的使用的自由变量没有定义,就会向外层作用域中寻找,如果外层没有就继续一直寻找下去,如果全局作用域都没有就是没有报错 ReferenceError这种不断向上搜索形成的链状关系称为作用域链

块级作用域

ES6 标准提出了使用 letconst 代替 var 关键字,来“创建块级作用域”

简单来说,花括号内 {...} 的区域就是块级作用域区域。

{
  let a = 1;
}

console.log(a); // ReferenceError

作用域的一个常见运用场景之一,就是 模块化。 全局作用域污染和变量名冲突,代码结构臃肿且复用性不高。

闭包: 能够访问其他函数内部变量的函数,被称为 闭包

是作用域应用的特殊情况

有两种表现:函数作为参数被传递函数作为返回值被返回

应用场景: 大多数是在需要维护内部变量的场景下。

例子一

function test() {
    const a = 30
    return function print() {
        console.log(a)
    }
}
const a = 100
test()() //30

例子二

const b = 30
function fun() {
    console.log(b)
}
function test2(fn) {
    const b = 100
    fn()
}
test2(fun) //30

所有的自由变量的查找,是在函数定义的地方,向上级作用域查找 不是在执行的地方!

由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露

8.this

this取什么值是在函数执行的时候确定不是定义的时候确定

function fn1() {
    console.log(this)
}
fn1() //window

fn1.call({ a: 1 }) //{ a: 1 }
fn1.bind({ b: 2 })() //{ b: 2 }
  1. 普通函数中:this->window
  2. 定时器中:this->window
  3. 构造函数中:this->当前实例化的对象
  4. 事件处理函数中:this->事件触发对象
  5. 在 js 中一般理解就是谁调用这个 this 就指向谁

9.创建10个 a 标签,点击的时候弹出对应的序号

考察块级作用域

for (let i = 0; i < 10; i++) {
    const a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}

10.实际开发中闭包的应用

隐藏数据 只提供API 保证数据安全

function createCache() {
    const data = {}
    return {
        set(key, val) {
            console.log(data, key, val)
            data[key] = val
        },
        get(key) {
            return data[key]
        },
    }
}
const c = createCache()
c.set('a', 'haha ')
console.log(c.get('a')) //haha

11.手写call,apply,bind函数

手写call,apply,bind 比较基础

手写callcall的性能比 apply

Function.prototype.call1 = function (obj, ...args) {
  // 保存传入的this
  const context = obj
  // 避免覆盖原来的方法,所以就使用 Symbol保证唯一性
  const fn = Symbol()
  // 把方法添加到传入的this,然后方法指向 call1的调用者this也就是方法
  context[fn] = this
  // 执行方法
  const result = context[fn](...args)
  // 删除传入this的fn方法
  delete context[fn]
  // 返回结果
  return result
}

// 方法的使用
function sum(num1, num2) {
  console.log(this.base + num1 + num2)
}

let obj = {
  base: 1,
}

sum.call1(obj, 100, 200) //301

手写apply

// 和上面call差不多,第二个参数不用展开了
Function.prototype.apply1 = function (obj, args) {
  // 保存传入的this
  const context = obj
  // 避免覆盖原来的方法,所以就使用 Symbol保证唯一性
  const fn = Symbol()
  // 把方法添加到传入的this,然后方法指向 call1的调用者this也就是方法
  context[fn] = this
  // 执行方法
  const result = context[fn](...args)
  // 删除传入this的fn方法
  delete context[fn]
  // 返回结果
  return result
}

// 方法的使用
function sum(num1, num2) {
  console.log(this.base + num1 + num2)
}

let obj = {
  base: 1,
}

sum.apply1(obj, [100, 200]) //301

手写bind

Function.prototype.mybind = function () {
    // 将参数转为数组 等价 const args = arguments.slice()
    const args = Array.prototype.slice.call(arguments)
    // 把传递this取出来
    const myThis = args.shift()
    // 当前函数的this先保存
    const self = this
    // 返回一个函数
    return function () {
        // 执行原函数并返回结果
        return self.apply(myThis, args)
    }
}

function fn(a, b) {
    console.log(this, a + b)
}
fn.mybind({ a: 1 }, 1, 2)() //{a: 1} 3

手写完整的bind 支持new

Function.prototype.bind = function(OThis, ...outherArgs) {
	// 缓存调用的函数
    const thatFunc = this
    const fBind = function(...innerArgs) {
        // 判断是否是new 还是直接执行函数
        return thisFunc.apply(
        	this instanceof thatFunc ? this : OThis, [...outherArgs, ...innerArgs]
        )
    }
    // 考虑不能污染到 thatFunc的prototype 这里直接用 Object.create()
    fBind.prototeye = Object.create(thatFunc.prototype)
    // 返回一个函数
    return fBind
}

例题

function fn1() {
  console.log('fn1')
}
function fn2() {
  console.log('fn2')
}

fn1.call.call(fn2) // 'fn2'
// 第一次调用call
// context = fn2 ; this = fn1
//return  fn2[fn1]() ---> fn2.fn1()
// 第二次调用
// context = fn2 ; this = call
//return fn2[call](window)  ---> window.fn2() ---> 'fn2'
fn1.call.call.call.call.call.call.call.call(fn2) // 'fn2'

12.异步

JS是单线程语言,只能同时做一件事儿

浏览器和nodejs已支持JS启动进程,如Web Worker

JSDOM渲染共用同一个线程,因为JS可修改DOM结构

因为是单线程

遇到等待(网络请求,定时任务)不能卡住,需要异步,采用了回调callback函数形式

同步和异步

  • 基于JS是单线程语言
  • 异步不会阻塞代码执行
  • 同步会阻塞代码执行
//异步
console.log(100)
setTimeout(function() {
    console.log(300)
})
console.log(200)
//不会阻塞代码
//同步
console.log(100)
alert(1)//执行在这里就卡住了
console.log(200)

应用场景 :

网络请求, 比如ajax图片加载

定时任务,比如 setTimeout

13.callback hell 回调地狱

比如这个例子,一直往下面嵌套(函数作为参数层层嵌套)

回调地狱最主要的就是因为功能逻辑代码嵌套的层次太多,导致可读性降低,维护困难,避免回调地狱的最重要的方面是将功能移开,保持代码简单

//狱取第一份数据
$.get(url1, (data1) => {
    console. log(datal)
    //获取第二份数据
    $.get(url2,(data2) => {
        console. Log(data2)
        //获取第三份数据
        $.get(url3, (data3) => {
            console. log(data3)
            //还可能获取更多的数据
        })
    })
})

14.Promise

Promise:是编写异步代码的一种方式,它仍然以自顶向下的方式执行,并且由于鼓励使用try / catch样式错误处理而处理更多类型的错误

Promise解决了回调地狱嵌套的问题

1.一个简单的Promise例子

function getData(url) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url,
            success(data) {
                resolve(data)
            },
            error(err) {
                reject(err)
            },
        })
    })
}
const url1 = '/data1.json'
const ur12 = '/data2.json'
const url3 = '/data3.json'
getData(url1)
.then((data1) => {
    console.log(data1)
    return getData(url2)
})
.then((data2) => {
    console.log(data2)
    return getData(url3)
})
.then((data3) => {
    console.log(data3)
})
.catch((err) => console.error(err))

2.手写用Promise加载一张图片

function loadImg(url) {
    return new Promise((resolve, reject) => {
        const img = document.createElement('img')
        img.onload = function () {
            resolve({ img, msg: '图片加载成功' })
        }
        img.onerror = function () {
            reject(new Error(`${img.src}图片加载失败`))
        }
        img.src = url
    })
}
loadImg('./img/2.jpg')
.then((res) => {
    console.log(res)
    document.body.appendChild(res.img)
})
.catch((res) => {
    console.log(res)
})

image-20210708110216819

15.event loop(事件循环)

异步和事件循环的关系

  • JS是单线程运行的
  • 异步要基于回调来实现
  • event loop就是异步回调的实现原理

JS代码是如何执行的

  • 从前到后,一行一-行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步
console.log('Hi')
setTimeout(function cb1() {
    console.Log('cb1') // cb即callback
}, 5000)
console.log('Bye')
// 打印顺序就是  Hi  Bye  cb1

事件循环过程(还没涉及宏任务/微任务)

  1. 同步代码,一行一 行放在Call Stack执行(同步任务在栈里压栈弹栈执行)
  2. 遇到异步,会先“记录”下,等待时机(定时、网络请求等)
  3. 时机到了,就移动到Callback Queue(回调队列)
  4. Call Stack为空(即同步代码执行完) Event Loop开始工作
  5. 轮询查找Callback Queue(回调队列) , 如有则移动到Call Stack执行
  6. 然后继续轮询查找(永动机一样)

自己画的图,不对的地方请多多指教

image-20210708121124506

16.Promise状态

三种状态

  • pending 不会触发then和catch
  • resolved 会触发后续的then回调函数
  • rejected 会触发后续的catch回调函数

状态的表现 和 变化 (变化是不可逆的

pending一> resolvedpending一> rejected

thencatch 对状态的影响(重要)

  • then正常返回resolved, 里面有报错则返回rejected
  • catch正常返回resolved,里面有报错则返回rejected

then和catch的链式调用(常考)

网上的promise习题很多的,我这里就编一个简单点的,最后打印的结果

const p1 = Promise.resolve().then(() => {
  return 100
})
// p1.then中没有出错,正常返回resolved 触发后续的then回调
p1.then((data) => {
  console.log(data) //100
}).catch(() => {
  console.error(new Error('err')) //这行代码必然不被执行
})

// rejected执行的是之后的catch回调
const p2 = Promise.reject().then(() => {
  throw new Error('then error') //这行代码必然不被执行
})

p2.then(() => {
  console.log('200') //这个也不执行
})
.catch(() => {
  console.log('300')
  throw new Error('err') //返回rejected 执行下面catch
})
.then(() => {
  console.log('400')//这个也不执行
})
.catch(() => {
  console.log('500') //返回resolved 执行下面then
})
.then(() => {
  console.log('600')
})

最后打印 的是 100 300 500 600

网上扒两个简单的

例题1

Promise.resolve()
  .then(() => {
    console.log(1)
    throw new Error('erro1')
  })
  .catch(() => {
    console.log(2)
  })
  .then(() => {
    console.log(3)
  })

打印的是 1 2 3

例题2

Promise.resolve()
  .then(() => {
    console.log(1) // 1
    throw new Error('erro1')
  })
  .catch(() => {
    console.log(2) 
  })
  .catch(() => {
    console.log(3) 
  })

打印的是 1 2

网上的例题很多哈,大家有点时候可以多练习

17.async / await

async / await 和 异步回调callback hell

  • Promise then catch链式调用,但也是基于回调函数
  • async/ await是同步语法,彻底消灭异步回调的终极武器
  • 但和Promise并不互斥 ,反而,两者相辅相成

async / awaitPromise的关系

  • 执行async函数,返回的是Promise对象
  • await相当于Promisethen
  • try...catch可捕获异常,代替了Promisecatch

下面几个例子

!(async function () {
  const p1 = Promise.resolve(300)
  const data = await p1 // await 相当于Promise then
  console.log('data', data)
})()

!(async function () {
  const datal = await 400 //await 相当于Promise.resolve(400)
  console.log('datal', datal)
})()

function fn1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(500)
    }, 1000)
  })
}

!(async function () {
  const data2 = await fn1()
  console.log('data2', data2)
})()

!(async function () {
  const p4 = Promise.reject('err') // rejected状态
  try {
    const res = await p4
    console.log(res)
  } catch (ex) {
    console.error(ex) // try..catch相当于promise catch
  }
})()

异步的本质

  • async/ await是消灭异步回调的终极武器
  • JS还是单线程,还得是有异步,还得是基于event loop
  • async/await只是一个语法糖,但这颗糖真香!

结合事件循环做两个例题

例题一

async function async1() {
  console.log('async1 start')
  await async2()
  // await 后面的代码都看做异步回调
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
async1()
console.log('script end')

结果打印的是 :

script start

async1 start

async2

script end

async1 end

例题二

!(async function () {
  async function async1() {
    console.log('async1 start') //2
    await async2()

    console.log('async1 end') //4
    await async3()
    console.log('async1 end 2') //6
  }

  async function async2() {
    console.log('async2') //3
  }

  async function async3() {
    console.log('async3') //5
  }
  console.log('script start') //1
  await async1()
  console.log('script end') //7
})()

打印的结果如下:

image-20210708141256573

18.for ... of

for .. in (以及forEach for )是常规的同步遍历

for .. of常用于异步的遍历

用个例子来说明两个的区别

function test(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(item * item)
    }, 1000)
  })
}

const arr = [100, 200, 300]

arr.forEach(async (i) => {
  console.log(await test(i))
})

上面的例子,使用同步遍历的时候 是在1000ms后全部把结果打印出来了,而不是隔一秒打印

1

使用for ... of 遍历

function test(item) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(item * item)
    }, 1000)
  })
}

const arr = [100, 200, 300]

!(async function () {
  for (let item of arr) {
    console.log(await test(item))
  }
})()

使用for...of 遍历会 1000ms 打印一次

2

19.宏任务/微任务

简单介绍:

微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序

  • 宏任务: setTimeout,setInterval,Ajax,DOM事件
  • 微任务: Promise async/await
  • 微任务执行时机比宏任务要早

先来个简单的demo

console.log(1)
setTimeout(() => {
  console.log(2)
}, 1)
Promise.resolve().then(() => {
  console.log(3)
})
console.log(4)

结果:1 4 3 2

说下DOM渲染

  • JS是单线程的,而且和DOM渲染共用一个线程
  • JS执行的时候,得留一-些时机供DOM渲染
  • 每次Call Stack(执行栈)清空(即每次轮询结束), 即同步任务执行完
  • 都是DOM重新渲染的机会,DOM结构如有改变则重新渲染
  • 然后再去触发下一次Event Loop(事件循环)

image-20210708144736486

宏任务: DOM渲染后触发,如setTimeout

微任务: DOM渲染前触发,如Promise

微任务是ES6语法规定的

宏任务是由浏览器规定的

image-20210708145221088

image-20210708145248926

事件循环宏任务/微任务 之间的关系

image-20210708151152089

写两个例题

例题一

async function async1() {
  console.log('async1 start')
  await async2()
  // await 后面作为异步回调  这里是微任务
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}

console.log('script start')

// 宏任务
setTimeout(() => {
  console.log('setitmeout')
}, 0)

async1()

结果: image-20210708152230776

例题二

new Promise((resolve) => {
  console.log('promise1')//初始化Promise时会立即执行
  resolve()
}).then(() => { 
  console.log('promise2')//微任务
})
console.log('script end')

结果:image-20210708152608397

20.DOM节点

DOM 是一种树结构 (DOM树)

节点操作 简单举几个例子,这些基本API不用太多介绍

const div1 = document.getElementById('diV1') / 元素
const divList = document.getElementsByTagName('div') //集合
console.log(divList.length)
console.log(divList[0])
const containerListe = document.getElementsByClassName('container') //集合
const pList = document.querySelectorAlL('p') //集合

DOM节点的property

const pListe = document.querySelectorAll(p)
const p = pList[0]
console.log(pList[0].style.width) / 获取样式
p.style.width = '100px' //修改样式
console.log(p.className) //获取class
p.className = 'p1' // 修改class
//获取nodeName和nodeType
console.log(p.nodeName)
console.log(p.nodeType)

DOM节点的attribute

const pListe = document.querySelectorAll(p)
const p = pList[0]
p.setAttribute('data-src', './21.pro.jpg')
console.log(p.getAttribute('data-src'))

p.setAttribute('style', 'color:#ccc;')
console.log(p.getAttribute('style'))
p.removeAttribute("style");

property :修改对象属性,不会体现到html结构中

attribute :修改html属性,会改变html结构

两者都有可能引起DOM重新渲染

21.DOM节点操作

节点操作

const p = document.createElement("p"); //创建DOM元素
p.remove(); // 删除p
p.removeChild(clildEle) // 删除p中的子元素
const p2 = p.cloneNode(true)//克隆
box.appendChild(p);//追加到最后
box.appendChild(p2);//如果p2之前就在DOM里,再次添加到box里面就是移动节点
p.insertBefore(newele,ele) // 在p元素中的 ele元素前插入 newele
box.replaceChild(newEle,oldEle) //替换dom
console.log(p.nodeType); // 判断p的类型

节点之间的联系

  • parentNode 查找父节点

  • childElementCount 返回子元素的个数不包括文本节点和注释

  • firstChild 查找指定节点的第一个字节点

  • lastChild 查找指定节点的最后一个字节点

  • previousSibling 查找指定节点的上一个节点

  • firstElementChild 返回第一个子元素

  • lastElementChild 返回最后一个子元素

  • previousElementSibling 返回前一个相邻兄弟元素

  • nextElementSibling 返回后一个相邻兄弟元素

  • nextSibling 查找指定节点的下一个节点

22.DOM性能

  • DOM操作非常"昂贵", 避免频繁的DOM操作
  • 对DOM查询做缓存
  • 将频繁操作改为一次性操作

DOM 查询做缓存

// 不缓存查询
for (let i = 0; i < document.getElementsByTagName('p').length; i++) {
  // 每次循环都要进行查询
}

// 缓存DOM 查询
const pList = document.getElementsByTagName('p')
const length = pList.length
for (let i = 0; i < length; i++) {
  // 缓存之后就查询一次
}

将频繁操作改做一次性操作

// 创建一个文档碎片
const fragment = document.createDocumentFragment()

// 插入内容
for (let i = 0; i < 10; i++) {
  const img = document.createElement('img')
  img.src = './img/2.jpg'
  fragment.appendChild(img)
}

// 完成之后,直接把文档片段插入DOM 中
document.body.appendChild(fragment)

23.BOM 简单的过一遍

BOM 的核心对象window:

  • window是全局对象
  • 浏览器窗口的JavaScript接口

navigator

客服端标识浏览器的标准,主要用来记录和检测浏览器与设备的主要信息

如何识别浏览器?

const ua = window.navigator.userAgent
const isChrome = ua.indexOf('Chrome')
console.log(isChrome)

screen

保存客服端能力的对象 举几个常见的属性

  • height: 屏幕像素高度
  • left: 当前屏幕左边的像素距离
  • top: 当前屏幕顶端的像素距离
  • width: 屏幕像素宽度

location 可以用location拆解url

  • protocol:协议
  • host:域名和端口
  • hostname:服务器名
  • pathnameurl路径
  • hash:哈希值
  • hrefurl字段
  • search:查询字段

history

  • go():跳转页面
  • forward:前进
  • back():后退

24.事件

事件绑定 简单介绍下,相信大家都会

let btn = docment.getElementById('btn')
//用on绑定
btn.onclick = funcition(){
    //事件处理
}

//使用addEventListener
btn.addEventListener('click',function(){
    //事件处理逻辑
},false)

简述事件冒泡的流程

  • 基于DOM树形结构
  • 事件会顺着触发元素网上冒泡
  • 应用场景:事件代理

事件代理的好处

  • 代码简洁
  • 减少浏览器内存占用
  • 但是,不要滥用
  • 对于那些新添加的元素,最好就使用事件代理

例子 : 无限下拉图片列表,如何监听每个图片的点击

  • 事件代理
  • 用e.target获取触发元素
  • 用matches来判断是否是触发元素

自己封装通用的事件绑定函数

Object.prototype.bindEvent = function (type, selector, fn) {
  // 如果第三个参数为空说明不是事件代理
  if (fn == null) {
    fn = selector
    selector = null
  }
  this.addEventListener(type, (e) => {
    // 保存事件的触发元素
    const target = e.target
    // 事件代理
    if (selector) {
      if (target.matches(selector)) {
        fn.call(target, e)
      }
    } else {
      // 普通事件绑定
      fn.call(target, e)
    }
  })
}

const ul = document.querySelector('ul')

// 普通使用
ul.bindEvent('click', function () {
  console.log(this)
})

// 事件代理
ul.bindEvent('click', 'li', function () {
  console.log(this)
})

25.ajax

手写一个简单的ajax

get请求

const xhr = new XMLHttpRequest()
// get请求
xhr.open('GET', './test.json', true)
xhr.onreadystatechange = function () {
  if (xhr.status === 200 && xhr.readyState === 4) {
    console.log(xhr.responseText)
  }
}

xhr.send(null)

post请求

const xhr = new XMLHttpRequest()
// post 请求
xhr.open('POST', 'localhost://mytest', true)
xhr.onreadystatechange = function () {
  if (xhr.status === 200 && xhr.readyState == 4) {
    console.log(xhr.responseText)
  }
}
xhr.send(
  JSON.stringify({
    name: 'zhangsan',
    age: 18,
  })
)

状态码 xhr.readyState

  • 0- (未初始化)还没有调用send(方法
  • 1- (载入)已调用send(方法,正在发送请求
  • 2- (载入完成) send()方法执行完成,已经接收到全部响应内容
  • 3- (交互)正在解析响应内容
  • 4- (完成)响应内容解析完成,可以在客户端调用

响应码 xhr.status

  • 2xx -表示成功处理请求,如200
  • 3xx -需要重定向,浏览器直接跳转,如301 302 304
  • 4xx -客户端请求错误,如404 403
  • 5xx -服务器端错误

26.跨域

什么事跨域

简单来说:由于同源策略的原因浏览器不能执行其他网站的脚本

所有的跨域,都必须经过server端允许和配合

未经server端允许就实现跨域,说明浏览器有漏洞,危险信号

同源政策

加载图片 、css、 js可无视同源策略

  • <img src=跨域的图片地址/>可用于统计打点,可使用第三E方统计服务
  • <link href=跨域的css地址/> 可使用CDN , CDN一般都是外域
  • <script src=跨域的js地址> </script> 可使用CDN,可实现JSON

jsonp

因为script标签可以发送get请求,比如引用CDN,我们可以动态的创建script标签,再去请求一个带参网址来实现跨域通信

  • script标签可绕过跨域限制
  • 服务器可以任意动态拼接数据返回
  • 所以,<script>就可以获得跨域的数据,只要服务端愿意返回

写个简易jsonp的例子

后台是 node.js + express

app.get('/info',(req,res)=>{
    const result = 'fn({name : '张三'})'
    //返回fn({name : '张三'})并执行
    res.send(result) //............3
})

前端

<script>
    //首先定义好一个fn函数 ............1
    window.fn = function (data) {
    //打印获取到的数据
    console.log(data) //...........4
}
</script>
//使用script标签发送jsonp请求 ..............2
<script src="http://localhost:8080/mytest/info"></script>

当然自己也可以自己封装一个jsonp函数

function jsonp(options) {
  // 动态创建script
  const script = document.createElement('script')
  // 拼接字符串变量
  let params = ''

  // 拼接多个参数
  for (let attr in options.data) {
    params += '&' + attr + '=' + options.data[attr]
  }
  // 随机生成函数名
  const fnName = 'myJsonp' + Math.random().toString().replace('.', '')
  // 把fnName变成全局函数
  window[fnName] = options.success
  // 给script添加src属性
  script.src = options.url + '?callback=' + fnName + params
  // script添加到页面中
  document.body.appendChild(script)
  // 触发后移除script
  script.onload = function () {
    document.body.removeChild(this)
  }
}

// 使用
btn.onclick = function () {
  jsonp({
    url: 'url地址',
    success: function (data) {
      console.log(data)
    },
  })
}

jQuery实现jsonp

$.ajax({
  url: 'url地址',
  //必须指定为jsonp
  dataType: 'jsonp',
  //自定义的回调函数名称,默认值为jqueryxxx 格式
  jsonpCallback: 'callback',
  success: function (data) {
    console.log(data)
  },
})

默认情况下,使用jQuery发起JSONP请求,会自动携带一个 callback=jQueryxxx的参数, jQuery 是随机生成的一个回调函数名称。

CORS -服务器设置http header

//允许客户端额外向服务器发送Content-Type 请求头和Xx-Custom-Header 请求头
//注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
//只允许POST、 GET、 DELETE、 HEAD请求方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
//允许所有的HTTP 请求方法
res.setHeader('Access-Control-Allow-Methods', '*')

27.ajax工具

jQuery中封装的$.ajax就不多说了

Fetch:简单的例子 默认GET请求

fetch('url地址')
  .then(data => console.log(data))
  .catch(err => console.log(err)); 

Fetch底层是用Promise实现,我们可以直接用async来优化上面的代码,减少回调,使其更加语义化、容易理解

!(async function(){
    try{
        const res = await fetch('url地址')
        console.log(res)
    }catch(err){
		console.log(err)       
    }
})() 

Fetch的POST请求

fetch(url, {
    method: 'post',
    headers: {
        "Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
    },
    body: 'name="zhangsan"&age=18'
})
.then(function (data) {
    console.log(data);
})
.catch(function (err) {
    console.log(err);
});

带上cookie需要设置credentials

fetch(url, {
  credentials: 'include'
})

axios的基本用法应该不必多说了。大家基本天天用吧

28.存储

描述cookie localStorage sessionStorage区别

cookie

  • 本身用于浏览器和server通讯
  • html5之前 被"借用”到本地存储来
  • 可用document.cookie = 来修改

image-20210708221336289

cookie缺点

  • 存储大小,最大4KB
  • http请求时需要发送到服务端,增加请求数据量
  • 只能用document.cookie = 来修改,太过简陋

localStoragesessionStorage

  • HTML5专门为存储而设计,最大可存5M
  • API简单易用setItem getItem
  • 不会随着http请求被发送出去
  • localStorage数据会永久存储,除非代码或手动删除
  • sessionStorage数据只存在于当前会话,浏览器关闭则清空
  • 一般用localStorage会更多一-些

29.HTTP

状态码分类

  • 1xx服务器收到请求
  • 2xx请求成功,如200
  • 3xx重定向,如302
  • 4xx客户端错误,如404
  • 5xx服务端错误,如500

常见状态码

  • 200成功
  • 301永久重定向(配合location ,浏览器自动处理)
  • 302临时重定向(配合location ,浏览器自动处理)
  • 304资源未被修改
  • 404资源未找到
  • 403没有权限
  • 500服务器错误
  • 504网关超时

关于协议和规范

  • 就是一个约定
  • 要求大家都跟着执行
  • 不要违反规范,例如IE浏览器

http headers

Request Headers

  • Accept浏览器可接收的数据格式
  • Accept-Encoding浏览器可接收的压缩算法,如gzip
  • Accept-Languange浏览器可接收的语言,如zh-CN
  • Connection: keep-alive - -次TCP连接重复使用
  • cookie
  • Host
  • User-Agent (简称UA )浏览器信息
  • Content-type发送数据的格式,如application/json

Response Headers

  • Content-type返回数据的格式,如application/json
  • Content-length返回数据的大小,多少字节
  • Content-Encoding返回数据的压缩算法,如gzip
  • Set-Cookie

自定义header了解下

header : {
    'X-Requested-with': 'XMLHttpRequest'
}

30.HTTP缓存

关于缓存

  • 减少带宽,减少请求数量,减轻服务器压力,提升性能
  • 浏览器缓存一般都是针对静态资源,比如 js、css、图片 等

强制缓存

image-20210709155528877

如果过期了 再次请求 ,返回Cache-Control

Cache-Control

Response Headers中 控制强制缓存的逻辑

例如Cache-Control: max-age=31536000 ( 单位是秒)

  • public:资源客户端和服务器都可以缓存。
  • privite:资源只有客户端可以缓存。
  • no-cache:客户端缓存资源,但是是否缓存需要经过协商缓存来验证。
  • no-store:不使用缓存。
  • max-age:缓存保质期。

关于Expires

  • 同在Response Headers
  • 同为控制缓存过期
  • 已被Cache-Control代替

协商缓存

  • 服务器端缓存策略
  • 服务器判断客户端资源,是否和服务端资源一样
  • 致则返回304 , 否则返回200和最新的资源

image-20210709155459528

资源标识

  • Response Headers中,有两种
  • Last-Modified资源的最后修改时间
  • Etag资源的唯一标识( -一个字符串,类似人类的指纹)

image-20210709160347279

image-20210709160501440

下面是一个Headers实例

image-20210709160613179

Last-ModifiedEtag

  • 会优先使用Etag
  • Last-Modified只能精确到秒级
  • 如果资源被重复生成,而内容不变,则Etag更精确

缓存流程图

image-20210709161010069

三种刷新操作对缓存的影响

正常操作:地址栏输入url ,跳转链接,前进后退等

手动刷新: F5,点击刷新按钮,右击菜单刷新

强制刷新: ctrl + F5

  • 正常操作:强制缓存有效,协商缓存有效
  • 手动刷新:强制缓存失效,协商缓存有效
  • 强制刷新:强制缓存失效,协商缓存失效

关于开发环境

1.git

最常用的代码版本管理工具

大型项目需要多人协作开发,必须熟用git

如果你不知道或者之前不用git ,不会通过面试

  • Mac OS自带git命令,windows可去官网下载安装
  • git服务端常见的有github coding.net等
  • 大公司会搭建自己的内网git服务

我这里就不用多多介绍了,我就说下我们经常用的操作

第一次使用git,配置用户信息

  • 用户名:git config --global user.name "用户名"
  • 户邮箱:git config --global user.email "邮箱地址"

初始化仓库

  • 创建一个新的本地仓库:git init
  • 或者从远程git仓库复制项目:git clone <远程git仓库url地址>

添加到暂存区

  • 添加指定文件:git add 文件1 文件2 ...
  • 添加文件夹:git add [文件夹]
  • 添加所有文件:git add .

撤销

  • 取消暂存区已经暂存的文件:git reset HEAD 文件1 ...
  • 隐藏当前变更,以便能够切换分支:git stash
  • 如果代码写错到了分支先 git stash 切换分支后 就git stash pop 释放刚才的内容

重命名文件

  • 并提交到暂存区:git mv [old] [new]

查看信息

  • 查询当前工作区所有文件的状态:git status
  • 比较当前修改的文件 和 暂存区的 差异:git diff 文件
  • 直接比较有修改的所有的文件:git diff

提交

提交到本地仓库:git commit -m "提交信息"

撤销上次提交:git commit --amend

跳过暂存区直接提交,自己不用add .git commit -a -m "提交信息"

分支管理

  • 显示所有分支:git branch
  • 创建分支:git branch <新分支名称>
  • 新建子分支:git checkout -b <子分支名称>
  • 切换到其他分支:git checkout <分支名称>
  • 当前分支与其他分支合并:git merge <分支名称>
  • 把远程分支合并到当前分支:git merge <远程分支>/<本地分支>
  • 删除分支:git branch -d <分支名称>
  • 添加远程仓库:git remote add [remote-name] [url]比如github这个例子git remote add origin url
  • 将本地仓库某分支推送到远程仓库上:git push [远程仓库] [分支名字] 这里举个例子git push origin master

2.抓包

移动端h5页,查看网络请求,需要用工具抓包

windows一般用fiddler

Mac OS - -般用charles

抓包+调试的工具spy-debugger

  1. 手机和电脑连同-一个局域网
  2. 将手机代理到电脑上
  3. 手机浏览网页,即可抓包

具体使用方法大家可以去看看其他大佬的帖子,他们讲的非常的好

推荐使用 spy-debugger 支持HTTP/HTTPS,无需USB连接设备

3.Webpack 和 babel 简单使用

为啥使用Webpack

  • ES6模块化,浏览器暂不支持

  • ES6语法,浏览器并不完全支持

  • 压缩代码,整合代码,以让网页加载更快

安装配置Webpack

安装 npm i -D webpack webpack-cli

安装本地项目模块 npm install webpack webpack-cli -D

配置webpack.config.js

const path = require('path')

module.exports = {
  mode: 'development',//   生产环境production
  entry: path.join(__dirname, '/src','index.js'), // 入口文件
  // 输出文件地址 和 名称 配置
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'bundle.js'
  },
}

配置package.jsonscript 之后就可以使用npm run build来运行该脚本命令

{
    "scripts" : {
		"build": "webpack"
    }
}

配置本地服务器

安装本地服务器 npm install webpack-dev-server -D

配置webpack.config.js

const path = require('path')

module.exports = {
  mode: 'development',//   生产环境production
  entry: path.join(__dirname, '/src','index.js'), // 入口文件
  // 输出文件地址 和 名称 配置
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    hot: true,
    port: '8080',
    inline: true,
    open: true,
    overlay: true,
    compress: true
  }
}
  • inline:文件改变时会自动刷新
  • open:第一次自动打开网页。
  • compress:启用 gzip 压缩,boolean 类型,默认为 false
  • overlay:页面上报错信息是否显示,默认为false
  • port: 端口

插件

配置package.jsonscript 终端输入npm run dev 运行服务器

{
    "scripts" : {
		"build": "webpack"
                "dev": "webpack-dev-server"
     }
}

添加个html的压缩 安装 npm install html-webpack-plugin -D

const path = require('path')
//导入 html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'development',//   生产环境production
  entry: path.join(__dirname, '/src','index.js'), // 入口文件
  // 输出文件地址 和 名称 配置
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'bundle.js'
  },
  devServer: {
    contentBase: path.join(__dirname, "dist"),
    hot: true,
    port: '8080',
    inline: true,
    open: true,
    overlay: true,
    compress: true
  },
  //配置插件
  plugin: {
      new HtmlWebpackPlugin({
      	//模板地址
      	template: path.join(__dirname, "src","index.html"),
        //输出名
        filename: 'index.html'
  	  })
  }
}

这样就能打包html了

加载器

loaders 配置css-loader

安装 npm install style-loader css-loader -D

再去 webpack.config.js 设置 这样就能打包css

module.exports = {
    module: {
    rules: [
      {
        test: /\.css$/,   // 正则匹配css
        use: ['style-loader', 'css-loader']  // 需要用的loader,从左到右的编译
      }
    ]
  }
}

babel

安装:npm install @babel/core @babel/preset -env babel -loader -D

项目的目录添加一个.babeirc文件

{
    "presets": ["@babel/preset-env"]
}

再去 webpack.config.js 设置

module.exports = {
    module: {
        rules: [
          {
            test: /\.js$/,   // 正则匹配js
            use: ['babel-loader'],
            include: path.join(__dirname,'src'),
            //除了node的模块
            exclude: /node_modules/
          }
        ]
   }
}

配置生产环境

项目根目录创建一个 webpack.prod.js

把开发环境的内容复制过来改改

更改modeproduction

输出文件加上bundle.[contenthash].js

const path = require('path')
//导入 html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'production',//   生产环境
  entry: path.join(__dirname, '/src','index.js'), // 入口文件
  // 输出文件地址 和 名称 配置
  output: {
    path: path.join(__dirname, '/dist'),
    filename: 'bundle.[contenthash].js'
  },
  //配置插件
  plugin: {
      new HtmlWebpackPlugin({
      	//模板地址
      	template: path.join(__dirname, "src","index.html"),
        //输出名
        filename: 'index.html'
  	  })
  }
}

更改package.jsonscript 中的build 为刚才新建的生产环境 webpack.prod.js

使用npm run build运行打包

{
    "scripts" : {
		"build": "webpack"
        "dev": "webpack-dev-server"
        "build": "webpack --config webpack.prod.js"
	}
}

4.ES6模块化规范

ES6的模块化之前有三种规范:CommonJS、AMD、CMD 这几个其他大佬都有详细介绍

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

我这里就是简单的简述下ES6中的模块化规范

ES6中的模块自动采用严格模式

ES6模块中的顶层this指向undefined

export

exprot一定要写在最顶层 导出的值是动态绑定的,不是缓存

下面演示的导出皆在 index.js

分别导出:

export let a = 'a';
export const b = 'b'
export const c = ()=>{ console.log(1) }

一次全部导出

let a = 'a';
const b = 'b'
const c = ()=>{ console.log(1) }
export { a, b, c }

在导出的时候通过 as 取别名

let a = 'a';
const b = 'b'
const c = ()=>{ console.log(1) }
export { a as lang1, b as lang2, c as lang3 }

import

通过结构赋值 获取 导出的元素

import { a, b, c} from './index.js'
console.log(a)

当然导入的时候也可以取别名

import { a as lang } from './index.js'
console.log(lang)

导入所有的元素

import * as lang from './index.js'
console.log(lang.a)

export default

可以指定模块默认输出

//index.js
export default function(){
  console.log('123')
}

import导入的元素可以为任意名称

import lang from './index.js'

总结

  • 输出单个值,使用export default
  • 输出多个值,使用export
  • export default与普通的export不要同时使用

5.Linux命令

这里我就摆几个常用的,因为这个是我的总结,自己翻出来看看,所以更全Linux请到其他大佬那里查看

cp 拷贝文件 :cp -rvf a/ /tmp/

mkdir 创建目录:mkdir dir

ls 当前目录的所有内容。

ls -l目录下详细信息

pwd 当前所在的目录

cd 打开目录

find 查找文件

cat 查看文件内容

grep对内容进行过滤

diff命令用来比较两个文件是否的差异

.tar gz 使用tar ,gzip命令操作

ps命令能够看到进程/线程状态

ifconfig查看ip地址

ping测试网络

关于运行环境

运行环境即浏览器( server端有node.js )

下载网页代码,渲染出页面,期间会执行若干JS

要保证代码在浏览器中: 稳定高效

1.页面加载的过程

1.从输入url到渲染出页面的整个过程

大致的流程

  1. DNS解析:域名->IP地址
  2. 浏览器根据IP地址向服务器发起http请求
  3. 服务器处理http请求,并返回给浏览器
  4. 浏览器解析渲染页面
  5. 连接结束

​ 渲染过程

  • 根据HTML代码生成DOM Tree
  • 根据CSS代码生成CSSOM
  • DOM TreeCSSOM整合行程Render Tree
  • 根据Render Tree渲染页面
  • 遇到<script>则暂停渲染,优先加载并执行JS代码,完成再继续
  • 直至把Render Tree渲染完成

2.window.onload和DOMContentLoaded的区别

  • window.onload资源全部加载完才能执行,包括图片
  • DOMContentLoaded DOM渲染完成即可,图片可能尚未下载

2.性能优化

是一个综合性问题,没有标准答案,但要求尽量全面

某些细节问题可能会单独提问:手写防抖、节流

只关注核心点,针对面试

性能优化原则

  • 多使用内存、缓存或其他方法
  • 减少CPU计算量,减少网络加载耗时
  • (适用于所有编程的性能优化一空间换时间 )

让加载更快

  • 减少资源体积:压缩代码
  • 减少访问次数:合并代码(多个js文件合并到一个文件然后加载), SSR服务器端渲染,缓存
  • 使用更快的网络: CDN
  • CSS放在headJS放在body最下面
  • 尽早开始执行JS,用DOMContentLoaded触发
  • 懒加载(图片懒加载,上滑加载更多)

让渲染更快

  • 对DOM查询进行缓存
  • 减少频繁DOM操作,合并到一起插入DOM结构
  • 节流throttle防抖debounce

缓存

  • 静态资源加hash后缀,根据文件内容计算hash
  • 文件内容不变,则hash不变,则url不变
  • url和文件不变,则会自动触发http缓存机制,返回304

SSR

  • 服务器端渲染:将网页和数据一起加载,一起渲染
  • SSR (前后端分离) :先加载网页,再加载数据,再渲染数据
  • 早先的JSP ASP PHP,现在的vue React SSR

防抖

监听一个输入框的,文字变化后触发change事件

直接用keyup事件,则会频发触发change事件

防抖:用户输入结束或暂停时,才会触发change事件

//封装一个防抖的函数
function debounce(fn, delay = 500){
    let timer = null
    //如果还存在一个定时器没有执行,就清空定时器,在下面重新开启定时器
    if(timer){
        cleartTimout(timer)
    }
    timer = seTimeout(()=>{
        fn.apply(this,arguments)
        timer = null
    },delay)
}
//调用
const input = document.getElementById('input')
input.addEventListener('keyup',debounce(()=>{
    console.log(input.value)
},600),false)

节流

拖拽一个元素时,要随时拿到该元素被拖拽的位置

直接用drag事件,则会频发触发,很容易导致卡顿

节流:无论拖拽速度多快,都会每隔100ms触发一次

//封装一个防抖的函数
function throttle(fn, delay = 500){
    let timer = null
    //如果还存在一个定时器没有执行就阻止向下执行,减少操作
    if(timer){
        return
    }
    timer = seTimeout(()=>{
        fn.apply(this,arguments)
        timer = null
    },delay)
}
//调用
const img = document.getElementById('img')
img.addEventListener('drag',throttle( function(e){
    console.log(e.offsetX,e.offsetY)
},600),false)

3.安全

XSRF攻击 这里简单的 介绍下

  • 你正在购物,看中了某个商品,商品id是100
  • 付费接口是xxx.com/pay?id=100 , 但没有任何验证
  • 我是攻击者,我看中了一一个商品,id是200
  • 我向你发送一封电子邮件 ,邮件标题很吸引人
  • 但邮件正文隐藏着<img src=xxx.com/pay?id=200 />
  • 你一查看邮件,就帮我购买了id是200的商品

XSRF预防

  • 使用post接口
  • 增加验证,例如密码、短信验证码、指纹等

关于一些真题

+ 号

  • 两个操作数如果是number 则直接相加出结果
  • 如果其中有一个操作数为string, 则将另一个操作数隐式的转换为string,然后进行字符串拼接得出结果
  • 如果操作数为对象或者是数组这种复杂的数据类型,那么就将两个操作数都转换为字符串,进行拼接
  • 如果操作数是像boolean这种的简单数据类型,那么就将操作数转换为number相加得出结果
  • []+{} 因为会被强制转换为",然后+运算符链接- 个{}, { }强制转换为字符串就是"[Object Object]"
  • {}当作一个空代码块,+0是强制将[]转换为number,转换的过程是+[0] => +"" =>0最终的结果就是0

var和let const的区别

  • varES5语法,let constES6语法;var有变量提升
  • varlet是变量,可修改;const是常量,不可修改;
  • let const有块级作用域,var没有

typeof返回哪些类型

一共是7种 值得注意的一点就是 typeof null //object 前面JS基础有提到

  • string
  • number
  • boolean
  • object
  • function
  • undefined
  • symbol(ES6)

列举强制类型转换和隐式类型转换

  • 强制: parseInt parseFloat toString
  • 隐式:if逻辑运算==+拼接字符串

手写深度比较,模拟lodash isEqual

这里没考虑function

// 判断对象是否是对象还是数组
function isObject(obj) {
  return typeof obj === 'object' && obj !== null
}

// 判断全相等
function isEqual(obj1, obj2) {
  // 判断不是对象数组的直接比较值
  if (!isObject(obj1) || !isObject(obj2)) {
    return obj1 === obj2
  }
  // 防止传入的 两个是同一对象 直接返回
  if (obj1 === obj2) {
    return true
  }
  // 是对象和数组(不考虑function)
  // 分别取出keys
  const obj1Keys = Object.keys(obj1)
  const obj2Keys = Object.keys(obj2)
  // 先判断keys的个数相等不
  if (obj1Keys.length !== obj2Keys.length) {
    return false
  }
  // 以obj1为基准 递归判断 obj2
  for (let key in obj1) {
    const result = isEqual(obj1[key], obj2[key])
    if (!result) return false
  }
  // 遍历 递归后到了这里就是全相等
  return true
}

//使用
// 传入普通
console.log(isEqual('123', '123')) //true
// 传入一样
console.log(isEqual(obj1, obj1)) //true
// 比较两个一样的
const obj1 = {
  x: 1,
  y: 2,
  s: {
    x: 1,
    y: 2,
  },
}

const obj2 = {
  x: 1,
  y: 2,
  s: {
    x: 1,
    y: 2,
  },
}
console.log(isEqual(obj1, obj2)) //true

split()和join()的区别

他们是一个相反的操做

[1,2,3].join('-') //'1-2-3'
'1-2-3'.split('-') //[1,2,3]

数组的pop push unshift shift分别做什么

一般从这三个角度回答

功能是什么?

返回值是什么?

是否会对原数组造成影响?

const arr = [10, 20, 30, 40]

// pop
const popRes = arr.pop()
console.log(popRes, arr) //40 [10, 20, 30] 返回的是删除的最后一个元素,会改变原数组

// push
const pushRes = arr.push(50)  //在数组最后追加 50
console.log(pushRes, arr) //4 [10, 20, 30 ,50] 返回 length 会改变原数组

// unshift
const unshiftRes = arr.unshift(5) //在数组最前追加 5 
console.log(unshiftRes, arr) //5 [5, 10, 20, 30 ,50]  返回 length 会改变原数组

// shift
const shiftRes = arr.shift() //删除第一个元素 
console.log(shiftRes, arr) //5 [10, 20, 30, 50] 返回的是删除的第一个元素,会改变原数组

扩展

纯函数

  1. 不改变源数组( 没有副作用)
  2. 返回一个数组
const arr = [10, 20, 30, 40]

// concat
const arr1 = arr.concat([50, 60, 70])
console.log(arr, arr1) //[10, 20, 30, 40]  [10, 20, 30, 40, 50, 60, 70]

// map
const arr2 = arr.map(num => num * 10)
console.log(arr, arr2) //[10, 20, 30, 40]  [100, 200, 300, 400]

// filter
const arr3 = arr.filter(num => num > 25)
console.log(arr, arr3) //[10, 20, 30, 40]  [30, 40]

// slice 没有参数 在这里是复制了一份数组
const arr4 = arr.slice()
console.log(arr, arr4) //[10, 20, 30, 40] [10, 20, 30, 40]

非纯函数

push pop shift unshift

forEach

some every

reduce

数组slice和splice的区别

  • 功能区别( slice-切片, splice-剪接)
  • 参数和返回值
  • 是否纯函数?
const arr = [10, 20, 30, 40, 50]

// slice 纯函数 原数组不变
const arr1 = arr.slice() //复制 [10, 20, 30, 40, 50]
const arr2 = arr.slice(1, 4) //按照范围截取 取得到左边取不到右边 [20, 30, 40]
const arr3 = arr.slice(2) //截取索引之后的 [30, 40, 50]
const arr4 = arr.slice(-3) //反向截取 [30, 40, 50]

// splice 非纯函数 会改变原数组
const spliceRes = arr.splice(1, 2, 'a', 'b', 'c')
//这里简单举个例子,重点是它改变了原数组
//从索引1开始截取,截取2个元素,并使用 'a', 'b', 'c'替换了之前的截取的空位
console.log(spliceRes, arr) //[20, 30]  [10, 'a', 'b', 'c', 40, 50]

[10, 20, 30].map(parseInt)返回结果是什么?

  • map的参数和返回值
  • parseInt参数和返回值
const res = [10,20,30].map(parseInt)
console.log(res) //[10, NaN, NaN]

//拆解
[10,20,30].map((item,index)=>{
    return parseInt(item,index)
})
//parseInt 第二个参数可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。

ajax请求get和post的区别?

  • get一般用于查询操作, post一般用户提交操作
  • get参数拼接在url上, post放在请求体内(数据体积可更大)
  • 安全性: post易于防止CSRF

函数bind,call和apply的区别

bind创建一个新的函数, 当被调用时 ,第一参数this值,在调用新函数时,接受的是若干个参数列表

call方法调用一个函数, 第一参数this值,接受的是若干个参数列表

apply方法调用一个函数, 第一参数this值,第二个参数是一个数组(或类似数组的对象)

function fn(){
    console.log(this)
}
const newFn = fn.bind(this,p1,p2,p3)
newFn()

fn.call(this,p1,p2,p3)

fn.apply(this,arguments)

事件代理(委托)是什么?

事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。

在上面JS基础24有写

闭包是什么,有什么特性?有什么负面影响?

  • 能够访问其他函数内部变量的函数,被称为 闭包
  • 回顾作用域和自由变量
  • 回顾闭包应用场景:作为参数被传入,作为返回值被返回
  • 回顾:自由变量的查找,要在函数定义的地方(而非执行的地方)
  • 影响:变量会常驻内存,得不到释放。闭包不要乱用

如何阻止事件冒泡和默认行为?

  • event.stopPropagation()
  • event.preventDefault()

查找、添加、删除、移动DOM节点的方法?

在上面 JS基础 20.DOM节点 21.DOM节点操作 有介绍

如何减少DOM操作?

  • 缓存DOM查询结果
  • 多次DOM操作,合并到一-次插入

解释jsonp的原理,为何它不是真正的ajax ?

  • jsonp不是官方的,script标签实现的请求。
  • jsonp是一个同步请求,只支持get请求
  • ajax通过xhr对象去实现,支持get post ,跨域请求需要后端配合解决跨域响应头

document load和ready的区别

  • ready,表示文档结构已经加载完成(不包含图片等非文字媒体文件)
  • onload,指示页面包含图片等文件在内的所有元素都加载完成

=====的不同

  • 双等:判断值相等,会自动转换数据类型
  • 三等:不会自动转换数据类型;全等是先判断左右两边的数据类型,如果数据类型不一致,则直接返回false,之后才会进行两边值的判断

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

  • 函数声明function fn() {...}
  • 函数表达式const fn = function() {..}
  • 函数声明会在代码执行前预加载,而函数表达式不会

new Object(和Object.create()的区别

  • {}等同于new Object(), 原型Object.prototype
  • Object.create(nul)没有原型
  • Object.create(..)可指定原型

关于this的场景题

这是一个简单的例子 this 是在执行的时候确定的

const User = {
    count : 1,
    getCount: function() {
        return this.count
    }
}
console.log(User.getCount()) //what?
const func = User.getCount
console.log( func() ) //what?

第一个打印的是 1

第二个打印的是 undefined 因为这个时候 this 指向是window

关于作用域和自由变量的场景题

let i
for(i = 1; i <= 3; i++) {
    setTimeout(function (){
       console.log(i) 
    },0)
}
//what?

这里打印的是 4 4 4

判断字符串以字母开头,后面字母数字下划线,长度6-30

这里是考察正则

var reg=/^[a-zA-Z]\w{5,29}$/

str.test(reg)

手写字符串trim方法,保证浏览器兼容性

String.prototype.trim = function(){
    return this.replace(/^\s+/,'').replace(/\s+$/,'')
}

原型, this, 正则表达式 把空白变为空字符串

如何获取多个数字中的最大值

自己封装max

function max() {
    const nums = Array.prototype.slice.call(arguments)//把参数变为数组
    let max = 0
    nums.forEach(item => {
        if(item > max) {
            max = item
        }
    })
    return max
}
//使用
console.log(max(1,2,3,4,5)) //5

使用Math.max

console.log(Math.max(1,2,3,4,5)) //5

如何用JS实现继承?

原型链继承

image-20210710155504269

function People() {
  this.name = 'lang'
  this.age = 21
}

People.prototype.eat = function () {
  console.log(this.name + '吃饭')
}

function Student() {}
//关键
Student.prototype = new People()

let lang = new Student()
lang.eat() //lang 吃饭

缺点:

多个实例对引用类型的操作会被篡改

借用构造函数继承

使用call直接 把子类的this 传到父类

function People(name, age) {
  this.name = name
  this.age = age
  this.dd = function () {
    console.log(this.name + '打车')
  }
}

People.prototype.eat = function () {
  console.log(this.name + '吃饭')
}

function Student(name, age) {
  // 直接偷
  People.call(this, name, age)
}

let lang = new Student('lang', 21)
console.log(lang.name,lang.age) //lang 21
lang.eat() //报错
lang.dd() //报错

优点

  • 可以传递参数
  • name,age的这些属性是添加在 实例自身上的不是共享

缺点

  • 使用不了 父类引用的变量 比如上面的dd
  • 使用不了prototype里面的 变量比如eat

组合继承

把 原型链继承 + 借用构造函数继承 组合起来

function People(name, age) {
  this.name = name
  this.age = age
  this.dd = function () {
    console.log(this.name + '打车')
  }
}

People.prototype.eat = function () {
  console.log(this.name + '吃饭')
}

function Student(name, age, sex) {
  // 直接偷构造
  People.call(this, name, age)
  // 顺便给自己添加个属性
  this.sex = sex
}
// 原型链继承
Student.prototype = new People()
// 添加子类的方法
Student.prototype.see = function () {
  console.log(this.name + '能看见千里之外的景物')
}

let lang = new Student('lang', 21, '男')
lang.eat() //lang吃饭
lang.dd() //lang打车
console.log(lang.sex, lang.name, lang.age) //男 lang 21
lang.dd = function () { //修改的是自己实例的方法,并不能影响到其他实例
  console.log('更改过了')
}
lang.dd() //更改过了
let jie = new Student('jie', 21, '男')
jie.dd() //jie打车
jie.see() //jie能看见千里之外的景物

优点

  • 可以传递参数
  • 父类引用的属性不被共享
  • 可以使用父类 prototype的属性

缺点

image-20210710161425100.png

  • 第一次的继承原型链的时候添加了 nameage 属性
  • 第二次new 子类的实例的时候又添加 nameage 属性
  • 根据原型链的查找规则 第二次会把第一次的覆盖

原型式继承

这个是真的用的少 ES5中的 Object.create()可以实现下面的 object函数

// 处理站 
function object(obj) {
  //一个空的构造
  function Fun() {}
  //原型指向传入的对象
  Fun.prototype = obj
  //返回实例
  return new Fun()
}

let people = {
  name: 'lang',
  age: 18,
  hobbys: ['book', 'music', 'learn'],
  eat: function () {
    console.log(this.name + '吃饭')
  },
}

let lang = object(people)
lang.name = 'lang'
lang.hobbys.push('sing')
lang.eat()

let jie = object(people)
jie.name = 'jie'
jie.hobbys.push('play')
jie.eat()
//这里就出了问题,因为是浅复制所有 引用属性被共享了
console.log(lang.hobbys) // ["book", "music", "learn", "sing", "play"]

缺点

  • 不能传参
  • 因为是浅复制,引用变量被共享了

寄生式继承

创建一个函数,在内部做了一些加强浅复制就是 新增属性和方法,以增强函数,最后返回构造

这里直接用 Object.create() 来实现了

function createObj (obj) {
    var clone = Object.create(obj);
    //增强操作
    clone.see = function () {
        console.log('see');
    }
    clone.haha = 'haha'
    return clone;
}

let people = {
  name: 'lang',
  age: 18,
  hobbys: ['book', 'music', 'learn'],
  eat: function () {
    console.log(this.name + '吃饭')
  },
}

//使用
let lang = createObj(people)

缺点(同原型式继承)

  • 不能传参
  • 因为是浅复制,引用变量被共享了

寄生组合继承

之前组合继承 调用了两次 构造参数

Student.prototype = new People();

let lang = new Student('lang', 21, '男')

  • 第一次的继承原型链的时候Student.prototype 添加了 nameage 属性
  • 第二次new 子类的实例的时候又添加 nameage 属性

避免重复添加, 我们让 Student.prototype 访问 People.prototype

// 寄生式组合继承
function inheritPrototype(child, parent) {
  // 新建一个原型
  let prototype = Object.create(parent.prototype)
  // 弥补缺陷添加 constructor 指向 child
  prototype.constructor = child
  // 子类的原形 指向 新建的原型
  child.prototype = prototype
}

function People(name, age) {
  this.name = name
  this.age = age
  this.dd = function () {
    console.log(this.name + '打车')
  }
}

People.prototype.eat = function () {
  console.log(this.name + '吃饭')
}

function Student(name, age, sex) {
  // 直接偷构造
  People.call(this, name, age)
  // 顺便给自己添加个属性
  this.sex = sex
}
// 寄生式组合继承
inheritPrototype(Student, People)

// 添加子类的方法
Student.prototype.see = function () {
  console.log(this.name + '能看见千里之外的景物')
}

let lang = new Student('lang', 21, '男')
lang.eat() //lang吃饭
lang.dd() //lang打车
console.log(lang.sex, lang.name, lang.age) //男 lang 21
lang.dd = function () {
  console.log('更改过了')
}
lang.dd() //更改过了
let jie = new Student('jie', 21, '男')
jie.dd() //jie打车
jie.see() //jie能看见千里之外的景物

优点

  • 父类方法可以复用
  • 可以传递参数到父类
  • 父类的引用属性不会被共享
  • 只调用一次父类构造函数

extends 继承

ES6提供了calss extends super来实现继承

// ES6继承

class People {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  dd() {
    console.log(this.name + '打车')
  }
}

People.prototype.eat = function () {
  console.log(this.name + '吃饭')
}

class Student extends People {
  constructor(name, age, sex) {
    super(name, age)
    this.sex = sex
  }
}

// 添加子类的方法
Student.prototype.see = function () {
  console.log(this.name + '能看见千里之外的景物')
}

let lang = new Student('lang', 21, '男')
lang.eat() //lang吃饭
lang.dd() //lang打车
console.log(lang.sex, lang.name, lang.age) //男 lang 21
lang.dd = function () {
  console.log('更改过了')
}
lang.dd() //更改过了
let jie = new Student('jie', 21, '男')
jie.dd() //jie打车
jie.see() //jie能看见千里之外的景物

如何捕获JS程序中的异常?

通过try...catch...捕获

try {
  	//to do
}
catch (ex) {
  console.log("error", ex.message); // error oops
}finally{
    //to do
}

window.onerror 自动捕获

第一,对跨域的js,如CDN的,不会有详细的报错信息 第二,对于压缩的js,还要配合sourceMap 反查到未压缩代码的行、列

window.onerror = function(message, source, linenom, colnom, error) { ... }
  • message:错误信息(字符串)
  • source:发生错误的脚本URL(字符串)
  • linenom:发生错误的行号(数字)
  • colnom:发生错误的列号(数字)
  • error:Error对象(对象)

window.addEventListener('error')

window.addEventListener('error', function(event) { ... })

什么是JSON ?

  • json是一种数据格式,本质是一-段字符串
  • json格式和JS对象结构一致,对JS语言更友好

获取当前页面url参数

传统方式,查找location.search

API URL SearchParams

传统方式

// 传统方式
function query(name) {
  const search = location.search.substr(1) // 类似 array.slice(1)
  const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
  const res = search.match(reg)
  if (res === null) {
    return null
  }
  return res[2]
}
query('d')

URL SearchParams

function query(name) {
  const search = location.search
  const p = new URLSearchParams(search)
  return p.get(name)
}
console.log(query('b'))

url参数解析为JS对象

传统方式, 分析 srarch

function queyrToObj() {
    const res = {}
    const search = location.search.substr(1)//截取?之后的参数
    search.split('&').forEach(paramStr => {
        const arr = paramStr.split('=')
        const key = arr[0]
        const val = arr[1]
        res[key] = val
    })
    return res
}

使用 URLSearchParams

function queryToObj() {
    const res = {}
    const pList = new URLSearchParams(location.search)
    pList.forEach((val,key) => {
        res[key] = val
    })
    return res
}

扁平化数组和手写数组flatern考虑多层级

比如 有这么一个数组 [1, 2, [3, 4, [10, 20, [100, 200]]], 5]

输出:[1, 2, 3, 4, 10, 20, 100, 200, 5]

使用 concat

let arr = [1, 2, [3, 4, [10, 20, [100, 200]]], 5]
while(arr.some( item => Array.isArray(item))){
    arr = [].concat(...arr)
}
console.log(arr)

使用stringify 把数组变成字符串 然后正则去除 所有[ ],根据,分割为数组,然后把每一项转为number

let arr = [1, 2, [3, 4, [10, 20, [100, 200]]], 5]
let res = JSON.stringify(arr).replace('/\[|\]/','').split(',').map(item => Number(item))
console.log(res)

使用toString 和 上面差不多

let arr = [1, 2, [3, 4, [10, 20, [100, 200]]], 5]
let res = arr.toString().split(',').map(item => Number(item))
console.log(res)

使用ES6 中flat就行

let arr = [1, 2, [3, 4, [10, 20, [100, 200]]], 5]
console.log(arr.flat(Infinity))

自己手写flatern

Array.prototype.myFlat = function() {
    function flat(){
        // 验证 arr 中,还有没有深层数组 [1, 2, [3, 4]]
        const isDeep = arr.some(item => item instanceof Array)
        if (!isDeep) {
            return arr // 已经是 flatern [1, 2, 3, 4]
        }

        const res = Array.prototype.concat.apply([], arr)
        return flat(res) // 递归
    }
}

const res = [1, 2, [3, 4, [10, 20, [100, 200]]], 5].myFlat()
console.log(res)

数组去重

  • 传统方式,遍历元素挨个比较、去重
  • 使用Set
  • 考虑计算效率

传统方式

function unique(arr) {
    const res = []
    arr.forEach(item => {
        if (res.indexOf(item) < 0) {
            res.push(item)
        }
    })
    return res
}

使用Set

function unique(arr) {
    const set = new Set(arr)
    return [...set]
}

手写深拷贝

// 封装深拷贝
function deepCopy(newObj, obj) {
    for (const key in obj) {
        //把对象或者数组的每一项都获取出来
        var item = obj[key];
        // 判断该项是否为对象
        if (item instanceof Object) {
            newObj[key] = {};
            deepCopy(newObj[key], item);
            //判断该项是否为数组
        } else if (item instanceof Array) {
            newObj[key] = [];
            deepCopy(newObj[key], item);
            // 这就是普通的类型的对象
        } else {
            newObj[key] = item;
        }
    }
    return newObj
}

实现一个不可改变的对象

  • Object.isExtensible()方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。

  • Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性,并且返回原对象。

  • Object.isSealed()方法判断一个对象是否是密封的。

  • Object.seal()方法可以让一个对象密封,并返回被密封后的对象。

  • Object.isFrozen()方法判断一个对象是否被冻结。

  • Object.freeze()方法可以冻结一个对象。

1.不可扩展: 不可以增加新的属性,老的属性可以删除,也可以改值 2.密封: 不可以增加新的属性,老的属性不可以删除,但可以改值 3.冻结:不可以增加新的属性,老的属性不可以删除,不可以改值

介绍一下RAF requestAnimateFrame

  • 要想动画流畅,更新频率要60帧/s , 即16.67ms更新- -次视图
  • setTimeout要手动控制频率,而RAF浏览器会自动控制
  • 后台标签或隐藏iframe中, RAF会暂停,而setTimeout依然执行
  • 这里使用了jQuery
// 3s 把宽度从 100px 变为 640px ,即增加 540px
// 60帧/s ,3s 180 帧 ,每次变化 3px

const $div1 = $('#div1')
let curWidth = 100
const maxWidth = 640

// setTimeout
function animate() {
    curWidth = curWidth + 3
    $div1.css('width', curWidth)
    if (curWidth < maxWidth) {
        setTimeout(animate, 16.7) // 自己控制时间
    }
}
animate()

// RAF
function animate() {
    curWidth = curWidth + 3
    $div1.css('width', curWidth)
    if (curWidth < maxWidth) {
        window.requestAnimationFrame(animate) // 时间不用自己控制
    }
}
animate()

前端性能如何优化? 一般从哪几个方面考虑?

  • 原则:多使用内存、缓存,减少计算、减少网路请求
  • 方向:加载页面,页面渲染,页面操作流畅度