当前位置:   article > 正文

前端面经真题解析2-字节/抖音/前端(四万字长文)_抖音前端面试题

抖音前端面试题

文章目录

一面


1. 个人对前端的理解,需要掌握的技能

前端开发是指构建和实现网页和Web应用程序用户界面的过程。作为前端开发者,你需要掌握以下技能:

1.HTML(超文本标记语言):

HTML是用于创建网页结构的标记语言。你需要熟悉HTML的各种标签、元素和属性,以及语义化的使用方法。

2.CSS(层叠样式表)

CSS用于为网页添加样式和布局。你需要学会使用CSS选择器、盒模型、浮动、定位等技术来设计和美化网页。

3.JavaScript:

JavaScript是一种用于实现网页交互和动态效果的脚本语言。你需要熟悉JavaScript的语法、DOM操作、事件处理、Ajax等技术,以及一些流行的JavaScript库和框架(如jQuery、React、Vue.js)。

4.响应式设计:

现代网页需要适应不同尺寸的设备(如手机、平板电脑、桌面电脑),因此你需要了解响应式设计的原理和技术,如媒体查询、弹性布局、网格系统等。

5.浏览器开发工具:

现代浏览器提供了强大的开发工具,如开发者控制台、调试器、性能分析器等,你需要学会使用这些工具来调试和优化你的前端代码。

6.版本控制系统:

使用版本控制系统(如Git)可以帮助你管理和追踪前端代码的变更。你需要学会使用基本的Git命令,如提交、分支、合并等。

7.前端构建工具:

前端构建工具(如Webpack、Parcel)可以帮助你自动化任务,如代码打包、文件压缩、代码转译等。你需要了解这些工具的基本原理和配置方法。

8.跨浏览器兼容性:

不同的浏览器对于前端技术的支持程度有所差异,你需要了解常见的兼容性问题,并学会使用一些技术手段(如CSS前缀、垫片库)来处理兼容性问题。

9.性能优化:

前端性能对于用户体验和网页加载速度非常重要,你需要学会优化前端代码,如减少HTTP请求、压缩文件、使用缓存等技术。

10.跨平台开发:

除了Web前端开发,你还可以了解一些跨平台开发技术,如Electron、React Native、Flutter,以便将你的前端技能应用到桌面应用和移动应用的开发中。

以上是前端开发中的一些核心技能,当然前端技术是不断演进的。


2. 前端路由实现

前端路由是指在单页应用(SPA)中实现页面之间的切换和导航的技术。在传统的多页应用中,每次点击链接或者提交表单都会导致整个页面的刷新,而在单页应用中,页面的切换是在前端进行处理,只刷新部分内容,从而提供更流畅的用户体验。

几种常见的前端路由实现方式:

  • 基于哈希(Hash)的路由:在URL的片段标识符(#)后面添加路由路径,例如:https://example.com/#/home。当URL中哈希部分发生变化时,JavaScript代码会捕获该变化,并相应地更新页面内容。

  • HTML5 历史 API 路由:HTML5引入了history对象,通过使用pushState和replaceState方法可以更新浏览器的历史记录,并且不触发页面刷新。你可以监听popstate事件来捕获URL的变化,然后相应地更新页面。

  • 第三方路由库:有许多流行的第三方路由库可用于前端开发,如React Router、Vue Router等。这些库提供了更高级的路由功能,包括路由配置、嵌套路由、路由参数传递等。你可以根据你使用的框架或库选择相应的路由库,并按照其文档进行配置和使用。

在实现前端路由时,你通常需要做以下几个步骤:

  1. 定义路由规则:

确定你的应用有哪些页面和对应的路由路径。你可以将路由规则定义在一个集中的地方,例如路由配置文件或者路由器组件中。

  1. 监听路由变化:

根据选择的路由实现方式,你需要监听URL的变化,以便在路由发生改变时更新页面内容。你可以使用浏览器提供的API(如hashchange事件或popstate事件)或者路由库提供的方法进行监听。

  1. 更新页面内容:

当路由发生改变时,根据路由规则,你需要更新相应的页面内容。这可以通过更新DOM元素、渲染不同的组件或触发相应的操作来实现。

  1. 导航链接处理:

在页面中提供导航链接(如菜单、按钮),当用户点击这些链接时,你需要阻止默认的页面刷新行为,而是使用前端路由进行页面切换。你可以监听链接的点击事件,并使用路由相关的方法(如pushState、replaceState或路由库提供的方法)进行页面切换。

在react中使用路由

一般在react中,会通过路由库来实现,常用的有browserrouter 和 hashrouter。

1. 不论是哪一种,首先都要安装react-router-dom:

npm install react-router-dom
  • 1

2. 使用路由组件BrowserRouter:

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Switch>
    </Router>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上述示例中,BrowserRouter组件用于包裹整个应用,Route组件用于定义不同的路由路径和对应的组件。Switch组件用于确保只有一个路由匹配,避免多个组件同时渲染。

导航链接:

import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
        <li>
          <Link to="/contact">Contact</Link>
        </li>
      </ul>
    </nav>
  );
}

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

在应用中提供导航链接,你可以使用React Router提供的Link组件来创建链接。Link组件会阻止默认的页面刷新行为,并使用前端路由进行页面切换。

在上述示例中,Link组件的to属性指定了要导航到的路径。

3. 使用路由组件HashRouter

import { HashRouter as Router, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </Switch>
    </Router>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

其他部分与BrowerRouter类似。

react-router-dom v5 和 v6 的区别

React Router DOM 6 是 React Router 库的最新版本,相对于 React Router DOM 5 有一些变化和改进:

1.命名参数支持:

React Router DOM 6 引入了对命名参数的支持。你可以在路由路径中使用 : 前缀指定参数名称,如 /users/:id。然后可以在组件中通过 useParams 钩子获取参数值。而在 React Router DOM 5 中,只能使用 :id 的方式定义参数,然后通过 props.match.params 获取参数值。

2.路由器组件名称更改:

在 React Router DOM 5 中,路由器组件的名称为 BrowserRouter 和 HashRouter。但在 React Router DOM 6 中,它们的名称变为了 Router 和 MemoryRouter,并且默认情况下使用 history 包提供的浏览器历史记录。如果你仍然想使用 BrowserRouter 或 HashRouter,可以在导入时使用别名,如 import { BrowserRouter as Router } from ‘react-router-dom’;。

3.路由钩子 API 的更改:

React Router DOM 6 改变了 useHistory、useLocation 和 useParams 钩子的返回值。在 React Router DOM 5 中,这些钩子返回的是一个包含 history、location 或 match 对象的 props,而在 React Router DOM 6 中,这些钩子返回的是原始的 history、location 或 match 对象。

4.新的 组件:

在 React Router DOM 6 中,你可以使用新的 组件来定义多个嵌套的路由。与 组件不同的是, 组件允许你定义更复杂的路由配置,如嵌套路由和命名参数。例如:


function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users" element={<Users />}>
          <Route path=":id" element={<User />} />
        </Route>
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在上述示例中, 组件定义了三个路由,其中 /users 是一个父级路由,/:id 是一个命名参数。

总之,React Router DOM 6 相对于 5 做了一些变化和改进,如支持命名参数、更改路由器组件名称、更改路由钩子 API 等,同时也引入了新的 组件来定义更复杂的路由配置。如果你正在使用 React Router DOM 5,需要留意这些变化。


3. 前端埋点、监控实现

前端埋点是指在前端代码中插入跟踪用户行为的代码,以便收集用户的行为数据。通过分析这些数据,可以了解用户的行为习惯、偏好和需求,进而优化产品体验和提高用户满意度。

如在 React 中实现前端埋点可以按照以下步骤进行:

1.确定需要监控的事件:

首先需要明确要收集哪些数据,例如页面加载时间、用户点击行为、错误信息等。

2.选择前端监控工具:

根据监控需求选择合适的前端监控工具,例如 Google Analytics、Mixpanel、Kissmetrics 等。

3.在组件中编写埋点代码:

在需要监控的组件中编写埋点代码。例如,在一个按钮的点击事件处理函数中添加埋点代码。

import React from 'react';
import Analytics from 'analytics';

const analytics = Analytics.initialize({ 
  // 填写你的监控工具的配置信息
});

function MyButton() {
  const handleClick = () => {
    // 添加埋点代码
    analytics.track('Button clicked', { button: 'MyButton' });
  };

  return (
    <button onClick={handleClick}>Click Me</button>
  );
}

export default MyButton;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在上面的例子中,我们使用了 analytics 这个前端监控工具,调用了 track 方法来记录用户的点击行为,并传递了一个包含 button 属性的对象,以便更好地区分不同的按钮。

4.数据的收集与分析

当用户进行相应的操作时,监控工具就可以收集到相应的数据。在收集到数据后,可以通过监控工具的界面进行数据的分析和可视化展示,以便更好地了解用户的行为习惯和需求。
需要注意的是,在编写埋点代码时,要注意命名规范、数据格式等,以便更好地管理和分析数据。此外,埋点代码的数量和质量也需要平衡,不能过多影响网页性能,也不能过少无法收集到足够的数据。

4. 三列布局

公共html部分:

function ThreeColum() {
  return (
    <div className="threecolum">
      <p>1.使用float实现</p>
      <div className='m1-wrapper'>
        <div className='m1-public m1-left'> </div>
        <div className='m1-public m1-center'></div>
        <div className='m1-public m1-right'> </div>
      </div>
      <div className='three-footer'>footer</div>
      <div className='three-footer2'>footer2</div>
    </div>
  );
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

公共css部分:

.three-footer {
  height: 30px;
  border: 1px solid red;
  text-align: center;
  /* clear: both; */
}
.three-footer2 {
  height: 30px;
  border: 1px solid red;
  text-align: center;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

1.使用float实现

/* 方法一:
使用float实现,
*/
.m1-wrapper {
  border: 1px solid royalblue;
}
.m1-wrapper::after {
  content: '';
  display: table;
  clear: both;
}
.m1-public {
  float: left;
  width: 30%;
  margin: 0 1.5%;
}
.m1-left {
  border: 1px solid black;
  height: 300px;
}

.m1-center {
  border: 1px solid red;
  height: 400px;
}

.m1-right {
  border: 1px solid green;
  height: 100px;
}
  • 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

给三列均设置浮动,实现三列布局。
但是要注意,三列设置浮动后,其父元素无法正确计算高度,也就是外部包裹组件的高度并没有被撑开。所以底部的footer元素位置不对。
因此,需要让footer及footer下方元素,出现在三列内容的下方。
这里提供两种方案:

1.修改footer元素css:

clear:both;
  • 1

clear:both;的作用是,清除元素浮动带来的影响,恢复正常布局位置。可以将其放在浮动元素的下方,清除浮动带来的影响。表现为其左右均不能有浮动元素,如果有,则布局在浮动元素的最下方。

这个方式可以使footer元素位置正常,其后footer2元素位置正常。但是,并没有撑开三列元素的父元素,其高度仍然为0。

所以选择另一种方案。

2.修改三列的父元素m1-wrapper的css:
(1)使用伪元素

.m1-wrapper::after {
  content: '';
  display: table;
  clear: both;
}
  • 1
  • 2
  • 3
  • 4
  • 5

(2)块级格式化上下文BFC

.m1-wrapper {
  border: 1px solid royalblue;
  overflow: hidden;
}
  • 1
  • 2
  • 3
  • 4

BFC在以下情况会被创建:

  • 根元素(即 元素)
  • 浮动元素(float 不为 none)
  • 绝对定位元素(position 为 absolute 或 fixed)
  • 行内块元素(display 为 inline-block)
  • 表格单元格(display 为 table-cell)
  • 表格标题(display 为 table-caption)
  • overflow 属性值不为 visible 的块级元素

2. 使用flexbox布局

.m2-wrapper {
 display: flex;
}

.m2-left {
  border: 1px solid black;
  height: 300px;
  width: 200px;

}

.m2-center {
  border: 1px solid red;
  height: 400px;
  flex: 1;
}

.m2-right {
  border: 1px solid green;
  height: 100px;
  width: 200px;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3.使用grid网格布局

/* 使用grid */
.m3-wrapper {
  display: grid;
  width: 100%;
  grid-template-columns: 200px auto 300px;
 }
 
 .m3-left {
  /*grid-column: 1/2;*/
   border: 1px solid black;
   height: 300px;
 
 }
 
 .m3-center {
   border: 1px solid red;
   height: 400px;
 }
 
 .m3-right {
   border: 1px solid green;
   height: 100px;
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

4.absolute实现

.m4-wrapper {
 width: 100%;
 position: relative;
}

.m4-left {
  border: 1px solid black;
  height: 300px;
  position: absolute;
  width: 200px;
  top: 0;
  left: 0;
}

.m4-center {
  border: 1px solid red;
  height: 400px;
  margin: 0 300px 0 200px;
}

.m4-right {
  border: 1px solid green;
  height: 100px;
  position: absolute;
  width: 200px;
  top: 0;
  right: 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

这里,要理解absolute绝对定位,是针对最近的已定位的父元素(position不为static)定位的。所以此处要给父元素设置position: relative。通过设置top,left,top,bottom指定位置,该方式会使元素从文档中脱离,其他元素布局不会受到其影响。

5.使用table实现

 /* 5.使用table */
 .m5-wrapper {
  width: 100%;
  display: table;
 }
 
 .m5-left {
   border: 1px solid black;
   height: 300px;
   display: table-cell;
   width: 200px;
 }
 
 .m5-center {
   border: 1px solid red;
   height: 400px;
   display: table-cell;
 }
 
 .m5-right {
   border: 1px solid green;
   height: 100px;
   display: table-cell;
   width: 300px;
 }
  • 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

此处子元素的高度,和父元素的高度,均等于子元素的最高高度。

6. 圣杯布局

html:

<p>6.圣杯布局实现</p>
<div className='m6-wrapper'>
  <div className='m6-center'>center</div>
  <div className='m6-left'>left </div>
  <div className='m6-right'>right </div>
</div>
<div className='three-footer'>footer</div>
<div className='three-footer2'>footer2</div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

css代码

/* 6.圣杯布局 */
.m6-wrapper {
  box-sizing: border-box;
  padding: 0 200px;
  overflow: hidden;
} 

.m6-center {
  box-sizing: border-box;
  border: 1px solid red;
  height: 400px;
  float: left;
  width: 100%;

}

.m6-left {
  box-sizing: border-box;
  border: 1px solid black;
  width: 200px;
  height: 200px;
  float: left;
  position: relative;

  margin-left: -100%;
  left: -200px;
}

.m6-right {
  box-sizing: border-box;
  border: 1px solid green;
  height: 100px;
  width: 200px;
  float: left;
  position: relative;

  margin-left: -200px;
  right: -200px;
}
  • 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

在html中,中间center部分,写在left和right前面,确保主体部分先加载。

7.双飞翼布局

html:

<div className='m7-wrapper'>
     <div className='m7-center-wrapper'>
          <div className='m7-center'>center</div>
     </div>
     <div className='m7-left'>left </div>

     <div className='m7-right'>right </div>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
/* 7.圣杯布局 */
.m7-wrapper {
box-sizing: border-box;
width: 100%;
overflow: hidden;
} 

.m7-center-wrapper {
box-sizing: border-box;
border: 1px solid red;
float: left;
width: 100%;
}
.m7-center {
box-sizing: border-box;
border: 1px solid red;
height: 400px;
margin: 0 200px;

}

.m7-left {
box-sizing: border-box;
border: 1px solid black;
width: 200px;
height: 200px;
float: left;
margin-left: -100%;
}

.m7-right {
box-sizing: border-box;
border: 1px solid green;
height: 100px;
width: 200px;
float: left;
margin-left: -200px;
}
  • 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

双飞翼布局与圣杯布局,都是实现的三列布局,两侧固定宽度,中间主体部分自适应调整,且主体内容先行加载。但是不同的是,圣杯布局是通过padding的方式,为主体部分留出位置。而双飞翼布局是通过margin的方式,留出主体位置。并且双飞翼布局主体部分多了一个div,通过div嵌套,结合margin边距,使得两侧边栏的定位更加简单。不需要通过relative布局进行第二次移动。

5. 是否了解箭头函数和一般函数,他们有什么区别?

1.写法与简写

箭头函数使用箭头(=>)来定义,而普通函数使用关键字 function 定义。

// 箭头函数
const arrowFunc = (arg1, arg2) => {
  // 函数体
};

// 普通函数
function regularFunc(arg1, arg2) {
  // 函数体
}
/// 箭头函数省略
const arrowFunc2 = a => a + 1;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

箭头函数如果只有一个参数,则小括号可以省略。如果执行部分只有一个返回值,则大括号和return都可以省略。普通函数不能省略。

2.构造函数

箭头函数不能作为构造函数使用,而普通函数可以作为构造函数来创建对象实例。
箭头函数没有自己的 new.target,并且不能使用 new 关键字调用

// 普通函数作为构造函数
function Person(name) {
  this.name = name;
}

const john = new Person('John');
console.log(john.name); // 输出 "John"

// 箭头函数不能作为构造函数
const Person = name => {
  this.name = name;
};

const john = new Person('John'); // 抛出 TypeError

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.arguments对象

每个普通函数调用后都有一个arguments对象,用来存储实际传递的参数。

const a = [1,2,3,4];
const f = function(a){
	console.log(arguments);
}
// output:
// [1,2,3,4]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

箭头函数没有自己的arguments,但是可以使用reset。

const f = ( ...reset) => {
	console.log(reset);
}

f(1, 2, 3, 4);
//output:
// [1, 2, 3, 4]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
const f = (param1, param2, ...reset) => {
	console.log(reset);
}

f(1, 2, 3, 4);
//output:
// [3, 4]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4.prototype原型

普通函数具有prototype原型,而箭头函数没有。从以下几个方面理解

(1)构造函数和原型链:

在 JavaScript 中,普通函数可以用作构造函数来创建对象实例,并且每个普通函数都有一个默认的 prototype 属性,它指向一个对象,该对象包含构造函数的原型方法和属性。通过原型链,对象可以继承构造函数的原型方法和属性。然而,箭头函数没有自己的 prototype,因为它们不可以用作构造函数,无法创建对象实例。

(2)继承关系:

由于普通函数有 prototype,它们可以作为原型对象,用于继承属性和方法。通过 prototype,可以将方法和属性添加到构造函数的原型上,从而使通过该构造函数创建的所有实例都能够访问这些方法和属性。而箭头函数没有 prototype,因此不能用于创建继承关系。

(3)对象的 proto 属性:

在 JavaScript 中,每个对象都有一个特殊的 proto 属性,指向其原型对象。普通函数的 prototype 对象被赋值给通过该构造函数创建的对象的 proto 属性,从而建立了原型链。但是,箭头函数没有 prototype,因此没有对应的原型对象可以赋值给 proto

综上所述,

箭头函数没有 prototype 原型是因为它们不能用作构造函数创建对象实例,也没有继承关系,无法作为原型对象。它们更适合用于简洁的函数表达式,而普通函数则更适合用于需要创建对象实例和实现继承的情况。

5.this指向

普通函数中this总是指向调用它的对象,如果作为构造函数,它指向创建的对象实例。

箭头函数的this指向的是父级作用域的this,是通过查找作用域链来确定 this 的值,它本身没有this。也就是说看的是上下文的this,指向的是定义它的对象,若无自定义上层,则代表window,而不是执行时所在的对象。

6.this指向改变

call()、apply()、bind()等方法能改变普通函数中this的指向,但不能改变箭头函数中this点指向。

6. 了解this指向?

  • 普通函数中this指向的是调用它的对象,如果作为构造函数,它指向创建的对象实例
  • 箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了之后不会改变

1.全局上下文
全局上下文:当在全局上下文中使用 this 时,它指向全局对象(在浏览器中是 window 对象,在 Node.js 中是 global 对象)。

console.log(this); // 在全局作用域中输出全局对象(window 或 global)
  • 1

2.函数调用
当函数作为独立函数调用时,this 指向全局对象。这是因为函数没有被绑定到任何对象上。

function myFunction() {
  console.log(this);
}

myFunction(); // 在浏览器中输出全局对象(window),在 Node.js 中输出全局对象(global)
  • 1
  • 2
  • 3
  • 4
  • 5

3.方法调用:
当函数作为对象的方法调用时,this 指向调用该方法的对象。

const obj = {
  name: 'John',
  sayHello: function() {
    console.log(`Hello, ${this.name}!`);
  }
};

obj.sayHello(); // 输出 "Hello, John!"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4.构造函数调用
当函数作为构造函数使用 new 关键字调用时,this 指向新创建的对象实例。

function Person(name) {
  this.name = name;
}

const john = new Person('John');
console.log(john.name); // 输出 "John"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5.箭头函数:
箭头函数的 this 是在定义时继承自外部作用域,并且在整个箭头函数生命周期内保持不变。

const obj = {
  name: 'John',
  sayHello: () => {
    console.log(`Hello, ${this.name}!`);
  }
};

obj.sayHello(); // 输出 "Hello, undefined!"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在箭头函数中,this 的值是继承自外部作用域,因此在上述例子中,箭头函数没有自己的 this,导致 this.name 为 undefined。

首先,这里有个问题是js分严格模式和非严格模式,严格模式和非严格模式下this指向稍微有点区别,主要就是全局作用域中普通函数中的this指向问题,严格模式下是指向undefined的,非严格模式下是指向window。

7. 了解new操作符?

在 JavaScript 中,new 操作符用于创建一个对象实例,并调用一个函数作为构造函数来初始化该对象。new 操作符的详细解释如下:

1.创建一个空对象:
new 操作符首先创建一个空对象,它将成为新创建的对象实例。

2.将构造函数的作用域赋给新对象:
new 操作符将构造函数的作用域赋给新对象,这样新对象可以访问构造函数中的属性和方法。

3.执行构造函数代码:
new 操作符调用构造函数,并传递任何指定的参数。构造函数可以在新对象上设置属性、方法或其他逻辑。

4.返回新对象实例:
如果构造函数中没有显式返回一个对象,new 操作符将返回新创建的对象实例。如果构造函数显式返回一个对象,那么该对象将取代新创建的实例作为 new 表达式的返回值。

下面是一个使用 new 操作符创建对象实例的示例:

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

const john = new Person('John', 25);
console.log(john.name); // 输出 "John"
console.log(john.age); // 输出 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在上面的示例中,Person 函数作为构造函数,通过 new 操作符创建了一个新的对象实例 john。构造函数中的 this 指向新创建的对象,因此可以将属性nameage设置在新对象上。

需要注意的是,使用 new 操作符创建对象实例时,构造函数中的 this 会自动指向新对象。这使得我们能够在构造函数内部设置对象的属性和方法。同时,new 操作符还会继承构造函数的原型对象,使新对象实例能够访问原型上定义的方法和属性

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}!`);
};

john.sayHello(); // 输出 "Hello, my name is John!"
  • 1
  • 2
  • 3
  • 4
  • 5

通过 new 操作符,我们可以轻松地创建多个具有相同属性和方法的对象实例。

8. vue 的价值

Vue 是一个流行的前端框架,它提供了一种组织和构建交互式的用户界面的方式。Vue 的价值主要体现在以下几个方面:

  • 响应式数据绑定:Vue 采用了基于数据驱动的编程模式,通过双向数据绑定实现了数据和视图之间的自动同步。当数据发生变化时,视图会自动更新;反之,当用户与视图交互时,数据也会相应地更新。这使得开发者可以更轻松地管理和维护应用程序的状态。

  • 组件化开发:Vue 将用户界面抽象为可复用的组件,每个组件拥有自己的模板、样式和逻辑。组件化开发使得项目结构更清晰,代码更易维护。开发者可以将复杂的界面拆分为多个组件,每个组件负责特定的功能,最后组合起来形成完整的应用。

  • 渐进式框架:Vue 被设计为渐进式框架,可以逐步应用到项目中的不同部分。你可以将 Vue 作为一个简单的库引入已有项目中,用于增强特定的功能,也可以将其用于构建整个单页应用。这种灵活性使得 Vue 可以适应不同规模和需求的项目。

  • 生态系统和插件支持:Vue 拥有庞大的生态系统,提供了许多官方和第三方的插件、工具和扩展库。这些插件可以帮助开发者解决各种问题,增强开发效率,扩展 Vue 的功能。例如,Vue Router 用于处理前端路由,Vuex 用于状态管理,Vue CLI 提供了快速创建 Vue 项目的脚手架工具等等。

  • 社区支持和文档丰富:Vue 拥有一个活跃且热情的开发者社区,社区成员之间积极交流和分享经验。Vue 的文档十分详细和易于理解,包括官方文档、教程、示例和案例等,使得学习和使用 Vue 变得更加容易。

总体来说,Vue 提供了一种简洁、灵活、高效的方式来构建交互式的用户界面。它的价值在于简化了前端开发的复杂性,提高了开发效率,并为开发者提供了丰富的工具和社区支持。无论是小型项目还是大型应用,Vue 都是一个强大而可靠的选择。

9. 为什么要双向绑定

双向数据绑定是前端框架中常见的功能,它的存在有以下几个主要原因:

1.数据与视图的同步:
在前端开发中,数据和视图之间存在密切的关联。当数据发生变化时,我们希望视图能够自动更新以反映最新的数据;反之,当用户与视图交互时,我们也希望能够方便地更新数据。双向数据绑定使得数据和视图之间的同步变得自动化,无需手动操作。

2.减少手动操作:
在传统的开发中,我们需要手动监听用户输入或视图变化,然后通过手动更新数据或视图来保持同步。这种手动操作非常繁琐且容易出错。通过双向数据绑定,框架会自动帮助我们处理这些操作,从而减少了手动干预的需求,提高了开发效率。

3.简化开发流程:
双向数据绑定可以减少开发者编写重复代码的工作量。开发者无需手动编写大量的数据更新和视图更新的逻辑,框架会根据数据的变化自动更新视图,并将用户的输入反映到数据中。这简化了开发流程,让开发者能够更专注于业务逻辑的实现。

4.提高用户体验:
双向数据绑定可以使用户界面更加动态和交互。当用户在输入框中输入内容时,数据会实时更新,反之,当数据更新时,用户界面也会立即响应变化。这种实时的反馈和交互可以提高用户体验,让用户感到应用程序更加灵活和响应。

尽管双向数据绑定提供了许多便利,但也需要注意合理使用。在某些情况下,单向数据流可能更加适合,特别是在涉及到复杂的状态管理和数据流控制时。因此,开发者需要根据具体的项目需求和复杂性来选择使用双向数据绑定或单向数据流。

10. 为什么 React 不双向绑定

React 之所以选择了单向数据流而不是双向数据绑定,主要是基于以下几个原因:

1.简化数据流:
单向数据流使得数据流动的路径更加明确和可追踪。数据只能通过 props 从父组件传递到子组件,子组件无法直接修改父组件的数据。这样可以降低数据流的复杂性,减少了潜在的数据变化和状态管理的问题,使得代码更易于理解和维护。

2.预测性和可维护性
单向数据流使得数据变化的原因更加可追溯。数据只能通过 props 从上层组件传递到下层组件,当数据发生变化时,我们可以准确地知道是哪个组件修改了数据。这提高了代码的可维护性,降低了出现 bug 和难以调试的可能性。

3.性能优化:
React 通过使用虚拟 DOM 和高效的 diff 算法来优化渲染性能。双向数据绑定通常需要在数据变化时立即更新视图,这可能导致频繁的重新渲染,对性能产生负面影响。而单向数据流可以更加精确地控制何时进行渲染和更新,提高了性能效率。

4.可预测的状态管理:
在 React 中,通常使用状态管理库(如 Redux、Mobx)来管理应用程序的状态。这些库提供了明确的状态更新方式,通过单向数据流和纯函数的方式来修改和更新状态。这种方式可以更好地组织和管理复杂的应用程序状态,降低了出现状态冲突和数据竞争的可能性。

尽管 React 本身没有内置双向数据绑定的机制,但是你仍然可以实现双向数据绑定的功能,例如通过在输入框上监听 onChange 事件来更新组件的状态。React 的设计目标是提供一种简单和可预测的方式来构建用户界面,而单向数据流正是为了实现这一目标。

11. class的public、private、static

在 JavaScript 类中,可以使用 public、private 和 static 关键字来定义类的成员的访问性和行为。

1.Public 成员:
默认情况下,类中的成员(属性和方法)是公开的(public)。公开成员可以在类的内部和外部访问。例如:

class MyClass {
  publicProperty = 42;

  publicMethod() {
    console.log('This is a public method.');
  }
}

const myObject = new MyClass();
console.log(myObject.publicProperty); // 输出 42
myObject.publicMethod(); // 输出 "This is a public method."
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2.Private 成员:私有成员(private)
只能在类的内部访问,外部无法访问。可以使用 # 前缀来声明私有成员。私有成员在类的外部是不可见的。例如:

class MyClass {
  #privateProperty = 42;

  #privateMethod() {
    console.log('This is a private method.');
  }

  publicMethod() {
    console.log('This is a public method.');
    this.#privateMethod(); // 在类的内部可以访问私有方法
  }
}

const myObject = new MyClass();
console.log(myObject.#privateProperty); // SyntaxError: Private field '#privateProperty' must be declared in an enclosing class
myObject.#privateMethod(); // SyntaxError: Private field '#privateMethod' must be declared in an enclosing class
myObject.publicMethod(); // 输出 "This is a public method." 和 "This is a private method."
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.Static 成员:静态成员(static)
是与类本身关联而不是与类的实例关联的成员。静态成员可以通过类本身直接访问,而不需要创建类的实例。例如:

class MyClass {
  static staticProperty = 42;

  static staticMethod() {
    console.log('This is a static method.');
  }
}

console.log(MyClass.staticProperty); // 输出 42
MyClass.staticMethod(); // 输出 "This is a static method."
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

静态成员在类的实例上不可访问,只能通过类本身访问。静态成员通常用于表示与类相关的共享数据或共享功能。

需要注意的是,私有成员和静态成员在 JavaScript 中是相对较新的概念,可能需要使用支持它们的 JavaScript 引擎或者进行特定的配置才能使用。

12. 无序列表,点击li显示index

// html
<ul id="myList">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>

// js:
window.function(){
    var ul=document.getElementById('myList');
    var ul_lis=ul.getElementsByTagName('li');
    for (var i = 0;i < ul_lis.length; i++) {
        ul_lis[i].index = i; // 绑定一个变量,要不然i取最后的值!
        ul_lis[i].onclick=function(){
            console.log(this.index+1,this.innerHTML);
        }
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

method2:

const list = document.getElementById('myList');
const listItems = list.getElementsByTagName('li');

// 遍历每个列表项并添加点击事件监听器
for (let i = 0; i < listItems.length; i++) {
  listItems[i].addEventListener('click', function() {
    console.log('Index:', i);
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

13. TCP 三次握手

TCP(Transmission Control Protocol)是一种面向连接的可靠传输协议。在建立 TCP 连接时,使用了三次握手(three-way handshake)的过程来确保通信双方能够正确地建立连接。以下是 TCP 三次握手的详细步骤:

1.第一次握手(SYN):

  • 客户端向服务器发送一个特殊的 TCP 报文段,称为 SYN 报文段(SYN = Synchronize Sequence Number)。
  • SYN 报文段中包含一个随机生成的初始序列号(Sequence Number)用于后续的数据传输。

2.第二次握手(SYN-ACK):

  • 服务器收到客户端发送的 SYN 报文段后,会发送一个响应的报文段给客户端。
  • 服务器向客户端发送的报文段称为 SYN-ACK 报文段(ACK = Acknowledgment)。
  • SYN-ACK 报文段中包含确认号(Acknowledgment Number),它是客户端发送的初始序列号加一,表示服务器已接收到客户端的请求。

3.第三次握手(ACK):

  • 客户端收到服务器发送的 SYN-ACK 报文段后,会向服务器发送一个确认报文段。
  • 客户端向服务器发送的报文段称为 ACK 报文段,其中确认号设置为服务器发送的初始序列号加一。
  • 服务器收到客户端发送的 ACK 报文段后,确认连接建立成功。

通过这个三次握手的过程,客户端和服务器可以互相确认彼此的能力,并同步初始的序列号。在建立连接之后,双方可以开始进行数据的传输。

TCP 三次握手的目的是为了保证可靠性和防止资源浪费。每一步握手都是为了确保双方都能够正常收发数据,建立起双向的通信信道。如果其中一方没有收到对方的确认,会重复发送握手报文段,直到连接建立成功或达到最大重试次数。

值得注意的是,TCP 三次握手只是建立连接的过程,并不涉及实际数据的传输。数据的传输需要在连接建立后通过 TCP 的数据传输机制进行。

14. HTTP 1.1 和 2.0区别

HTTP(Hypertext Transfer Protocol)是一种用于传输超文本的应用层协议。HTTP 1.0、HTTP 1.1 和 HTTP 2.0 是 HTTP 协议的不同版本,它们在性能、功能和协议特性上有一些区别。

1. HTTP 1.0:

  • 顺序传输:HTTP 1.0 是基于请求/响应模型的协议,每个请求需要等待前一个请求的响应返回才能进行下一次请求。
  • 短连接:每个请求和响应都使用独立的连接,完成后立即关闭连接。这意味着每个请求都需要建立新的 TCP 连接,带来较大的开销。
  • 无状态:HTTP 1.0 默认是无状态的,每个请求之间相互独立,服务器不会保留客户端请求的任何状态信息。

2. HTTP 1.1:

  • 持久连接:HTTP 1.1 引入了持久连接,允许多个请求和响应复用同一个连接。这减少了建立和关闭连接的开销,提高了性能。
  • 流水线化:HTTP 1.1 支持请求和响应的流水线化,允许在一个连接上同时发送多个请求,提高了请求的并发性。
  • 虚拟主机:HTTP 1.1 支持虚拟主机,允许多个域名共享同一个 IP 地址,提高了服务器资源的利用率。
  • 缓存机制:HTTP 1.1 引入了更强大的缓存机制,包括强缓存和协商缓存,减少了网络传输和服务器负载。

3. HTTP 2.0:

  • 多路复用:HTTP 2.0 使用二进制分帧层,支持在同一个连接上同时发送多个请求和响应,实现了请求和响应的多路复用,提高了并发性能。
  • 服务器推送:HTTP 2.0 支持服务器主动推送资源,服务器可以在客户端请求之前将相关资源发送给客户端,减少了额外的请求延迟。
  • 头部压缩:HTTP 2.0 使用首部压缩算法,减少了头部信息的大小,降低了网络传输的开销。
  • 二进制传输:HTTP 2.0 将传输数据分解为二进制帧,提高了传输的效率和可靠性。

HTTP 2.0 相对于 HTTP 1.x 版本在性能方面有了显著的提升,主要是通过多路复用、头部压缩和二进制传输等特性实现的。它可以更高效地利用网络资源,减少延迟和带宽消耗。HTTP 2.0 的引入改进了用户体验,提升了网站的性能和效率。

15. 事件绑定方式

在前端开发中,有多种方式可以用来绑定事件处理程序到 HTML 元素上。以下是几种常见的事件绑定方式:

1.HTML 属性绑定:

在 HTML 元素上直接使用内联事件处理程序,通过属性将事件与 JavaScript 代码绑定。
例如:<button onclick="myFunction()">Click me</button>
这种方式简单直接,适用于简单的事件处理逻辑,但不太适合复杂的交互和维护大型项目。

2.DOM 属性绑定:

通过 JavaScript 获取到DOM元素,然后使用 DOM 属性将事件处理程序绑定到元素上。
例如:document.getElementById('myButton').onclick = myFunction;
这种方式也比较简单,但需要注意只能绑定一个事件处理程序,如果需要绑定多个处理程序,后面的处理程序会覆盖前面的。

3.addEventListener 方法:

使用addEventListener方法来为元素添加事件监听器。
例如:document.getElementById('myButton').addEventListener('click', myFunction);
这种方式可以添加多个事件处理程序,而且不会覆盖之前绑定的处理程序。还可以通过第三个参数指定事件的捕获或冒泡阶段。

4.框架自带方法,如jQuery:

如果你使用 jQuery 库,可以使用其提供的事件绑定方法来简化事件处理。
例如:$('#myButton').on('click', myFunction);
jQuery 提供了丰富的事件处理方法,支持链式调用和处理多个事件。

总体而言,推荐使用 addEventListener 方法进行事件绑定,因为它提供了更灵活、可维护性更高的方式来处理事件。此外,现代的前端框架(如React、Vue等)通常也提供了自己的事件绑定机制,可以根据框架的要求和特性来选择合适的方式。

16. promise.all实现

Promise.all 是一个 Promise 静态方法,用于接收一个包含多个 Promise 的可迭代对象,并返回一个新的 Promise。这个新的 Promise 在所有传入的 Promise 都成功解析(resolve)时才会被解析,如果任何一个 Promise 被拒绝(reject),则返回的 Promise 会立即被拒绝,并返回拒绝的原因。

下面是一个示例,展示如何使用 Promise.all 方法以及它的实现:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 1 resolved');
  }, 2000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise 2 resolved');
  }, 1000);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('Promise 3 rejected');
  }, 1500);
});

Promise.all([promise1, promise2])
  .then(results => {
    console.log('All promises resolved:', results);
  })
  .catch(error => {
    console.log('At least one promise rejected:', error);
  });

// 输出:
// At least one promise rejected: Promise 3 rejected

  • 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

在上面的例子中,我们创建了三个 Promise 对象:promise1、promise2 和 promise3。其中,promise1 和 promise2 在规定的时间后被解析,而 promise3 在规定的时间后被拒绝。

然后,我们使用 Promise.all 方法将这三个 Promise 对象作为参数传递。在 Promise.all 返回的新 Promise 中,当所有传入的 Promise 都被解析时,then 方法中的回调函数会被执行,结果数组包含了每个 Promise 的解析值。如果有任何一个 Promise 被拒绝,catch 方法中的回调函数会被执行,传递拒绝的原因。

在上面的例子中,promise3 被拒绝,所以最终返回的 Promise 被拒绝,输出了相应的错误信息。

下面是一个简单的自定义实现 Promise.all 的示例:

function customPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let count = 0;

    promises.forEach((promise, index) => {
      promise
        .then(result => {
          results[index] = result;
          count++;

          if (count === promises.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在自定义的 customPromiseAll 函数中,我们创建了一个新的 Promise,并通过迭代传入的 Promise 数组来添加每个 Promise 的解析和拒绝处理。当所有 Promise 都被解析时,通过 resolve 方法返回结果数组;如果有任何一个 Promise 被拒绝,通过 reject 方法拒绝返回的 Promise。这样,我们就实现了一个简单的 Promise.all 功能。

需要注意的是,自定义的实现没有考虑一些 Promise.all 的边界情况和性能优化,这只是一个简单的示例来展示原理。在实际应用中,建议使用原生的 Promise.all 方法或成熟的 Promise 库来处理 Promise 数组的并行解析。

17. 多个请求控制并发

在前端开发中,有时需要同时发送多个请求,但为了控制并发量,避免过多的请求同时发送,可以使用以下几种方式来实现多个请求的并发控制:

1.Promise 和 async/await:

使用 Promise 和 async/await 结合可以实现异步请求的顺序控制和并发控制。
创建一个 Promise 数组,每个 Promise 表示一个请求任务,然后使用 Promise.all 控制并发数量。
例如:

async function sendRequests() {
  const urls = ['url1', 'url2', 'url3']; // 请求的URL列表
  const maxConcurrentRequests = 2; // 最大并发请求数量

  const requests = urls.map(url => fetch(url)); // 发起请求,得到Promise数组

  const results = [];
  for (let i = 0; i < requests.length; i += maxConcurrentRequests) {
    const batch = requests.slice(i, i + maxConcurrentRequests);
    const batchResults = await Promise.all(batch);
    results.push(...batchResults);
  }

  console.log(results); // 处理请求结果
}

sendRequests();

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

2.Axios 并发控制:

如果使用 Axios 这样的 HTTP 请求库,可以利用其提供的并发控制功能来限制并发请求数量。
通过 axios.all 方法可以同时发送多个请求,并设置 maxContentLength 来限制并发请求数量。
例如:

const axios = require('axios');

async function sendRequests() {
  const urls = ['url1', 'url2', 'url3']; // 请求的URL列表
  const maxConcurrentRequests = 2; // 最大并发请求数量

  const requests = urls.map(url => axios.get(url)); // 创建请求对象数组

  const results = [];
  for (let i = 0; i < requests.length; i += maxConcurrentRequests) {
    const batch = requests.slice(i, i + maxConcurrentRequests);
    const batchResults = await axios.all(batch);
    results.push(...batchResults);
  }

  console.log(results); // 处理请求结果
}

sendRequests();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3.控制台限制:

浏览器控制台通常有一定数量的并发请求限制。在开发和测试过程中,可以通过浏览器控制台的网络选项来限制并发请求数量。
例如,在 Chrome DevTools 的 Network 选项卡中,可以设置并发请求的最大值。

不同浏览器对并发请求数量的限制可能会有所不同。以下是一些常见浏览器的默认并发限制:

  • Google Chrome:
    在较新的版本中,Google Chrome 默认限制为六个并发请求。这个限制是基于每个域名(主机名)的,并且可以在浏览器的开发者工具中进行修改。

  • Mozilla Firefox:
    Mozilla Firefox 通常默认限制为六个并发请求。与 Chrome 类似,这个限制也是基于每个域名的,并且可以通过修改配置进行调整。

  • Safari:
    Safari 也限制为六个并发请求。这个限制是基于每个域名的,可以在 Safari 的开发者选项中进行更改。

需要注意的是,并发请求的限制可能会随着浏览器版本的更新而有所变化,因此以上提到的限制仅作为参考。此外,浏览器通常还会根据一些策略来调整并发请求的数量,例如根据网络类型(如 3G、4G、WiFi)和系统资源等进行自动调整。

如果你想更精确地了解特定浏览器的并发限制,最好参考该浏览器的官方文档或开发者工具的说明。另外,在实际开发中,还可以通过代码控制并发请求的数量,以便更好地适应不同的环境和需求。

这些方法可以帮助控制并发请求的数量,确保不会同时发送过多的请求,从而优化前端应用的性能和资源利用。根据具体需求和使用的工具,选择合适的方式来控制并发请求。

18. 获取数组最大深度(这里写了代码)

要获取数组的最大深度,即数组嵌套的层数最深的深度,可以使用递归的方式来实现。以下是一个示例函数 getArrayMaxDepth 来获取数组的最大深度:

function getArrayMaxDepth(arr) {
  // 如果不是数组,返回 0
  if (!Array.isArray(arr)) {
    return 0;
  }

  // 遍历数组中的每个元素,递归调用获取嵌套数组的深度
  let maxDepth = 0;
  for (let i = 0; i < arr.length; i++) {
    const depth = getArrayMaxDepth(arr[i]);
    if (depth > maxDepth) {
      maxDepth = depth;
    }
  }

  // 返回嵌套深度加 1
  return maxDepth + 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

使用示例:

const arr1 = [1, 2, 3];
console.log(getArrayMaxDepth(arr1)); // 输出: 1

const arr2 = [1, [2, [3, 4]]];
console.log(getArrayMaxDepth(arr2)); // 输出: 3

const arr3 = [1, [2, [3, [4]]]];
console.log(getArrayMaxDepth(arr3)); // 输出: 4
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在上述示例中,getArrayMaxDepth 函数使用递归的方式遍历数组中的每个元素,并在每次递归调用中将深度加 1。通过比较每个递归调用的深度,最终返回数组的最大深度。

需要注意的是,该函数假设数组中的每个元素要么是基本类型(如数字、字符串等),要么是数组。如果数组中存在其他类型的嵌套,可能会导致深度计算不准确。另外,对于非常大的数组或嵌套层数非常深的数组,可能会导致递归的性能问题。在实际使用中,请根据具体情况进行评估和优化。

19. 短期目标

二面

1. 结合我的过往经历大致问了一些问题(我是按着我之前的经历对这段工作有哪些帮助大致说了一下)

前端实习经历对当前工作有以下几个方面的帮助:

  • 技术经验积累:前端实习使你有机会实际应用所学的前端技术,深入了解并熟练掌握各种前端技术栈和工具。通过实践,你可能已经熟悉了 HTML、CSS、JavaScript、前端框架等,并积累了解决问题和调试代码的经验。

  • 实际项目经验:前端实习让你参与了实际的项目开发,通过与团队合作、跟踪任务进度、协调需求变更等,你获得了项目管理和团队协作的经验。这些经验在当前的工作中同样有价值,让你能够更好地理解和适应团队工作的流程和要求。

  • 解决问题的能力:前端实习中可能会遇到各种技术挑战和问题,如浏览器兼容性、性能优化、调试等。通过解决这些问题,你发展了分析和解决问题的能力,学会了独立思考和主动寻找解决方案。

  • 沟通与合作能力:在前端实习中,你可能与设计师、后端开发人员和产品经理等不同角色的人进行交流和合作。这培养了你的沟通能力、团队合作能力和跨职能协作的能力,对于与团队协同工作、理解需求、进行有效沟通等都非常有帮助。

  • 快速学习和适应能力:前端实习期间,你可能需要面对不同的项目和技术栈,需要迅速学习和适应新的环境和工具。这锻炼了你的学习能力和适应能力,使你能够快速上手新的项目和技术,适应工作中的变化和挑战。

总之,前端实习经历为你打下了坚实的技术基础,培养了解决问题、沟通合作和快速学习的能力,这些对于当前的工作是非常有帮助的。你可以将实习期间的经验和技能应用到工作中,并不断发展和提升自己,为团队的成功做出贡献。

2. Oauth2.0 协议与流程

OAuth2.0介绍

OAuth 2.0是一种开放标准的授权协议,用于在不直接共享用户凭据的情况下授权第三方应用访问受保护的资源。与以前的版本相比,OAuth 2.0引入了一些重要的变化和改进。下面是OAuth 2.0与之前版本的主要区别:

1.简化流程:

OAuth 2.0简化了授权流程,使得开发人员更容易实现。它引入了访问令牌(Access Token)的概念,用于代表授权访问资源的权限。通过获取访问令牌,第三方应用可以直接向资源服务器发出请求,而不需要涉及用户的凭据。

2.专注于Web应用:
OAuth 2.0的设计主要关注Web应用程序的授权需求。它提供了适用于Web应用的不同授权流程,如授权码授权流程(Authorization Code Flow)、隐式授权流程(Implicit Flow)和资源所有者密码授权流程(Resource Owner Password Credentials Flow)等。

3.可扩展性:
OAuth 2.0提供了一种基于插件机制的方式,允许在不修改协议核心的情况下扩展其功能。这使得OAuth 2.0可以适应不同的身份验证和授权需求,例如使用JSON Web令牌(JWT)或SAML等。

4.安全性改进:
OAuth 2.0在安全性方面有所改进,引入了访问令牌的时效性和范围限制。访问令牌具有有限的生命周期,并且可以限制其可以访问的资源范围,从而提高了安全性。

5.对移动设备的支持:
OAuth 2.0在移动设备上的支持更加完善。它提供了适用于移动应用的授权流程,允许应用在不暴露用户凭据的情况下获得访问令牌。

需要注意的是,OAuth 2.0并非完美无缺,仍然存在一些安全和实施上的挑战。因此,在使用OAuth 2.0时,开发人员应该仔细理解其规范和最佳实践,并采取适当的安全措施,以确保数据和用户的安全性。

OAuth2.0使用

1.注册应用程序:
首先,您需要在目标身份提供商(如Google、Facebook、GitHub等)上注册您的应用程序,并获取客户端ID和客户端密钥。这些凭据将用于在授权过程中标识和验证您的应用程序。

2.选择授权流程:
根据您的应用程序类型和需求,选择适合的授权流程。常见的流程包括授权码授权流程(Authorization Code Flow)、隐式授权流程(Implicit Flow)和客户端凭证授权流程(Client Credentials Flow)等。

3.发起授权请求:
在您的应用程序中,向身份提供商发起授权请求。这包括将用户重定向到身份提供商的授权端点,并提供所需的参数,如客户端ID、重定向URI和请求的范围。

4.用户授权:
用户在身份提供商的登录页面上进行登录,并被要求授权您的应用程序访问其受保护的资源。用户可以选择接受或拒绝授权请求。

5.获取授权码或访问令牌:
如果使用授权码授权流程,一旦用户授权,身份提供商将重定向用户回到您的应用程序,并附带一个授权码。您的应用程序需要使用该授权码通过后端请求交换访问令牌。如果使用其他流程,您可以直接从重定向URI中获取访问令牌。

6.访问受保护的资源:
使用获得的访问令牌,您可以通过将其包含在请求中,向资源服务器发出请求,以访问用户的受保护资源。

7.刷新令牌(可选):
访问令牌具有一定的时效性。如果令牌过期,您可以使用刷新令牌来获取新的访问令牌,而无需再次请求用户的授权。

需要注意的是,具体的实现步骤和代码取决于您使用的身份提供商和编程语言/框架。身份提供商通常会提供相关的文档和示例代码,以帮助您集成OAuth 2.0协议到您的应用程序中。建议查阅身份提供商的官方文档,以获取更具体的实施指南。

3. 错误监控怎么做的,如何捕获资源加载错误

前端错误监控是一个重要的任务,它可以帮助您及时发现和解决前端应用程序中的错误。下面是一些常用的方法来捕获资源加载错误:

1.使用window.onerror事件:
可以通过注册window.onerror事件来捕获全局的JavaScript错误,包括资源加载错误。当页面上发生JavaScript错误时,该事件将被触发,您可以在事件处理程序中记录错误信息。

window.onerror = function(message, source, lineno, colno, error) {
  // 在这里处理错误,可以将错误信息发送到后端或记录到日志中
};
  • 1
  • 2
  • 3

2.监听资源加载事件:
可以通过监听window对象的load和error事件来捕获资源(如脚本、样式表、图像等)的加载情况。如果资源加载失败,会触发error事件,您可以在事件处理程序中进行相应的处理。

window.addEventListener('error', function(event) {
  var target = event.target;
  if (target.tagName === 'SCRIPT' || target.tagName === 'LINK' || target.tagName === 'IMG') {
    // 处理资源加载错误
  }
}, true);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.使用Performance API:
Performance API提供了一组接口来监控和测量网页性能,包括资源加载情况。您可以使用Performance API中的相关接口和事件来捕获资源加载错误。

window.addEventListener('error', function(event) {
  if (event.target instanceof HTMLScriptElement || event.target instanceof HTMLLinkElement || event.target instanceof HTMLImageElement) {
    // 处理资源加载错误
  }
}, true);
  • 1
  • 2
  • 3
  • 4
  • 5

4.第三方错误监控工具:
还可以使用第三方错误监控工具,如Sentry、Bugsnag、TrackJS等。这些工具提供了更全面和专业的错误监控功能,包括资源加载错误的捕获和报告。您可以根据自己的需求选择适合的工具,并按照其提供的文档和指南进行集成和配置。
无论您选择哪种方法,捕获资源加载错误后,您可以将错误信息记录到日志中、发送给后端进行分析,或者在前端界面上显示给用户。这样可以帮助您及时发现和解决资源加载问题,提高应用程序的可靠性和用户体验。

4. react-router的原理,部署注意点

React Router是一个用于构建单页应用程序(SPA)的库,它提供了在React应用中进行路由管理的功能。它基于React的组件化模型,允许你在应用程序中定义路由,然后根据用户导航的情况来动态渲染相应的组件。

React Router的工作原理如下:

1.路由定义:

你可以使用React Router的组件(如或)来定义你的应用程序的根路由。这些组件可以将URL路径与React组件进行关联。

2.路由匹配:

React Router会根据当前的URL路径,与你定义的路由进行匹配。匹配过程是从上到下的,直到找到与当前URL匹配的路由。当找到匹配的路由时,React Router将决定渲染哪个组件。

3.组件渲染:

当React Router确定了要渲染的组件后,它将渲染该组件并将其呈现在页面上。同时,React Router还会将一些与路由相关的属性(如路由参数)传递给渲染的组件。

4.导航管理:

React Router还提供了一些导航组件(如或),用于在应用程序中进行导航。这些组件可以生成包含正确URL路径的链接,并在用户点击时更新URL路径,从而触发路由匹配和组件渲染的过程。

在部署React Router应用时,以下是一些注意点

1.服务器配置:

如果你的应用程序使用了BrowserRouter,你需要确保你的服务器已正确地配置来处理所有可能的URL路径,以便在刷新页面时能够正确地加载应用程序。

2.基准路径:

如果你的应用程序不是部署在网站的根目录下,你需要在BrowserRouter或HashRouter组件上设置basename属性,以指定应用程序的基准路径。

3.404 页面:

在React Router中,404页面是指当没有与当前URL路径匹配的路由时显示的页面。你可以创建一个专门的404组件,并将其放置在你的路由配置的最后,以确保在找不到路由时显示该组件。

5.动态加载组件:

如果你的应用程序非常大或有许多页面,可以考虑使用React的代码分割和动态导入功能,以按需加载组件。这可以提高应用程序的性能和加载速度。

6.路由保护:

如果你的应用程序需要进行身份验证或授权,你可以在路由组件中实现路由保护逻辑,以确保只有授权用户可以访问特定的路由。

7.状态管理:

React Router只处理路由相关的逻辑,对于应用程序的状态管理,你可能需要使用其他的状态

5. http状态码

HTTP状态码是由HTTP协议定义的一组三位数字代码,用于表示客户端和服务器之间的通信状态。以下是常见的HTTP状态码及其含义:

1.1xx(信息提示):

表示服务器已接收到请求,正在继续处理。

  • 100(继续):服务器已接收到请求的起始部分,客户端应该继续发送剩余的请求。
  • 101(切换协议):服务器已根据客户端的请求切换协议。

2.2xx(成功):

表示请求已成功被服务器接收、理解和处理。

  • 200(成功):请求成功,并返回相应的结果。
  • 201(已创建):请求成功并在服务器上创建了新的资源。
  • 204(无内容):服务器已成功处理请求,但不需要返回任何实体内容。

3.3xx(重定向):

表示需要进一步操作来完成请求。

  • 301(永久重定向):请求的资源已永久移动到新的URL。
  • 302(临时重定向):请求的资源暂时移动到新的URL。
  • 304(未修改):请求的资源未修改,可以使用缓存的版本。

4.4xx(客户端错误):

表示客户端发送的请求有错误。

  • 400(错误请求):请求有语法错误或无法被服务器理解。
  • 401(未授权):请求需要身份验证。
  • 404(未找到):请求的资源不存在。

5.5xx(服务器错误):

表示服务器在处理请求时发生了错误。

  • 500(服务器内部错误):服务器遇到了意外的错误,无法完成请求。
  • 503(服务不可用):服务器暂时无法处理请求,通常是由于过载或维护。
  • 504 (网关超时):用于指示在充当网关或代理的服务器等待上游服务器的响应时发生超时。

这些HTTP状态码帮助客户端和服务器之间进行准确的通信,使得在请求和响应过程中能够传递必要的信息和指示。

6. 输入url发生了什么

当在浏览器中输入URL并按下回车后,以下是详细的步骤:

1.URL解析:

  • 提取协议:浏览器解析URL以确定使用的协议(如HTTP、HTTPS)。
  • 解析域名:如果URL包含域名,浏览器将解析该域名以获取对应的IP地址。

2.DNS解析:

  • 浏览器向本地DNS缓存查找域名对应的IP地址。如果有匹配,则跳至步骤 4。
  • 如果本地缓存中没有找到对应的IP地址,则浏览器向操作系统发送DNS查询请求。
  • 操作系统将查询发送到配置的DNS服务器,以获取域名对应的IP地址。
  • DNS服务器返回IP地址给操作系统,然后操作系统将其返回给浏览器。

3.建立TCP连接:

  • 使用获取到的IP地址,浏览器与服务器之间建立TCP连接。
  • 这涉及通过三次握手来确保连接的可靠性和完整性。

4.发送HTTP请求:

  • 浏览器向服务器发送HTTP请求。
  • 请求包括请求方法(如GET、POST)、路径、请求头和可选的请求体。

5.服务器处理请求:

  • 服务器接收到请求后,根据请求的路径和其他信息来处理请求。
  • 这可能涉及执行服务器端的代码、访问数据库等操作。

6.服务器发送HTTP响应:

  • 服务器根据请求的处理结果生成HTTP响应。
  • 响应包括响应状态码、响应头和响应体等。

7.接收并解析响应:

  • 浏览器接收到服务器发送的HTTP响应。
  • 浏览器解析响应头,获取响应的元数据,如状态码、内容类型等。
  • 浏览器开始逐步接收和解析响应体,获取实际的响应内容。

8.渲染页面:

  • 如果响应是HTML文档,浏览器开始解析HTML,并构建DOM(文档对象模型)树。
  • 浏览器解析CSS样式表,构建CSSOM(CSS对象模型)树,并将其与DOM树合并形成渲染树。
  • 浏览器执行JavaScript代码,可能会修改DOM树和CSSOM树,以及处理事件等。
  • 浏览器根据渲染树计算布局和绘制,将页面的可视化表示生成为位图。

9.显示页面:

  • 浏览器将渲染好的页面显示给用户。
  • 页面上的文本、图像、链接和其他元素可见并交互。
  • 如果页面包含其他资源(如样式表、图像、脚本文件),浏览器将并行下载这些资源并进行相应的处理。

DNS解析详细过程(补充)

1.查找本地DNS缓存:

  • 浏览器首先会查找自己的本地DNS缓存,该缓存保存了最近解析的域名和对应的IP地址。
  • 如果在本地缓存中找到了对应的域名和IP地址,浏览器会直接使用该IP地址进行连接。

2.发送DNS查询请求:

  • 如果在本地缓存中没有找到对应的IP地址,浏览器会向操作系统发送DNS查询请求。
  • 操作系统会根据配置的DNS服务器信息,选择一个可用的DNS服务器进行查询。

3.与DNS服务器通信:

  • 操作系统将DNS查询请求发送给所选择的DNS服务器,使用UDP或TCP协议进行通信。
  • DNS服务器一般由Internet服务提供商(ISP)或其他网络设备提供。

4.迭代或递归查询:

  • DNS服务器接收到查询请求后,它可以执行两种类型的查询:迭代查询或递归查询。
  • 迭代查询:如果DNS服务器支持迭代查询,它会向其他DNS服务器发送进一步的查询请求,以获取最终的IP地址。这个过程中,DNS服务器从根域名服务器开始,依次向下查询,直到找到对应的IP地址。
  • 递归查询:如果DNS服务器不支持迭代查询,它会尝试在自己的缓存中查找域名对应的IP地址。如果找到,则返回结果;否则,它会向其他DNS服务器发送递归查询请求,并等待结果。

5.获取IP地址:

  • DNS服务器最终会找到域名对应的IP地址。
  • 它将IP地址作为响应发送回操作系统,并由操作系统传递给浏览器。

6.缓存DNS解析结果:

浏览器收到IP地址后,会将该解析结果缓存在本地,以备将来的访问使用。
这样在下一次访问相同的域名时,就可以直接使用缓存的IP地址,而无需进行DNS解析。

通过这些步骤,浏览器能够将域名解析为对应的IP地址,以便建立与服务器的连接。DNS解析过程中可能会涉及多个DNS服务器之间的交互,以找到最终的IP地址。

7. 如何判断一个变量是对象还是数组?

1.使用typeof运算符:

使用typeof运算符可以判断一个变量的类型。
对于对象类型,包括数组,typeof运算符会返回字符串"object"。:

const variable = [];
if (typeof variable === "object" && Array.isArray(variable)) {
  console.log("变量是数组");
} else if (typeof variable === "object") {
  console.log("变量是对象");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2.使用Array.isArray()方法:

Array.isArray()是一个全局方法,用于确定一个变量是否为数组。
如果变量是数组,Array.isArray()方法将返回true;否则,返回false。
示例代码:

const variable = [];
if (Array.isArray(variable)) {
  console.log("变量是数组");
} else {
  console.log("变量不是数组");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3.使用instanceof运算符:

instanceof运算符用于检查一个对象是否属于特定类的实例。
对于数组,可以使用instanceof Array来判断变量是否为数组。
示例代码:

const variable = [];
if (variable instanceof Array) {
  console.log("变量是数组");
} else {
  console.log("变量不是数组");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这些方法都可以用于判断一个变量是对象还是数组。根据实际需求,选择其中的一种方法即可。

补充:instanceof介绍及使用

instanceof 是 JavaScript 中的运算符,用于检测一个对象是否属于特定类的实例。它的语法如下:

object instanceof constructor
  • 1

其中,object 是要检测的对象,constructor 是要检测的类或构造函数。instanceof 运算符返回一个布尔值,表示对象是否是指定类的实例或继承自该类。

下面是 instanceof 运算符的用法和一些注意事项:

1.用法示例:

const obj = new Array();
console.log(obj instanceof Array); // true

const str = "Hello";
console.log(str instanceof String); // false

function Person() {}
const person = new Person();
console.log(person instanceof Person); // true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

需要注意的是,数组是对象的一种特殊类型,因此数组也是 Object 的实例。因此,对于数组来说,arr instanceof Object 也会返回 true。

2.注意事项:

  • instanceof 运算符主要适用于检测自定义的对象或基本的内置对象(如数组、函数等)。
    当检测引用类型时,instanceof 运算符会检查对象是否是指定类的实例,或者是否继承自该类的原型链上的类。
  • instanceof 运算符对于检测基本数据类型如字符串、数字、布尔值等)不太适用,因为这些类型不是对象,而是原始值
  • 如果对象是通过自定义的构造函数创建的,instanceof 运算符会返回 true。
  • 当对象是通过字面量方式创建的,instanceof 运算符可能会返回 false,因为字面量创建的对象不会继承构造函数的原型。

需要注意的是,instanceof 运算符的结果可能会受到原型链的影响。如果对象在原型链上继承了指定类的原型,instanceof 运算符仍然会返回 true。因此,在使用 instanceof 运算符时,请确保了解对象的继承关系和原型链。

typeof介绍及使用(补充):

在 JavaScript 中,使用 typeof 运算符可以用于检查变量的类型。但是,对于数组、函数、日期和正则表达式,typeof 运算符的结果都会返回 “object”,这可能会导致一些混淆。

下面是对不同类型使用 typeof 运算符的结果:

  • 对于数组(Array):
const arr = [1, 2, 3];
console.log(typeof arr); // "object"
  • 1
  • 2
  • 对于函数(Function):
function foo() {
  // 函数体
}
console.log(typeof foo); // "function"
  • 1
  • 2
  • 3
  • 4
  • 对于日期(Date):
const now = new Date();
console.log(typeof now); // "object"
  • 1
  • 2
  • 对于正则表达式(RegExp):
const pattern = /abc/;
console.log(typeof pattern); // "object"
  • 1
  • 2

对于数组、函数、日期和正则表达式,typeof 运算符返回的结果是 “object”,这是因为它们在 JavaScript 内部都被视为对象。这是一个历史遗留问题,并不是一个准确的类型检测方式。

如果你需要更精确地检测特定类型,可以使用其他方式,例如:

  • 对于数组,可以使用 Array.isArray() 方法来检测:
console.log(Array.isArray(arr)); // true
  • 1
  • 对于函数,可以使用 typeof 运算符判断类型为 “function”:
console.log(typeof foo === 'function'); // true
  • 1
  • 对于日期,可以使用 instanceof 运算符来检测:
console.log(now instanceof Date); // true
  • 1
  • 对于正则表达式,也可以使用 instanceof 运算符进行检测:
console.log(pattern instanceof RegExp); // true
  • 1

通过使用这些更精确的方式,你可以在编程中正确地检测特定类型,避免 typeof 运算符可能引发的误解。

8. 浏览器前端缓存 协商缓存 强制缓存?

浏览器缓存策略可以分为强缓存和协商缓存。

  • 强缓存是指客户端向服务器发送的请求中包含了强缓存控制头,如“Cache-Control”和“Expires”等,服务器根据这些头中的信息来决定是否使用缓存数据。如果请求中包含了强缓存控制头,服务器将直接使用缓存数据,而不需要进行协商。

  • 协商缓存是指客户端向服务器发送的请求中不包含强缓存控制头,而是通过缓存协商的方式,由客户端和服务器之间进行协商,来确定是否使用缓存数据和缓存的有效期等信息。在协商缓存中,客户端会向服务器发送一个缓存请求,服务器根据请求中的信息来决定是否使用缓存数据,如果使用缓存数据,服务器也会返回一个缓存版本号给客户端,以便客户端判断是否需要更新缓存数据。

相比强缓存,协商缓存需要更多的网络交互和服务器资源,但是可以更好地利用缓存,避免缓存数据被重复使用,从而提高网站的性能和响应速度。

在实际应用中,强缓存和协商缓存可以结合使用,以实现更好的缓存控制效果。同时,需要注意缓存数据的安全和完整性,以避免缓存数据被篡改或失效。


强缓存:

服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。

  • 强缓存命中直接读取浏览器本地资源,在network中显示的是from memory 或者 from disk,如在微信小程序开发中,命中图片缓存,显示from disk。

  • 控制强缓存的字段有:Cache-Control(http1.1)和Expires(http1.0)

  • 协商缓存,让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,将缓存信息中的Etag和Last-Modified
    通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

  • Cache-control是一个相对时间,用以表达自上次请求正确的资源之后的多少秒的时间段内缓存有效。

  • Expires是一个绝对时间。用以表达在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求

  • Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。
    如果同时存在则使用Cache-control。

强缓存-expires:

  • 优势特点
    1、HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
    2、以时刻标识失效时间。
  • 劣势问题
    1、时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。
    2、存在版本问题,到期之前的修改客户端是不可知的。
    HTTP缓存都是从第二次请求开始的:

强缓存-cache-conche:

  • 优势特点
    1、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
    2、比Expires多了很多选项设置。
  • 劣势问题
    1、存在版本问题,到期之前的修改客户端是不可知的。

协商缓存:

  • 协商缓存的状态码由服务器决策返回200或者304
  • 当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。
  • 对比缓存在请求数上和没有缓存是一致的,但如果是 304 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。
  • 协商缓存有 2 组字段(不是两个),控制协商缓存的字段有:Last-Modified/If-Modified-since(http1.0)和Etag/If-None-match(http1.1)
  • Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;Etag/If-None-match表示的是服务器资源的唯一标
    识,只要资源变化,Etag就会重新生成。
  • Etag/If-None-match的优先级比Last-Modified/If-Modified-since高。

协商缓存-协商缓存-Last-Modified/If-Modified-since

  • 1.服务器通过 Last-Modified 字段告知客户端,资源最后一次被修改的时间,例如 Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  • 2.浏览器将这个值和内容一起记录在缓存数据库中。
  • 3.下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段
  • 4.服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。

优势特点

  • 1、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。
    劣势问题
    1、只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。
  • 2、以时刻作为标识,无法识别一秒内进行多次修改的情况。 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
  • 3、某些服务器不能精确的得到文件的最后修改时间。
  • 4.、如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。

协商缓存-Etag/If-None-match

为了解决上述问题,出现了一组新的字段 Etag 和 If-None-Match

Etag 存储的是文件的特殊标识(一般都是 hash 生成的),服务器存储着文件的 Etag 字段。之后的流程和 Last-Modified 一致,只是 Last-Modified 字段和它所表示的更新时间改变成了 Etag 字段和它所表示的文件 hash,把 If-Modified-Since 变成了 If-None-Match。服务器同样进行比较,命中返回 304, 不命中返回新资源和 200。

浏览器在发起请求时,服务器返回在Response header中返回请求资源的唯一标识。在下一次请求时,会将上一次返回的Etag值赋值给If-No-Matched并添加在Request Header中。服务器将浏览器传来的if-no-matched跟自己的本地的资源的ETag做对比,如果匹配,则返回304通知浏览器读取本地缓存,否则返回200和更新后的资源。
Etag 的优先级高于 Last-Modified。

优势特点

  • 1、可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
  • 2、不存在版本问题,每次请求都回去服务器进行校验。
    劣势问题
  • 1、计算ETag值需要性能损耗。
  • 2、分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时现ETag不匹配的情况。

第一次请求资源时,服务器返回资源,并在response header中回传资源的缓存策略;

第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。

9. react hooks

React Hooks 是 React 库中一种功能强大的特性,引入自 React 16.8 版本。它们提供了一种函数式的方式来编写 React 组件,并且解决了之前类组件中存在的一些问题。

使用 React Hooks,你可以在函数组件中使用状态(State)和其他 React 特性,而不再需要使用类组件。它们使得在无需编写类的情况下,可以轻松地管理组件的状态和生命周期方法,使组件的逻辑更加简洁、可复用和易于测试。

以下是一些常用的 React Hooks:

  • useState;
  • useEffect
  • useContext
  • useReducer等

10. 是否了解node.js

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,用于在服务器端运行 JavaScript 代码。它提供了一组强大的特性和 API,使得开发者可以构建高性能的网络应用程序。

以下是 Node.js 的一些关键特点和优势:

1.非阻塞式、事件驱动的 I/O:

Node.js 使用事件驱动的方式处理 I/O 操作,使得应用程序能够以非阻塞的方式处理大量并发请求。这种设计使得 Node.js 在处理高并发的场景下表现出色,并具有较低的资源消耗。

2.单线程:

Node.js 使用单线程模型处理请求,但它通过利用异步和非阻塞的特性,可以处理大量并发连接。此外,Node.js 还提供了一些工具和模块来充分利用多核系统。

3.轻量高效:

Node.js 以 JavaScript 作为开发语言,并且专注于高效性能。它采用了 V8 引擎,该引擎是由 Google Chrome 使用的 JavaScript 引擎,具有快速的执行速度。

4.跨平台:

Node.js 可以运行在多个操作系统平台上,包括 Windows、Mac OS 和 Linux。这使得开发人员可以在不同的环境中构建和部署 Node.js 应用程序。

5.丰富的模块生态系统:

Node.js 拥有庞大的模块生态系统,即 npm(Node Package Manager)。开发人员可以使用 npm 安装和管理各种开源模块,从而快速构建复杂的应用程序。

Node.js 可以用于构建各种类型的应用程序,例如 Web 服务器、实时聊天应用、API 服务、后端服务和命令行工具等。它已经成为流行的选择,因为它简化了前后端开发的一致性,并提供了高效的开发工具和架构。

同时,Node.js 的活跃社区和丰富的文档资源,为开发人员提供了广泛的支持和解决方案,使得 Node.js 成为现代 Web 开发的重要工具之一。

11. 是否了解git

Git 是一种分布式版本控制系统,用于跟踪文件和目录的变化,并协同多个开发者在同一个项目上进行合作开发。Git 可以记录文件的修改历史、分支管理、合并代码等,它提供了一套命令行工具和一系列的操作来管理代码仓库。

下面是一些常用的 Git 命令及其作用:

  • git init:在当前目录初始化一个新的 Git 仓库,创建一个空的 Git 仓库或重新初始化一个已存在的仓库。

  • git clone [url]:克隆远程仓库到本地,创建一个本地副本用于开发和版本控制。

  • git add [file]:将文件或目录添加到 Git 的暂存区,准备将其提交到版本控制。

  • git commit -m “[message]”:提交暂存区中的文件或目录变更,并添加一条描述性的提交消息。

  • git status:查看当前工作目录的文件状态,显示已修改、已暂存或未跟踪的文件。

  • git diff:显示当前工作目录与暂存区之间的文件差异。

  • git log:显示提交历史记录,包括提交作者、日期、提交消息等信息。

  • git branch:列出当前仓库的所有分支,显示当前所在的分支。

  • git checkout [branch]:切换到指定的分支,可以是已存在的分支或新建的分支。

  • git merge [branch]:将指定分支的代码合并到当前分支,用于合并不同分支的代码更改。

  • git push [remote] [branch]:将本地的提交推送到远程仓库,将本地分支的更改上传到远程仓库。

  • git pull [remote] [branch]:从远程仓库拉取最新的更改并合并到当前分支,用于更新本地代码。

  • git remote add [name] [url]:将远程仓库添加到本地仓库的远程仓库列表中。

  • git rm [file]:从版本控制中删除文件,并将删除操作提交到版本历史记录中。

以上只是 Git 命令的一小部分,Git 提供了更多的命令和选项,用于支持不同的版本控制操作和工作流程。熟悉这些基本的 Git 命令,可以帮助你进行代码版本控制、分支管理、协作开发以及解决代码冲突等任务。

12. 实现React计数器

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const decrement = () => {
    setCount(count - 1);
  };

  return (
    <div>
      <h1>Counter</h1>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
}

export default Counter;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在上面的代码中,我们使用了 React 的函数式组件和 useState 钩子来实现计数器。useState 用于在函数组件中声明和管理状态,其中 count 是状态变量,setCount 是更新状态的函数。初始状态为 0。

通过 increment 函数和 decrement 函数,我们可以分别增加和减少计数器的值。点击按钮时,它们会调用对应的函数,并通过 setCount 更新 count 的值。

在组件的返回部分,我们渲染了一个标题和计数器的当前值。通过 {count} 将状态值显示在页面上。还创建了两个按钮,分别绑定了 increment 和 decrement 函数,用于增加和减少计数器的值。

13. 实现reactive函数

前端中的 Reactive 函数是指具有响应式能力的函数,它们能够根据输入数据的变化自动更新其计算结果,并将更新后的结果传递给相关的组件或其他依赖项。

在 React 中,通常使用 React Hooks 中的 useState、useEffect 和 useMemo 等钩子函数来实现响应式行为。下面是一个简单的示例,展示了如何使用 useState 和 useEffect 创建一个响应式的函数:

import React, { useState, useEffect } from 'react';

function ReactiveFunction() {
  const [inputValue, setInputValue] = useState('');
  const [result, setResult] = useState('');

  useEffect(() => {
    const calculateResult = () => {
      // 在这里执行计算逻辑
      const calculatedResult = inputValue.toUpperCase();
      setResult(calculatedResult);
    };

    calculateResult();
  }, [inputValue]);

  const handleInputChange = (event) => {
    setInputValue(event.target.value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={handleInputChange} />
      <p>Result: {result}</p>
    </div>
  );
}

export default ReactiveFunction;
  • 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

在上面的代码中,我们定义了一个 ReactiveFunction 组件,其中使用了两个状态变量 inputValue 和 result,分别用于保存输入的值和计算的结果。

通过 useEffect 钩子函数,我们创建了一个副作用函数,并在依赖项数组 [inputValue] 发生变化时执行。副作用函数中执行了计算逻辑,将输入值转换为大写,并通过 setResult 更新 result 的值。

在组件的返回部分,我们渲染了一个文本输入框,通过 value 和 onChange 属性将输入值与 inputValue 进行绑定,并通过 handleInputChange 处理输入变化的事件。最后,我们展示了计算结果。

当用户在输入框中输入内容时,inputValue 的值发生变化,触发 useEffect 中的副作用函数重新计算结果,并更新 result 的值。这样,我们就实现了一个简单的前端响应式函数。

这只是一个简单的示例,实际应用中可以根据需求进行更复杂的响应式函数的设计和实现。

14. 实现tooltip css样式(这三个都是写代码的题了)

要在前端实现 Tooltip(工具提示),你可以使用 CSS 样式和一些简单的 HTML 结构来创建。

下面是一个使用纯 CSS 实现 Tooltip 的示例:

HTML 结构:

<div class="tooltip-container">
  <span class="tooltip-trigger">Hover me</span>
  <div class="tooltip-content">
    This is the tooltip message.
  </div>
</div>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

CSS 样式:

.tooltip-container {
  position: relative;
  display: inline-block;
}

.tooltip-content {
  display: none;
  position: absolute;
  background-color: #333;
  color: #fff;
  padding: 5px;
  border-radius: 4px;
  z-index: 1;
}

.tooltip-trigger:hover + .tooltip-content {
  display: block;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 在上面的示例中,我们首先创建了一个包含触发元素和提示内容的容器。.tooltip-container 类用于定位容器,设置其相对定位。

  • .tooltip-content 类定义了 Tooltip 的样式,其中 display: none; 将其隐藏起来,position: absolute; 将其定位到触发元素的上方,background-color、color、padding 和 border-radius 等属性用于设置样式。

  • 最后,使用 .tooltip-trigger:hover + .tooltip-content 选择器,当鼠标悬停在触发元素上时,将显示提示内容。

  • 通过以上的 HTML 结构和 CSS 样式,当鼠标悬停在触发元素上时,Tooltip 的内容会显示出来。你可以根据需要修改样式,以适应你的项目需求。

当然,还有其他的实现 Tooltip 的方式,比如使用 JavaScript 或使用第三方库,它们提供了更多的功能和定制选项。但是使用纯 CSS 实现是一种简单且常见的方式,适用于基本的 Tooltip 需求。

三面

1. 还是先问了我之前的实习经历,问我从之前的实习经历中获得了什么

当介绍前端实习经历时,你可以按照以下结构组织你的介绍:

  • 公司和项目背景:首先,简要介绍你实习的公司和项目的背景。说明公司是什么样的组织,它的规模和业务领域。然后,解释你所参与的具体项目是什么,它的目标和重要性。

  • 实习职责和贡献:说明你在实习期间承担的职责和贡献。列举你参与的具体任务和项目模块,以及你在其中所做的工作。强调你的工作是如何与团队协作和项目目标对齐的。

  • 技术栈和工具:列出你在实习期间使用的主要技术栈和工具。包括编程语言、框架、库和其他相关技术。说明你在实习中使用这些技术的原因以及它们在项目中的作用。

  • 面对的挑战和解决方案:描述你在实习期间面对的挑战和困难。这可以是技术上的问题、与团队合作的挑战或时间管理等方面的挑战。说明你是如何解决这些挑战的,并分享你从中学到的经验和教训。

  • 学习和成长:分享你在实习期间学到的知识和技能。说明你在实习中的成长和进步,包括技术能力、沟通技巧和团队协作能力等方面。举例说明你如何应用新学到的知识解决问题或改进项目。

  • 成果和反馈:列举你在实习期间取得的具体成果。这可以是你完成的功能模块、性能优化、用户界面改进或其他具体项目成果。分享你收到的正面反馈或奖励,以展示你的工作受到认可。

  • 总结和自我评价:总结你的实习经历,强调你对前端开发的热情和承诺。自我评价你在实习中的表现,并提出你未来的学习和发展目标。

确保在介绍实习经历时,突出你的个人贡献和团队合作能力。使用具体的例子和数据来支持你的介绍,并与面试官保持良好的沟通。准备好回答与实习经历相关的问题,展示你的学习能力和适应能力。

2. 然后问我为什么离开了上一段实习

3. 之前的mentor/leader对我的评价

4. 给了我一套编程,问我看出有什么问题

5. 又给了几个程序问我实现过程

6. CSS选择器权重计算规则?

在 CSS 中,不同的选择器和规则可能会同时应用于同一个元素,因此需要根据规则的权重来确定应用的优先级。

CSS 权重计算规则如下,按照优先级从高到低排列:

  • !important:具有 !important 标记的样式具有最高优先级,会覆盖其他样式。
    内联样式:使用 style 属性直接在 HTML 元素中定义的样式具有较高优先级。
  • ID 选择器:通过元素的 ID 属性选择元素的样式。例如,#myElement。
  • 类选择器、属性选择器和伪类选择器:通过类名、属性或伪类选择元素的样式。例如,.myClass、[type=“text”]、:hover。
  • 元素选择器和伪元素选择器:通过元素类型或伪元素选择元素的样式。例如,div、::before。
  • 通用选择器和子选择器:通用选择器 * 和子选择器 > 拥有较低的优先级。
    如果多个规则具有相同的权重,则最后定义的规则将覆盖之前的规则。当样式冲突时,具有更高优先级的规则将被应用。

还有一些特殊情况需要考虑:

  • 继承:一些属性是可以继承的,子元素会继承父元素的样式。但是如果子元素自身定义了具有相同属性的样式,则会覆盖继承的样式。

  • 嵌套选择器:如果选择器是嵌套的,内部选择器的优先级将比外部选择器的优先级高。

理解 CSS 权重计算规则对于正确应用样式和处理样式冲突非常重要,可以帮助我们控制样式的优先级和应用效果。在编写 CSS 样式时,应根据具体需求和设计来选择合适的选择器和规则,避免样式冲突和意外的样式覆盖。

7. 实习的时间和目标

四面

面试官首先介绍了自己是hr(我之前也了解到字节一般会把最后一面安排是hr面)

1. 为什么做前端

2. 未来的规划

3. 实习的收获

前面有提到。

4. 学习的途径

确认了我的入职时间,实习时长这些问题

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/505494
推荐阅读
相关标签
  

闽ICP备14008679号