一名前端双非学生的大厂面试之旅(腾讯,字节,快手,网易,百度)

8,862 阅读34分钟

前言

两个月前,本以为我暑期实习的路程已经到站了,当时已经拿了同程的offer了,自己还特地写了一篇求职的文章来记录和分享当时的面经哈哈。

2023年一位大三学生的一些中大厂的面试题分享(包含干货分享) - 掘金 (juejin.cn)

玩了两个月之后,发现身边的人好像在陆陆续续地接到了大厂的面试以及又想到当时面同程的时候,自己之前准备的手写和算法好像还没用武之地,就也想投一投简历试一试,反正有个保底的offer,结果怎么样不在乎了。最后真的让我意外的拿了网易的offer了哈哈

记录

快手:两次一面挂😅

腾讯:二面挂

蔚来:一面offer

网易:二面offer

字节: 一面挂

小红书: 二面拒(因为当时网易意向了)

得物: 一面挂(电话面的以后一律不面了,就是刷KPI)

快手

5.29一面

常见的git操作

git branch 查看本地所有分支
git status 查看当前状态
git commit 提交
git branch -a 查看所有的分支
git branch -r 查看远程所有分支
git commit -am "init" 提交并且加注释
git remote add origin git@192.168.1.119:ndshow
git push origin master 将文件给推到服务器上
git remote show origin 显示远程库 origin 里的资源
git push origin master:develop
git push origin master:hb-dev 将本地库与服务器上的库进行关联
git checkout --track origin/dev 切换到远程 dev 分支
git branch -D master develop 删除本地库 develop
git checkout -b dev 建立一个新的本地分支 dev
git merge origin/dev 将分支 dev 与当前分支进行合并
git checkout dev 切换到本地 dev 分支
git remote show 查看远程库
git add .
git rm 文件名(包括路径) 从 git 中删除指定文件
git clone git://github.com/schacon/grit.git 从服务器上将代码给拉下来
git config --list 看所有用户
git ls-files 看已经被提交的
git rm [file name] 删除一个文件
git commit -a 提交当前 repos 的所有的改变
git add [file name] 添加一个文件到 git index
git commit -v 当你用-v 参数的时候可以看 commit 的差异
git commit -m "This is the message describing the commit" 添加 commit 信息
git commit -a -a 是代表 add,把所有的 change 加到 git index 里然后再 commit
git commit -a -v 一般提交命令
git log 看你 commit 的日志
git diff 查看尚未暂存的更新
git rm a.a 移除文件(从暂存区和工作区中删除)
git rm --cached a.a 移除文件(只从暂存区中删除)
git commit -m "remove" 移除文件(从 Git 中删除)
git rm -f a.a 强行移除修改后文件(从暂存区和工作区中删除)
git diff --cached 或 $ git diff --staged 查看尚未提交的更新
git stash push 将文件给 push 到一个临时空间中
git stash pop 将文件从临时空间 pop 下来
git rebase 是Git中用于将一个分支的修改应用到另一个分支上的命令。

当然光了解git基本操作不够,你最好还得准备解释一下什么是git,分为哪几个区

git冲突

  • 如果在远程的某一个文件内容发生修改了,而本地没有进行 pull 拉取,就会导致本地的分支落后,当修改完成之后 push 到远程的时候,就会产生冲突

解决

  1. 先将本地修改存储起来
$ git stash
  1. 暂存了本地修改之后,就可以 pull 了
$ git pull
  1. 还原暂存的内容
  $ git stash pop 
  //  git stash pop
  1. 再提交并push到远程即可

基本数据类型

- 存在着7种原始值
        - boolean
        - null
        - undefined
        - number
        - string
        - Symbol
        - Bigint
        // BigInt 类型使用 n 作为后缀来表示,例如 123456789012345678901234567890n 表示一个 30 位的大整数。

类型检测方法

- typeof 是否能正确判断类型?  => 简单数据
    - 对于原始类型来说,除了``null``都可以调用typeof显示正确的类型
    ```
    typeof 1 // 'number'
    typeof '1' // 'string'
    typeof undefined // 'undefined'
    typeof true // 'boolean'
    typeof Symbol() // 'symbol'
    typeof null // object
    ```
    - 对于引用数据类型,除了``函数``之外,都会显示object
    ```
    typeof [] // 'object'
    typeof {} // 'object'
    typeof console.log // 'function'
    ```
    - 对于NaN也不行
    typeof NaN -> number
    
    因此采用typeof判断对象数据类型是不合适的

    - 判断不了null的原因
        在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0typeof null 也因此返回 "object"。

- instanceof
    原理是基于原型链的查询,只要处于原型链中,判断永远为true
    > ``实例对象`` instanceof  ``构造函数``  
    ```
    const Person = function() {}
    const p1 = new Person()
    p1 instanceof Person // true

    var str1 = 'hello world'
    str1 instanceof String // false

    var str2 = new String('hello world')
    str2 instanceof String // true
    ```
    缺陷:
        不能检测基本数据类型
        原型链可能被修改,导致检测结果不准确
        只要能在原型链上找到构造函数,就返回 true,所以类型可能不准确

- Object.prototype.toString.call()  => 引用类型  内置的子类 Date Regexp 

    Object.prototype.toString.call(num),  // '[object Number]'
    Object.prototype.toString.call(str),  // '[object String]'
    Object.prototype.toString.call(bool),  // '[object Boolean]'
    Object.prototype.toString.call(arr),  // '[object Array]'
    Object.prototype.toString.call(obj),  // '[object Object]'
    Object.prototype.toString.call(func),  // '[object Function]'
    Object.prototype.toString.call(und),  // '[object Undefined]'
    Object.prototype.toString.call(nul),  // '[object Null]'
    Object.prototype.toString.call(date),  // '[object Date]'
    Object.prototype.toString.call(reg),  // '[object RegExp]'
    Object.prototype.toString.call(error)  // '[object Error]'

原型链

记住这张图即可 image.png

数组常见的方法

1. 操作(增删改查)
    - 增:push  unshift  splice concat
    - 删:pop  shift  splice  slice
    - 改: splice
    - 查: indexof  includes  find

2. 排序
    - sort()
    - reverse()

3. 转换
    - join()  数组转字符串
4. 迭代
    - forEach()
    - filter
    - map
    - some
    - every

围绕增删改查的思路去答即可

介绍一下promise

// 首先可以介绍promise的产生
  Promise 是异步编程的一种新的解决方案,比以往通过回调函数和事件来解决异步操作更加强大。
  
// 其次就是promise的基本使用
  Promise 是一个构造函数,我们可以用它来生成 Promise 实例,并且该构造函数接受一个函数作为参数
  
// 特点
Promise的三种状态:

pending :进行中,表示 Promise 还在执行阶段,没有执行完成。

fulfilled:成功状态,表示 Promise 成功执行完成。

rejected:拒绝状态,表示 Promise 执行被拒绝,也就是失败。

Promise 的状态,只可能是其中一种状态,从进行中变为成功或失败状态之后,状态就固定了,不会再发生改变。

另外你这里可以再介绍一下promsie有哪些方法

介绍一下react hook

  • React Hooks的几个常用钩子
    1. useState() 状态钩子
    2. useContext() 共享状态钩子
    3. useReducer() action钩子
    4. useEffect() 副作用钩子
    5. useCallback() 缓存了函数自身
    6. useMemo() 缓存了函数的返回值

useCallback 和 useMemo的区别

前者缓存的是函数自身,后者是缓存的函数结果

promise的方法

  • promsie的方法

    • catch方法

      catch是用于指定发生错误的回调函数。

    new Promise((resolve, reject) => {
        reject('失败');
    }).catch(error => {
        console.log(error); // 失败
    });
    
    • Promise的finally方法

    finally方法用于指定不管Promis对象最后状态如何,都会执行的操作。

    new Promise((resolve, reject) => {
        resolve();
    }).then(res => {
        console.log('success');
    }).catch(error => {
        console.log('error');
    }).finally(() =>{
        console.log('finally');
    })
    
    • Promise的all方法

      Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。在all方法中可以传递多个Promise对象,当所有的Promise对象状态都返回fufilled,才会返回fulfilled,否则返回rejected。

    const promise1 = new Promise((resolve, reject) => {
        resolve();
    })
    onst promise2 = new Promise((resolve, reject) => {
         resolve();
    })
    const promise3 = new Promise((resolve, reject) => {
        resolve();
    })
    
    const promiseAll = Promise.all([promise1, promise2, promise3]).then(res => {
    console.log('all');
    })
    
    • Promise的race方法

      Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。可以传递多个Promise对象作为参数,如果实例红有一个实例率先改变状态,那么race的状态就会跟着改变。

    const promise1 = new Promise((resolve, reject) => {
        reject();
    })
    const promise2 = new Promise((resolve, reject) => {
        resolve();
    })
    const promise3 = new Promise((resolve, reject) => {
        reject();
    })
    
    const promiseRace = Promise.race([promise1, promise2, promise3]).then(res => {
        console.log('race then');
    }).catch(error => {
        console.log('race catch');
    })
    
    • Promise的any方法

      要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。

    const p = Promise.any([p1, p2, p3]);
    
    • Promise.allSettled()

    接受多个Promise对象 数组形式传递参数,所有的Promise都敲定状态时就返回成功结果,无论结果是fulfilled还是rejected都会在then获取

手写一个promise.all

const promiseAll = (array) => {
    if (!Array.isArray(array)) {
        throw new Error('要传入数组')
    }
    return new Promise((resolve, reject) => {
        let result = [];
        let count = 0;
        array.forEach((item, index) => {
            if (item instanceof Promise) {
                item.then(res => {
                    result[index] = res
                    count++;
                    if (count == array.length) {
                        return resolve(result)
                    }
                },
                    err => reject(err))
            }
            else {
                result[index] = item
                count++;
                if (count == array.length) {
                    return resolve(result)
                }
            }
        })
    })
}

let p1 = new Promise((res, err) => {
    setTimeout(() => {
        res(100)
    }, 3000)
})

let p2 = new Promise((res, err) => {
    setTimeout(() => {
        res(200)
    }, 2000)
})

let p3 = new Promise((res, err) => {
    setTimeout(() => {
        res(300)
    }, 5000)
})


promiseAll([p1, p2, p3]).then(res => console.log(res), err => console.log(err))

有兴趣的朋友可以参考我的这篇文章

别看了,进来一起手写一个Promise(all,race)吧 - 掘金 (juejin.cn)

输入url到浏览器渲染的过程

八股经典中的经典,开始吟唱~

在不考虑用户输入搜索关键字的情况:

  • 用户输入 url 并回车

  • 浏览器(进程)检测 url,组装协议,构成完整的 url发送给网络进程。

  • 网络进程接收到 url 请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程

  • 如果没有,网络进程向 web 服务器发起 http 请求

    1. 进行 DNS 解析域名(url地址的别名),获取服务器 ip 地址,端口(进程的位置) 先到本地域名服务器 =》 根域名服务器 =》 顶级域名服务器

      例如,www.wikipedia.org是一个域名,和IP地址208.80.152.2相对应。DNS就像是一个自动的电话号码簿,我们可以直接拨打wikipedia的名字来代替电话号码(IP地址)

    2. 利用 ip 地址和服务器建立 tcp 连接 三次握手:
      1. 建立握手请求
      2. 服务器向客户端做应答
      3. 客户端确定自己接受到了应答
    3. 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容 四次挥手:
      1. 客户端向服务端发起断开连接的请求
      2. 服务端收到关闭的请求,把未发完的数据发送完
      3. 服务端向客户端发送释放连接请求
      4. 客户端收到释放连接请求,断开连接
  • 网络进程解析响应流程;

    1. 检查状态码,如果是 301/302,则需要重定向,从 响应头中的Location 自动中读取地址,重新进行第三步,如果是 200,则继续处理请求。

    2. 200 响应处理: 检查响应类型 Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续的渲染,如果是 html 则通知浏览器进程准备进行渲染。

  • 进入渲染流程

    渲染页面主要做的事:

  • 将浏览器无法直接理解和使用的HTML,转换为浏览器能够理解的结构--DOM 树。

  • 把 CSS 转换为浏览器能够理解的结构--styleSheets,并转换样式表中的属性值,使其标准化,计算出 DOM 树中每个节点的具体样式(根据继承规则和层叠规则)。

  • 确定DOM 元素的几何位置信息--布局树,遍历 DOM 树中的所有可见节点,加入到布局树(display:none不包含),并计算布局树节点的坐标位置。

  • 如果页面有复杂的效果,如常见的页面滚动,或者使用 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

  • 图层绘制,把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表(联想自己画画)。

  • tiles:将图层转换成图块。

  • 光栅化:通过进程实现图块转换成位图。

  • display:浏览器进程拿到DrawQuad信息生成页面显示。

介绍变量提升以及let const var 的区别

  • 什么是变量提升

通俗来说,变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值为 undefined。

  • let,const 和 var 的区别

    • var 声明的变量会挂载在 window 上,而 letconst 声明的变量不会

    • var 声明变量存在变量提升,letconst 不存在变量提升

    • letconst 声明形成块级作用域

      
      if(1){
      var a = 100;
      let b = 10;
      }
      
      console.log(a); // 100
      console.log(b)  // 报错:b is not defined  ===> 找不到b这个变量
      
      -------------------------------------------------------------
      
      if(1){
      var a = 100;
      const c = 1;
      }
      console.log(a); // 100
      console.log(c)  // 报错:c is not defined  ===> 找不到c这个变量
      
    • 暂时性死区 ,凡是在声明变量之前使用变量就会报错

       if (true) {
         // 死区开始
         lzp = 'lut'; //  ReferenceError
         console.log(lzp); //  ReferenceError
      
         // 开始声明变量,死区结束
         let lzp; 
         console.log(lzp); // undefined
      
         lzp = 520;
         console.log(lzp); // 520
       }
      
      
    • const 变量不能修改指针(栈中的内存),但是可以修改值,比如我们定义一个对象,我们可以修改对象里面的值。但用 const 定义简单数据类型,值不能修改。

小结

当时面试的时候,在手写promise.all这里准备不充分翻了车,得到面试官的评价是:你基础感觉不错,但是又好像没那么好。。。。

两天后就感谢信了。。。

6.9一面

没错,换了个部门我又来了

与寒假的实习有关

  • 实习的项目

  • tailwind的好处

  • Zustand的好处

  • redux 和 Zustand的区别及其底层原理

  • 介绍一下websocket协议

    HTML5 开始提供的一种浏览器服务器进行全双工通讯的网络技术,属于应用层协议。它基于 TCP 传输协议,并复用 HTTP 的握手通道。

    websocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

  • 项目里面的懒加载如何实现的

  • 介绍egg.js

Koa的原理

Koa是一个基于Node.js的Web框架,它的核心思想是“中间件”(middleware)。Koa的中间件机制可以将一组处理请求和响应的函数按照顺序依次执行,每个中间件函数都可以对请求和响应进行处理,并可以选择将处理结果传递给下一个中间件

这里的说法很显然是满足面试官的,这里还需要自己后面去看一下koa的源码

移动端适配如何做

  • rem + 媒体查询
  • rem + flexble.js
  • vh + vw
  • 使用媒体查询来针对不同屏幕尺寸设置不同的flex属性值,从而改变弹性项目的宽度和高度。
@media screen and (max-width: 768px) {
  .item {
    flex-basis: 50%; /* 宽度占据一半 */
  }
}

@media screen and (max-width: 480px) {
  .item {
    flex-basis: 100%; /* 宽度占据整个容器 */
  }
}

如何实现判断一个元素是否在可视区中

  • 利用常见属性

    1.window.innerHeight 是浏览器可视区的高度;

    2.document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离;

    3.imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离);

    4.内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop;

  • 利用api getBoundingClientRect

如果一个元素在视窗之内的话,那么它一定满足下面四个条件:

top 大于等于 0
left 大于等于 0
bottom 小于等于视窗高度
right 小于等于视窗宽度
function isInViewPort(element) {
  const viewWidth = window.innerWidth || document.documentElement.clientWidth;
  const viewHeight = window.innerHeight || document.documentElement.clientHeight;
  const {
    top,
    right,
    bottom,
    left,
  } = element.getBoundingClientRect();

  return (
    top >= 0 &&
    left >= 0 &&
    right <= viewWidth &&
    bottom <= viewHeight
  );
}

介绍一下git,以及常见的git操作

上面讲过了,略~

git冲突

git的场景:使用 git 产生冲突的条件:如果在远程的某一个文件内容发生修改了,而本地没有进行 pull 拉取,就会导致本地的分支落后,当修改完成之后 push 到远程的时候,就会产生冲突怎么解决?

也和上面一样~

git rebase

git rebase是Git版本控制系统中一个非常常用的命令,主要用于将一个分支的修改应用到另一个分支上。具体来说,它可以将一个分支上的提交记录“重演”在另一个分支上,从而使两个分支的提交记录更加清晰、简洁。

作用域的种类

  • 块级作用域
  • 全局作用域
  • 函数作用域

口撕promise实现

可以参考这篇文章

🤪🤪别看了,进来一起手写一个promise吧 - 掘金 (juejin.cn)

代码题1

image.png

也是经典题了,这里面试官大概这几个问题:

  • 这里的代码输出是什么

    6 6 6 6 6

  • 怎么改成让它打印1,2,3,4,5?

    1. let
    2. 用自执行函数
     for (var i = 1; i <= 5; i++) {
         (function (i) {
             setTimeout(function () {
                 console.log(i);
            }, i * 1000);
         })(i);
     }
    

代码题2,3

有点不记得了,大概是一题eventloop的题和一道算法题

小结

本以为这一面可以过的,因为就连我最头疼的一个算法题都OK了,没想到最后还是挂了,可能是之前一两个小的地方答太快了答错了?

腾讯

一面

介绍闭包,闭包能解决什么问题

闭包

在 js 执行引擎中,一个函数被执行完毕后该函数的执行上下文就会被垃圾回收,但是,当一个函数内部的函数返回出来执行,那么内部函数对外部函数中的变量存在引用时,引用的这些变量的集合称之为闭包,这些集合不会随外部函数的执行上下文的回收而消失。

简单来说,就是父函数嵌套子函数,子函数内部引用了父函数的变量,引用的这些变量集合称之为闭包

解决的问题(应用场景)

  • 结果缓存

    如果函数调用处理耗时,我们可以将结果在内存中缓存起来,下次执行时,若存在内存中,则直接返回,提升执行效率。

    let cacheObj = function(){
    let cache = {} // 缓存对象
    return {
        search:function(key){
        if(key in cache){
            // 存在缓存中,直接返回
            return cache[key]
        }
        // 耗时的函数处理
        let result = dealFn(key)
        // 更新缓存结果
        cache[key] = result
        return result
        }
    }
    }
    let cacheBox = cache()
    cacheBox.search(1)
    
  • 防抖节流中

  • 模块化

参考文章

let const 区别

const为什么让数据不可变

在JavaScript中,变量的值可以随时被修改,因此在编写大型应用程序时,如果没有对变量赋值进行限制,可能会导致代码的可维护性和可读性变差。而通过使用const关键字声明常量。

移动端适配

同上

手写flexible.js 或者说设计它有什么思路吗

  • 大致原理

    flexible.js用来处理移动端各种设备兼容问题。也就是怎么解决移动端尺寸众多的问题,我们的设计稿是固定,怎么办,如果设计稿是弹性的可以随意缩放该多好,想想,有办法了,就像本来你在一张大的纸上面了一副画,现在让你在小的纸上在画一次要怎么画,就是所有东西都等比例画小,如果要画到更大的纸上也是一个道理,等比画大,对不对。

    现在我们把设计稿分成10等份,设计稿 A = W/10,我们把设备可视区域也就是我们的各种移动端设备的这个画布也分成10份,并赋值给根元素的fontSize,我们都知道rem是根据根元素字体大小计算的,所以我们的1rem也就是设备可视区域/10,现在设计稿上有一块区域宽B,那它是不是等比放到设备可视区域的宽度为 B/A rem。再啰嗦一下,B在设计稿上占B/A份,那在设备可视区域上也要占B/A份对不对,所以宽是B/A rem

// 首先是一个立即执行函数,执行时传入的参数是window和document
(function flexible (window, document) {
  var docEl = document.documentElement  // 返回文档的root元素
  var dpr = window.devicePixelRatio || 1 // 获取设备的dpr,即当前设置下物理像素与虚拟像素的比值
 
  // adjust body font size 设置默认字体大小,默认的字体大小继承自body
  function setBodyFontSize () {
    if (document.body) {
      document.body.style.fontSize = (12 * dpr) + 'px'
    }
    else {
      document.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  setBodyFontSize();
 
  // set 1rem = viewWidth / 10
  function setRemUnit () {
    var rem = docEl.clientWidth / 10
    docEl.style.fontSize = rem + 'px'
  }
 
  setRemUnit()
 
  // reset rem unit on page resize
  window.addEventListener('resize', setRemUnit)
  window.addEventListener('pageshow', function (e) {
    if (e.persisted) {
      setRemUnit()
    }
  })
 
  // detect 0.5px supports  检测是否支持0.5像素,解决1px在高清屏多像素问题,需要css的配合。
  if (dpr >= 2) {
    var fakeBody = document.createElement('body')
    var testElement = document.createElement('div')
    testElement.style.border = '.5px solid transparent'
    fakeBody.appendChild(testElement)
    docEl.appendChild(fakeBody)
    if (testElement.offsetHeight === 1) {
      docEl.classList.add('hairlines')
    }
    docEl.removeChild(fakeBody)
  }
}(window, document))

什么是设备像素比

  • 逻辑(视觉)像素

    浏览器内的一切长度都是以CSS像素为单位的,CSS像素的单位是px,px是一个相对单位,相对的是物理像素(device pixel),其相对性体现在在同一个设备上或在不同设备之间每1个px所代表的物理像素是可以变化的 页面缩放比为1,css中1px = 1 物理像素

  • 物理像素

    设备像素(物理像素),顾名思义,显示屏是由一个个物理像素点组成的,通过控制每个像素点的颜色,使屏幕显示出不同的图像,屏幕从工厂出来那天起,它上面的物理像素点就固定不变了,单位pt。

  • 设备像素比(dpr 描述的是未缩放状态下,物理像素逻辑像素的初始比例关系) 以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV元素宽度为 414px,这个 DIV 就会填满手机的宽度;

而如果有一把尺子来实际测量这部手机的物理像素,实际为 1242*2688 物理像素;经过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素=3个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。

dpr = 1 : 1,css 1px = 1物理像素
dpr = 2 : 1,css 1px = (2 * 2)个物理像素
dpr = 3: 1,css 1px = (3 * 3) 个物理像素

介绍ES6

多围绕一下es6简单聊

let const
promise
箭头函数
class类
 S6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

 - 传统方式

 ```
 function Point(x, y) {
 this.x = x;
 this.y = y;
 }

 Point.prototype.toString = function () {
 return '(' + this.x + ', ' + this.y + ')';
 };

 var p = new Point(1, 2);
 ```

 - class 方式

   ```
   class Point {
   constructor(x, y) {
       this.x = x;
       this.y = y;
   }

   toString() {
       return '(' + this.x + ', ' + this.y + ')';
   }
   }
   ```
  • 模板字符串
     在 es6 之前,如果我们要拼接字符串,则需要像这样:
     var name = 'kris'
     var age = 24
     var info = 'My name is ' + this.name + ', I am ' + this.age

     但是在 es6 之后,我们只需要写成以下形式
     const name = 'kris'
     const age = 24
     const info = `My name is ${name}, I am ${age}`
Set,Map 对象
  • Set是一种叫集合的数据结构,是由一堆无序的,相关联的,且不重复的内存结构组成的。

    • S6 提供了新的数据结构 Set,类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。
    • Set 实现数组去重
    const numbers = [2,3,4,4,2,3,3,4,4,5,5,6,6,7,5,32,3,4,5]
    console.log([...new Set(numbers)])
    // [2, 3, 4, 5, 6, 7, 32]
    

    Map 是一组键值对的结构

    // 初始化Map
    let map = new Map();
    
    // 添加key和value值
    map.set('Amy','女')
    
    // 是否存在key
    map.has('Amy') // true
    
    // 根据key获取value
    map.get('Amy') // 女
    
    
展开运算符
    数组合并
    let ary1 = [1, 2, 3];
    let ary2 = [4, 5, 6];
    let ary3 = [...ary1,...ary2];// [1, 2, 3, 4, 5, 6];
Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示唯一的值。

  • 应用场景
    1. 使用Symbol来作为对象属性名(key)
    let obj = {
      abc: 123,
      "hello": "world"
    }
    
    obj["abc"] // 123
    obj["hello"] // 'world'
    而现在,Symbol可同样用于对象属性的定义和访问:
    
    const PROP_NAME = Symbol()
    const PROP_AGE = Symbol()
    
    let obj = {
    [PROP_NAME]: "一斤代码"
    }
    obj[PROP_AGE] = 18
    
    obj[PROP_NAME] // '一斤代码'
    obj[PROP_AGE] // 18
    
    1. 代替const
    const TYPE_AUDIO = 'AUDIO'
    const TYPE_VIDEO = 'VIDEO'
    const TYPE_IMAGE = 'IMAGE'
    
Promise

promise解决什么问题

介绍回调异步函数的发展历程

回调函数 => promise

async await

  1. 函数前面使用 async 修饰
  2. 函数内部,promise 操作使用 await 修饰
async function wait(time){
    await sleep2(time);
    fun();
}

wait(3000)

如何利用async await实现合并请求

这里没答上来

防抖节流

可以参考我的这篇文章

✅✅包教包会——手写防抖节流 - 掘金 (juejin.cn)

url输入到页面渲染

上面已经提过

https的优点

可以从http的缺点回答

  • HTTP 存在的问题
    1. 可能被窃听

      1. HTTP 本身不具备加密的功能,HTTP 报文使用明文方式发送
      2. 由于互联网是由联通世界各个地方的网络设施组成,所有发送和接收经过某些设备的数据都可能被截获或窥视。(例如大家都熟悉的抓包工具:Wireshark)
    2. 认证问题

      1. 无法确认你发送到的服务器就是真正的目标服务器(可能是服务器伪装的)
      2. 无法确定返回的客户端是否是按照真实意图接收的客户端(可能是伪装的客户端)
      3. 无法确定正在通信的对方是否具备访问权限,Web 服务器上某些重要的信息,只想发给特定用户即使是无意义的请求也会照单全收。无法阻止海量请求下的 DoS 攻击(Denial of Service,拒绝服务攻击)。
    3. 可能被篡改

      请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击被称为中间人攻击

还有https的特点

  • 什么是 HTTPS

    应用层 超文本传输安全协议(英语:Hypertext Transfer Protocol Secure,缩写:HTTPS,常称为 HTTP over TLS,HTTP over SSL 或 HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

  • TLS/SSL 协议

    HTTPS 协议的主要功能基本都依赖于 TLS/SSL 协议,TLS/SSL 的功能实现主要依赖于三类基本算法:散列函数 、对称加密和非对称加密,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列函数验证信息的完整性。

    对发起 HTTP 请求的数据进行加密操作和对接收到 HTTP 的内容进行解密操作

  • 对称加密:对称加密是指加密和解密都使用的是相同的密钥。

    故密钥也容易被黑客破解

  • 非对称加密

    非对称加密算法有 A、B 两把密钥,如果你用 A 密钥来加密,那么只能使用 B 密钥来解密;反过来,如果你要 B 密钥来加密,那么只能用 A 密钥来解密。

    在 HTTPS 中,服务器会将其中的一个密钥通过明文的形式发送给浏览器,我们把这个密钥称为公钥,服务器自己留下的那个密钥称为私钥。顾名思义,公钥是每个人都能获取到的,而私钥只有服务器才能知道,不对任何人公开。下图是使用非对称加密改造的 HTTPS 协议:

  • 添加数字证书

    通过对称和非对称混合方式,我们完美地实现了数据的加密传输。不过这种方式依然存在着问题,比如我要打开极客时间的官网,但是黑客通过 DNS 劫持将极客时间官网的 IP 地址替换成了黑客的 IP 地址,这样我访问的其实是黑客的服务器了,黑客就可以在自己的服务器上实现公钥和私钥,而对浏览器来说,它完全不知道现在访问的是个黑客的站点。所以我们还需要服务器向浏览器提供证明“我就是我”,那怎么证明呢?

    由第三方的权威机构进行证明 对于浏览器来说,数字证书有两个作用:一个是通过数字证书向浏览器证明服务器的身份,另一个是数字证书里面包含了服务器公钥

服务器返回的证书里面有哪些内容

这里感觉有点硬扣数字证书里的字段啥的了

react hook介绍

前面也介绍过

有没有看过usestate源码

原来看过,但不太熟

useEffect介绍

  • useEffect函数的作用就是为了react函数组件提供副作用处理的

  • 常见的副作用

    1. 数据请求ajax发送
    2. 手动修改dom
    3. localstorage操作
  • 依赖项

    1. 添加空数组

    组件只在首次渲染时执行一次

    useEffect(() => {
        console.log('11111')
    },[])
    
    1. 添加特定依赖项

    副作用函数在首次渲染时执行,在依赖项发生变化时重新执行

        class Example extends React.Component {
    
        // 等价于
        import React, { useState, useEffect } from 'react';
    
        function Example() {
        const [count, setCount] = useState(0);
    
        useEffect(() => {
            document.title = `You clicked ${count} times`;
        },[count]);
    
        return (
            <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
            </div>
        );
        }
    
    
    1. 默认情况

      useEffect的第一个入参是一个匿名函数,在第一次渲染和后续的渲染都会被调用

      import React, { useState, useEffect } from 'react'
      
      function HookCounter() {
      const [count, setCount] = useState(0)
      
      useEffect(() => {
          document.title = `${count} times`
      })
      
      return (
          <div>
          <button onClick={() => {
              setCount(prevCount => prevCount + 1)
          }} >Clicked {count} times</button>
          </div>
      )
      }
      
      export default HookCounter
      
      
    • useEffect()的返回值

      useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。

      useEffect(() => {
          const subscription = props.source.subscribe();
          return () => {
              subscription.unsubscribe();
          };
      }, [props.source]);
      

为什么不能在if里面写hook

可能会导致 hooks 的执行顺序发生改变,因为 React Hooks 内部是通过 hooks 的调用顺序来区分是哪个 hook

useState 为例,在 react 内部,每个组件(Fiber)的 hooks 都是以链表的形式存在 memoizeState。 update 阶段,每次调用 useState,链表就会执行 next 向后移动一步。如果将 useState 写在条件判断中,假设条件判断不成立,没有执行里面的 useState 方法,会导致接下来所有的 useState 的取值出现偏移,从而导致异常发生。

为什么推崇函数式组件而不是类组件

类组件 是通过 ES6 中的 class 关键字来定义的,它继承了 React.Component 并实现了一个 render() 方法来返回需要渲染的 UI。类组件可以包含自己的状态(state)和方法(methods),并可以使用生命周期(lifecycle)钩子函数来控制组件的行为。

函数式组件是一个纯函数,它接受一些输入(props)并返回一个 React 元素来表示组件的输出。函数式组件通常比类组件更简单且更易于测试、重构和优化。在 React 16.8 版本中,Hooks API 的引入使得函数式组件也能够拥有自己的状态和生命周期函数。

总的来说,如果你需要处理一些复杂的状态或者需要使用生命周期函数,那么类组件可能更合适;但如果你只需要根据输入(props)渲染出一个 UI,那么函数式组件可能更具有可读性和可维护性。

实现阿拉伯数字转中文 (算法题)

题目有点忘了,大概是这个意思。

二面

二面难度直接飙升

这里我就大概分享一下面试题吧

WebSocket协议以及怎么建立连接的

前面也有过这个题

为什么要使用WebSocket协议,轮询不行吗?什么是长轮询?

介绍一下jwt

可以参考我的这篇文章

❤️❤️包教包会——Cookie、Session、Token、JWT - 掘金 (juejin.cn)

token为什么要用cookie或者localStorge存储,为什么不考虑session?

这里我聊了一下它们直接的区别

一些常见的网络攻击

  • XSS跨站脚本攻击

    XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

    • 如何阻止XSS攻击

    但无论是何种类型的XSS攻击,它们都有一个共同点,那就是首先往浏览器中注入恶意脚本,然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上。 所以要阻止XSS攻击,我们可以通过阻止恶意javaScript脚本的注入和恶意消息的发送来实现。

    1. 服务器对输入脚本进行过滤或转码
    <script>alert('你被xss攻击了')</script>
    

    过滤之后为空

    1. 充分利用csp

    虽然在服务器端执行过滤或者转码可以阻止 XSS 攻击的发生,但完全依靠服务器端依然是不够的,我们还需要把 CSP 等策略充分地利用起来,以降低 XSS 攻击带来的风险和后果。

    限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是无法被加载的; 禁止向第三方域提交数据,这样用户数据也不会外泄;禁止执行内联脚本和未授权的脚本;

    还提供了上报机制,这样可以帮助我们尽快发现有哪些 XSS 攻击,以便尽快修复问题。

  • CSRF攻击

    CSRF,又称为跨站请求伪造,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事

  • 如何防止CSRF攻击

    • 充分利用好Cookie的SameSite属性

      原因:黑客会利用用户的登录状态来发起CSRF攻击,而Cookie正是浏览器和服务器之间维护登录状态的一个关键数据 通常CSRF攻击都是从第三方站点发起的,要防止CSRF攻击,我们最好能实现从第三方站点发送请求时禁止Cookie的发送

    • 验证请求的来源站点

      在服务器端验证请求来源的站点。 通过HTTP请求头中的Referer和Origin属性

    • CSRF Token

      第一步:在浏览器向服务器发起请求时,服务器生成一个CSRF Token。CSRF Token 其实就是服务器生成的字符串,然后将该字符串植入到返回的页面中 第二步:在浏览器端如果要发起转账的请求,那么需要带上页面中的 CSRF Token,然后服务器会验证该 Token 是否合法。如果是从第三方站点发出的请求,那么将无法获取到 CSRF Token 的值,所以即使发出了请求,服务器也会因为 CSRF Token 不正确而拒绝请求。

  • DNS查询攻击

    DNS查询攻击(DNS Query Flood)是向被攻击的服务器发送海量的随机生成的域名解析请求,大部分根本就不存在,并且通过伪造端口和客户端IP,防止查询请求被ACL过滤。

    被攻击的DNS服务器在接收到域名解析请求后,首先会在服务器上查找是否有对应的缓存,当没有缓存并且该域名无法直接由该DNS服务器进行解析的时候,DNS服务器会向其上层DNS服务器递归查询域名信息,直到全球互联网的13台根DNS服务器。

    大量不存在的域名解析请求,给服务器带来了很大的负载,当解析请求超过一定量的时候,就会造成DNS服务器解析域名超时,这样攻击者便达成了攻击目的。

    解决办法: 对突然发起大量频度较低的域名解析请求的源 IP 地址进行带宽限制; 限制每个源 IP 地址每秒的域名解析请求次数。

  • SQL注入攻击

    所谓SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。

    • 预防:

      • 检查数据类型

      检查输入数据的数据类型,在很大程度上可以对抗SQL注入。比如用户在输入邮箱时,必须严格按照邮箱的格式;输入时间、日期时,必须严格按照时间、日期的格式,等等,都能避免用户数据造成破坏。但数据类型检查并非万能,如果需求就是需要用户提交字符串,比如一段短文,则需要依赖其他的方法防范SQL注入。

      • 过滤和转义特殊字符

介绍react hook

同上

介绍一下redux以及为什么会考虑到使用redux进行状态管理

  • redux是什么?

    1. redux 是一个专门用于做状态管理的JS库(不是react插件库)
    2. 它可以用在react,angular,vue等项目中,但基本与react配合使用
    3. 作用:集中式管理react应用中多个组件共享的状态,当需要更新状态的时候,仅需要对这个管理集中处理,而不用去关心状态是如何分发到每一个组件内部的。
  • redux三大基本原则

    • 单一数据源

    整个应用的state被储存在一颗object tree 中,并且这个 object tree 只存在于唯一一个 store 中

    • state是只读的

    想要改变State必须通过Action,而具体使action在state上更新生效的是reducer;

    只读的好处 这种设计可以避免在多个组件之间共享state时出现冲突或意外修改,同时也方便进行状态追踪和调试。因为Redux的工作流程非常明确,我们可以清楚地知道每个state的来源和变化过程,从而更容易定位问题。 总之,保证state是只读的可以让Redux的状态管理更加可靠和可维护,也能够提高开发效率和代码质量。

    • Reducer必须是一个纯函数

    Reducer内部的执行操作必须是无副作用的,不能对state进行直接修改,当状态发生变化时,需要返回一个全新的对象代表新的state。 保证reducer是一个纯函数,可以使得Redux的行为更加可预测且易于调试。如果reducer改变了传入的state或者产生了副作用,那么在调用相同的action时,就无法得到相同的结果,从而导致应用程序的行为不可预测。

  • redux的工作过程

    Redux主要分为几个部分:dispatch,action,state。 dispatch是redux流程的第一步,在用户界面中通过执行dispatch,传入相对应的action对象参数,action是一个描述类型的对象,紧接着执行reducer,最后整体返回一个store对象

  • 什么情况下需要使用redux?

    1. 某个组件的状态需要让其他组件可以随时拿到(共享)
    2. 一个组件需要改变另一个组件的状态(通信)
    3. 总体原则:能不用就不用,如果不用比较吃力才考虑使用

redux中怎么发起异步请求其原理是什么?

寄了~

react为什么能运行redux?

这怎么答

懒加载的原理

得从代码层面回答,其底层原理,而不是大概实现思路

跨域

jsonp

Jsonp(JSON with Padding) 是 json 的一种‘使用模式’,可以跨域的获取到数据。

原理:

利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。

JSONP 请求一定需要对方的服务器做支持才可以。
  • 准备带有 padding 的请求 url,得到一个字符串 dataStr

  • 构造 script

  • js 代码使用 document.createElement 创建一个 script 标签,

  • 在函数作用域中外界如何访问到?使用 window

缺陷

  • 只能用get进行请求,不能用post

  • 浏览器默认加载资源的方式就是get请求

cors

如何设置

    Http请求头:
    Origin: http://api.bob.com (发送的源)

    反应:
    Access-Control-Allow-Origin: * (允许所有跨域)

    module.exports = async (ctx,next) => {
    // 设置响应头
    // 允许访问跨域服务的白名单 *允许所有
    ctx.set('Access-Control-Allow-Origin','*');
    ctx.set('Access-Control-Allow-Headers','Content-Type');
    ctx.set('Access-Control-Allow-Methods','GET,POST,OPTIONS');
    // OPTIONS 获取资源允许访问的方式
    // 其实在正式跨域之前浏览器会根据需要发起一次预检也就是option请求用来让服务端返回允许的方法
    await(next())

    }

缺陷

复杂请求的时候得先预检(option)再发请求

优点

在后端设置
websocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

Node中间件代理(两次跨域)

实现原理:

同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。

代理服务器,需要做以下几个步骤:

接受客户端请求 。
将请求 转发给服务器。
拿到服务器 响应 数据。
将 响应 转发给客户端。

  - 前端代理
```
  "proxy":{
"/api": {
  "target": "http://172.19.5.35:9536",
  "ws": true
  },

"/apc": {
  "target": "http://179.19.5.35:9536",
  "ws": true
  }
},
```
  • 缺点

    增加了网络请求

iframe + postMessage

这里是使用3300端口父页面向内嵌子页面3301端口发送消息

 //a.html
<!-- 使用iframe,src指向3301端口 -->
 <iframe src="http://127.0.0.1:3301/b.html" id="frame" onload="load()"></iframe>
 <script>
 const data = {
   name : '某车',
   like:  '前端'
 }
 const load = function(){
   //负责发布消息
   let frame = document.getElementById('frame'); 
   const targetWindow = frame.contentWindow;//得到目标窗口的引用
   targetWindow.postMessage(data, 'http://127.0.0.1:3301'); //发送新消息
   //也监听信息
   window.onmessage = function(event) {
     console.log(event.data)
   }
 }
 </script>

<iframe> 标签用于在 HTML 文档中嵌入另一个 HTML 文档,它可以显示来自不同网站的内容。

postMessage() 方法是 HTML5 中提供的一种跨文档通信的API,用于在两个窗口之间传递消息。

它接收两个参数:第一个参数是要发送的消息,可以是字符串、数字、对象等;第二个参数指定接收消息的窗口的源和目标窗口的 URL。

nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

  • nginx是什么?

    1.nginx是一个高性能的Web服务器和反向代理服务器,也可作为电子邮件代理服务器。 2.在连接高并发的情况下,nginx是Apache服务不错的替代品,能够支持高达 50,000 个并发连接数的响应。

  • 正向代理和反向代理

    1.反向代理是指以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回到Internet上请求连接的客户端,此时代理服务器对外表现为一个反向代理服务器。

    2.正向代理是指用户无法访问某网站,但是可以访问某代理服务器,而代理服务器可以访问该网站。于是用户通过访问代理服务器去访问该网站,代理服务器把响应信息返回给用户。

  • 主要应用

    • 负载均衡

      网站服务器由多台服务器组成一个集群对外提供服务,当用户输入域名进行访问的时候,负载均衡负责将用户请求分发到集群中的不同服务器,从而提高并发处理能力。nginx作为访问的统一入口,分发请求,实现负载均衡。 负载均衡实现方式有硬件负载均衡和软件负载均衡。

    • 虚拟主机

      虚拟主机就是把一台物理服务器划分成多个“虚拟”的服务器,这样我们的一台物理服务器就可以当做多个服务器来使用,从而可以配置多个网站。 Nginx下,一个server标签就是一个虚拟主机,设置多个虚拟主机,配置多个server即可。

为什么script能进行跨域的请求

总感觉面试官是想问为什么要这么设计的原因

jsonp的方式是怎么获取数据加载到页面中的

一个文件名字hash化怎么实现?以及其原理?

结尾

非常抱歉,我发现哪怕现在已经三万字了,可还是才勉勉强强写完两家的面经😭😭

出于时间关系,后面的面经可能要过段时间实习的时候不忙的时候才会出了,如果有只想看面经的友友们,可以看我的牛客

闪亮啦啦啦 - 个人主页动态 - 牛客网 (nowcoder.com)

希望能帮助到屏幕前的你~

🌹🌹🌹