『1W7字中高级前端面试必知必会』终极版

2024-02-10 07:10

本文主要是介绍『1W7字中高级前端面试必知必会』终极版,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  • 作者:陈大鱼头
  • github: KRISACHAN

Chrome 浏览器进程

在资源不足的设备上,将服务合并到浏览器进程中

浏览器主进程

  • 负责浏览器界面显示

  • 各个页面的管理,创建以及销毁

  • 将渲染进程的结果绘制到用户界面上

  • 网络资源管理

GPU 进程

  • 用于 3D 渲染绘制

网络进程

  • 发起网络请求

插件进程

  • 第三方插件处理,运行在沙箱中

渲染进程

  • 页面渲染

  • 脚本执行

  • 事件处理

网络传输流程

生成 HTTP 请求消息

  1. 输入网址

  2. 浏览浏览器解析 URL

  3. 生成 HTTP 请求信息

  4. 收到响应

    状态码含义
    1xx告知请求的处理进度和情况
    2xx成功
    3xx表示需要进一步操作
    4xx客户端错误
    5xx服务端错误

向 DNS 服务器查询 Web 服务器的 IP 地址

  1. Socket 库提供查询 IP 地址的功能

  2. 通过解析器向 DNS 服务器发出查询

全世界 DNS 服务器的大接力

  1. 寻找相应的 DNS 服务器并获取 IP 地址

  2. 通过缓存加快 DNS 服务器的响应

委托协议栈发送消息

协议栈通过 TCP 协议收发数据的操作。

  1. 创建套接字

  • 浏览器,邮件等一般的应用程序收发数据时用 TCP
  • DNS 查询等收发较短的控制数据时用 UDP
  1. 连接服务器

浏览器调用 Socket.connect

  • 在 TCP 模块处创建表示连接控制信息的头部
  • 通过 TCP 头部中的发送方和接收方端口号找到要连接的套接字

  1. 收发数据

浏览器调用 Socket.write

  • 将 HTTP 请求消息交给协议栈

  • 对较大的数据进行拆分,拆分的每一块数据加上 TCP 头,由 IP 模块来发送

  • 使用 ACK 号确认网络包已收到

  • 根据网络包平均往返时间调整 ACK 号等待时间

  • 使用窗口有效管理 ACK 号

  • ACK 与窗口的合并

  • 接收 HTTP 响应消息

  1. 断开管道并删除套接字

浏览器调用 Socket.close

  • 数据发送完毕后断开连接

  • 删除套接字

    1. 客户端发送 FIN
    2. 服务端返回 ACK 号
    3. 服务端发送 FIN
    4. 客户端返回 ACK 号

跨域

同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

如果两个 URL 的 protocolport (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。

例如:

URL结果原因
http://store.company.com/dir2/other.html同源只有路径不同
http://store.company.com/dir/inner/another.html同源只有路径不同
https://store.company.com/secure.html失败协议不同
http://store.company.com:81/dir/etc.html失败端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html失败主机不同

主要的跨域处理

JSONP

JSONP的原理是:静态资源请求不受同源策略影响。实现如下:

const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://www.domain.com/a?data=1&callback=cb'
const cb = res => {console.log(JSON.stringify(res))
}

CORS

CORS:跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。

在各种服务端代码实现如下:

// 根据不同语言规则,具体语法有所不同,此处以NodeJs的express为例
//设置跨域访问  
app.all('*', function(req, res, next) {  res.header("Access-Control-Allow-Origin", "*");  res.header("Access-Control-Allow-Headers", "X-Requested-With");  res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");next();  
});   

Nginx实现如下:

server {...add_header Access-Control-Allow-Credentials true;add_header Access-Control-Allow-Origin $http_origin;location /file {if ($request_method = 'OPTIONS') {add_header Access-Control-Allow-Origin $http_origin;add_header Access-Control-Allow-Methods $http_access_control_request_method;add_header Access-Control-Allow-Credentials true;add_header Access-Control-Allow-Headers $http_access_control_request_headers;add_header Access-Control-Max-Age 1728000;return 204;}         }...
}

网络协议

TCP

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。

  • 基于流的方式
  • 面向连接
  • 丢包重传
  • 保证数据顺序

UDP

Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。RFC 768 描述了 UDP。

  • UDP 是非连接的协议,也就是不会跟终端建立连接
  • UDP 包信息只有 8 个字节
  • UDP 是面向报文的。既不拆分,也不合并,而是保留这些报文的边界
  • UDP 可能丢包
  • UDP 不保证数据顺序

HTTP

  • HTTP/0.9:GET,无状态的特点形成

  • HTTP/1.0:支持 POST,HEAD,添加了请求头和响应头,支持任何格式的文件发送,添加了状态码、多字符集支持、多部分发送、权限、缓存、内容编码等

  • HTTP/1.1:默认长连接,同时 6 个 TCP 连接,CDN 域名分片

  • HTTPS:HTTP + TLS( 非对称加密对称加密

    1. 客户端发出 https 请求,请求服务端建立 SSL 连接
    2. 服务端收到 https 请求,申请或自制数字证书,得到公钥和服务端私钥,并将公钥发送给客户端
    3. 户端验证公钥,不通过验证则发出警告,通过验证则产生一个随机的客户端私钥
    4. 客户端将公钥与客户端私钥进行对称加密后传给服务端
    5. 服务端收到加密内容后,通过服务端私钥进行非对称解密,得到客户端私钥
    6. 服务端将客户端私钥和内容进行对称加密,并将加密内容发送给客户端
    7. 客户端收到加密内容后,通过客户端私钥进行对称解密,得到内容
  • HTTP/2.0:多路复用(一次 TCP 连接可以处理多个请求),服务器主动推送,stream 传输。

  • HTTP/3:基于 UDP 实现了 QUIC 协议

    • 建立好 HTTP2 连接
    • 发送 HTTP2 扩展帧
    • 使用 QUIC 建立连接
    • 如果成功就断开 HTTP2 连接
    • 升级为 HTTP3 连接

注:RTT = Round-trip time

页面渲染流程

构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成

  1. 创建 DOM tree

    • 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中。
    • 不可见的节点会被布局树忽略掉。
  2. 样式计算

    • 创建 CSSOM tree
    • 转换样式表中的属性值
    • 计算出 DOM 节点样式
  3. 生成 layout tree

  4. 分层

    • 生成图层树(LayerTree)
    • 拥有层叠上下文属性的元素会被提升为单独的一层
    • 需要剪裁(clip)的地方也会被创建为图层
    • 图层绘制
  5. 将图层转换为位图

  6. 合成位图并显示在页面中

页面更新机制

  • 更新了元素的几何属性(重排)
  • 更新元素的绘制属性(重绘)
  • 直接合成
    • CSS3 的属性可以直接跳到这一步

JS 执行机制

代码提升(为了编译)

  • 变量提升
  • 函数提升(优先级最高)

编译代码

V8 编译 JS 代码的过程

  1. 生成抽象语法树(AST)和执行上下文

  2. 第一阶段是分词(tokenize),又称为词法分析

  3. 第二阶段是解析(parse),又称为语法分析

  4. 生成字节码

    字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

  5. 执行代码

高级语言编译器步骤:

  1. 输入源程序字符流
  2. 词法分析
  3. 语法分析
  4. 语义分析
  5. 中间代码生成
  6. 机器无关代码优化
  7. 代码生成
  8. 机器相关代码优化
  9. 目标代码生成

执行代码

  • 执行全局代码时,创建全局上下文
  • 调用函数时,创建函数上下文
  • 使用 eval 函数时,创建 eval 上下文
  • 执行局部代码时,创建局部上下文

类型

基本类型

  • Undefined
  • Null
  • Boolean
  • String
  • Symbol
  • Number
  • Object
  • BigInt

复杂类型

  • Object

隐式转换规则

基本情况

  • 转换为布尔值
  • 转换为数字
  • 转换为字符串

转换为原始类型

对象在转换类型的时候,会执行原生方法 ToPrimitive

其算法如下:

  1. 如果已经是 原始类型,则返回当前值;
  2. 如果需要转 字符串 则先调用toSting方法,如果此时是 原始类型 则直接返回,否则再调用valueOf方法并返回结果;
  3. 如果不是 字符串,则先调用valueOf方法,如果此时是 原始类型 则直接返回,否则再调用toString方法并返回结果;
  4. 如果都没有 原始类型 返回,则抛出 TypeError 类型错误。

当然,我们可以通过重写Symbol.toPrimitive来制定转换规则,此方法在转原始类型时调用优先级最高。

const data = {valueOf() {return 1;},toString() {return "1";},[Symbol.toPrimitive]() {return 2;}
};
data + 1; // 3

转换为布尔值

对象转换为布尔值的规则如下表:

参数类型结果
Undefined返回 false
Null返回 false
Boolean返回 当前参数。
Number如果参数为+0-0NaN,则返回 false;其他情况则返回 true
String如果参数为空字符串,则返回 false;否则返回 true
Symbol返回 true
Object返回 true

转换为数字

对象转换为数字的规则如下表:

参数类型结果
Undefined返回 NaN
NullReturn +0.
Boolean如果参数为 true,则返回 1false则返回 +0
Number返回当前参数。
String先调用 ToPrimitive ,再调用 ToNumber ,然后返回结果。
Symbol抛出 TypeError错误。
Object先调用 ToPrimitive ,再调用 ToNumber ,然后返回结果。

转换为字符串

对象转换为字符串的规则如下表:

参数类型结果
Undefined返回 "undefined"
Null返回 "null"
Boolean如果参数为 true ,则返回 "true";否则返回 "false"
Number调用 NumberToString ,然后返回结果。
String返回 当前参数。
Symbol抛出 TypeError错误。
Object先调用 ToPrimitive ,再调用 ToString ,然后返回结果。

this

this 是和执行上下文绑定的。

执行上下文:

  • 全局执行上下文:全局执行上下文中的 this 也是指向 window 对象。
  • 函数执行上下文:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。
  • eval 执行上下文:执行 eval 环境内部的上两个情况。

根据优先级最高的来决定 this 最终指向哪里。

首先,new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。

三点注意:

  1. 当函数作为对象的方法调用时,函数中的 this 就是该对象;
  2. 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
  3. 嵌套函数中的 this 不会继承外层函数的 this 值。
  4. 我们还提了一下箭头函数,因为箭头函数没有自己的执行上下文,所以箭头函数的 this 就是它外层函数的 this。

闭包

没有被引用的闭包会被自动回收,但还存在全局变量中,则依然会内存泄漏。

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

var getNum;
function getCounter() {var n = 1;var inner = function() {n++;return n;};return inner;
}
getNum = getCounter();
getNum(); // 2
getNum(); // 3
getNum(); // 4
getNum(); // 5

作用域

全局作用域

对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。

函数作用域

函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

局部作用域

使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。

作用域链

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。

原型&原型链

其实每个 JS 对象都有 __proto__ 属性,这个属性指向了原型。

原型也是一个对象,并且这个对象中包含了很多函数,对于 obj 来说,可以通过 __proto__ 找到一个原型对象,在该对象中定义了很多函数让我们来使用。

原型链:

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • 函数的 prototype 是一个对象
  • 对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链

V8 工作原理

数据存储

  • 栈空间:先进后出的数据结构,调用栈,存储执行上下文,以及存储原始类型的数据。
  • 堆空间:用数组实现的二叉树,存储引用类型。堆空间很大,能存放很多大的数据。存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。

原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。

垃圾回收

  • 回收调用栈内的数据:执行上下文结束且没有被引用时,则会通过向下移动 记录当前执行状态的指针(称为 ESP) 来销毁该函数保存在栈中的执行上下文。

  • 回收堆里的数据:

    V8 中会把堆分为新生代和老生代两个区域,

    新生代中存放的是生存时间短的对象,

    老生代中存放的生存时间久的对象。

    垃圾回收重要术语:

    • 代际假说
      • 大部分对象在内存中存在的时间很短
      • 不死的对象,会活得更久
    • 分代收集

副垃圾回收器:

主要负责新生代的垃圾回收。

这个区域不大,但是垃圾回收比较频繁。

新生代的垃圾回收算法是 Scavenge 算法。

主要把新生代空间对半划分为两个区域:对象区域,空闲区域。

当对象区域快被写满时,则会进行一次垃圾清理。

流程如下:

  1. 对对象区域中的垃圾做标记
  2. 把存活的对象复制到空闲区域中
  3. 把这些对象有序地排列起来
  4. 清理完之后,对象区域会与空闲区域互换

主垃圾回收器:

主垃圾回收器主要负责老生区中的垃圾回收。

除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。

因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

流程如下:

  1. 从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,区分活动对象以及垃圾数据
  2. 标记过程和清除过程使用标记 - 清除算法
  3. 碎片过多会导致大对象无法分配到足够的连续内存时,会使用标记 - 整理算法

一旦执行垃圾回收算法,会导致 全停顿(Stop-The-World)

但是 V8 有 增量标记算法

V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成。

事件循环

微任务(microtask)

  • process.nextTick
  • promise
  • Object.observe (已废弃)
  • MutationObserver

宏任务(macrotask)

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

执行顺序

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务需要执行
  3. 必要的话渲染 UI
  4. 然后开始下一轮 Event loop,执行宏任务中的异步代码

浏览器安全

攻击方式

  • xss:将代码注入到网页

    • 持久型 :写入数据库
    • 非持久型 :修改用户代码
  • csrf:跨站请求伪造。攻击者会虚构一个后端请求地址,诱导用户通过某些途径发送请求。

  • 中间人攻击:中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了。攻击者不仅能获得双方的通信信息,还能修改通信信息。

    • DNS 欺骗:入侵 DNS 来将用户访问目标改为入侵者指定机器
    • 会话劫持:在一次正常的通信过程中,攻击者作为第三方参与到其中,或者是在数据里加入其他信息,甚至将双方的通信模式暗中改变,即从直接联系变成有攻击者参与的联系。

防御措施

  1. 预防 XSS
  • 使用转义字符过滤 html 代码

    const escapeHTML = value => {if (!value || !value.length) {return value;}return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
    };
    
  • 过滤 SQL 代码

    const replaceSql = value => {if (!value || !value.length) {return value;}return value.replace(/select|update|delete|exec|count|'|"|=|;|>|<|%/gi, "");
    };
    
  1. 预防 CSRF

    • 验证 HTTP Referer 字段
    • 在请求地址中添加 token 并验证
    • 在 HTTP 头中自定义属性并验证
    • Get 请求不对数据进行修改
    • 接口防跨域处理
    • 不让第三方网站访问用户 cookie
  2. 预防中间人攻击

  • 对于 DNS 欺骗:检查本机的 HOSTS 文件
  • 对于会话劫持:使用交换式网络代替共享式网络,还必须使用静态 ARP、捆绑 MAC+IP 等方法来限制欺骗,以及采用认证方式的连接等。
  1. 内容安全策略(CSP)

内容安全策略 (CSP) 是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等。无论是数据盗取、网站内容污染还是散发恶意软件,这些攻击都是主要的手段。

措施如下:

  • HTTP Header 中的 Content-Security-Policy
  • <meta http-equiv="Content-Security-Policy">

浏览器性能

DNS 预解析

  • <link rel="dns-prefetch" href="" />
  • Chrome 和 Firefox 3.5+ 能自动进行预解析
  • 关闭 DNS 预解析:<meta http-equiv="x-dns-prefetch-control" content="off|on">

强缓存

  1. Expires

    • 缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。
    • Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
  2. Cache-Control

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。

  • 服务器响应头:Last-Modified,Etag
  • 浏览器请求头:If-Modified-Since,If-None-Match

Last-ModifiedIf-Modified-Since 配对。Last-Modified 把 Web 应用最后修改时间告诉客户端,客户端下次请求之时会把 If-Modified-Since 的值发生给服务器,服务器由此判断是否需要重新发送资源,如果不需要则返回 304,如果有则返回 200。这对组合的缺点是只能精确到秒,而且是根据本地打开时间来记录的,所以会不准确。

EtagIf-None-Match 配对。它们没有使用时间作为判断标准,而是使用了一组特征串。Etag把此特征串发生给客户端,客户端在下次请求之时会把此特征串作为If-None-Match的值发送给服务端,服务器由此判断是否需要重新发送资源,如果不需要则返回 304,如果有则返回 200。

NodeJs

单线程

基础概念:

  • 进程:进程(英语:process),是指计算机中已运行的程序。进程曾经是分时系统的基本运作单位。

  • 线程:线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。

  • 协程:协程(英语:coroutine),又称微线程,是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。

Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的,各个线程如下:

  • 主线程:编译、执行代码。

  • 编译/优化线程:在主线程执行的时候,可以优化代码。

  • 分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据。

  • 垃圾回收的几个线程。

非阻塞 I/O

阻塞 是指在 Node.js 程序中,其它 JavaScript 语句的执行,必须等待一个非 JavaScript 操作完成。这是因为当 阻塞 发生时,事件循环无法继续运行 JavaScript。

在 Node.js 中,JavaScript 由于执行 CPU 密集型操作,而不是等待一个非 JavaScript 操作(例如 I/O)而表现不佳,通常不被称为 阻塞。在 Node.js 标准库中使用 libuv 的同步方法是最常用的 阻塞 操作。原生模块中也有 阻塞 方法。

事件循环

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │└───────────────────────────┘

注意:每个框被称为事件循环机制的一个阶段。

在 Windows 和 Unix/Linux 实现之间存在细微的差异,但这对演示来说并不重要。

阶段概述:

  • 定时器 :本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。

  • 待定回调 :执行延迟到下一个循环迭代的 I/O 回调。

  • idle, prepare :仅系统内部使用。

  • 轮询 :检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。

  • 检测setImmediate() 回调函数在这里执行。

  • 关闭的回调函数 :一些关闭的回调函数,如:socket.on('close', ...)

在每次运行的事件循环之间,Node.js 检查它是否在等待任何异步 I/O 或计时器,如果没有的话,则完全关闭。

process.nextTick() :它是异步 API 的一部分。从技术上讲不是事件循环的一部分。不管事件循环的当前阶段如何,都将在当前操作完成后处理 nextTickQueue。这里的一个操作被视作为一个从底层 C/C++ 处理器开始过渡,并且处理需要执行的 JavaScript 代码。

Libuv

Libuv 是一个跨平台的异步 IO 库,它结合了 UNIX 下的 libev 和 Windows 下的 IOCP 的特性,最早由 Node.js 的作者开发,专门为 Node.js 提供多平台下的异步 IO 支持。Libuv 本身是由 C++ 语言实现的,Node.js 中的非阻塞 IO 以及事件循环的底层机制都是由 libuv 实现的。

在 Windows 环境下,libuv 直接使用 Windows 的 IOCP 来实现异步 IO。在 非 Windows 环境下,libuv 使用多线程(线程池 Thread Pool)来模拟异步 IO,这里仅简要提一下 libuv 中有线程池的概念,之后的文章会介绍 libuv 如何实现进程间通信。

手写代码

new 操作符

var New = function(Fn) {var obj = {}; // 创建空对象var arg = Array.prototype.slice.call(arguments, 1);obj.__proto__ = Fn.prototype; // 将obj的原型链__proto__指向构造函数的原型prototypeobj.__proto__.constructor = Fn; // 在原型链 __proto__上设置构造函数的构造器constructor,为了实例化FnFn.apply(obj, arg); // 执行Fn,并将构造函数Fn执行objreturn obj; // 返回结果
};

深拷贝

const getType = data => {// 获取数据类型const baseType = Object.prototype.toString.call(data).replace(/^\[object\s(.+)\]$/g, "$1").toLowerCase();const type = data instanceof Element ? "element" : baseType;return type;
};
const isPrimitive = data => {// 判断是否是基本数据类型const primitiveType = "undefined,null,boolean,string,symbol,number,bigint,map,set,weakmap,weakset".split(","); // 其实还有很多类型return primitiveType.includes(getType(data));
};
const isObject = data => getType(data) === "object";
const isArray = data => getType(data) === "array";
const deepClone = data => {let cache = {}; // 缓存值,防止循环引用const baseClone = _data => {let res;if (isPrimitive(_data)) {return data;} else if (isObject(_data)) {res = { ..._data };} else if (isArray(_data)) {res = [..._data];}// 判断是否有复杂类型的数据,有就递归Reflect.ownKeys(res).forEach(key => {if (res[key] && getType(res[key]) === "object") {// 用cache来记录已经被复制过的引用地址。用来解决循环引用的问题if (cache[res[key]]) {res[key] = cache[res[key]];} else {cache[res[key]] = res[key];res[key] = baseClone(res[key]);}}});return res;};return baseClone(data);
};

手写 bind

Function.prototype.bind2 = function(context) {if (typeof this !== "function") {throw new Error("...");}var that = this;var args1 = Array.prototype.slice.call(arguments, 1);var bindFn = function() {var args2 = Array.prototype.slice.call(arguments);var that2 = this instanceof bindFn ? this : context; // 如果当前函数的this指向的是构造函数中的this 则判定为new 操作。如果this是构造函数bindFn new出来的实例,那么此处的this一定是该实例本身。return that.apply(that2, args1.concat(args2));};var Fn = function() {}; // 连接原型链用Fn// 原型赋值Fn.prototype = this.prototype; // bindFn的prototype指向和this的prototype一样,指向同一个原型对象bindFn.prototype = new Fn();return bindFn;
};

手写函数柯里化

const curry = fn => {if (typeof fn !== "function") {throw Error("No function provided");}return function curriedFn(...args) {if (args.length < fn.length) {return function() {return curriedFn.apply(null, args.concat([].slice.call(arguments)));};}return fn.apply(null, args);};
};

手写 Promise

// 来源于 https://github.com/bailnl/promise/blob/master/src/promise.js
const PENDING = 0;
const FULFILLED = 1;
const REJECTED = 2;const isFunction = fn => typeof fn === "function";
const isObject = obj => obj !== null && typeof obj === "object";
const noop = () => {};const nextTick = fn => setTimeout(fn, 0);const resolve = (promise, x) => {if (promise === x) {reject(promise, new TypeError("You cannot resolve a promise with itself"));} else if (x && x.constructor === Promise) {if (x._stauts === PENDING) {const handler = statusHandler => value => statusHandler(promise, value);x.then(handler(resolve), handler(reject));} else if (x._stauts === FULFILLED) {fulfill(promise, x._value);} else if (x._stauts === REJECTED) {reject(promise, x._value);}} else if (isFunction(x) || isObject(x)) {let isCalled = false;try {const then = x.then;if (isFunction(then)) {const handler = statusHandler => value => {if (!isCalled) {statusHandler(promise, value);}isCalled = true;};then.call(x, handler(resolve), handler(reject));} else {fulfill(promise, x);}} catch (e) {if (!isCalled) {reject(promise, e);}}} else {fulfill(promise, x);}
};const reject = (promise, reason) => {if (promise._stauts !== PENDING) {return;}promise._stauts = REJECTED;promise._value = reason;invokeCallback(promise);
};const fulfill = (promise, value) => {if (promise._stauts !== PENDING) {return;}promise._stauts = FULFILLED;promise._value = value;invokeCallback(promise);
};const invokeCallback = promise => {if (promise._stauts === PENDING) {return;}nextTick(() => {while (promise._callbacks.length) {const {onFulfilled = value => value,onRejected = reason => {throw reason;},thenPromise} = promise._callbacks.shift();let value;try {value = (promise._stauts === FULFILLED ? onFulfilled : onRejected)(promise._value);} catch (e) {reject(thenPromise, e);continue;}resolve(thenPromise, value);}});
};class Promise {static resolve(value) {return new Promise((resolve, reject) => resolve(value));}static reject(reason) {return new Promise((resolve, reject) => reject(reason));}constructor(resolver) {if (!(this instanceof Promise)) {throw new TypeError(`Class constructor Promise cannot be invoked without 'new'`);}if (!isFunction(resolver)) {throw new TypeError(`Promise resolver ${resolver} is not a function`);}this._stauts = PENDING;this._value = undefined;this._callbacks = [];try {resolver(value => resolve(this, value), reason => reject(this, reason));} catch (e) {reject(this, e);}}then(onFulfilled, onRejected) {const thenPromise = new this.constructor(noop);this._callbacks = this._callbacks.concat([{onFulfilled: isFunction(onFulfilled) ? onFulfilled : void 0,onRejected: isFunction(onRejected) ? onRejected : void 0,thenPromise}]);invokeCallback(this);return thenPromise;}catch(onRejected) {return this.then(void 0, onRejected);}
}

手写防抖函数

const debounce = (fn = {}, wait = 50, immediate) => {let timer;return function() {if (immediate) {fn.apply(this, arguments);}if (timer) {clearTimeout(timer);timer = null;}timer = setTimeout(() => {fn.apply(this, arguments);}, wait);};
};

手写节流函数

var throttle = (fn = {}, wait = 0) => {let prev = new Date();return function() {const args = arguments;const now = new Date();if (now - prev > wait) {fn.apply(this, args);prev = new Date();}};
};

手写 instanceOf

const instanceOf = (left, right) => {let proto = left.__proto__;let prototype = right.prototype;while (true) {if (proto === null) {return false;} else if (proto === prototype) {return true;}proto = proto.__proto__;}
};

其它知识

typeof vs instanceof

instanceof 运算符用来检测 constructor.prototype是否存在于参数 object 的原型链上。

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

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

递归

递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。

例如:

大雄在房里,用时光电视看着未来的情况。电视画面中的那个时候,他正在房里,用时光电视,看着未来的情况。电视画面中的电视画面的那个时候,他正在房里,用时光电视,看着未来的情况……

简单来说,就是 无限套娃

我们以斐波那契数列(Fibonacci sequence)为例,看看输入结果会为正无穷的值的情况下,各种递归的情况。

首先是普通版

const fib1 = n => {if (typeof n !== "number") {throw new Error("..");}if (n < 2) {return n;}return fib1(n - 1) + fib1(n - 2);
};

从上面的代码分析,我们不难发现,在fib1里,JS 会不停创建执行上下文,压入栈内,而且在得出结果前不会销毁,所以数大了之后容易爆栈。

所以我们可以对其进行优化,就是利用 尾调用 进行优化。

尾调用是指函数的最后一步只返回一个纯函数的调用,而没有别的数据占用引用。代码如下:

const fib2 = (n, a = 0, b = 1) => {if (typeof n !== "number") {throw new Error("..");}if (n === 0) {return a;}return fib2(n - 1, b, a + b);
};

不过很遗憾,在 Chrome 83.0.4103.61 里还是会爆。

然后我们还有备忘录递归法,就是另外申请空间去存储每次递归的值,是个自顶向下的算法。

可惜,还是挂了。

不过在一些递归问题上,我们还可以利用动态规划(Dynamic programming,简称 DP)来解决。

动态规划是算法里比较难掌握的一个概念之一,但是基本能用递归来解决的问题,都能用动态规划来解决。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。

跟备忘录递归刚好相反,是自底向上的算法。具体代码如下:

const fib3 = n => {if (typeof n !== "number") {throw new Error("..");}if (n < 2) {return n;}let a = 0;let b = 1;while (n--) {[a, b] = [b, a + b];}return a;
};

效果很好,正确输出了正无穷~

参考资料

  1. 浏览器工作原理与实践
  2. 浏览器的运行机制—2.浏览器都包含哪些进程?
  3. 「中高级前端面试」JavaScript 手写代码无敌秘籍
  4. JavaScript 深拷贝
  5. bailnl/promise
  6. 网络是怎样连接的?
  7. 浏览器工作原理与实践
  8. 浏览器的工作原理:新式网络浏览器幕后揭秘
  9. 内容安全策略( CSP )
  10. 前端面试之道
  11. HTTP 各版本的区别
  12. CORS解决跨域问题(Nginx跨域配置)
  13. 你觉得 Node.js 是单线程这个结论对吗?
  14. Node 指南
  15. 深入理解浏览器的缓存机制

后记

如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。
鱼头的微信号是:krisChans95
也可以扫码添加好友,备注“csdn”就行(加好友送200M前端面试资料)

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/base/wx-qrcode1.jpg

这篇关于『1W7字中高级前端面试必知必会』终极版的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/696458

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

最大流、 最小费用最大流终极版模板

最大流  const int inf = 1000000000 ;const int maxn = 20000 , maxm = 500000 ;struct Edge{int v , f ,next ;Edge(){}Edge(int _v , int _f , int _next):v(_v) ,f(_f),next(_next){}};int sourse , mee

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo