写点什么

2023 秋招前端面试必会的面试题

作者:coder2028
  • 2023-02-28
    浙江
  • 本文字数:9266 字

    阅读完需:约 30 分钟

发布订阅模式(事件总线)

描述:实现一个发布订阅模式,拥有 on, emit, once, off 方法


class EventEmitter {    constructor() {        // 包含所有监听器函数的容器对象        // 内部结构: {msg1: [listener1, listener2], msg2: [listener3]}        this.cache = {};    }    // 实现订阅    on(name, callback) {        if(this.cache[name]) {            this.cache[name].push(callback);        }        else {            this.cache[name] = [callback];        }    }    // 删除订阅    off(name, callback) {        if(this.cache[name]) {            this.cache[name] = this.cache[name].filter(item => item !== callback);        }        if(this.cache[name].length === 0) delete this.cache[name];    }    // 只执行一次订阅事件    once(name, callback) {        callback();        this.off(name, callback);    }    // 触发事件    emit(name, ...data) {        if(this.cache[name]) {            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环            let tasks = this.cache[name].slice();            for(let fn of tasks) {                fn(...data);            }        }    }}
复制代码

事件是什么?事件模型?

事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。


事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:


  • DOM0 级事件模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。所有浏览器都兼容这种方式。直接在 dom 对象上注册事件名称,就是 DOM0 写法。

  • IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。

  • DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。

Compositon api

Composition API也叫组合式 API,是 Vue3.x 的新特性。


通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用程序变得非常大的时候——想想几百个组件。在处理如此大的应用程序时,共享和重用代码变得尤为重要


  • Vue2.0 中,随着功能的增加,组件变得越来越复杂,越来越难维护,而难以维护的根本原因是 Vue 的 API 设计迫使开发者使用watch,computed,methods选项组织代码,而不是实际的业务逻辑。

  • 另外 Vue2.0 缺少一种较为简洁的低成本的机制来完成逻辑复用,虽然可以minxis完成逻辑复用,但是当mixin变多的时候,会使得难以找到对应的data、computed或者method来源于哪个mixin,使得类型推断难以进行。

  • 所以Composition API的出现,主要是也是为了解决 Option API 带来的问题,第一个是代码组织问题,Compostion API可以让开发者根据业务逻辑组织自己的代码,让代码具备更好的可读性和可扩展性,也就是说当下一个开发者接触这一段不是他自己写的代码时,他可以更好的利用代码的组织反推出实际的业务逻辑,或者根据业务逻辑更好的理解代码。

  • 第二个是实现代码的逻辑提取与复用,当然mixin也可以实现逻辑提取与复用,但是像前面所说的,多个mixin作用在同一个组件时,很难看出property是来源于哪个mixin,来源不清楚,另外,多个mixinproperty存在变量命名冲突的风险。而Composition API刚好解决了这两个问题。


通俗的讲:


没有Composition API之前 vue 相关业务的代码需要配置到 option 的特定的区域,中小型项目是没有问题的,但是在大型项目中会导致后期的维护性比较复杂,同时代码可复用性不高。Vue3.x 中的 composition-api 就是为了解决这个问题而生的


compositon api 提供了以下几个函数:


  • setup

  • ref

  • reactive

  • watchEffect

  • watch

  • computed

  • toRefs

  • 生命周期的hooks


都说 Composition API 与 React Hook 很像,说说区别


从 React Hook 的实现角度看,React Hook 是根据 useState 调用的顺序来确定下一次重渲染时的 state 是来源于哪个 useState,所以出现了以下限制


  • 不能在循环、条件、嵌套函数中调用 Hook

  • 必须确保总是在你的 React 函数的顶层调用 Hook

  • useEffect、useMemo等函数必须手动确定依赖关系


而 Composition API 是基于 Vue 的响应式系统实现的,与 React Hook 的相比


  • 声明在setup函数内,一次组件实例化只调用一次setup,而 React Hook 每次重渲染都需要调用 Hook,使得 React 的 GC 比 Vue 更有压力,性能也相对于 Vue 来说也较慢

  • Compositon API的调用不需要顾虑调用顺序,也可以在循环、条件、嵌套函数中使用

  • 响应式系统自动实现了依赖收集,进而组件的部分的性能优化由 Vue 内部自己完成,而React Hook需要手动传入依赖,而且必须必须保证依赖的顺序,让useEffectuseMemo等函数正确的捕获依赖变量,否则会由于依赖不正确使得组件性能下降。


虽然Compositon API看起来比React Hook好用,但是其设计思想也是借鉴React Hook的。

代码输出结果

function a(xx){  this.x = xx;  return this};var x = a(5);var y = a(6);
console.log(x.x) // undefinedconsole.log(y.x) // 6
复制代码


输出结果: undefined 6


解析:


  1. 最关键的就是 var x = a(5),函数 a 是在全局作用域调用,所以函数内部的 this 指向 window 对象。所以 this.x = 5 就相当于:window.x = 5。之后 return this,也就是说 var x = a(5) 中的 x 变量的值是 window,这里的 x 将函数内部的 x 的值覆盖了。然后执行 console.log(x.x), 也就是 console.log(window.x),而 window 对象中没有 x 属性,所以会输出 undefined。

  2. 当指向 y.x 时,会给全局变量中的 x 赋值为 6,所以会打印出 6。

代码输出结果

var friendName = 'World';(function() {  if (typeof friendName === 'undefined') {    var friendName = 'Jack';    console.log('Goodbye ' + friendName);  } else {    console.log('Hello ' + friendName);  }})();
复制代码


输出结果:Goodbye Jack


我们知道,在 JavaScript 中, Function 和 var 都会被提升(变量提升),所以上面的代码就相当于:


var name = 'World!';(function () {    var name;    if (typeof name === 'undefined') {        name = 'Jack';        console.log('Goodbye ' + name);    } else {        console.log('Hello ' + name);    }})();
复制代码


这样,答案就一目了然了。

常见浏览器所用内核

(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;


(2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink 内核;


(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;


(4) Safari 浏览器内核:Webkit 内核;


(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;


(6) 360 浏览器、猎豹浏览器内核:IE + Chrome 双内核;


(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);


(8) 百度浏览器、世界之窗内核:IE 内核;


(9) 2345 浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;


(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。


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

代码输出结果

function runAsync(x) {  const p = new Promise(r =>    setTimeout(() => r(x, console.log(x)), 1000)  );  return p;}function runReject(x) {  const p = new Promise((res, rej) =>    setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)  );  return p;}Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])  .then(res => console.log("result: ", res))  .catch(err => console.log(err));
复制代码


输出结果如下:


0Error: 0123
复制代码


可以看到在 catch 捕获到第一个错误之后,后面的代码还不执行,不过不会再被捕获了。


注意:allrace传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被 then 的第二个参数或者后面的 catch 捕获;但并不会影响数组中其它的异步任务的执行。

懒加载与预加载的区别

这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。


  • 懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。

  • 预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

代码输出结果

console.log(1);
setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3) });});
new Promise((resolve, reject) => { console.log(4) resolve(5)}).then((data) => { console.log(data);})
setTimeout(() => { console.log(6);})
console.log(7);
复制代码


代码输出结果如下:


1475236
复制代码


代码执行过程如下:


  1. 首先执行 scrip 代码,打印出 1;

  2. 遇到第一个定时器 setTimeout,将其加入到宏任务队列;

  3. 遇到 Promise,执行里面的同步代码,打印出 4,遇到 resolve,将其加入到微任务队列;

  4. 遇到第二个定时器 setTimeout,将其加入到红任务队列;

  5. 执行 script 代码,打印出 7,至此第一轮执行完成;

  6. 指定微任务队列中的代码,打印出 resolve 的结果:5;

  7. 执行宏任务中的第一个定时器 setTimeout,首先打印出 2,然后遇到 Promise.resolve().then(),将其加入到微任务队列;

  8. 执行完这个宏任务,就开始执行微任务队列,打印出 3;

  9. 继续执行宏任务队列中的第二个定时器,打印出 6。

有哪些可能引起前端安全的问题?

  • 跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了与 CSS 区分所以被称作 XSS。早期常⻅于⽹络论坛, 起因是⽹站没有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但不限于 JavaScript / CSS / Flash 等;

  • iframe 的滥⽤: iframe 中的内容是由第三⽅来提供的,默认情况下他们不受控制,他们可以在 iframe 中运⾏JavaScirpt 脚本、Flash 插件、弹出对话框等等,这可能会破坏前端⽤户体验;

  • 跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信息或设定信息等某些状态更新,属于被动攻击

  • 恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起安全问题。

代码输出结果

function runAsync (x) {  const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))  return p}Promise.race([runAsync(1), runAsync(2), runAsync(3)])  .then(res => console.log('result: ', res))  .catch(err => console.log(err))
复制代码


输出结果如下:


1'result: ' 123
复制代码


then 只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被 then 捕获了。

代码输出结果

var a = 1;function printA(){  console.log(this.a);}var obj={  a:2,  foo:printA,  bar:function(){    printA();  }}
obj.foo(); // 2obj.bar(); // 1var foo = obj.foo;foo(); // 1
复制代码


输出结果: 2 1 1


解析:


  1. obj.foo(),foo 的 this 指向 obj 对象,所以 a 会输出 2;

  2. obj.bar(),printA 在 bar 方法中执行,所以此时 printA 的 this 指向的是 window,所以会输出 1;

  3. foo(),foo 是在全局对象中执行的,所以其 this 指向的是 window,所以会输出 1;

代码输出结果

(function(){   var x = y = 1;})();var z;
console.log(y); // 1console.log(z); // undefinedconsole.log(x); // Uncaught ReferenceError: x is not defined
复制代码


这段代码的关键在于:var x = y = 1; 实际上这里是从右往左执行的,首先执行 y = 1, 因为 y 没有使用 var 声明,所以它是一个全局变量,然后第二步是将 y 赋值给 x,讲一个全局变量赋值给了一个局部变量,最终,x 是一个局部变量,y 是一个全局变量,所以打印 x 是报错。

代码输出结果

async function async1() {  console.log("async1 start");  await async2();  console.log("async1 end");  setTimeout(() => {    console.log('timer1')  }, 0)}async function async2() {  setTimeout(() => {    console.log('timer2')  }, 0)  console.log("async2");}async1();setTimeout(() => {  console.log('timer3')}, 0)console.log("start")
复制代码


输出结果如下:


async1 startasync2startasync1 endtimer2timer3timer1
复制代码


代码的执行过程如下:


  1. 首先进入async1,打印出async1 start

  2. 之后遇到async2,进入async2,遇到定时器timer2,加入宏任务队列,之后打印async2

  3. 由于async2阻塞了后面代码的执行,所以执行后面的定时器timer3,将其加入宏任务队列,之后打印start

  4. 然后执行 async2 后面的代码,打印出async1 end,遇到定时器 timer1,将其加入宏任务队列;

  5. 最后,宏任务队列有三个任务,先后顺序为timer2timer3timer1,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。

代码输出结果

console.log('1');
setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') })})process.nextTick(function() { console.log('6');})new Promise(function(resolve) { console.log('7'); resolve();}).then(function() { console.log('8')})
setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') })})
复制代码


输出结果如下:


176824359111012
复制代码


(1)第一轮事件循环流程分析如下:


  • 整体 script 作为第一个宏任务进入主线程,遇到console.log,输出 1。

  • 遇到setTimeout,其回调函数被分发到宏任务 Event Queue 中。暂且记为setTimeout1

  • 遇到process.nextTick(),其回调函数被分发到微任务 Event Queue 中。记为process1

  • 遇到Promisenew Promise直接执行,输出 7。then被分发到微任务 Event Queue 中。记为then1

  • 又遇到了setTimeout,其回调函数被分发到宏任务 Event Queue 中,记为setTimeout2



上表是第一轮事件循环宏任务结束时各 Event Queue 的情况,此时已经输出了 1 和 7。发现了process1then1两个微任务:


  • 执行process1,输出 6。

  • 执行then1,输出 8。


第一轮事件循环正式结束,这一轮的结果是输出 1,7,6,8。


(2)第二轮时间循环从**setTimeout1**宏任务开始:


  • 首先输出 2。接下来遇到了process.nextTick(),同样将其分发到微任务 Event Queue 中,记为process2

  • new Promise立即执行输出 4,then也分发到微任务 Event Queue 中,记为then2



第二轮事件循环宏任务结束,发现有process2then2两个微任务可以执行:


  • 输出 3。

  • 输出 5。


第二轮事件循环结束,第二轮输出 2,4,3,5。


(3)第三轮事件循环开始,此时只剩 setTimeout2 了,执行。


  • 直接输出 9。

  • process.nextTick()分发到微任务 Event Queue 中。记为process3

  • 直接执行new Promise,输出 11。

  • then分发到微任务 Event Queue 中,记为then3



第三轮事件循环宏任务执行结束,执行两个微任务process3then3


  • 输出 10。

  • 输出 12。


第三轮事件循环结束,第三轮输出 9,11,10,12。


整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。

代码输出问题

window.number = 2;var obj = { number: 3, db1: (function(){   console.log(this);   this.number *= 4;   return function(){     console.log(this);     this.number *= 5;   } })()}var db1 = obj.db1;db1();obj.db1();console.log(obj.number);     // 15console.log(window.number);  // 40
复制代码


这道题目看清起来有点乱,但是实际上是考察 this 指向的:


  1. 执行 db1()时,this 指向全局作用域,所以 window.number * 4 = 8,然后执行匿名函数, 所以 window.number * 5 = 40;

  2. 执行 obj.db1();时,this 指向 obj 对象,执行匿名函数,所以 obj.numer * 5 = 15。

什么是 XSS 攻击?

(1)概念

XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。


XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。


攻击者可以通过这种攻击方式可以进行以下操作:


  • 获取页面的数据,如 DOM、cookie、localStorage;

  • DOS 攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;

  • 破坏页面结构;

  • 流量劫持(将链接指向某网站);

(2)攻击类型

XSS 可以分为存储型、反射型和 DOM 型:


  • 存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。

  • 反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后当做脚本执行,最终完成 XSS 攻击。 

  • DOM 型指的通过修改页面的 DOM 节点形成的 XSS。


1)存储型 XSS 的攻击步骤:


  1. 攻击者将恶意代码提交到⽬标⽹站的数据库中。

  2. ⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。

  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。

  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。


这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评论、⽤户私信等。


2)反射型 XSS 的攻击步骤:


  1. 攻击者构造出特殊的 URL,其中包含恶意代码。

  2. ⽤户打开带有恶意代码的 URL 时,⽹站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。

  3. ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。

  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。


反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥。


反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击。


3)DOM 型 XSS 的攻击步骤:


  1. 攻击者构造出特殊的 URL,其中包含恶意代码。

  2. ⽤户打开带有恶意代码的 URL。

  3. ⽤户浏览器接收到响应后解析执⾏,前端 JavaScript 取出 URL 中的恶意代码并执⾏。

  4. 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。


DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端 JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。

代码输出结果

async function async1 () {  console.log('async1 start');  await new Promise(resolve => {    console.log('promise1')    resolve('promise1 resolve')  }).then(res => console.log(res))  console.log('async1 success');  return 'async1 end'}console.log('srcipt start')async1().then(res => console.log(res))console.log('srcipt end')
复制代码


这里是对上面一题进行了改造,加上了 resolve。


输出结果如下:


script startasync1 startpromise1script endpromise1 resolveasync1 successasync1 end
复制代码

代码输出问题

function fun(n, o) {  console.log(o)  return {    fun: function(m){      return fun(m, n);    }  };}var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);var b = fun(0).fun(1).fun(2).fun(3);var c = fun(0).fun(1);  c.fun(2);  c.fun(3);
复制代码


输出结果:


undefined  0  0  0undefined  0  1  2undefined  0  1  1
复制代码


这是一道关于闭包的题目,对于 fun 方法,调用之后返回的是一个对象。我们知道,当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为 undefined 值。所以 console.log(o); 会输出 undefined。而 a 就是是 fun(0)返回的那个对象。也就是说,函数 fun 中参数 n 的值是 0,而返回的那个对象中,需要一个参数 n,而这个对象的作用域中没有 n,它就继续沿着作用域向上一级的作用域中寻找 n,最后在函数 fun 中找到了 n,n 的值是 0。了解了这一点,其他运算就很简单了,以此类推。

代码输出结果

Promise.resolve().then(() => {    console.log('1');    throw 'Error';}).then(() => {    console.log('2');}).catch(() => {    console.log('3');    throw 'Error';}).then(() => {    console.log('4');}).catch(() => {    console.log('5');}).then(() => {    console.log('6');});
复制代码


执行结果如下:


1 3 5 6
复制代码


在这道题目中,我们需要知道,无论是 thne 还是 catch 中,只要 throw 抛出了错误,就会被 catch 捕获,如果没有 throw 出错误,就被继续执行后面的 then。


用户头像

coder2028

关注

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

还未添加个人简介

评论

发布
暂无评论
2023秋招前端面试必会的面试题_JavaScript_coder2028_InfoQ写作社区