写点什么

阿里前端常考面试题集锦

作者:loveX001
  • 2022-12-16
    浙江
  • 本文字数:11744 字

    阅读完需:约 39 分钟

如何避免 ajax 数据请求重新获取

一般而言,ajax 请求的数据都放在 redux 中存取。

代码输出结果

async function async1 () {  await async2();  console.log('async1');  return 'async1 success'}async function async2 () {  return new Promise((resolve, reject) => {    console.log('async2')    reject('error')  })}async1().then(res => console.log(res))
复制代码


输出结果如下:


async2Uncaught (in promise) error
复制代码


可以看到,如果 async 函数中抛出了错误,就会终止错误结果,不会继续向下执行。


如果想要让错误不足之处后面的代码执行,可以使用 catch 来捕获:


async function async1 () {  await Promise.reject('error!!!').catch(e => console.log(e))  console.log('async1');  return Promise.resolve('async1 success')}async1().then(res => console.log(res))console.log('script start')
复制代码


这样的输出结果就是:


script starterror!!!async1async1 success
复制代码

Nginx 的概念及其工作原理

Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。


传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于 event-driven 模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。


Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和 Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的 HTTP 请求,而每个 Apache process 只能处理一个。

如何对项目中的图片进行优化?

  1. 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。

  2. 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。

  3. 小图使用 base64 格式

  4. 将多个图标文件整合到一张图片中(雪碧图)

  5. 选择正确的图片格式:

  6. 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好

  7. 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替

  8. 照片使用 JPEG

Virtual Dom 的优势在哪里?

Virtual Dom 的优势」其实这道题目面试官更想听到的答案不是上来就说「直接操作/频繁操作 DOM 的性能差」,如果 DOM 操作的性能如此不堪,那么 jQuery 也不至于活到今天。所以面试官更想听到 VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差。


首先我们需要知道:


DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程) JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化, 引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。


其次是 VDOM 和真实 DOM 的区别和优化:


  1. 虚拟 DOM 不会立马进行排版与重绘操作

  2. 虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多 DOM 节点排版与重绘损耗

  3. 虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部

为什么需要浏览器缓存?

对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。


所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。


使用浏览器缓存,有以下优点:


  • 减少了服务器的负担,提高了网站的性能

  • 加快了客户端网页的加载速度

  • 减少了多余网络数据传输


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

常见的浏览器内核比较

  • Trident: 这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。

  • Gecko: 这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。

  • Presto: Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快 3 倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。

  • Webkit: Webkit 是 Safari 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。

  • Blink: 谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是 KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。

什么是 CSRF 攻击?

(1)概念

CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。


CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。

(2)攻击类型

常见的 CSRF 攻击有三种:


  • GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。

  • POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。

  • 链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。

代码输出结果

const promise1 = new Promise((resolve, reject) => {  console.log('promise1')  resolve('resolve1')})const promise2 = promise1.then(res => {  console.log(res)})console.log('1', promise1);console.log('2', promise2);
复制代码


输出结果如下:


promise11 Promise{<resolved>: resolve1}2 Promise{<pending>}resolve1
复制代码


需要注意的是,直接打印 promise1,会打印出它的状态值和参数。


代码执行过程如下:


  1. script 是一个宏任务,按照顺序执行这些代码;

  2. 首先进入 Promise,执行该构造函数中的代码,打印promise1

  3. 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来;

  4. 碰到promise1.then这个微任务,将它放入微任务队列;

  5. promise2是一个新的状态为pendingPromise

  6. 执行同步代码 1, 同时打印出promise1的状态是resolved

  7. 执行同步代码 2,同时打印出promise2的状态是pending

  8. 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。

IndexedDB 有哪些特点?

IndexedDB 具有以下特点:


  • 键值对储存:IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

  • 异步:IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

  • 支持事务:IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

  • 同源限制: IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

  • 储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。

  • 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

实现节流函数和防抖函数

函数防抖的实现:


function debounce(fn, wait) {  var timer = null;
return function() { var context = this, args = [...arguments];
// 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; }
// 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); };}
复制代码


函数节流的实现:


// 时间戳版function throttle(fn, delay) {  var preTime = Date.now();
return function() { var context = this, args = [...arguments], nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } };}
// 定时器版function throttle (fun, wait){ let timeout = null return function(){ let context = this let args = [...arguments] if(!timeout){ timeout = setTimeout(() => { fun.apply(context, args) timeout = null }, wait) } }}
复制代码

浏览器的渲染过程

浏览器渲染主要有以下步骤:


  • 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。

  • 然后对 CSS 进行解析,生成 CSSOM 规则树。

  • 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。

  • 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

  • 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。


大致过程如图所示:


注意: 这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?

  • 点击刷新按钮或者按 F5: 浏览器直接对本地的缓存文件过期,但是会带上 If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。

  • 用户按 Ctrl+F5(强制刷新): 浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。

  • 地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。

什么是中间人攻击?如何防范中间人攻击?

中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。


攻击过程如下:


  • 客户端发送请求到服务端,请求被中间⼈截获

  • 服务器向客户端发送公钥

  • 中间⼈截获公钥,保留在⾃⼰⼿上。然后⾃⼰⽣成⼀个伪造的公钥,发给客户端

  • 客户端收到伪造的公钥后,⽣成加密 hash 值发给服务器

  • 中间⼈获得加密 hash 值,⽤⾃⼰的私钥解密获得真秘钥,同时⽣成假的加密 hash 值,发给服务器

  • 服务器⽤私钥解密获得假密钥,然后加密数据传输给客户端

手写发布订阅

class EventListener {    listeners = {};    on(name, fn) {        (this.listeners[name] || (this.listeners[name] = [])).push(fn)    }    once(name, fn) {        let tem = (...args) => {            this.removeListener(name, fn)            fn(...args)        }        fn.fn = tem        this.on(name, tem)    }    removeListener(name, fn) {        if (this.listeners[name]) {            this.listeners[name] = this.listeners[name].filter(listener => (listener != fn && listener != fn.fn))        }    }    removeAllListeners(name) {        if (name && this.listeners[name]) delete this.listeners[name]        this.listeners = {}    }    emit(name, ...args) {        if (this.listeners[name]) {            this.listeners[name].forEach(fn => fn.call(this, ...args))        }    }}
复制代码

instanceof

题目描述:手写 instanceof 操作符实现


实现代码如下:


function myInstanceof(left, right) {  while (true) {    if (left === null) {      return false;    }    if (left.__proto__ === right.prototype) {      return true;    }    left = left.__proto__;  }}
复制代码

ES6 之前使用 prototype 实现继承

Object.create() 会创建一个 “新” 对象,然后将此对象内部的 [[Prototype]] 关联到你指定的对象(Foo.prototype)。Object.create(null) 创建一个空 [[Prototype]] 链接的对象,这个对象无法进行委托。


function Foo(name) {  this.name = name;}
Foo.prototype.myName = function () { return this.name;}
// 继承属性,通过借用构造函数调用function Bar(name, label) { Foo.call(this, name); this.label = label;}
// 继承方法,创建备份Bar.prototype = Object.create(Foo.prototype);
// 必须设置回正确的构造函数,要不然在会发生判断类型出错Bar.prototype.constructor = Bar;
// 必须在上一步之后Bar.prototype.myLabel = function () { return this.label;}
var a = new Bar("a", "obj a");
a.myName(); // "a"a.myLabel(); // "obj a"
复制代码

介绍下 promise 的特性、优缺点,内部是如何实现的,动手实现 Promise

1)Promise 基本特性


  • 1、Promise 有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)

  • 2、Promise 对象接受一个回调函数作为参数, 该回调函数接受两个参数,分别是成功时的回调 resolve 和失败时的回调 reject;另外 resolve 的参数除了正常值以外, 还可能是一个 Promise 对象的实例;reject 的参数通常是一个 Error 对象的实例。

  • 3、then 方法返回一个新的 Promise 实例,并接收两个参数 onResolved(fulfilled 状态的回调);onRejected(rejected 状态的回调,该参数可选)

  • 4、catch 方法返回一个新的 Promise 实例

  • 5、finally 方法不管 Promise 状态如何都会执行,该方法的回调函数不接受任何参数

  • 6、Promise.all()方法将多个多个 Promise 实例,包装成一个新的 Promise 实例,该方法接受一个由 Promise 对象组成的数组作为参数(Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例),注意参数中只要有一个实例触发 catch 方法,都会触发 Promise.all()方法返回的新的实例的 catch 方法,如果参数中的某个实例本身调用了 catch 方法,将不会触发 Promise.all()方法返回的新实例的 catch 方法

  • 7、Promise.race()方法的参数与 Promise.all 方法一样,参数中的实例只要有一个率先改变状态就会将该实例的状态传给 Promise.race()方法,并将返回值作为 Promise.race()方法产生的 Promise 实例的返回值

  • 8、Promise.resolve()将现有对象转为 Promise 对象,如果该方法的参数为一个 Promise 对象,Promise.resolve()将不做任何处理;如果参数 thenable 对象(即具有 then 方法),Promise.resolve()将该对象转为 Promise 对象并立即执行 then 方法;如果参数是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 fulfilled,其参数将会作为 then 方法中 onResolved 回调函数的参数,如果 Promise.resolve 方法不带参数,会直接返回一个 fulfilled 状态的 Promise 对象。需要注意的是,立即 resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。

  • 9、Promise.reject()同样返回一个新的 Promise 对象,状态为 rejected,无论传入任何参数都将作为 reject()的参数


2)Promise 优点


  • ①统一异步 API

  • Promise 的一个重要优点是它将逐渐被用作浏览器的异步 API ,统一现在各种各样的 API ,以及不兼容的模式和手法。

  • ②Promise 与事件对比

  • 和事件相比较, Promise 更适合处理一次性的结果。在结果计算出来之前或之后注册回调函数都是可以的,都可以拿到正确的值。 Promise 的这个优点很自然。但是,不能使用 Promise 处理多次触发的事件。链式处理是 Promise 的又一优点,但是事件却不能这样链式处理。

  • ③Promise 与回调对比

  • 解决了回调地狱的问题,将异步操作以同步操作的流程表达出来。

  • ④Promise 带来的额外好处是包含了更好的错误处理方式(包含了异常处理),并且写起来很轻松(因为可以重用一些同步的工具,比如 Array.prototype.map() )。


3)Promise 缺点


  • 1、无法取消 Promise,一旦新建它就会立即执行,无法中途取消。

  • 2、如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。

  • 3、当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

  • 4、Promise 真正执行回调的时候,定义 Promise 那部分实际上已经走完了,所以 Promise 的报错堆栈上下文不太友好。


4)简单代码实现 最简单的 Promise 实现有 7 个主要属性, state(状态), value(成功返回值), reason(错误信息), resolve 方法, reject 方法, then 方法


class Promise{  constructor(executor) {    this.state = 'pending';    this.value = undefined;    this.reason = undefined;    let resolve = value => {      if (this.state === 'pending') {        this.state = 'fulfilled';        this.value = value;      }    };    let reject = reason => {      if (this.state === 'pending') {        this.state = 'rejected';        this.reason = reason;      }    };    try {      // 立即执行函数      executor(resolve, reject);    } catch (err) {      reject(err);    }  }  then(onFulfilled, onRejected) {    if (this.state === 'fulfilled') {      let x = onFulfilled(this.value);    };    if (this.state === 'rejected') {      let x = onRejected(this.reason);    };  }}
复制代码


5)面试够用版


function myPromise(constructor){ let self=this;  self.status="pending" //定义状态改变前的初始状态   self.value=undefined;//定义状态为resolved的时候的状态   self.reason=undefined;//定义状态为rejected的时候的状态   function resolve(value){    //两个==="pending",保证了了状态的改变是不不可逆的     if(self.status==="pending"){      self.value=value;      self.status="resolved";     }  }  function reject(reason){     //两个==="pending",保证了了状态的改变是不不可逆的     if(self.status==="pending"){        self.reason=reason;        self.status="rejected";       }  }  //捕获构造异常   try{      constructor(resolve,reject);  }catch(e){    reject(e);    } }myPromise.prototype.then=function(onFullfilled,onRejected){   let self=this;  switch(self.status){    case "resolved": onFullfilled(self.value); break;    case "rejected": onRejected(self.reason); break;    default:   }}
// 测试var p=new myPromise(function(resolve,reject){resolve(1)}); p.then(function(x){console.log(x)})//输出1
复制代码


6)大厂专供版


const PENDING = "pending"; const FULFILLED = "fulfilled"; const REJECTED = "rejected";const resolvePromise = (promise, x, resolve, reject) => {  if (x === promise) {    // If promise and x refer to the same object, reject promise with a TypeError as the reason.    reject(new TypeError('循环引用'))  }  // if x is an object or function,  if (x !== null && typeof x === 'object' || typeof x === 'function') {    // If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.    let called    try { // If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.      let then = x.then // Let then be x.then      // If then is a function, call it with x as this      if (typeof then === 'function') {        // If/when resolvePromise is called with a value y, run [[Resolve]](promise, y)        // If/when rejectPromise is called with a reason r, reject promise with r.        then.call(x, y => {          if (called) return          called = true          resolvePromise(promise, y, resolve, reject)        }, r => {          if (called) return          called = true          reject(r)        })      } else {        // If then is not a function, fulfill promise with x.        resolve(x)      }    } catch (e) {      if (called) return      called = true      reject(e)    }  } else {    // If x is not an object or function, fulfill promise with x    resolve(x)  }}function Promise(excutor) {  let that = this; // 缓存当前promise实例例对象  that.status = PENDING; // 初始状态  that.value = undefined; // fulfilled状态时 返回的信息  that.reason = undefined; // rejected状态时 拒绝的原因   that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数  that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数  function resolve(value) { // value成功态时接收的终值    if(value instanceof Promise) {      return value.then(resolve, reject);    }    // 实践中要确保 onFulfilled 和 onRejected ⽅方法异步执⾏行行,且应该在 then ⽅方法被调⽤用的那⼀一轮事件循环之后的新执⾏行行栈中执⾏行行。    setTimeout(() => {      // 调⽤用resolve 回调对应onFulfilled函数      if (that.status === PENDING) {        // 只能由pending状态 => fulfilled状态 (避免调⽤用多次resolve reject)        that.status = FULFILLED;        that.value = value;        that.onFulfilledCallbacks.forEach(cb => cb(that.value));      }    });  }  function reject(reason) { // reason失败态时接收的拒因    setTimeout(() => {      // 调⽤用reject 回调对应onRejected函数      if (that.status === PENDING) {        // 只能由pending状态 => rejected状态 (避免调⽤用多次resolve reject)        that.status = REJECTED;        that.reason = reason;        that.onRejectedCallbacks.forEach(cb => cb(that.reason));      }    });  }
// 捕获在excutor执⾏行行器器中抛出的异常 // new Promise((resolve, reject) => { // throw new Error('error in excutor') // }) try { excutor(resolve, reject); } catch (e) { reject(e); }}Promise.prototype.then = function(onFulfilled, onRejected) { const that = this; let newPromise; // 处理理参数默认值 保证参数后续能够继续执⾏行行 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value; onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; }; if (that.status === FULFILLED) { // 成功态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try{ let x = onFulfilled(that.value); resolvePromise(newPromise, x, resolve, reject); //新的promise resolve 上⼀一个onFulfilled的返回值 } catch(e) { reject(e); // 捕获前⾯面onFulfilled中抛出的异常then(onFulfilled, onRejected); } }); }) } if (that.status === REJECTED) { // 失败态 return newPromise = new Promise((resolve, reject) => { setTimeout(() => { try { let x = onRejected(that.reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); } if (that.status === PENDING) { // 等待态// 当异步调⽤用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中 return newPromise = new Promise((resolve, reject) => { that.onFulfilledCallbacks.push((value) => { try { let x = onFulfilled(value); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); that.onRejectedCallbacks.push((reason) => { try { let x = onRejected(reason); resolvePromise(newPromise, x, resolve, reject); } catch(e) { reject(e); } }); }); }};
复制代码

渲染过程中遇到 JS 文件如何处理?

JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。

为什么 0.1 + 0.2 != 0.3,请详述理由

因为 JS 采用 IEEE 754 双精度版本(64 位),并且只要采用 IEEE 754 的语言都有该问题。


我们都知道计算机表示十进制是采用二进制表示的,所以 0.1 在二进制表示为


// (0011) 表示循环0.1 = 2^-4 * 1.10011(0011)
复制代码


那么如何得到这个二进制的呢,我们可以来演算下


小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)


回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.10.2 都是无限循环的二进制了,所以在小数位末尾处需要判断是否进位(就和十进制的四舍五入一样)。


所以 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004


下面说一下原生解决办法,如下代码所示


parseFloat((0.1 + 0.2).toFixed(10))
复制代码


用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
阿里前端常考面试题集锦_JavaScript_loveX001_InfoQ写作社区