写点什么

有哪些前端面试题是面试官必考的

作者:coder2028
  • 2023-03-01
    浙江
  • 本文字数:20962 字

    阅读完需:约 69 分钟

Iterator 迭代器

Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。


Iterator 语法:


const obj = {    [Symbol.iterator]:function(){}}
复制代码


[Symbol.iterator] 属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。


  • 迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据

  • 每一次的 next 都会返回一个对象,该对象有两个属性

  • value 代表想要获取的数据

  • done 布尔值,false 表示当前指针指向的数据有值,true 表示遍历已经结束


Iterator 的作用有三个:


  • 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  • 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。

  • 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。

  • 不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。


每一次调用 next 方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value 和 done 两个属性的对象。其中,value 属性是当前成员的值,done 属性是一个布尔值,表示遍历是否结束。


let arr = [{num:1},2,3]let it = arr[Symbol.iterator]() // 获取数组中的迭代器console.log(it.next())  // { value: Object { num: 1 }, done: false }console.log(it.next())  // { value: 2, done: false }console.log(it.next())  // { value: 3, done: false }console.log(it.next())  // { value: undefined, done: true }
复制代码


对象没有布局 Iterator 接口,无法使用for of 遍历。下面使得对象具备 Iterator 接口


  • 一个数据结构只要有 Symbol.iterator 属性,就可以认为是“可遍历的”

  • 原型部署了 Iterator 接口的数据结构有三种,具体包含四种,分别是数组,类似数组的对象,Set 和 Map 结构


为什么对象(Object)没有部署 Iterator 接口呢?


  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。然而遍历遍历器是一种线性处理,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换

  • 对对象部署Iterator接口并不是很必要,因为Map弥补了它的缺陷,又正好有Iteraotr接口


let obj = {    id: '123',    name: '张三',    age: 18,    gender: '男',    hobbie: '睡觉'}
obj[Symbol.iterator] = function () { let keyArr = Object.keys(obj) let index = 0 return { next() { return index < keyArr.length ? { value: { key: keyArr[index], val: obj[keyArr[index++]] } } : { done: true } } }}
for (let key of obj) { console.log(key)}
复制代码


Generator

GeneratorES6中新增的语法,和 Promise 一样,都可以用来异步编程。Generator 函数可以说是 Iterator 接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。


  • function* 用来声明一个函数是生成器函数,它比普通的函数声明多了一个*,*的位置比较随意可以挨着 function 关键字,也可以挨着函数名

  • yield 产出的意思,这个关键字只能出现在生成器函数体内,但是生成器中也可以没有yield 关键字,函数遇到 yield 的时候会暂停,并把 yield 后面的表达式结果抛出去

  • next作用是将代码的控制权交还给生成器函数


function *foo(x) {  let y = 2 * (yield (x + 1))  let z = yield (y / 3)  return (x + y + z)}let it = foo(5)console.log(it.next())   // => {value: 6, done: false}console.log(it.next(12)) // => {value: 8, done: false}console.log(it.next(13)) // => {value: 42, done: true}
复制代码


上面这个示例就是一个 Generator 函数,我们来分析其执行过程:


  • 首先 Generator 函数调用时它会返回一个迭代器

  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6

  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8

  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42


yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置



总结一下Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行


遍历器对象生成函数,最大的特点是可以交出函数的执行权


  • function 关键字与函数名之间有一个星号;

  • 函数体内部使用 yield表达式,定义不同的内部状态;

  • next指针移向下一个状态


这里你可以说说 Generator的异步编程,以及它的语法糖 asyncawiat,传统的异步编程。ES6 之前,异步编程大致如下


  • 回调函数

  • 事件监听

  • 发布/订阅


传统异步编程方案之一:协程,多个线程互相协作,完成异步任务。


// 使用 * 表示这是一个 Generator 函数// 内部可以通过 yield 暂停代码// 通过调用 next 恢复执行function* test() {  let a = 1 + 2;  yield 2;  yield 3;}let b = test();console.log(b.next()); // >  { value: 2, done: false }console.log(b.next()); // >  { value: 3, done: false }console.log(b.next()); // >  { value: undefined, done: true }
复制代码


从以上代码可以发现,加上 *的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现


// cb 也就是编译过的 test 函数function generator(cb) {  return (function() {    var object = {      next: 0,      stop: function() {}    };
return { next: function() { var ret = cb(object); if (ret === undefined) return { value: undefined, done: true }; return { value: ret, done: false }; } }; })();}// 如果你使用 babel 编译后可以发现 test 函数变成了这样function test() { var a; return generator(function(_context) { while (1) { switch ((_context.prev = _context.next)) { // 可以发现通过 yield 将代码分割成几块 // 每次执行 next 函数就执行一块代码 // 并且表明下次需要执行哪块代码 case 0: a = 1 + 2; _context.next = 4; return 2; case 4: _context.next = 6; return 3; // 执行完毕 case 6: case "end": return _context.stop(); } } });}
复制代码

作用域

  • 作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找

  • 作用域链: 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。


作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。


  • 当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找

  • 作用域链的创建过程跟执行上下文的建立有关....


作用域可以理解为变量的可访问性,总共分为三种类型,分别为:


  • 全局作用域

  • 函数作用域

  • 块级作用域,ES6 中的 letconst 就可以产生该作用域


其实看完前面的闭包、this 这部分内部的话,应该基本能了解作用域的一些应用。


一旦我们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何访问需要的变量或者函数的。


  • 首先作用域链是在定义时就被确定下来的,和箭头函数里的 this 一样,后续不会改变,JS 会一层层往上寻找需要的内容。

  • 其实作用域链这个东西我们在闭包小结中已经看到过它的实体了:[[Scopes]]



图中的 [[Scopes]] 是个数组,作用域的一层层往上寻找就等同于遍历 [[Scopes]]


1. 全局作用域


全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量


var globalName = 'global';function getName() {   console.log(globalName) // global  var name = 'inner'  console.log(name) // inner} getName();console.log(name); // console.log(globalName); //globalfunction setName(){   vName = 'setName';}setName();console.log(vName); // setName
复制代码


  • 从这段代码中我们可以看到,globalName 这个变量无论在什么地方都是可以被访问到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的

  • 当然全局作用域有相应的缺点,我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题。


2. 函数作用域


函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域


function getName () {  var name = 'inner';  console.log(name); //inner}getName();console.log(name);
复制代码


除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的


3. 块级作用域


ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。


在 JS 编码过程中 if 语句for 语句后面 {...} 这里面所包括的,就是块级作用域


console.log(a) //a is not definedif(true){  let a = '123';  console.log(a); // 123}console.log(a) //a is not defined
复制代码


从这段代码可以看出,变量 a 是在 if 语句{...} 中由 let 关键词进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义

HTTPS 的特点

HTTPS 的优点如下:


  • 使用 HTTPS 协议可以认证用户和服务器,确保数据发送到正确的客户端和服务器;

  • 使用 HTTPS 协议可以进行加密传输、身份认证,通信更加安全,防止数据在传输过程中被窃取、修改,确保数据安全性;

  • HTTPS 是现行架构下最安全的解决方案,虽然不是绝对的安全,但是大幅增加了中间人攻击的成本;


HTTPS 的缺点如下:


  • HTTPS 需要做服务器和客户端双方的加密个解密处理,耗费更多服务器资源,过程复杂;

  • HTTPS 协议握手阶段比较费时,增加页面的加载时间;

  • SSL 证书是收费的,功能越强大的证书费用越高;

  • HTTPS 连接服务器端资源占用高很多,支持访客稍多的网站需要投入更大的成本;

  • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名。

执行上下文

当执行 JS 代码时,会产生三种执行上下文


  • 全局执行上下文

  • 函数执行上下文

  • eval 执行上下文


每个执行上下文中都有三个重要的属性


  • 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问

  • 作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)

  • this


var a = 10function foo(i) {  var b = 20}foo()
复制代码


对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。


stack = [    globalContext,    fooContext]
复制代码


对于全局上下文来说,VO大概是这样的


globalContext.VO === globeglobalContext.VO = {    a: undefined,    foo: <Function>,}
复制代码


对于函数 foo 来说,VO 不能访问,只能访问到活动对象(AO


fooContext.VO === foo.AOfooContext.AO {    i: undefined,    b: undefined,    arguments: <>}// arguments 是函数独有的对象(箭头函数没有)// 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素// 该对象中的 `callee` 属性代表函数本身// `caller` 属性代表函数的调用者
复制代码


对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]]属性查找上级变量


fooContext.[[Scope]] = [    globalContext.VO]fooContext.Scope = fooContext.[[Scope]] + fooContext.VOfooContext.Scope = [    fooContext.VO,    globalContext.VO]
复制代码


接下来让我们看一个老生常谈的例子,var


b() // call bconsole.log(a) // undefined
var a = 'Hello world'
function b() { console.log('call b')}
复制代码


想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。


  • 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升


b() // call b second
function b() { console.log('call b fist')}function b() { console.log('call b second')}var b = 'Hello world'
复制代码


var会产生很多错误,所以在 ES6中引入了 letlet不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。


  • 对于非匿名的立即执行函数需要注意以下一点


var foo = 1(function foo() {    foo = 10    console.log(foo)}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
复制代码


因为当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到 foo,但是这个值又是只读的,所以对它的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。


specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;foo.[[Scope]] = Scope;specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // remove specialObject from the front of scope chain
复制代码


总结


执行上下文可以简单理解为一个对象:


它包含三个部分:


  • 变量对象(VO)

  • 作用域链(词法作用域)

  • this指向


它的类型:


  • 全局执行上下文

  • 函数执行上下文

  • eval执行上下文


代码执行过程:


  • 创建 全局上下文 (global EC)

  • 全局执行上下文 (caller) 逐行 自上而下 执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层

  • 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起

  • 函数执行完后,calleepop移除出执行栈,控制权交还全局上下文 (caller),继续执行

HTTP 状态码

状态码的类别:


1. 2XX (Success 成功状态码)

状态码 2XX 表示请求被正常处理了。

(1)200 OK

200 OK 表示客户端发来的请求被服务器端正常处理了。

(2)204 No Content

该状态码表示客户端发送的请求已经在服务器端正常处理了,但是没有返回的内容,响应报文中不包含实体的主体部分。一般在只需要从客户端往服务器端发送信息,而服务器端不需要往客户端发送内容时使用。

(3)206 Partial Content

该状态码表示客户端进行了范围请求,而服务器端执行了这部分的 GET 请求。响应报文中包含由 Content-Range 指定范围的实体内容。

2. 3XX (Redirection 重定向状态码)

3XX 响应结果表明浏览器需要执行某些特殊的处理以正确处理请求。

(1)301 Moved Permanently

永久重定向。 该状态码表示请求的资源已经被分配了新的 URI,以后应使用资源指定的 URI。新的 URI 会在 HTTP 响应头中的 Location 首部字段指定。若用户已经把原来的 URI 保存为书签,此时会按照 Location 中新的 URI 重新保存该书签。同时,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。


使用场景:


  • 当我们想换个域名,旧的域名不再使用时,用户访问旧域名时用 301 就重定向到新的域名。其实也是告诉搜索引擎收录的域名需要对新的域名进行收录。

  • 在搜索引擎的搜索结果中出现了不带 www 的域名,而带 www 的域名却没有收录,这个时候可以用 301 重定向来告诉搜索引擎我们目标的域名是哪一个。

(2)302 Found

临时重定向。 该状态码表示请求的资源被分配到了新的 URI,希望用户(本次)能使用新的 URI 访问资源。和 301 Moved Permanently 状态码相似,但是 302 代表的资源不是被永久重定向,只是临时性质的。也就是说已移动的资源对应的 URI 将来还有可能发生改变。若用户把 URI 保存成书签,但不会像 301 状态码出现时那样去更新书签,而是仍旧保留返回 302 状态码的页面对应的 URI。同时,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回 302 代码,搜索引擎认为新的网址只是暂时的。


使用场景:


  • 当我们在做活动时,登录到首页自动重定向,进入活动页面。

  • 未登陆的用户访问用户中心重定向到登录页面。

  • 访问 404 页面重新定向到首页。

(3)303 See Other

该状态码表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。303 状态码和 302 Found 状态码有着相似的功能,但是 303 状态码明确表示客户端应当采用 GET 方法获取资源。


303 状态码通常作为 PUT 或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用 GET。


注意:


  • 当 301、302、303 响应状态码返回时,几乎所有的浏览器都会把 POST 改成 GET,并删除请求报文内的主体,之后请求会再次自动发送。

  • 301、302 标准是禁止将 POST 方法变成 GET 方法的,但实际大家都会这么做。

(4)304 Not Modified

浏览器缓存相关。 该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。304 状态码返回时,不包含任何响应的主体部分。304 虽然被划分在 3XX 类别中,但是和重定向没有关系。


带条件的请求(Http 条件请求):使用 Get 方法 请求,请求报文中包含(if-matchif-none-matchif-modified-sinceif-unmodified-sinceif-range)中任意首部。


状态码 304 并不是一种错误,而是告诉客户端有缓存,直接使用缓存中的数据。返回页面的只有头部信息,是没有内容部分的,这样在一定程度上提高了网页的性能。

(5)307 Temporary Redirect

307 表示临时重定向。 该状态码与 302 Found 有着相同含义,尽管 302 标准禁止 POST 变成 GET,但是实际使用时还是这样做了。


307 会遵守浏览器标准,不会从 POST 变成 GET。但是对于处理请求的行为时,不同浏览器还是会出现不同的情况。规范要求浏览器继续向 Location 的地址 POST 内容。规范要求浏览器继续向 Location 的地址 POST 内容。

3. 4XX (Client Error 客户端错误状态码)

4XX 的响应结果表明客户端是发生错误的原因所在。

(1)400 Bad Request

该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码。

(2)401 Unauthorized

该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。若之前已进行过一次请求,则表示用户认证失败


返回含有 401 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以质询(challenge)用户信息。当浏览器初次接收到 401 响应,会弹出认证用的对话窗口。


以下情况会出现 401:


  • 401.1 - 登录失败。

  • 401.2 - 服务器配置导致登录失败。

  • 401.3 - 由于 ACL 对资源的限制而未获得授权。

  • 401.4 - 筛选器授权失败。

  • 401.5 - ISAPI/CGI 应用程序授权失败。

  • 401.7 - 访问被 Web 服务器上的 URL 授权策略拒绝。这个错误代码为 IIS 6.0 所专用。

(3)403 Forbidden

该状态码表明请求资源的访问被服务器拒绝了,服务器端没有必要给出详细理由,但是可以在响应报文实体的主体中进行说明。进入该状态后,不能再继续进行验证。该访问是永久禁止的,并且与应用逻辑密切相关。


IIS 定义了许多不同的 403 错误,它们指明更为具体的错误原因:


  • 403.1 - 执行访问被禁止。

  • 403.2 - 读访问被禁止。

  • 403.3 - 写访问被禁止。

  • 403.4 - 要求 SSL。

  • 403.5 - 要求 SSL 128。

  • 403.6 - IP 地址被拒绝。

  • 403.7 - 要求客户端证书。

  • 403.8 - 站点访问被拒绝。

  • 403.9 - 用户数过多。

  • 403.10 - 配置无效。

  • 403.11 - 密码更改。

  • 403.12 - 拒绝访问映射表。

  • 403.13 - 客户端证书被吊销。

  • 403.14 - 拒绝目录列表。

  • 403.15 - 超出客户端访问许可。

  • 403.16 - 客户端证书不受信任或无效。

  • 403.17 - 客户端证书已过期或尚未生效

  • 403.18 - 在当前的应用程序池中不能执行所请求的 URL。这个错误代码为 IIS 6.0 所专用。

  • 403.19 - 不能为这个应用程序池中的客户端执行 CGI。这个错误代码为 IIS 6.0 所专用。

  • 403.20 - Passport 登录失败。这个错误代码为 IIS 6.0 所专用。

(4)404 Not Found

该状态码表明服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。以下情况会出现 404:


  • 404.0 -(无) – 没有找到文件或目录。

  • 404.1 - 无法在所请求的端口上访问 Web 站点。

  • 404.2 - Web 服务扩展锁定策略阻止本请求。

  • 404.3 - MIME 映射策略阻止本请求。

(5)405 Method Not Allowed

该状态码表示客户端请求的方法虽然能被服务器识别,但是服务器禁止使用该方法。GET 和 HEAD 方法,服务器应该总是允许客户端进行访问。客户端可以通过 OPTIONS 方法(预检)来查看服务器允许的访问方法, 如下


Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
复制代码

4. 5XX (Server Error 服务器错误状态码)

5XX 的响应结果表明服务器本身发生错误.

(1)500 Internal Server Error

该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web 应用存在的 bug 或某些临时的故障。

(2)502 Bad Gateway

该状态码表明扮演网关或代理角色的服务器,从上游服务器中接收到的响应是无效的。注意,502 错误通常不是客户端能够修复的,而是需要由途经的 Web 服务器或者代理服务器对其进行修复。以下情况会出现 502:


  • 502.1 - CGI (通用网关接口)应用程序超时。

  • 502.2 - CGI (通用网关接口)应用程序出错。

(3)503 Service Unavailable

该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入 RetryAfter 首部字段再返回给客户端。


使用场景:


  • 服务器停机维护时,主动用 503 响应请求;

  • nginx 设置限速,超过限速,会返回 503。

(4)504 Gateway Timeout

该状态码表示网关或者代理的服务器无法在规定的时间内获得想要的响应。他是 HTTP 1.1 中新加入的。


使用场景:代码执行时间超时,或者发生了死循环。

5. 总结

(1)2XX 成功


  • 200 OK,表示从客户端发来的请求在服务器端被正确处理

  • 204 No content,表示请求成功,但响应报文不含实体的主体部分

  • 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容

  • 206 Partial Content,进行范围请求


(2)3XX 重定向


  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL

  • 302 found,临时性重定向,表示资源临时被分配了新的 URL

  • 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源

  • 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况

  • 307 temporary redirect,临时重定向,和 302 含义类似,但是期望客户端保持请求方法不变向新的地址发出请求


(3)4XX 客户端错误


  • 400 bad request,请求报文存在语法错误

  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息

  • 403 forbidden,表示对请求资源的访问被服务器拒绝

  • 404 not found,表示在服务器上没有找到请求的资源


(4)5XX 服务器错误


  • 500 internal sever error,表示服务器端在执行请求时发生了错误

  • 501 Not Implemented,表示服务器不支持当前请求所需要的某个功能

  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求


参考 前端进阶面试题详细解答

介绍 plugin

插件系统是 Webpack 成功的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。


一个最简单的 plugin 是这样的:


class Plugin{      // 注册插件时,会调用 apply 方法      // apply 方法接收 compiler 对象      // 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作      apply(compiler){          // compilation 是监听每次编译循环          // 每次文件变化,都会生成新的 compilation 对象并触发该事件        compiler.plugin('compilation',function(compilation) {})      }}
复制代码


注册插件:


// webpack.config.jsmodule.export = {    plugins:[        new Plugin(options),    ]}
复制代码


事件流机制:


Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。


  • 通过链式调用,按顺序串起一个个 Loader;

  • 通过事件流机制,让 Plugin 可以插入到整个生产过程中的每个步骤中;


Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:


const { SyncHook } = require("tapable")
const hook = new SyncHook(['arg'])
// 订阅hook.tap('event', (arg) => { // 'event-hook' console.log(arg)})
// 广播hook.call('event-hook')
复制代码


Webpack 中两个最重要的类 CompilerCompilation 便是继承于 Tapable,也拥有这样的事件流机制。


  • Compiler : 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 options, loaders, plugins 等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递;

  • Compilation: 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;

  • 区别:

  • Compiler 全局唯一,且从启动生存到结束;

  • Compilation对应每次编译,每轮编译循环均会重新创建;

  • 常用 Plugin:

  • UglifyJsPlugin: 压缩、混淆代码;

  • CommonsChunkPlugin: 代码分割;

  • ProvidePlugin: 自动加载模块;

  • html-webpack-plugin: 加载 html 文件,并引入 css / js 文件;

  • extract-text-webpack-plugin / mini-css-extract-plugin: 抽离样式,生成 css 文件; DefinePlugin: 定义全局变量;

  • optimize-css-assets-webpack-plugin: CSS 代码去重;

  • webpack-bundle-analyzer: 代码分析;

  • compression-webpack-plugin: 使用 gzip 压缩 js 和 css;

  • happypack: 使用多进程,加速代码构建;

  • EnvironmentPlugin: 定义环境变量;

  • 调用插件 apply 函数传入 compiler 对象

  • 通过 compiler 对象监听事件


loader 和 plugin 有什么区别?


webapck 默认只能打包 JS 和 JOSN 模块,要打包其它模块,需要借助 loader,loader 就可以让模块中的内容转化成 webpack 或其它 laoder 可以识别的内容。


  • loader就是模块转换化,或叫加载器。不同的文件,需要不同的loader来处理。

  • plugin是插件,可以参与到整个 webpack 打包的流程中,不同的插件,在合适的时机,可以做不同的事件。


webpack 中都有哪些插件,这些插件有什么作用?


  • html-webpack-plugin 自动创建一个 HTML 文件,并把打包好的 JS 插入到 HTML 文件中

  • clean-webpack-plugin 在每一次打包之前,删除整个输出文件夹下所有的内容

  • mini-css-extrcat-plugin 抽离 CSS 代码,放到一个单独的文件中

  • optimize-css-assets-plugin 压缩 css

工程化

介绍一下 webpack 的构建流程

核心概念


  • entry:入口。webpack 是基于模块的,使用 webpack 首先需要指定模块解析入口(entry),webpack 从入口开始根据模块间依赖关系递归解析和处理所有资源文件。

  • output:输出。源代码经过 webpack 处理之后的最终产物。

  • loader:模块转换器。本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。

  • plugin:扩展插件。基于事件流框架 Tapable,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

  • module:模块。除了 js 范畴内的es module、commonJs、AMD等,css @import、url(...)、图片、字体等在 webpack 中都被视为模块。


解释几个 webpack 中的术语


  • module:指在模块化编程中我们把应用程序分割成的独立功能的代码模块

  • chunk:指模块间按照引用关系组合成的代码块,一个 chunk 中可以包含多个 module

  • chunk group:指通过配置入口点(entry point)区分的块组,一个 chunk group 中可包含一到多个 chunk

  • bundling:webpack 打包的过程

  • asset/bundle:打包产物


webpack 的打包思想可以简化为 3 点:


  • 一切源代码文件均可通过各种 Loader 转换为 JS 模块 (module),模块之间可以互相引用。

  • webpack 通过入口点(entry point)递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle) 文件。

  • 每一个入口点都是一个块组(chunk group),在不考虑分包的情况下,一个 chunk group 中只有一个 chunk,该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件(asset/bundle



打包流程


  1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置参数。

  2. 开始编译:从上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。

  3. 确定入口:根据配置中的 entry 找出所有的入口文件。

  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。

  5. 完成模块编译:经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。

  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。



简版


  • Webpack CLI 启动打包流程;

  • 载入 Webpack 核心模块,创建 Compiler 对象;

  • 使用 Compiler 对象开始编译整个项目;

  • 从入口文件开始,解析模块依赖,形成依赖关系树;

  • 递归依赖树,将每个模块交给对应的 Loader 处理;

  • 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。


在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到相关事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果


构建流程核心概念:


  • Tapable:一个基于发布订阅的事件流工具类,CompilerCompilation 对象都继承于 Tapable

  • Compiler:compiler 对象是一个全局单例,他负责把控整个 webpack 打包的构建流程。在编译初始化阶段被创建的全局单例,包含完整配置信息、loaders、plugins 以及各种工具方法

  • Compilation:代表一次 webpack 构建和生成编译资源的的过程,在watch模式下每一次文件变更触发的重新编译都会生成新的 Compilation 对象,包含了当前编译的模块 module, 编译生成的资源,变化的文件, 依赖的状态等

  • 而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过 Loader 解析完成之后,会通过acorn库生成模块代码的 AST 语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。


最终Webpack打包出来的bundle文件是一个IIFE的执行函数。


// webpack 5 打包的bundle文件内容
(() => { // webpackBootstrap var __webpack_modules__ = ({ 'file-A-path': ((modules) => { // ... }) 'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... }) })
// The module cache var __webpack_module_cache__ = {};
// The require function function __webpack_require__(moduleId) { // Check if module is in cache var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } // Create a new module (and put it into the cache) var module = __webpack_module_cache__[moduleId] = { // no module.id needed // no module.loaded needed exports: {} };
// Execute the module function __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module return module.exports; }
// startup // Load entry module and return exports // This entry module can't be inlined because the eval devtool is used. var __webpack_exports__ = __webpack_require__("./src/index.js");})
复制代码



webpack 详细工作流程


watch 的理解

watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听


注意:Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

diff 算法是怎么运作

每一种节点类型有自己的属性,也就是 prop,每次进行 diff 的时候,react 会先比较该节点类型,假如节点类型不一样,那么 react 会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较 prop 是否有更新,假如有 prop 不一样,那么 react 会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点


  • 把树形结构按照层级分解,只比较同级元素。

  • 给列表结构的每个单元添加唯一的key属性,方便比较。

  • React 只会匹配相同 classcomponent(这里面的class指的是组件的名字)

  • 合并操作,调用 componentsetState 方法的时候, React 将其标记为 - dirty.到每一个事件循环结束, React 检查所有标记 dirtycomponent重新绘制.

  • 选择性子树渲染。开发人员可以重写shouldComponentUpdate提高diff的性能


优化⬇️


为了降低算法复杂度,Reactdiff会预设三个限制:


  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。

  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React 会销毁div及其子孙节点,并新建p及其子孙节点。

  3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:


Diff 的思路


该如何设计算法呢?如果让我设计一个Diff算法,我首先想到的方案是:


  1. 判断当前节点的更新属于哪种情况

  2. 如果是新增,执行新增逻辑

  3. 如果是删除,执行删除逻辑

  4. 如果是更新,执行更新逻辑


  • 按这个方案,其实有个隐含的前提——不同操作的优先级是相同的

  • 但是React团队发现,在日常开发中,相较于新增删除更新组件发生的频率更高。所以Diff会优先判断当前节点是否属于更新


基于以上原因,Diff算法的整体逻辑会经历两轮遍历:


  • 第一轮遍历:处理更新的节点。

  • 第二轮遍历:处理剩下的不属于更新的节点。



diff 算法的作用


计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行原生 DOM 操作,而非重新渲染整个页面。


传统 diff 算法


通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n 是树的节点数,这个有多可怕呢?——如果要展示 1000 个节点,得执行上亿次比较。。即便是 CPU 快能执行 30 亿条命令,也很难在一秒内计算出差异。


React 的 diff 算法


  1. 什么是调和?


将 Virtual DOM 树转换成 actual DOM 树的最少操作的过程 称为 调和 。


  1. 什么是 React diff 算法?


diff算法是调和的具体实现。


diff 策略


React 用 三大策略 将 O(n^3)复杂度 转化为 O(n)复杂度


策略一(tree diff):


  • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。


策略二(component diff):


  • 拥有相同类的两个组件 生成相似的树形结构,

  • 拥有不同类的两个组件 生成不同的树形结构。


策略三(element diff):


对于同一层级的一组子节点,通过唯一 id 区分。


tree diff


  • React 通过 updateDepth 对 Virtual DOM 树进行层级控制。

  • 对树分层比较,两棵树 只对同一层次节点 进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。

  • 只需遍历一次,就能完成整棵 DOM 树的比较。



那么问题来了,如果 DOM 节点出现了跨层级操作,diff 会咋办呢?


答:diff 只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。



如上图所示,以 A 为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行 DOM 节点跨层级操作,可以通过 CSS 隐藏、显示节点,而不是真正地移除、添加 DOM 节点


component diff


React 对不同的组件间的比较,有三种策略


  1. 同一类型的两个组件,按原策略(层级比较)继续比较 Virtual DOM 树即可。

  2. 同一类型的两个组件,组件 A 变化为组件 B 时,可能 Virtual DOM 没有任何变化,如果知道这点(变换的过程中,Virtual DOM 没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate() 来判断是否需要 判断计算。

  3. 不同类型的组件,将一个(将被改变的)组件判断为dirty component(脏组件),从而替换 整个组件的所有节点。


注意:如果组件 D 和组件 G 的结构相似,但是 React 判断是 不同类型的组件,则不会比较其结构,而是删除 组件 D 及其子节点,创建组件 G 及其子节点。


element diff


当节点处于同一层级时,diff 提供三种节点操作:删除、插入、移动。


  • 插入:组件 C 不在集合(A,B)中,需要插入

  • 删除:

  • 组件 D 在集合(A,B,D)中,但 D 的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。

  • 组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。

  • 移动:组件 D 已经在集合(A,B,C,D)里了,且集合更新时,D 没有发生更新,只是位置改变,如新集合(A,D,B,C),D 在第二个,无须像传统 diff,让旧集合的第二个 B 和新集合的第二个 D 比较,并且删除第二个位置的 B,再在第二个位置插入 D,而是 (对同一层级的同组子节点) 添加唯一 key 进行区分,移动即��。


总结


  1. tree diff:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动


如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。


这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。



这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。


  1. component diff:如果不是同一类型的组件,会删除旧的组件,创建新的组件



  1. element diff:对于同一层级的一组子节点,需要通过唯一 id 进行来区分


  • 如果没有 id 来进行区分,一旦有插入动作,会导致插入位置之后的列表全部重新渲染

  • 这也是为什么渲染列表时为什么要使用唯一的 key。


diff 的不足与待优化的地方


尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响 React 的渲染性能


与其他框架相比,React 的 diff 算法有何不同?



diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新



具体的流程是这样的:


  • 真实 DOM 与虚拟 DOM 之间存在一个映射关系。这个映射关系依靠初始化时的 JSX 建立完成;

  • 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;

  • 最后再根据 patch 去更新真实的 DOM,反馈到用户的界面上。



在回答有何不同之前,首先需要说明下什么是 diff 算法。


  • diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁

  • React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。

  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。

  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。

  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。同一层级的子节点,可以通过标记 key 的方式进行列表对比。

  • 以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点

  • 然后拿 Vue 和 Preact 与 React 的 diff 算法进行对比

  • PreactDiff 算法相较于 React,整体设计思路相似,但最底层的元素采用了真实 DOM 对比操作,也没有采用 Fiber 设计。Vue 的 Diff 算法整体也与 React 相似,同样未实现 Fiber 设计

  • 然后进行横向比较,React 拥有完整的 Diff 算法策略,且拥有随时中断更新的时间切片能力,在大批量节点更新的极端情况下,拥有更友好的交互体验。

  • Preact 可以在一些对性能要求不高,仅需要渲染框架的简单场景下应用。

  • Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。


学习原理的目的就是应用。那如何根据 React diff 算法原理优化代码呢?这个问题其实按优化方式逆向回答即可。


  • 根据 diff 算法的设计原则,应尽量避免跨层级节点移动。

  • 通过设置唯一 key 进行优化,尽量减少组件层级深度。因为过深的层级会加深遍历深度,带来性能问题。

  • 设置 shouldComponentUpdate 或者 React.pureComponet 减少 diff 次数。

vue 渲染过程


  • 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:

  • parse 使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。模板 -> AST (最消耗性能)

  • optimize 遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化runtime的性能

  • generate 将最终的 AST 转化为 render 函数字符串

  • 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象

  • 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

React 有哪些优化性能的手段

类组件中的优化手段


  • 使用纯组件 PureComponent 作为基类。

  • 使用 shouldComponentUpdate 生命周期函数来自定义渲染逻辑。


方法组件中的优化手段


  • 使用 React.memo 高阶函数包装组件,React.memo 可以实现类似于 shouldComponentUpdate 或者 PureComponent 的效果

  • 使用 useMemo

  • 使用React.useMemo精细化的管控,useMemo 控制的则是是否需要重复执行某一段逻辑,而React.memo 控制是否需要重渲染一个组件

  • 使用 useCallBack


其他方式


  • 在列表需要频繁变动时,使用唯一 id 作为 key,而不是数组下标。

  • 必要时通过改变 CSS 样式隐藏显示组件,而不是通过条件判断显示隐藏组件。

  • 使用 Suspense 和 lazy 进行懒加载,例如:


import React, { lazy, Suspense } from "react";
export default class CallingLazyComponents extends React.Component { render() { var ComponentToLazyLoad = null;
if (this.props.name == "Mayank") { ComponentToLazyLoad = lazy(() => import("./mayankComponent")); } else if (this.props.name == "Anshul") { ComponentToLazyLoad = lazy(() => import("./anshulComponent")); }
return ( <div> <h1>This is the Base User: {this.state.name}</h1> <Suspense fallback={<div>Loading...</div>}> <ComponentToLazyLoad /> </Suspense> </div> ) }}
复制代码

如何判断一个对象是否属于某个类?

  • 第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

  • 第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。

  • 第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。

null 和 undefined 区别

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。


undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。


undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。


当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

createElement 过程

React.createElement(): 根据指定的第一个参数创建一个 React 元素


React.createElement(  type,  [props],  [...children])
复制代码


  • 第一个参数是必填,传入的是似 HTML 标签名称,eg: ul, li

  • 第二个参数是选填,表示的是属性,eg: className

  • 第三个参数是选填, 子节点,eg: 要显示的文本内容


//写法一:
var child1 = React.createElement('li', null, 'one'); var child2 = React.createElement('li', null, 'two'); var content = React.createElement('ul', { className: 'teststyle' }, child1, child2); // 第三个参数可以分开也可以写成一个数组 ReactDOM.render( content, document.getElementById('example') );
//写法二:
var child1 = React.createElement('li', null, 'one'); var child2 = React.createElement('li', null, 'two'); var content = React.createElement('ul', { className: 'teststyle' }, [child1, child2]); ReactDOM.render( content, document.getElementById('example') );
复制代码

forEach 和 map 方法有什么区别

这方法都是用来遍历数组的,两者区别如下:


  • forEach()方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;

  • map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

常见的 HTTP 请求头和响应头

HTTP Request Header 常见的请求头:


  • Accept:浏览器能够处理的内容类型

  • Accept-Charset:浏览器能够显示的字符集

  • Accept-Encoding:浏览器能够处理的压缩编码

  • Accept-Language:浏览器当前设置的语言

  • Connection:浏览器与服务器之间连接的类型

  • Cookie:当前页面设置的任何 Cookie

  • Host:发出请求的页面所在的域

  • Referer:发出请求的页面的 URL

  • User-Agent:浏览器的用户代理字符串


HTTP Responses Header 常见的响应头:


  • Date:表示消息发送的时间,时间的描述格式由 rfc822 定义

  • server:服务器名称

  • Connection:浏览器与服务器之间连接的类型

  • Cache-Control:控制 HTTP 缓存

  • content-type:表示后面的文档属于什么 MIME 类型


常见的 Content-Type 属性值有以下四种:


(1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。


(2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。


(3)application/json:服务器消息主体是序列化后的 JSON 字符串。


(4)text/xml:该种方式主要用来提交 XML 格式的数据。

position 的属性有哪些,区别是什么

position 有以下属性值:



前面三者的定位方式如下:


  • relative: 元素的定位永远是相对于元素自身位置的,和其他元素没关系,也不会影响其他元素。

  • fixed: 元素的定位是相对于 window (或者 iframe)边界的,和其他元素没有关系。但是它具有破坏性,会导致其他元素位置的变化。

  • absolute: 元素的定位相对于前两者要复杂许多。如果为 absolute 设置了 top、left,浏览器会根据什么去确定它的纵向和横向的偏移量呢?答案是浏览器会递归查找该元素的所有父元素,如果找到一个设置了position:relative/absolute/fixed的元素,就以该元素为基准定位,如果没找到,就以浏览器边界定位。如下两个图所示:

介绍一下 Vue 中的 Diff 算法

在新老虚拟 DOM 对比时


  • 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换

  • 如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)

  • 比较如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff 核心)。 匹配时,找到相同的子节点,递归比较子节点


在 diff 中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从 O(n^3)降低值 O(n),也就是说,只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。

HTTP 协议的优点和缺点

HTTP 是超文本传输协议,它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传输层协议,保证了数据传输的可靠性。


HTTP 协议具有以下优点


  • 支持客户端/服务器模式

  • 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快。

  • 无连接:无连接就是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。

  • 无状态:HTTP 协议是无状态协议,这里的状态是指通信过程的上下文信息。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能会导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就比较快。

  • 灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。


HTTP 协议具有以下缺点


  • 无状态: HTTP 是一个无状态的协议,HTTP 服务器不会保存关于客户的任何信息。

  • 明文传输: 协议中的报文使用的是文本形式,这就直接暴露给外界,不安全。

  • 不安全


(1)通信使用明文(不加密),内容可能会被窃听;(2)不验证通信方的身份,因此有可能遭遇伪装;(3)无法证明报文的完整性,所以有可能已遭篡改;


用户头像

coder2028

关注

还未添加个人签名 2022-09-08 加入

还未添加个人简介

评论

发布
暂无评论
有哪些前端面试题是面试官必考的_JavaScript_coder2028_InfoQ写作社区