字节前端高频面试题
什么是原型什么是原型链?
forEach 和 map 方法有什么区别
这方法都是用来遍历数组的,两者区别如下:
forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
JSONP
JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求;
说一下原型链和原型链的继承吧
所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype,其包含了 JavaScript 中许多通用的功能
为什么能创建 “类”,借助一种特殊的属性:所有的函数默认都会拥有一个名为 prototype 的共有且不可枚举的属性,它会指向另外一个对象,这个对象通常被称为函数的原型
在发生 new 构造函数调用时,会将创建的新对象的 [[Prototype]] 链接到 Person.prototype 指向的对象,这个机制就被称为原型链继承
方法定义在原型上,属性定义在构造函数上
首先要说一下 JS 原型和实例的关系:每个构造函数 (constructor)都有一个原型对象(prototype),这个原型对象包含一个指向此构造函数的指针属性,通过 new 进行构造函数调用生成的实例,此实例包含一个指向原型对象的指针,也就是通过 [[Prototype]] 链接到了这个原型对象
然后说一下 JS 中属性的查找:当我们试图引用实例对象的某个属性时,是按照这样的方式去查找的,首先查找实例对象上是否有这个属性,如果没有找到,就去构造这个实例对象的构造函数的 prototype 所指向的对象上去查找,如果还找不到,就从这个 prototype 对象所指向的构造函数的 prototype 原型对象上去查找
什么是原型链:这样逐级查找形似一个链条,且通过 [[Prototype]] 属性链接,所以被称为原型链
什么是原型链继承,类比类的继承:当有两个构造函数 A 和 B,将一个构造函数 A 的原型对象的,通过其 [[Prototype]] 属性链接到另外一个 B 构造函数的原型对象时,这个过程被称之为原型继承。
** 标准答案更正确的解释**
什么是原型链?
当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。这种通过 通过原型链接的逐级向上的查找链被称为原型链
什么是原型继承?
一个对象可以使用另外一个对象的属性或者方法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另外一个对象的属性和方法了。
选择排序--时间复杂度 n^2
题目描述:实现一个选择排序
实现代码如下:
HTTP 1.1 和 HTTP 2.0 的区别
二进制协议:HTTP/2 是一个二进制协议。在 HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。
多路复用: HTTP/2 实现了多路复用,HTTP/2 仍然复用 TCP 连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"【1】的问题。
数据流: HTTP/2 使用了数据流的概念,因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流 ID ,用来区分它属于哪个数据流。
头信息压缩: HTTP/2 实现了头信息压缩,由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
服务器推送: HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。
【1】队头堵塞:
队头阻塞是由 HTTP 基本的“请求 - 应答”模型所导致的。HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求是没有优先级的,只有入队的先后顺序,排在最前面的请求会被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本,造成了队头堵塞的现象。
箭头函数与普通函数的区别
(1)箭头函数比普通函数更加简洁
如果没有参数,就直接写一个空括号即可
如果只有一个参数,可以省去参数的括号
如果有多个参数,用逗号分割
如果函数体的返回值只有一句,可以省略大括号
如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个 void 关键字。最常见的就是调用一个函数:
(2)箭头函数没有自己的 this
箭头函数不会创建自己的 this, 所以它没有自己的 this,它只会在自己作用域的上一层继承 this。所以箭头函数中 this 的指向在它在定义时已经确定了,之后不会改变。
(3)箭头函数继承来的 this 指向永远不会改变
对象 obj 的方法 b 是使用箭头函数定义的,这个函数中的 this 就永远指向它定义时所处的全局执行环境中的 this,即便这个函数是作为对象 obj 的方法调用,this 依旧指向 Window 对象。需要注意,定义对象的大括号{}
是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
(4)call()、apply()、bind()等方法不能改变箭头函数中 this 的指向
(5)箭头函数不能作为构造函数使用
构造函数在 new 的步骤在上面已经说过了,实际上第二步就是将函数中的 this 指向该对象。 但是由于箭头函数时没有自己的 this 的,且 this 指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
(6)箭头函数没有自己的 arguments
箭头函数没有自己的 arguments 对象。在箭头函数中访问 arguments 实际上获得的是它外层函数的 arguments 值。
(7)箭头函数没有 prototype
(8)箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
代码输出问题
输出结果:1 undefined 2
解析:
console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1;
console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined;
console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。
冒泡排序--时间复杂度 n^2
题目描述:实现一个冒泡排序
实现代码如下:
new 一个构造函数,如果函数返回 return {}
、 return null
, return 1
, return true
会发生什么情况?
如果函数返回一个对象,那么 new 这个函数调用返回这个函数的返回对象,否则返回 new 创建的新对象
对作用域、作用域链的理解
1)全局作用域和函数作用域
(1)全局作用域
最外层函数和最外层函数外面定义的变量拥有全局作用域
所有未定义直接赋值的变量自动声明为全局作用域
所有 window 对象的属性拥有全局作用域
全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
(2)函数作用域
函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
使用 ES6 中新增的 let 和 const 指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由
{ }
包裹的代码片段)let 和 const 声明的变量不会有变量提升,也不可以重复声明
在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到 window 对象就被终止,这一层层的关系就是作用域链。
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
DNS 同时使用 TCP 和 UDP 协议?
DNS 占用 53 号端口,同时使用 TCP 和 UDP 协议。 (1)在区域传输的时候使用 TCP 协议
辅域名服务器会定时(一般 3 小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用 TCP 而不是 UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。
TCP 是一种可靠连接,保证了数据的准确性。
(2)在域名解析的时候使用 UDP 协议
客户端向 DNS 服务器查询域名,一般返回的内容都不超过 512 字节,用 UDP 传输即可。不用经过三次握手,这样 DNS 服务器负载更低,响应更快。理论上说,客户端也可以指定向 DNS 服务器查询时用 TCP,但事实上,很多 DNS 服务器进行配置的时候,仅支持 UDP 查询包。
箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
普通函数通过 function 关键字定义, this 无法结合词法作用域使用,在运行时绑定,只取决于函数的调用方式,在哪里被调用,调用位置。(取决于调用者,和是否独立运行)
箭头函数使用被称为 “胖箭头” 的操作
=>
定义,箭头函数不应用普通函数 this 绑定的四种规则,而是根据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无法被修改(new 也不行)。箭头函数常用于回调函数中,包括事件处理器或定时器
箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
没有原型、没有 this、没有 super,没有 arguments,没有 new.target
不能通过 new 关键字调用
一个函数内部有两个方法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 方法,创建一个实例对象,然后再执行这个函数体,将函数的 this 绑定在这个实例对象上
当直接调用时,执行 [[Call]] 方法,直接执行函数体
箭头函数没有 [[Construct]] 方法,不能被用作构造函数调用,当使用 new 进行函数调用时会报错。
进程和线程的区别
进程可以看做独立应用,线程不能
资源:进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位);线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)。
通信方面:线程间可以通过直接共享同一进程中的资源,而进程通信需要借助 进程间通信。
调度:进程切换比线程切换的开销要大。线程是 CPU 调度的基本单位,线程的切换不会引起进程切换,但某个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存、I/O 等,其开销远大于创建或撤销线程时的开销。同理,在进行进程切换时,涉及当前执行进程 CPU 环境还有各种各样状态的保存及新调度进程状态的设置,而线程切换时只需保存和设置少量寄存器内容,开销较小。
渲染过程中遇到 JS 文件如何处理?
JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
列表转成树形结构
题目描述:
实现代码如下:
对 Service Worker 的理解
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install
事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
打开页面,可以在开发者工具中的 Application
看到 Service Worker 已经启动了: 在 Cache 中也可以发现所需的文件已被缓存:
评论