专栏首页鱼头的Web海洋『1W7字中高级前端面试必知必会』终极版

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

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

Chrome 浏览器进程

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

浏览器主进程

  • 负责浏览器界面显示
  • 各个页面的管理,创建以及销毁
  • 将渲染进程的结果绘制到用户界面上
  • 网络资源管理

GPU 进程

  • 用于 3D 渲染绘制

网络进程

  • 发起网络请求

插件进程

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

渲染进程

  • 页面渲染
  • 脚本执行
  • 事件处理

网络传输流程

生成 HTTP 请求消息

  1. 输入网址
  2. 浏览浏览器解析 URL
  3. 生成 HTTP 请求信息
  1. 收到响应 状态码含义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

转换为布尔值

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

返回 false
。

转换为数字

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

返回 NaN
。

转换为字符串

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

返回 "undefined"
。

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 inner;
}
getNum = getCounter();
getNum(); // 2
getNum(); // 3
getNum(); // 5
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__指向构造函数的原型prototype
  obj.__proto__.constructor = Fn; // 在原型链 __proto__上设置构造函数的构造器constructor,为了实例化Fn
  Fn.apply(obj, arg); // 执行Fn,并将构造函数Fn执行obj
  return 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. 深入理解浏览器的缓存机制

本文分享自微信公众号 - 鱼头的Web海洋(krissarea),作者:陈大鱼头

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-05-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【适合收藏】为了多点时间陪女朋友,我向BAT大佬跪求了这15条JS技巧

    为了减少加班,从而挤出更多的时间来陪女朋友,我就厚着脸皮向一些BAT大佬求来了这15条JS技巧,现在分享给大家,千万别错过。

    陈大鱼头
  • 像监听页面一样监听戈多的动态

    不知道各位童鞋有木有看过 《等待戈多》 这部出名的荒诞戏剧 。其剧情大概就是 戈戈 与 狄狄 等待 戈多 的过程中发生的一些琐事,一共两幕。等了这么多年,也不知...

    陈大鱼头
  • 『初中级前端必看』谈谈如何更有质量地看源码

    作为一个优秀(或说合格)的开源项目,它的代码一定不会是晦涩难懂的。不仅是代码本身,这些项目配套的注释,单元测试,示例代码,函数名以及文档一定是能够很好地辅助你读...

    陈大鱼头
  • 名字随便起——es6 Proxy

    类似Object.defineProperty的set和get,拦截set和get操作进行一些其他逻辑。但是proxy操作的是一个新的代理对象,是对原对象的一个...

    IMWeb前端团队
  • 名字随便起——es6 Proxy 0. 前言1. 拦截展示结果2. 驼峰命名3. 自定义cgi名字

    类似Object.defineProperty的set和get,拦截set和get操作进行一些其他逻辑。但是proxy操作的是一个新的代理对象,是对原对象的一个...

    lhyt
  • Cisco 3560X Config Cheat

    happy123.me
  • Netty 黏包拆包机制

    黏包和拆包的产生是由于TCP拥塞控制算法(比如angle算法)和TCP缓冲区机制导致的,angle算法简单来说就是通过一些规则来尽可能利用网络带宽,尽可能的发送...

    luoxn28
  • CCIE知识点总结——二层技术

    版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/...

    魏晓蕾
  • 数据结构——java实现栈

    说故事的五公子
  • Educational Codeforces Round 54 (Rated for Div. 2) B. Divisor Subtraction(思维)

    题目链接:http://codeforces.com/contest/1076/problem/B

    Ch_Zaqdt

扫码关注云+社区

领取腾讯云代金券