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

Chrome 浏览器进程

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

浏览器主进程

  • 负责浏览器界面显示html

  • 各个页面的管理,建立以及销毁前端

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

  • 网络资源管理java

GPU 进程

  • 用于 3D 渲染绘制

网络进程

  • 发起网络请求

插件进程

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

渲染进程

  • 页面渲染node

  • 脚本执行android

  • 事件处理nginx

网络传输流程

生成 HTTP 请求消息

  1. 输入网址git

  2. 浏览浏览器解析 URLgithub

  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
Null Return +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 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, "&")
        .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. 深刻理解浏览器的缓存机制

后记

若是你喜欢探讨技术,或者对本文有任何的意见或建议,很是欢迎加鱼头微信好友一块儿探讨,固然,鱼头也很是但愿能跟你一块儿聊生活,聊爱好,谈天说地。 鱼头的微信号是:krisChans95 扫码公众号,回复『面试资料』能够获取约200M前端高质量面试资料,千万不要错过哦。

https://user-gold-cdn.xitu.io/2020/5/23/17240ccfa92ee33d?w=1000&h=480&f=png&s=311000
相关文章
相关标签/搜索