史上最全常见前端大厂面试知识点汇总(持续实战更新,建议收藏!)【已过腾讯teg,华为,字节抖音】

大厂面试 专栏收录该内容
2 篇文章 0 订阅

文章目录

网络,浏览器篇

dns解析

  1. 客户端通过浏览器访问域名为 www.baidu.com (http://www.baidu.com) 的网站,发起查询该域名的 IP 地址的 DNS 请求。该请求发送到了本地 DNS 服务器上。本地 DNS 服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果。如果没有,本地 DNS 服务器还要向 DNS 根服务器进行查询。

  2. 本地 DNS 服务器向根服务器发送 DNS 请求,请求域名为 www.baidu.com (http://www.baidu.com) 的 IP 地址。

  3. 根服务器经过查询,没有记录该域名及 IP 地址的对应关系。但是会告诉本地 DNS 服务器,可以到域名服务器上继续查询,并给出域名服务器的地址(.com 服务器)。

  4. 本地 DNS 服务器向 .com 服务器发送 DNS 请求,请求域名 www.baidu.com (http://www.baidu.com) 的 IP 地址。

  5. .com 服务器收到请求后,不会直接返回域名和 IP 地址的对应关系,而是告诉本地 DNS 服务器,该域名可以在 baidu.com 域名服务器上进行解析获取 IP 地址,并告诉 baidu.com 域名服务器的地址。

  6. 本地 DNS 服务器向 baidu.com 域名服务器发送 DNS 请求,请求域名 www.baidu.com (http://www.baidu.com) 的 IP 地址。

  7. baidu.com 服务器收到请求后,在自己的缓存表中发现了该域名和 IP 地址的对应关系,并将IP地址返回给本地 DNS 服务器。

  8. 本地 DNS 服务器将获取到与域名对应的 IP 地址返回给客户端,并且将域名和 IP 地址的对应关系保存在缓存中,以备下次别的用户查询时使用。

TCP协议、IP协议、HTTP协议分别在哪一层吗?

运输层,网络层和应用层

tcp/ip协议有几层

tcp/ip协议包含4层,分别是:1、应用层,为应用进程提供服务的;2、运输层;3、网络层,可以进行网络连接的建立和终止;4、网络接口层,是传输数据的物理媒介。

网络七层协议内容

OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层

浏览器进程和线程

线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
进程是系统中正在运行的一个程序,程序一旦运行就是进程。进程可以看成程序执行的一个实例,可以视作为浏览器打开了多个tab页,每个tab页相当于独立的进程。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。

谷歌浏览器为什么使用多进程

传统的浏览器被设计为显示网页,而Chrome的设计目标是支撑“Web App”(当时的js和相关技术已经相当发达了,Gmail等服务也很成功)。这就要求Chrome提供一个类似于“操作系统”感觉的架构,支持App的运行。而App会变得相当的复杂,这就难以避免出现bug,然后crash。同时浏览器也要面临可能运行“恶意代码”。流览器不能决定上面的js怎么写,会不会以某种形式有意无意的攻击浏览器的渲染引擎。如果将所有这些App和浏览器实现在一个进程里,一旦挂,就全挂。因此Chrome一开始就设计为把隔离性作为基本的设计原则,用进程的隔离性来实现对App的隔离。这样用户就不用担心:

  • 一个Web App挂掉造成其他所有的Web App全部挂掉(稳定性)
  • 一个Web App可以以某种形式访问其他App的数据(安全性)

进程通信的方式

程间通信的方式——信号、管道、消息队列、共享内存

https加密过程

验证证书 ,创建密钥,传上去验证

url到页面过程

域名解析(递归域名),三次握手,服务器端返回请求数据,渲染页面,四次挥手

http2和http1的区别

1、HTTP2使用的是二进制传送,HTTP1.X是文本(字符串)传送。二进制传送的单位是帧和流。帧组成了流,同时流还有流ID标示

2、HTTP2支持多路复用。因为有流ID,所以通过同一个http请求实现多个http请求传输变成了可能,可以通过流ID来标示究竟是哪个流从而定位到是哪个http请求

3、HTTP2头部压缩。HTTP2通过gzip和compress压缩头部然后再发送,同时客户端和服务器端同时维护一张头信息表,所有字段都记录在这张表中,这样后面每次传输只需要传输表里面的索引Id就行,通过索引ID查询表头的值

4、HTTP2支持服务器推送。HTTP2支持在未经客户端许可的情况下,主动向客户端推送内容

如何实现有状态的http协议

HTTP是一种无状态协议,即服务器不保留与客户交易时的任何状态。
用户登录后,切换到其他界面,进行操作,服务器端是无法判断是哪个用户登录的。 每次进行页面跳转的时候,得重新登录。使用Cookie session来实现有状态的http协议

强缓存和协商缓存(浏览器缓存机制)

强缓存

Expires 是以前用来控制缓存的http头,Cache-Control是新版的API。
现在首选 Cache-Control。
如果在Cache-Control响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。
响应头设置方式:
Expires: Wed, 21 Oct 2015 07:28:00 GMT
Expires 响应头包含日期/时间, 即在此时候之后,响应过期。
注意: 因为过期标准的时间用的是本地时间,所以不靠谱,所以要使用Cache-Control代替Expires

  • cache-control: max-age=xxxx,public
    客户端和代理服务器都可以缓存该资源;
    客户端在xxx秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,如果用户做了刷新操作,就向服务器发起http请求

  • cache-control: max-age=xxxx,private
    只让客户端可以缓存该资源;代理服务器不缓存
    客户端在xxx秒内直接读取缓存,statu code:200

  • cache-control: max-age=xxxx,immutable
    客户端在xxx秒的有效期内,如果有请求该资源的需求的话就直接读取缓存,statu code:200 ,即使用户做了刷新操作,也不向服务器发起http请求

  • cache-control: no-cache
    跳过设置强缓存,但是不妨碍设置协商缓存;一般如果你做了强缓存,只有在强缓存失效了才走协商缓存的,设置了no-cache就不会走强缓存了,每次请求都回询问服务端。

  • cache-control: no-store
    不缓存,这个会让客户端、服务器都不缓存,也就没有所谓的强缓存、协商缓存了

协商缓存

  • etag:每个文件有一个,改动文件了就变了,就是个文件hash,每个文件唯一。

  • last-modified:文件的修改时间,精确到秒

ETag和Last-Modified的作用和用法,他们的区别:

1.Etag要优于Last-Modified。Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;

2.在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值;

3.在优先级上,服务器校验优先考虑Etag。

浏览器缓存过程

1.浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存;

2.下一次加载资源时,先比较当前时间和上一次返回200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持HTTP1.1,则用expires判断是否过期);如果时间过期,则向服务器发送header带有If-None-Match和If-Modified-Since的请求

3.服务器收到请求后,优先根据Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;;

4.如果服务器收到的请求没有Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;

跨域请求

协议域名ip。属于浏览器的同源策略,服务器可以接受到,同源策略判断的条件是协议,域名和端口号

跨域方式

  • document.domain + iframe (只有在主域相同的时候才能使用该方法)
  • window.name + iframe (name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。)
  • JSONP(只支持get,用一个动态script标签直接进行get请求,因为同源策略对script无效)
  • web sockets(web sockets是一种浏览器的API,它的目标是在一个单独的持久连接上提供全双工、双向通信。)

TCP三次握手为什么不能两次

为了实现可靠数据传输, TCP 协议的通信双方, 都必须维护一个序列号, 以标识发送出去的数据包中, 哪些是已经被对方收到的。 三次握手的过程即是通信双方相互告知序列号起始值, 并确认对方已经收到了序列号起始值的必经步骤
如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认

Tcp为什么是4次挥手

这个因为第一次挥手表示客户端发送了一个fin的包,表示客户端已发送数据完毕,但是服务端这个时候可能还有数据没有发送完成,先发送给客户端一个ask的包,等待自己的数据发送完成才能向客户端发送一个 fin的包,表示自己的数据也已发送完成。这样中间就必须为两次来发送ask和fin。

javascript内存机制

JS内存空间分为栈(stack)、堆(heap)、池(一般也会归类为栈中)。
其中栈存放基本数据类型,堆存放引用类型,池存放常量。

DOM 树 和 渲染树 的区别

DOM 树与 HTML 标签一一对应,包括 head 和隐藏元素
渲染树不包括 head 和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的 css 属性

怎么避免js,css等资源阻塞

  • 内联javascript: 如果页面的初始渲染的确依赖于page.js,我们可以考虑使用内联JavaScript。
  • 推迟加载:如果页面的初始渲染并不依赖于page.js,我们可以考虑推迟加载page.js,让其在页面初始内容渲染完成后再加载。
  • 异步加载:HTML5允许我们给 script 标签添加属性: “async” 来告诉浏览器不必停下来等待该脚本执行,什么时候下载完什么时候执行该脚本就可以了。这样的话浏览器会边下载page.js边渲染后面的内容。

XSS 和 CSRF

XSS是注入攻击的一种,攻击者通过将代码注入被攻击者的网站中,用户一旦访问访问网页便会执行被注入的恶意脚本。反射型XSS的脚本被解析的地方是浏览器,而存储型XSS的脚本被解析的地方是服务器
CSRF全程 Cross Site Request Forgery, 跨站域请求伪造,伪造表单诱骗用户点击

XSS防范方法

1.代码里对用户输入的地方和变量都需要仔细检查长度和对”<”,”>”,”;”,”’”等字符做过滤;其次任何内容写到页面之前都必须加以encode,避免不小心把html tag 弄出来。这一个层面做好,至少可以堵住超过一半的XSS 攻击。
2.避免直接在cookie 中泄露用户隐私,例如email、密码等等。
3.通过使cookie 和系统ip 绑定来降低cookie 泄露后的危险。这样攻击者得到的cookie 没有实际价值,不可能拿来重放。
4.尽量采用POST 而非GET 提交表单

CSRF防范方法

  • referer 验证:根据HTTP协议,在http请求头中包含一个referer的字段,这个字段记录了该http请求的原地址.通常情况下,执行转账操作的post请求www.bank.com/transfer.php应该是点击www.bank.com网页的按钮来触发的操作,这个时候转账请求的referer应该是www.bank.com.而如果黑客要进行csrf攻击,只能在自己的网站www.hacker.com上伪造请求.伪造请求的referer是www.hacker.com.所以我们通过对比post请求的referer是不是www.bank.com就可以判断请求是否合法.这种方式验证比较简单,网站开发者只要在post请求之前检查referer就可以,但是由于referer是由浏览器提供的.虽然http协议有要求不能篡改referer的值.但是一个网站的安全性绝对不能交由其他人员来保证.
  • token 验证:从上面的样式可以发现,攻击者伪造了转账的表单,那么网站可以在表单中加入了一个随机的token来验证.token随着其他请求数据一起被提交到服务器.服务器通过验证token的值来判断post请求是否合法.由于攻击者没有办法获取到页面信息,所以它没有办法知道token的值.那么伪造的表单中就没有该token值.服务器就可以判断出这个请求是伪造的.

get和post

get是获取数据的,而post是提交数据的, get参数置于url,post参数置于报文

http请求头中Referer的含义和作用

表示页面的来源页面

HTTPS和HTTP的主要区别

https协议需要到CA申请证书,一般免费证书较少,因而需要一定费用。

http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl/tls加密传输协议。

http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

http的连接很简单,是无状态的;HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全

cookie、sessionStorage和localStorage的区别

在这里插入图片描述

TCP和UDP的区别

TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,也就是说,在收发数据前,必须和对方建立可靠的连接。 一个TCP连接必须要经过三次“对话”才能建立起来
UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。

TCP 是面向连接的,UDP 是面向无连接的
UDP程序结构较简单
TCP 是面向字节流的,UDP 是基于数据报的
TCP 保证数据正确性,UDP 可能丢包
TCP 保证数据顺序,UDP 不保证

js原理篇

prototype 和 proto 的关系是什么

proto 属性指向其构造函数的 prototype 属性
prototype 是函数对象(包括 Function 以及 Object)的专有属性;
proto 存在于所有对象属性中,是原型链遍历中实际访问的属性。

手写jsonp

/**
* jsonp获取请求数据
* @param {string}url
* @param {object}params
* @param {function}callback
*/
function jsonp({ url, params, callback }) {
    return new Promise((resolve, reject) => {
        let script = document.createElement('script');
        params = JSON.parse(JSON.stringify(params));
        let arrs = [];
        for (let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        arrs.push(`callback=${callback}`);
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
        window[callback] = function (data) {
            resolve(data);
            document.body.removeChild(script);
        }
    })
}

js继承

https://blog.csdn.net/Yuki_yuhan/article/details/108248086

预编译

  • JavaScript 脚本的运行由两个阶段组成:预编译阶段 和 执行阶段,先进行预编译,再执行语句;
  • 预编译,简单理解,就是在内存中开辟一块空间,用来存放变量和函数。在 JavaScript 中,JavaScript 会在内存中为使用 var 关键字声明的变量和使用 function 关键字声明的函数开辟一块空间,用来存放使用这两者声明的变量和函数;
  • 在预编译时,function 的优先级比 var
  • 在预编译阶段,只进行 变量/函数声明,不会进行变量的初始化(即变量赋值,所有变量的值都是 undefined);变量赋值 是在执行阶段才进行的。

长列表优化

setTimeout => requestAnimationFrame按照帧数回调 => DocumentFragment避免新建dom回流
DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。
可以使用document.createDocumentFragment方法或者构造函数来创建一个空的DocumentFragment

深浅拷贝

浅拷贝

function clone(target) {
    // 如果不是数组或者对象
    if (target && typeof target !== 'object') {
        // 直接复制
        var clone = target
        console.log(clone)
    }
    // 如果是数组或对象 
    else if (target && typeof target === 'object') {
        // 先判断是数组还是对象
        var clone = Array.isArray(target) ? [] : {}
        // 遍历目标
        for (var key in target) {
            clone[key] = target[key]
        }
    }
    return clone
}

深拷贝

function deepClone(target) {
  let clone;
  if (target && typeof target !== 'object') {
    clone = target;
  } else if (target && typeof target === 'object') {
    clone = Array.isArray(target) ? [] : {};
    for (let key in target) {
    // 判断是方法还是属性
      if (target.hasOwnPrototype(key)) {
        if (target[key] && typeof target[key] === 'object') {
          clone[key] = deepClone(target[key]);
        } else {
          clone[key] = target[key];
        }
      } else {
        clone[key] = target[key];
      }
    }
  }
  return clone;
}

object.create和object.assign

  • object.create:Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
  • object.assign:函数参数为一个目标对象(该对象作为最终的返回值),源对象(此处可以为任意多个)。通过调用该函数可以拷贝所有可被枚举的自有属性值到目标对象中。

writable:是否可任意写
configurable:是否能够删除,是否能够被修改
enumerable:是否能用 for in 枚举
value:值
get(): 访问
set(): 设置

尾递归(避免栈溢出的方法)

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
使用尾递归相对可以优化一点性能

function factorial(n) {
  if (n === 1) return 1;  return n * factorial(n - 1);
}
factorial(5) // 120

这是正常的递归函数,最多会计算n个调用记录

function factorial(n, total) {
  if (n === 1) return total;  return factorial(n - 1, n * total);
}
factorial(5, 1) // 120

如果改写成尾递归,只保留一个调用记录,复杂度 O(1)
更好的优化方式是采用蹦床函数去实现。蹦床函数(trampoline)可以将递归执行转为循环执行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }  return f;
}

它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

typeof

typeof null “object”
typeof undefined “undefined”
typeof 其他对象 “object"

js原型和原型链

js是基于原型的编程语言
我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
每一个构造函数都拥有一个 prototype 属性,这个属性指向一个对象,也就是原型对象
原型对象默认拥有一个 constructor 属性,指向指向它的那个构造函数
每个对象都拥有一个隐藏的属性 proto,指向它的原型对象
JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链

jsv8引擎原理

编译型语言

在程序执行之前必须进行专门的编译过程,有如下特点:

只须编译一次就可以把源代码编译成机器语言,后面的执行无须重新编译,直接使用之前的编译结果就可以;因此其执行的效率比较高;
编译性语言代表:C、C++、Java、Pascal/Object Pascal(Delphi);
程序执行效率比较高,但比较依赖编译器,因此跨平台性差一些;
不同平台对编译器影响较大。
16位系统下int是2个字节(16位),而32位系统下int占4个字节(32位);
32位系统下long类型占4字节,而64位系统下long类型占8个字节;

解释性语言

解释行语言,支持动态类型,弱类型,在程序运行的时候才进行编译,而编译前需要确定变量的类型,效率比较低,对不同系统平台有较大的兼容性.

源代码不能直接翻译成机器语言,而是先翻译成中间代码,再由解释器对中间代码进行解释运行;

源代码—>中间代码—>机器语言
程序不需要编译,程序在运行时才翻译成机器语言,每执行一次都要翻译一次;

解释性语言代表:Python、JavaScript、Shell、Ruby、MATLAB等;

运行效率一般相对比较低,依赖解释器,跨平台性好;

比较

一般,编译性语言的运行效率比解释性语言更高;但是不能一概而论,部分解释性语言的解释器通过在运行时动态优化代码,甚至能使解释性语言的性能超过编译性语言;
编译性语言的跨平台特性比解释性语言差一些;

v8引擎

V8引擎是一个JavaScript引擎实现,最初由一些语言方面专家设计,后被谷歌收购,随后谷歌对其进行了开源。
V8使用C++开发,在运行JavaScript之前,相比其它的JavaScript的引擎转换成字节码或解释执行,V8将其编译成原生机器码(IA-32, x86-64, ARM, or MIPS CPUs),并且使用了如内联缓存(inline caching)等方法来提高性能。
有了这些功能,JavaScript程序在V8引擎下的运行速度媲美二进制程序。
V8支持众多操作系统,如windows、linux、android等,也支持其他硬件架构,如IA32,X64,ARM等,具有很好的可移植和跨平台特性。

js执行机制

单线程执行

”JS是单线程的”指的是JS 引擎线程
在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。
Node环境中,只有JS 线程。

  • GUI渲染线程:负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。
  • (例如V8引擎)JS引擎线程负责解析Javascript脚本,运行代码。JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

注意:GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

宿主

JS运行的环境。一般为浏览器或者Node

执行栈

是一个存储函数调用的栈结构,遵循先进后出的原则。

Event Loop

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。
Event Loop中,每一次循环称为tick,每一次tick的任务如下:

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空;
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。
  • 在这里插入图片描述

宏任务和微任务

ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

js为什么设计成单线程语言

如果设计成多个线程,线程之间dom操作矛盾会导致浏览器不知道以哪个为主,web worker可以实现多个线程,但是不能操作dom,原理上是一样的

手写节流,防抖

防抖:连续点击推迟事件执行

function debounce(func, wait) {
    var timeout = null;
    var context = this;
    var args = arguments;
    return function () {
        if (timeout) clearTimeout(timeout);
        var callNow = !timeout;
        timeout = setTimeout(function () {
            timeout = null;
        }, wait)
        if (callNow) func.apply(context, args)
    }
}

节流:连续点击单位时间执行一次

function throttle(func, wait) {
    var previous = 0;
    return function () {
        var now = Date.now();
        var context = this;
        var args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

页面性能优化

提升页面性能优化的常见方式:

1、资源压缩合并,减少http请求

2、非核心代码异步加载 --> 异步加载的方式 --> 异步加载的区别

3、利用浏览器缓存 --> 缓存的分类 --> 缓存的原理

defer和async的区别

defer:在HTML解析完之后才会执行。如果是多个,则按照加载的顺序依次执行。

async:在加载完之后立即执行。如果是多个,执行顺序和加载顺序无关。

手写new

(1) 创建一个新对象;
(2) 将构造函数中的this指向该对象
(3) 执行构造函数中的代码(为这个新对象添加属性) ;
(4) 返回新对象。

function _new(obj, ...rest){
    // 基于obj的原型创建一个新的对象
    const newObj = Object.create(obj.prototype);

    // 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
    const result = obj.apply(newObj, rest);

    // 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
    return typeof result === 'object' ? result : newObj;
}

手写event类

class Event {
    constructor() {
        this.handlers = {}
    }

    on(eventName, cb) {
        const eventCallbackStack = this._getHandler(eventName).callbackStack
        eventCallbackStack.push(cb)
    }

    emit(eventName, ...args) {
        if(this.handlers[eventName]) {
            this.handlers[eventName].callbackStack.forEach(cb => {
                // 修正this指向
                cb.call(cb, ...args)
            })
            // 移除once事件
            if(this.handlers[eventName].isOnce) {
                this.off(eventName)
            }
        }
    }

    off(eventName) {
        this.handlers[eventName] && delete this.handlers[eventName]
    }

    once(eventName, cb) {
        const eventCallbackStack = this._getHandler(eventName, true).callbackStack
        eventCallbackStack.push(cb)
    }

    /**
     * 根据事件名获取事件对象
     * @param eventName
     * @param isOnce  // 是否为once事件
     */
    _getHandler(eventName, isOnce = false){
        if(!this.handlers[eventName]) {
            this.handlers[eventName] = {
                isOnce,
                callbackStack: [],
            }
        }
        return this.handlers[eventName]
    }
}

ES6中class是语法还是语法糖

语法糖,js的继承还是基于原型的

原型的终点

null

手写实现promise,promise. all,promise. race

promise

class Promsie {
    constructor(fn) {
        //三个状态
        this.status = 'pending',
        this.resolve = undefined;
        this.reject = undefined;
        let resolve = value => {
            if (this.status === 'pending') {
                this.status = 'resolved';
                this.resolve = value;
            }
        };
        let reject = value => {
            if (this.status === 'pending') {
                this.status = 'rejected';
                this.reject = value;
            }
        }
        try {
            fn(resolve, reject)
        } catch (e) {
            reject(e)
        }
    }
    then(onResolved, onRejected) {
        switch (this.status) {
            case 'resolved': onResolved(this.resolve); break;
            case 'rejected': onRejected(this.resolve); break;
            default:
        }
    }

}

promise.all

   function promiseAll(promises) {
            if (!Array.isArray(promises)) {
                throw new Error("promises must be an array")
            }
            return new Promise(function (resolve, reject) {

                let promsieNum = promises.length;
                let resolvedCount = 0;
                let resolveValues = new Array(promsieNum);
                for (let i = 0; i < promsieNum; i++) {
                    Promise.resolve(promises[i].then(function (value) {
                        resolveValues[i] = value;
                        resolvedCount++;
                        if (resolvedCount === promsieNum) {
                            return resolve(resolveValues)
                        }
                    }, function (reason) {
                        return reject(reason);
                    }))

                }
            })
        }

promise.race

 function promiseRace(promises) {
            if (!Array.isArray(promises)) {
                throw new Error("promises must be an array")
            }
            return new Promise(function (resolve, reject) {
                promises.forEach(p =>
                    Promise.resolve(p).then(data => {
                        resolve(data)
                    }, err => {
                        reject(err)
                    })
                )
            })
        }

闭包优缺点

1.保护函数内的变量安全,加强了封装性 2.在内存中维持一个变量(用的太多就变成了缺点,占内存)
闭包之所以会占用资源是当函数a执行结束后, 变量i不会因为函数a的结束而销毁, 因为b的执行需要依赖a中的变量。
不适合场景:返回闭包的函数是个非常大的函数

function和object的关系

Function.prototype.__proto__ === Object.prototype;//true
Object.__proto__ === Function.prototype;//true

手写call

Function.prototype.myCall = function(context, ...args) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = window
  }
  let fnSymbol = Symbol()
  context[fnSymbol] = this
  let fn = context[fnSymbol] (...args)
  delete context[fnSymbol] 
  return fn
}
// context[fnSymbol]中的this指向的是context

异步执行顺序(易错)

由一道字节面试题进入主题:

async function async1() {
	console.log('async1 start')
	await async2()
	console.log('async1 end')
}
async function async2() {
	console.log('async2')
}
console.log('script start')
setTimeout(function() {
	console.log('settimeout')
}
async1()
new Promise(function(resolve) {
	console.log('promise1')
	resolve()
}).then(function() {
	console.log('promise2')
})
console.log('script end')

答案:

script start
async1 start
async2
promise1
script end
async1 end
promise2
settimeout

setTimeout

setTimeout的回调时一个task,它会被放到宏任务的队列中,即使是0ms的情况

promise

promise本身是同步的立即执行函数,当在executor中执行resolve或者reject的时候,此时是异步操作,会执行then、catch,这个才是异步的task,会放到微任务的栈中

async/await

async函数返回一个promise对象,当函数执行的时候,一旦遇到await就会先返回,可以理解成让出了线程,跳出了async函数体,每个await之后的操作是一个then,会被放入微任务队列中,刚开始的await之前的操作是同步的操作

async func1() {
	console.log(1);
	await func2(); // 让线程给func2
	console.log(2); // 这一步是异步的,会被放入微任务队列
}

如果在async1前面加async呢

this指向

箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。

var name = 'window'; // 其实是window.name = 'window'

var A = {
   name: 'A',
   sayHello: function(){
      console.log(this.name)
   }
}

A.sayHello();// 输出A

var B = {
  name: 'B'
}

A.sayHello.call(B);//输出B

A.sayHello.call();//不传参数指向全局window对象,输出window.name也就是window

箭头函数的this不指向调用的对象,而指向运行时的上下文环境

var name = 'window'; 

var A = {
   name: 'A',
   sayHello: () => {
      console.log(this.name)
   }
}

A.sayHello();// window

怎么把sayHello的this永远绑定A?只要把箭头函数再包在一个function里即可,因为箭头函数的this永远指向运行时的环境

var name = 'window'; 

var A = {
   name: 'A',
   sayHello: function(){
      var s = () => console.log(this.name)
      return s//返回箭头函数s
   }
}

var sayHello = A.sayHello();
sayHello();// 输出A 

var B = {
   name: 'B';
}

sayHello.call(B); //还是A
sayHello.call(); //还是A

this绑定优先级

new绑定 > 显示绑定(call, apply ,bind) > 隐式绑定(指向调用者) > 默认绑定

“硬绑定” bind为什么在new之后

// 这是MDN提供的一种bind(..)实现,代码进行了排版
// 代码会判断硬绑定函数是否被new调用,如果是的话就会使用新创建的this
// 替换硬绑定的this
if (!Function.prototype.bind1) {
    Function.prototype.bind1 = function (oThis) {
        // oThis = null;
        if (typeof this !== "function") {
            // 与 ECMAScript 5 最接近的
            // 内部 IsCallable 函数
            throw new TypeError(
                "Function.proptotype.bind  -  what is trying " +
                "to be bound is not callable"
            );
        }
        var aArgs = Array.prototype.slice.call(arguments, 1),
            fToBind = this, // foo函数体
            fNOP = function () {},
            fBound = function () {
                // this instanceof fNOP === true时,说明返回的fBound被当做new的构造函数调用
               
                return fToBind.apply(
                    (
                        this instanceof fNOP ? this : oThis
                    ),
                    aArgs.concat(
                        Array.prototype.slice.call(arguments)
                    )
                );
            };

        // 维护原型关系
        if (this.prototype) {
            // Function.prototype doesn't have a prototype property
            fNOP.prototype = this.prototype;
        }
        // 下行的代码使fBound.prototype是fNOP的实例,因此
        // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,
        // 新对象的__proto__就是fNOP的实例
        fBound.prototype = new fNOP();
        return fBound;
    }
}

function foo(p1, p2) {
    this.val = p1 + p2;
}

// 之所以使用null时因为在本例中我们并不关心硬绑定的this是什么
// 反正使用new时this会被修改

var bar = foo.bind1(null, 'p1');
var baz = new bar('p2');
console.log(baz.val); // "p1p2"

事件委托

一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

regexp正则,讲讲贪婪模式

正则默认是贪婪模式,默认的贪婪模式会尽可能多的匹配所搜索的字符串。

非贪婪模式:

当?字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串

例如,对于字符串 “oooo”,‘o+?’ 将匹配单个 “o”,而 ‘o+’ 将匹配所有 ‘o’。

css篇

css盒子模型

css盒子模型分为两种,标准盒模型和ie盒模型,分别对应content-box和border-box,第一个width只对应content的宽度,第二个content + padding + border的宽度

css3新特性

新增选择器 p:nth-child(n){color: rgba(255, 0, 0, 0.75)}

弹性盒模型 display: flex;

多列布局 column-count: 5;

媒体查询 @media (max-width: 480px) {.box: {column-count: 1;}}

个性化字体 @font-face{font-family:BorderWeb;src:url(BORDERW0.eot);}

颜色透明度 color: rgba(255, 0, 0, 0.75);

圆角 border-radius: 5px;

渐变 background:linear-gradient(red, green, blue);

阴影 box-shadow:3px 3px 3px rgba(0, 64, 128, 0.3);

倒影 box-reflect: below 2px;

文字装饰 text-stroke-color: red;

文字溢出 text-overflow:ellipsis;

背景效果 background-size: 100px 100px;

边框效果 border-image:url(bt_blue.png) 0 10;

旋转 transform: rotate(20deg);

倾斜 transform: skew(150deg, -10deg);

位移 transform:translate(20px, 20px);

缩放 transform: scale(。5);

平滑过渡 transition: all .3s ease-in .1s;

动画 @keyframes anim-1 {50% {border-radius: 50%;}} animation: anim-1 1s;

css动画

transition

在CSS 3引入Transition(过渡)这个概念之前,CSS是没有时间轴的。也就是说,所有的状态变化,都是即时完成。
transition的作用在于,指定状态变化所需要的时间。

transition-delay

完成动画的时间,可以加上css属性表明只有某个部分增加动画

img{
    transition: 1s height;
}

我们还可以指定transition适用的属性,比如只适用于height。

transition-timing-function

状态变化速度(又称timing function),默认不是匀速的,而是逐渐放慢,这叫做ease。除了ease以外,其他模式还包括

(1)linear:匀速

(2)ease-in:加速

(3)ease-out:减速

(4)cubic-bezier函数:自定义速度模式

animation

CSS Animation需要指定动画一个周期持续的时间,以及动画效果的名称

div:hover {
  animation: 1s rainbow;
}

上面代码表示,当鼠标悬停在div元素上时,会产生名为rainbow的动画效果,持续时间为1秒。为此,我们还需要用keyframes关键字,定义rainbow效果。

@keyframes rainbow {
  0% { background: #c00; }
  50% { background: orange; }
  100% { background: yellowgreen; }
}

css权重

第零等:!important, 大过了其它任何设置。
第一等:代表内联样式,如: style=””,权值为1000。
第二等:代表ID选择器,如:#content,权值为0100。
第三等:代表类,伪类和属性选择器,如.content,权值为0010。
第四等:代表类型选择器和伪元素选择器,如div p::after,权值为0001。
第五等:通配符、子选择器、相邻选择器等的。如*、>、+,权值为0000。
第六等:继承的样式没有权值。

三栏布局和二栏布局的实现

flex:1
第一个参数表示: flex-grow 定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大
第二个参数表示: flex-shrink 定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小
第三个参数表示: flex-basis给上面两个属性分配多余空间之前, 计算项目是否有多余空间, 默认值为 auto, 即项目本身的大小

实现等比矩形

css实现宽高等比的矩形:用一个div包住一个div,子div设置width:100%, height: 0, padding-bottom: 对应比例,因为padding是根据宽度计算的,外层的div用来控制具体的i宽高是多少

inline和inline-block

inline元素不会独占一行,多个相邻的行内元素会排列在同一行里,直到一行排列不下,才会新换一行,其宽度随元素的内容而变化。
block元素会独占一行,多个block元素会各自新起一行。默认情况下,block元素宽度自动填满其父元素宽度。
inline-block简单来说就是将对象呈现为inline对象,但是对象的内容作为block对象呈现。之后的内联对象会被排列在同一行内。比如我们可以给一个link(a元素)inline-block属性值,使其既具有block的宽度高度特性又具有inline的同行特性。

BFC

在讲 BFC 之前,我们先来了解一下常见的定位方案,定位方案是控制元素的布局,有三种常见方案:

  • 普通流 (normal flow)

在普通流中,元素按照其在 HTML 中的先后位置至上而下布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行,块级元素则会被渲染为完整的一个新行,除非另外指定,否则所有元素默认都是普通流定位,也可以说,普通流中元素的位置由该元素在 HTML 文档中的位置决定。

  • 浮动 (float)

在浮动布局中,元素首先按照普通流的位置出现,然后根据浮动的方向尽可能的向左边或右边偏移,其效果与印刷排版中的文本环绕相似。

  • 绝对定位 (absolute positioning)

在绝对定位布局中,元素会整体脱离普通流,因此绝对定位元素不会对其兄弟元素造成影响,而元素具体的位置由绝对定位的坐标决定。

BFC 即 Block Formatting Contexts (块级格式化上下文),它属于上述定位方案的普通流。

具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。

通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。

只要元素满足下面任一条件即可触发 BFC 特性:

  • body 根元素
  • 浮动元素:float 除 none 以外的值
  • 绝对定位元素:position (absolute、fixed)
    0 display 为 inline-block、table-cells、flex
  • overflow 除了 visible 以外的值 (hidden、auto、scroll)

clientHeight, scrollHeight, offsetHeight ,scrollTop和offsetTop的区别

clientHeight

客户端高度,即视口高度,屏幕中的可视高度

scrollHeight

dom的完整高度,也就是它的可滚动高度,包括没显示在当前屏幕内的部分

offsetHeight

在clientHeight的基础上加上滚动栏的高度

scrollTop

当前元素顶端距离窗口顶端距离,鼠标滚轮会影响其数值.

offsetTop

当前元素顶端距离父元素顶端距离,鼠标滚轮不会影响其数值.

css渲染过程

css的渲染规则,是从上到下,从右到左渲染的。
例如:

.main h4 a { font-size: 14px; }

渲染过程是这样的:首先先找到所有的 a,沿着 a 的父元素查找h4,然后再沿着 h4,查找.main。中途找到了符合匹配规则的节点就加入结果集。如果找到根元素的 html 都没有匹配,则这条路径不再遍历。下一个 a 开始重复这个查找匹配,直至没有a继续查找。

浏览器的这种查找规则是为了尽早过滤掉一些无关的样式规则和元素。

框架篇

vue双向绑定为什么数组采用拦截原型来实现

如果出现[0, undefined…, 0]的数组情况,数组长度很长,但是只有首尾有参数,中间的value并不用,这样对中间参数的不必要监听会影响页面性能

keep-alive原理

Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode节点。
因此,Vue 的 keep-alive 缓存也是基于 VNode节点 而不是直接存储 DOM 节点。

将需要缓存的VNode节点保存在this.cache中,在render时,如果VNode的name符合在缓存条件(可以用include以及exclude控制),则会从this.cache中取出之前缓存的VNode实例进行渲染。

vuex

基础

  • state: Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT (opens new window))”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照
  • getters: 类似于计算属性
  • mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation, 不可直接调用,实现commit调用
  • actions: Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。Action 通过 store.dispatch 方法触发
  • modules: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割

源码思考

  • 使用Vuex只需执行 Vue.use(Vuex),并在Vue的配置中传入一个store对象的示例,store是如何实现注入的?

Vue.use(Vuex) 方法执行的是install方法,它实现了Vue实例对象的init方法封装和注入,使传入的store对象被设置到Vue上下文环境的 s t o r e 中 。 因 此 在 V u e C o m p o n e n t 任 意 地 方 都 能 够 通 过 t h i s . store中。因此在Vue Component任意地方都能够通过this. storeVueComponentthis.store访问到该store。

  • state内部支持模块配置和模块嵌套,如何实现的?

在store构造方法中有makeLocalContext方法,所有module都会有一个local context,根据配置时的path进行匹配。所以执行如dispatch(‘submitOrder’, payload)这类action时,默认的拿到都是module的local state,如果要访问最外层或者是其他module的state,只能从rootState按照path路径逐步进行访问。

  • 在执行dispatch触发action(commit同理)的时候,只需传入(type, payload),action执行函数中第一个参数store从哪里获取的?

store初始化时,所有配置的action和mutation以及getters均被封装过。在执行如dispatch(‘submitOrder’, payload)的时候,actions中type为submitOrder的所有处理方法都是被封装后的,其第一个参数为当前的store对象,所以能够获取到 { dispatch, commit, state, rootState } 等数据。

  • Vuex如何区分state是外部直接修改,还是通过mutation方法修改的?

Vuex中修改state的唯一渠道就是执行 commit(‘xx’, payload) 方法,其底层通过执行 this._withCommit(fn) 设置_committing标志变量为true,然后才能修改state,修改完毕还需要还原_committing变量。外部修改虽然能够直接修改state,但是并没有修改_committing标志位,所以只要watch一下state,state change时判断是否_committing值为true,即可判断修改的合法性。

  • 调试时的”时空穿梭”功能是如何实现的?

devtoolPlugin中提供了此功能。因为dev模式下所有的state change都会被记录下来,’时空穿梭’ 功能其实就是将当前的state替换为记录中某个时刻的state状态,利用 store.replaceState(targetState) 方法将执行this._vm.state = state 实现。

vue3的相关改动

1、组件式api的调整,将逻辑层集中了,并且抽离出了vue原先的生命周期和状态函数,可以更加灵活地调用,解决了mixin重名的问题
2、响应式实现上的调整,改用proxy去实现响应式,proxy相比defineProperty是针对一个对象的全部操作的,这样就不会有vue2不能监听到的问题
3、惰性响应式,vue2会把每一个对象数据都编程响应式的,vue3通过ref和reactive增加响应式,只需要给需要的对象或属性增加响应式
4、vue2的数据更新粒度是组件级的,对于一些静态节点,也会去遍历判断,而vue3是在编译阶段就对静态模板进行了分析,生成了block tree,这样就避免了对静态节点的不必要判断
5、vue2在父组件更新的时候,会强制使子组件更新,而vue3中优化了slot的生成,只有slot中的属性更新才会触发子组件更新

vue3哪些方面变快了

  • diff方法优化vue2.x中的虚拟dom是进行全量的对比。而vue3.0新增了静态标记。在与上次虚拟节点进行对比的时候,只对比带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容。
  • hoistStatic(静态提升):vue2.x中无论元素是否参与更新,每次都会重新创建,然后再渲染。vue3.0中对于不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用即可。
  • cacheHandlers(事件侦听器缓存):默认情况下,如onClick事件会被视为动态绑定,所以每次都会追踪它的变化,但是因为是同一个函数,所以不用追踪变化,直接缓存起来复用即可。

vue3 patchFlags

export const enum PatchFlags {
  // 动态文字内容
  TEXT = 1,

  // 动态 class
  CLASS = 1 << 1,

  // 动态样式
  STYLE = 1 << 2,

  // 动态 props
  PROPS = 1 << 3,

  // 有动态的key,也就是说props对象的key不是确定的
  FULL_PROPS = 1 << 4,

  // 合并事件
  HYDRATE_EVENTS = 1 << 5,

  // children 顺序确定的 fragment
  STABLE_FRAGMENT = 1 << 6,

  // children中有带有key的节点的fragment
  KEYED_FRAGMENT = 1 << 7,

  // 没有key的children的fragment
  UNKEYED_FRAGMENT = 1 << 8,

  // 只有非props需要patch的,比如`ref`
  NEED_PATCH = 1 << 9,

  // 动态的插槽
  DYNAMIC_SLOTS = 1 << 10,

  // SPECIAL FLAGS -------------------------------------------------------------

  // 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag

  // 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff
  HOISTED = -1,

  // 用来表示一个节点的diff应该结束
  BAIL = -2,
}

以上其实就是所有的patchFlags,和他的名字含义一致,他就是一系列的标志,来标识一个节点该如何进行更新的。

可能有些同学不是很懂他的值CLASS = 1 << 1啥意思,为什么要用这样的值来进行表示,那这个其实很简单,这其实是对每个 flag 使用二进制数中的某一位来表示,在以上的例子中:

TEXT = 0000000001;

CLASS = 0000000010;

STYLE = 0000000100;

// 以此类推

每个变量都至少有一位是 1,那么这么做有什么好处呢?

很容易进行复合,我们可以通过TEXT | CLASS来得到0000000011,而这个值可以表示他即有TEXT的特性,也有CLASS的特性
方便进行对比,我们拿到一个值FLAG的时候,想要判断他有没有TEXT特性,只需要FLAG & TEXT > 0就行
方便扩展,在足够位数的情况下,我们新增一种特性就只需要让他左移的位数加一就不会重复

vue单向数据流

在vue文档组件模块里有介绍到单向数据流主要是避免子组件修改父组件的状态出现应用数据流混乱的状态,避免不必要的理解,维持父子组件正常的数据依赖关系。

mvc和mvvm

MVC数据传递的方式是单向的
MVVM数据传递的方式是双向的
MVC:M 指的是Model(模型)是后端传递的数据,V指的是View(视图)所看到的页面,
C指的是Controller是应用程序中处理用户交互的部分

父子组件执行顺序

父beforeCreate -> 父created -> 父beforeMount
-> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted
-> 父mounted

scope是怎么实现的

PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom,这种做法使得样式只作用于含有该属性的dom元素(组件内部的dom)

算法

顺序存储结构和链式存储结构的优缺点

① 顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。

优点:存储密度大(=1),存储空间利用率高。缺点:插入或删除元素时不方便。

②链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针

优点:插入或删除元素时很方便,使用灵活。缺点:存储密度小(<1),存储空间利用率低。

顺序表适宜于做查找这样的静态操作;链表宜于做插入、删除这样的动态操作。

若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;

若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。

但是对于js的数组而言,并不是单纯的顺序存储,V8中对数组做了一层封装,使其有两种实现方式:快数组和慢数组,快数组底层是连续内存,通过索引直接定位,慢数组底层是哈希表,通过计算哈希值来定位。两种实现方式各有特点,有各自的使用情况,也会相互转换。

堆、栈和队列

  • 堆(heap)也被称为优先队列,队列中允许的操作是 先进先出(FIFO),在队尾插入元素,在队头取出元素。而堆也是一样,在堆底插入元素,在堆顶取出元素。
  • 栈(Stack)又名堆栈,作为一个 先进后出 的数据结构。(注意:这里的堆栈本身就是栈,只是换了个抽象的名字。)
  • 队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
  • 队列采用 先进先出 FIFO(first in first out),新元素(等待进入队列的元素)总是被插入到链表的尾部,而读取的时候总是从链表的头部开始读取。
  • 2
    点赞
  • 4
    评论
  • 12
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值