写点什么

前端必会面试题总结

作者:loveX001
  • 2022-11-04
    浙江
  • 本文字数:19331 字

    阅读完需:约 63 分钟

HTTPS 的特点

HTTPS 的优点如下:


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

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

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


HTTPS 的缺点如下:


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

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

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

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

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

Promise.all

描述:所有 promise 的状态都变成 fulfilled,就会返回一个状态为 fulfilled 的数组(所有promisevalue)。只要有一个失败,就返回第一个状态为 rejectedpromise 实例的 reason


实现


Promise.all = function(promises) {    return new Promise((resolve, reject) => {        if(Array.isArray(promises)) {            if(promises.length === 0) return resolve(promises);            let result = [];            let count = 0;            promises.forEach((item, index) => {                Promise.resolve(item).then(                    value => {                        count++;                        result[index] = value;                        if(count === promises.length) resolve(result);                    },                     reason => reject(reason)                );            })        }        else return reject(new TypeError("Argument is not iterable"));    });}
复制代码

DOCTYPE(⽂档类型) 的作⽤

DOCTYPE 是 HTML5 中一种标准通用标记语言的文档类型声明,它的目的是告诉浏览器(解析器)应该以什么样(html 或 xhtml)的文档类型定义来解析文档,不同的渲染模式会影响浏览器对 CSS 代码甚⾄ JavaScript 脚本的解析。它必须声明在 HTML⽂档的第⼀⾏。


浏览器渲染页面的两种模式(可通过 document.compatMode 获取,比如,语雀官网的文档类型是 CSS1Compat):


  • CSS1Compat:标准模式(Strick mode),默认模式,浏览器使用 W3C 的标准解析渲染页面。在标准模式中,浏览器以其支持的最高标准呈现页面。

  • **BackCompat:怪异模式(混杂模式)(Quick mode)**,浏览器使用自己的怪异模式解析渲染页面。在怪异模式中,页面以一种比较宽松的向后兼容的方式显示。

代码输出结果

var A = {n: 4399};var B =  function(){this.n = 9999};var C =  function(){var n = 8888};B.prototype = A;C.prototype = A;var b = new B();var c = new C();A.n++console.log(b.n);console.log(c.n);
复制代码


输出结果:9999 4400


解析:


  1. console.log(b.n),在查找 b.n 是首先查找 b 对象自身有没有 n 属性,如果没有会去原型(prototype)上查找,当执行 var b = new B()时,函数内部 this.n=9999(此时 this 指向 b) 返回 b 对象,b 对象有自身的 n 属性,所以返回 9999。

  2. console.log(c.n),同理,当执行 var c = new C()时,c 对象没有自身的 n 属性,向上查找,找到原型 (prototype)上的 n 属性,因为 A.n++(此时对象 A 中的 n 为 4400), 所以返回 4400。

代码输出结果

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


输出结果如下:


13842567
复制代码


代码执行过程如下:


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

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

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

  4. 遇到第二个定时器,加入到宏任务队列;

  5. 遇到第三个定时器,加入到宏任务队列;

  6. 继续执行 script 代码,打印出 8,第一轮执行结束;

  7. 执行微任务队列,打印出第一个 Promise 的 resolve 结果:4;

  8. 开始执行宏任务队列,执行第一个定时器,打印出 2;

  9. 此时没有微任务,继续执行宏任务中的第二个定时器,首先打印出 5,遇到 Promise,首选打印出 6,遇到 resolve,将其加入到微任务队列;

  10. 执行微任务队列,打印出 6;

  11. 执行宏任务队列中的最后一个定时器,打印出 7。

知道 ES6 的 Class 嘛?Static 关键字有了解嘛

为这个类的函数对象直接添加方法,而不是加在这个函数对象的原型对象上

常见的图片格式及使用场景

(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以 BMP 格式的图片通常是较大的文件。


(2)GIF 是无损的、采用索引色的点阵图。采用 LZW 压缩算法进行编码。文件小,是 GIF 格式的优点,同时,GIF 格式还具有支持动画以及透明的优点。但是 GIF 格式仅支持 8bit 的索引色,所以 GIF 格式适用于对色彩要求不高同时需要文件体积较小的场景。


(3)JPEG 是有损的、采用直接色的点阵图。JPEG 的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG 非常适合用来存储照片,与 GIF 相比,JPEG 不适合用来存储企业 Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较 GIF 更大。


(4)PNG-8 是无损的、使用索引色的点阵图。PNG 是一种比较新的图片格式,PNG-8 是非常好的 GIF 格式替代者,在可能的情况下,应该尽可能的使用 PNG-8 而不是 GIF,因为在相同的图片效果下,PNG-8 具有更小的文件体积。除此之外,PNG-8 还支持透明度的调节,而 GIF 并不支持。除非需要动画的支持,否则没有理由使用 GIF 而不是 PNG-8。


(5)PNG-24 是无损的、使用直接色的点阵图。PNG-24 的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24 格式的文件大小要比 BMP 小得多。当然,PNG24 的图片还是要比 JPEG、GIF、PNG-8 大得多。


(6)SVG 是无损的矢量图。SVG 是矢量图意味着 SVG 图片由直线和曲线以及绘制它们的方法组成。当放大 SVG 图片时,看到的还是线和曲线,而不会出现像素点。这意味着 SVG 图片在放大时,不会失真,所以它非常适合用来绘制 Logo、Icon 等。


(7)WebP 是谷歌开发的一种新图片格式,WebP 是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为 Web 而生的,什么叫为 Web 而生呢?就是说相同质量的图片,WebP 具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有 Chrome 浏览器和 Opera 浏览器支持 WebP 格式,兼容性不太好。


  • 在无损压缩的情况下,相同质量的 WebP 图片,文件大小要比 PNG 小 26%;

  • 在有损压缩的情况下,具有相同图片精度的 WebP 图片,文件大小要比 JPEG 小 25%~34%;

  • WebP 图片格式支持图片透明度,一个无损压缩的 WebP 图片,如果要支持透明度只需要 22%的格外文件大小。


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

代码输出结果

f = function() {return true;};   g = function() {return false;};   (function() {      if (g() && [] == ![]) {         f = function f() {return false;};         function g() {return true;}      }   })();   console.log(f());
复制代码


输出结果: false


这里首先定义了两个变量 f 和 g,我们知道变量是可以重新赋值的。后面是一个匿名自执行函数,在 if 条件中调用了函数 g(),由于在匿名函数中,又重新定义了函数 g,就覆盖了外部定义的变量 g,所以,这里调用的是内部函数 g 方法,返回为 true。第一个条件通过,进入第二个条件。


第二个条件是[] == ![],先看 ![] ,在 JavaScript 中,当用于布尔运算时,比如在这里,对象的非空引用被视为 true,空引用 null 则被视为 false。由于这里不是一个 null, 而是一个没有元素的数组,所以 [] 被视为 true, 而 ![] 的结果就是 false 了。当一个布尔值参与到条件运算的时候,true 会被看作 1, 而 false 会被看作 0。现在条件变成了 [] == 0 的问题了,当一个对象参与条件比较的时候,它会被求值,求值的结果是数组成为一个字符串,[] 的结果就是 '' ,而 '' 会被当作 0 ,所以,条件成立。


两个条件都成立,所以会执行条件中的代码, f 在定义是没有使用 var,所以他是一个全局变量。因此,这里会通过闭包访问到外部的变量 f, 重新赋值,现在执行 f 函数返回值已经成为 false 了。而 g 则不会有这个问题,这里是一个函数内定义的 g,不会影响到外部的 g 函数。所以最后的结果就是 false。

闭包的应用场景

  • 柯里化 bind

  • 模块

实现函数原型方法

call


使用一个指定的 this 值和一个或多个参数来调用一个函数。


实现要点:


  • this 可能传入 null;

  • 传入不固定个数的参数;

  • 函数可能有返回值;


Function.prototype.call2 = function (context) {    var context = context || window;    context.fn = this;
var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); }
var result = eval('context.fn(' + args +')');
delete context.fn return result;}
复制代码


apply


apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。


实现要点:


  • this 可能传入 null;

  • 传入一个数组;

  • 函数可能有返回值;


Function.prototype.apply2 = function (context, arr) {    var context = context || window;    context.fn = this;
var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') }
delete context.fn return result;}
复制代码


bind


bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。


实现要点:


  • bind() 除了 this 外,还可传入多个参数;

  • bing 创建的新函数可能传入多个参数;

  • 新函数可能被当做构造函数调用;

  • 函数可能有返回值;


Function.prototype.bind2 = function (context) {    var self = this;    var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); }
fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound;}
复制代码


实现 new 关键字


new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。


实现要点:


  • new 会产生一个新对象;

  • 新对象需要能够访问到构造函数的属性,所以需要重新指定它的原型;

  • 构造函数可能会显示返回;


function objectFactory() {    var obj = new Object()    Constructor = [].shift.call(arguments);    obj.__proto__ = Constructor.prototype;    var ret = Constructor.apply(obj, arguments);
// ret || obj 这里这么写考虑了构造函数显示返回 null 的情况 return typeof ret === 'object' ? ret || obj : obj;};
复制代码


使用:


function person(name, age) {    this.name = name    this.age = age}let p = objectFactory(person, '布兰', 12)console.log(p)  // { name: '布兰', age: 12 }
复制代码


实现 instanceof 关键字


instanceof 就是判断构造函数的 prototype 属性是否出现在实例的原型链上。


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


上面的 left.proto 这种写法可以换成 Object.getPrototypeOf(left)。


实现 Object.create


Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。


Object.create2 = function(proto, propertyObject = undefined) {    if (typeof proto !== 'object' && typeof proto !== 'function') {        throw new TypeError('Object prototype may only be an Object or null.')    if (propertyObject == null) {        new TypeError('Cannot convert undefined or null to object')    }    function F() {}    F.prototype = proto    const obj = new F()    if (propertyObject != undefined) {        Object.defineProperties(obj, propertyObject)    }    if (proto === null) {        // 创建一个没有原型对象的对象,Object.create(null)        obj.__proto__ = null    }    return obj}
复制代码


实现 Object.assign


Object.assign2 = function(target, ...source) {    if (target == null) {        throw new TypeError('Cannot convert undefined or null to object')    }    let ret = Object(target)     source.forEach(function(obj) {        if (obj != null) {            for (let key in obj) {                if (obj.hasOwnProperty(key)) {                    ret[key] = obj[key]                }            }        }    })    return ret}
复制代码


实现 JSON.stringify


JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space


  1. 基本数据类型:

  2. undefined 转换之后仍是 undefined(类型也是 undefined)

  3. boolean 值转换之后是字符串 "false"/"true"

  4. number 类型(除了 NaN 和 Infinity)转换之后是字符串类型的数值

  5. symbol 转换之后是 undefined

  6. null 转换之后是字符串 "null"

  7. string 转换之后仍是 string

  8. NaN 和 Infinity 转换之后是字符串 "null"

  9. 函数类型:转换之后是 undefined

  10. 如果是对象类型(非函数)

  11. 如果是一个数组:如果属性值中出现了 undefined、任意的函数以及 symbol,转换成字符串 "null" ;

  12. 如果是 RegExp 对象:返回 {} (类型是 string);

  13. 如果是 Date 对象,返回 Date 的 toJSON 字符串值;

  14. 如果是普通对象;

  15. 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。

  16. 如果属性值中出现了 undefined、任意的函数以及 symbol 值,忽略。

  17. 所有以 symbol 为属性键的属性都会被完全忽略掉。

  18. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。


function jsonStringify(data) {    let dataType = typeof data;
if (dataType !== 'object') { let result = data; //data 可能是 string/number/null/undefined/boolean if (Number.isNaN(data) || data === Infinity) { //NaN 和 Infinity 序列化返回 "null" result = "null"; } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') { //function 、undefined 、symbol 序列化返回 undefined return undefined; } else if (dataType === 'string') { result = '"' + data + '"'; } //boolean 返回 String() return String(result); } else if (dataType === 'object') { if (data === null) { return "null" } else if (data.toJSON && typeof data.toJSON === 'function') { return jsonStringify(data.toJSON()); } else if (data instanceof Array) { let result = []; //如果是数组 //toJSON 方法可以存在于原型链中 data.forEach((item, index) => { if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') { result[index] = "null"; } else { result[index] = jsonStringify(item); } }); result = "[" + result + "]"; return result.replace(/'/g, '"');
} else { //普通对象 /** * 循环引用抛错(暂未检测,循环引用时,堆栈溢出) * symbol key 忽略 * undefined、函数、symbol 为属性值,被忽略 */ let result = []; Object.keys(data).forEach((item, index) => { if (typeof item !== 'symbol') { //key 如果是symbol对象,忽略 if (data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') { //键值如果是 undefined、函数、symbol 为属性值,忽略 result.push('"' + item + '"' + ":" + jsonStringify(data[item])); } } }); return ("{" + result + "}").replace(/'/g, '"'); } }}
复制代码


实现 JSON.parse


介绍 2 种方法实现:


  • eval 实现;

  • new Function 实现;


eval 实现


第一种方式最简单,也最直观,就是直接调用 eval,代码如下:


var json = '{"a":"1", "b":2}';var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之后得到的对象
复制代码


但是直接调用 eval 会存在安全问题,如果数据中可能不是 json 数据,而是可执行的 JavaScript 代码,那很可能会造成 XSS 攻击。因此,在调用 eval 之前,需要对数据进行校验。


var rx_one = /^[\],:{}\s]*$/;var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
if ( rx_one.test( json.replace(rx_two, "@") .replace(rx_three, "]") .replace(rx_four, "") )) { var obj = eval("(" +json + ")");}
复制代码


new Function 实现


Function 与 eval 有相同的字符串参数特性。


var json = '{"name":"小姐姐", "age":20}';var obj = (new Function('return ' + json))();
复制代码


实现 Promise


实现 Promise 需要完全读懂 Promise A+ 规范,不过从总体的实现上看,有如下几个点需要考虑到:


  • then 需要支持链式调用,所以得返回一个新的 Promise;

  • 处理异步问题,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 分别把成功和失败的回调存起来;

  • 为了让链式调用正常进行下去,需要判断 onFulfilled 和 onRejected 的类型;

  • onFulfilled 和 onRejected 需要被异步调用,这里用 setTimeout 模拟异步;

  • 处理 Promise 的 resolve;


const PENDING = 'pending';const FULFILLED = 'fulfilled';const REJECTED = 'rejected';
class Promise { constructor(executor) { this.status = PENDING; this.value = undefined; this.reason = undefined; this.onResolvedCallbacks = []; this.onRejectedCallbacks = [];
let resolve = (value) = > { if (this.status === PENDING) { this.status = FULFILLED; this.value = value; this.onResolvedCallbacks.forEach((fn) = > fn()); } };
let reject = (reason) = > { if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; this.onRejectedCallbacks.forEach((fn) = > fn()); } };
try { executor(resolve, reject); } catch (error) { reject(error); } }
then(onFulfilled, onRejected) { // 解决 onFufilled,onRejected 没有传值的问题 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v; // 因为错误的值要让后面访问到,所以这里也要抛出错误,不然会在之后 then 的 resolve 中捕获 onRejected = typeof onRejected === "function" ? onRejected : (err) = > { throw err; }; // 每次调用 then 都返回一个新的 promise let promise2 = new Promise((resolve, reject) = > { if (this.status === FULFILLED) { //Promise/A+ 2.2.4 --- setTimeout setTimeout(() = > { try { let x = onFulfilled(this.value); // x可能是一个proimise resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }
if (this.status === REJECTED) { //Promise/A+ 2.2.3 setTimeout(() = > { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }
if (this.status === PENDING) { this.onResolvedCallbacks.push(() = > { setTimeout(() = > { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); });
this.onRejectedCallbacks.push(() = > { setTimeout(() = > { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } });
return promise2; }}const resolvePromise = (promise2, x, resolve, reject) = > { // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1 if (promise2 === x) { return reject( new TypeError("Chaining cycle detected for promise #<Promise>")); } // Promise/A+ 2.3.3.3.3 只能调用一次 let called; // 后续的条件要严格判断 保证代码能和别的库一起使用 if ((typeof x === "object" && x != null) || typeof x === "function") { try { // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1 let then = x.then; if (typeof then === "function") { // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3 then.call( x, (y) = > { // 根据 promise 的状态决定是成功还是失败 if (called) return; called = true; // 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1 resolvePromise(promise2, y, resolve, reject); }, (r) = > { // 只要失败就失败 Promise/A+ 2.3.3.3.2 if (called) return; called = true; reject(r); }); } else { // 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4 resolve(x); } } catch (e) { // Promise/A+ 2.3.3.2 if (called) return; called = true; reject(e); } } else { // 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4 resolve(x); }};
复制代码


Promise 写完之后可以通过 promises-aplus-tests 这个包对我们写的代码进行测试,看是否符合 A+ 规范。不过测试前还得加一段代码:


// promise.js// 这里是上面写的 Promise 全部代码Promise.defer = Promise.deferred = function () {    let dfd = {}    dfd.promise = new Promise((resolve,reject)=>{        dfd.resolve = resolve;        dfd.reject = reject;    });    return dfd;}module.exports = Promise;

复制代码


全局安装:


npm i promises-aplus-tests -g
复制代码


终端下执行验证命令:


promises-aplus-tests promise.js
复制代码


上面写的代码可以顺利通过全部 872 个测试用例。


Promise.resolve


Promsie.resolve(value) 可以将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。


Promise.resolve = function(value) {    // 如果是 Promsie,则直接输出它    if(value instanceof Promise){        return value    }    return new Promise(resolve => resolve(value))}
复制代码


Promise.reject


和 Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。


Promise.reject = function(reason) {    return new Promise((resolve, reject) => reject(reason))}
复制代码


Promise.all


Promise.all 的规则是这样的:


  • 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;

  • 只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;

  • 只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;


Promise.all = function(promiseArr) {    let index = 0, result = []    return new Promise((resolve, reject) => {        promiseArr.forEach((p, i) => {            Promise.resolve(p).then(val => {                index++                result[i] = val                if (index === promiseArr.length) {                    resolve(result)                }            }, err => {                reject(err)            })        })    })}
复制代码


Promise.race


Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。


Promise.race = function(promiseArr) {    return new Promise((resolve, reject) => {        promiseArr.forEach(p => {            Promise.resolve(p).then(val => {                resolve(val)            }, err => {                rejecte(err)            })        })    })}
复制代码


Promise.allSettled


Promise.allSettled 的规则是这样:


  • 所有 Promise 的状态都变化了,那么新返回一个状态是 fulfilled 的 Promise,且它的值是一个数组,数组的每项由所有 Promise 的值和状态组成的对象;

  • 如果有一个是 pending 的 Promise,则返回一个状态是 pending 的新实例;


Promise.allSettled = function(promiseArr) {    let result = []
return new Promise((resolve, reject) => { promiseArr.forEach((p, i) => { Promise.resolve(p).then(val => { result.push({ status: 'fulfilled', value: val }) if (result.length === promiseArr.length) { resolve(result) } }, err => { result.push({ status: 'rejected', reason: err }) if (result.length === promiseArr.length) { resolve(result) } }) }) }) }
复制代码


Promise.any


Promise.any 的规则是这样:


  • 空数组或者所有 Promise 都是 rejected,则返回状态是 rejected 的新 Promsie,且值为 AggregateError 的错误;

  • 只要有一个是 fulfilled 状态的,则返回第一个是 fulfilled 的新实例;

  • 其他情况都会返回一个 pending 的新实例;


Promise.any = function(promiseArr) {    let index = 0    return new Promise((resolve, reject) => {        if (promiseArr.length === 0) return         promiseArr.forEach((p, i) => {            Promise.resolve(p).then(val => {                resolve(val)
}, err => { index++ if (index === promiseArr.length) { reject(new AggregateError('All promises were rejected')) } }) }) })}
复制代码

为什么 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))
复制代码

V8 的垃圾回收机制是怎样的

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。


(1)新生代算法


新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。


在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。


(2)老生代算法


老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。


先来说下什么情况下对象会出现在老生代空间中:


  • 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。

  • To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。


老生代中的空间很复杂,有如下几个空间


enum AllocationSpace {  // TODO(v8:7464): Actually map this space's memory as read-only.  RO_SPACE,    // 不变的对象空间  NEW_SPACE,   // 新生代用于 GC 复制算法的空间  OLD_SPACE,   // 老生代常驻对象空间  CODE_SPACE,  // 老生代代码对象空间  MAP_SPACE,   // 老生代 map 对象  LO_SPACE,    // 老生代大空间对象  NEW_LO_SPACE,  // 新生代大空间对象  FIRST_SPACE = RO_SPACE,  LAST_SPACE = NEW_LO_SPACE,  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE};
复制代码


在老生代中,以下情况会先启动标记清除算法:


  • 某一个空间没有分块的时候

  • 空间中被对象超过一定限制

  • 空间不能保证新生代中的对象移动到老生代中


在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。


清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:


[1, [2, [3]]].flat(2)  // [1, 2, 3]
复制代码


现在就是要实现 flat 这种效果。


ES5 实现:递归。


function flatten(arr) {    var result = [];    for (var i = 0, len = arr.length; i < len; i++) {        if (Array.isArray(arr[i])) {            result = result.concat(flatten(arr[i]))        } else {            result.push(arr[i])        }    }    return result;}
复制代码


ES6 实现:


function flatten(arr) {    while (arr.some(item => Array.isArray(item))) {        arr = [].concat(...arr);    }    return arr;}
复制代码

用过 TypeScript 吗?它的作用是什么?

为 JS 添加类型支持,以及提供最新版的 ES 语法的支持,是的利于团队协作和排错,开发大型项目

前端储存的⽅式有哪些?

  • cookies: 在 HTML5 标准前本地储存的主要⽅式,优点是兼容性好,请求头⾃带 cookie⽅便,缺点是⼤⼩只有 4k,⾃动请求头加⼊cookie 浪费流量,每个 domain 限制 20 个 cookie,使⽤起来麻烦,需要⾃⾏封装;

  • localStorage:HTML5 加⼊的以键值对(Key-Value)为标准的⽅式,优点是操作⽅便,永久性储存(除⾮⼿动删除),⼤⼩为 5M,兼容 IE8+ ;

  • sessionStorage:与 localStorage 基本类似,区别是 sessionStorage 当⻚⾯关闭后会被清理,⽽且与 cookie、localStorage 不同,他不能在所有同源窗⼝中共享,是会话级别的储存⽅式;

  • Web SQL:2010 年被 W3C 废弃的本地数据库数据存储⽅案,但是主流浏览器(⽕狐除外)都已经有了相关的实现,web sql 类似于 SQLite,是真正意义上的关系型数据库,⽤sql 进⾏操作,当我们⽤JavaScript 时要进⾏转换,较为繁琐;

  • IndexedDB: 是被正式纳⼊HTML5 标准的数据库储存⽅案,它是 NoSQL 数据库,⽤键值对进⾏储存,可以进⾏快速读取操作,⾮常适合 web 场景,同时⽤JavaScript 进⾏操作会⾮常便。

AJAX

const getJSON = function(url) {    return new Promise((resolve, reject) => {        const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');        xhr.open('GET', url, false);        xhr.setRequestHeader('Accept', 'application/json');        xhr.onreadystatechange = function() {            if (xhr.readyState !== 4) return;            if (xhr.status === 200 || xhr.status === 304) {                resolve(xhr.responseText);            } else {                reject(new Error(xhr.responseText));            }        }        xhr.send();    })}
复制代码

实现数组原型方法

forEach


Array.prototype.forEach2 = function(callback, thisArg) {    if (this == null) {        throw new TypeError('this is null or not defined')    }    if (typeof callback !== "function") {        throw new TypeError(callback + ' is not a function')    }    const O = Object(this)  // this 就是当前的数组    const len = O.length >>> 0  // 后面有解释    let k = 0    while (k < len) {        if (k in O) {            callback.call(thisArg, O[k], k, O);        }        k++;    }}
复制代码


O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型


map


基于 forEach 的实现能够很容易写出 map 的实现:


- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.map2 = function(callback, thisArg) {    if (this == null) {        throw new TypeError('this is null or not defined')    }    if (typeof callback !== "function") {        throw new TypeError(callback + ' is not a function')    }    const O = Object(this)    const len = O.length >>> 0-   let k = 0+   let k = 0, res = []    while (k < len) {        if (k in O) {-           callback.call(thisArg, O[k], k, O);+           res[k] = callback.call(thisArg, O[k], k, O);        }        k++;    }+   return res}
复制代码


filter


同样,基于 forEach 的实现能够很容易写出 filter 的实现:


- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.filter2 = function(callback, thisArg) {    if (this == null) {        throw new TypeError('this is null or not defined')    }    if (typeof callback !== "function") {        throw new TypeError(callback + ' is not a function')    }    const O = Object(this)    const len = O.length >>> 0-   let k = 0+   let k = 0, res = []    while (k < len) {        if (k in O) {-           callback.call(thisArg, O[k], k, O);+           if (callback.call(thisArg, O[k], k, O)) {+               res.push(O[k])                +           }        }        k++;    }+   return res}
复制代码


some


同样,基于 forEach 的实现能够很容易写出 some 的实现:


- Array.prototype.forEach2 = function(callback, thisArg) {+ Array.prototype.some2 = function(callback, thisArg) {    if (this == null) {        throw new TypeError('this is null or not defined')    }    if (typeof callback !== "function") {        throw new TypeError(callback + ' is not a function')    }    const O = Object(this)    const len = O.length >>> 0    let k = 0    while (k < len) {        if (k in O) {-           callback.call(thisArg, O[k], k, O);+           if (callback.call(thisArg, O[k], k, O)) {+               return true+           }        }        k++;    }+   return false}
复制代码


reduce


Array.prototype.reduce2 = function(callback, initialValue) {    if (this == null) {        throw new TypeError('this is null or not defined')    }    if (typeof callback !== "function") {        throw new TypeError(callback + ' is not a function')    }    const O = Object(this)    const len = O.length >>> 0    let k = 0, acc
if (arguments.length > 1) { acc = initialValue } else { // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值 while (k < len && !(k in O)) { k++ } if (k > len) { throw new TypeError( 'Reduce of empty array with no initial value' ); } acc = O[k++] } while (k < len) { if (k in O) { acc = callback(acc, O[k], k, O) } k++ } return acc}
复制代码

代码输出结果

Promise.resolve(1)  .then(res => {    console.log(res);    return 2;  })  .catch(err => {    return 3;  })  .then(res => {    console.log(res);  });
复制代码


输出结果如下:


1   2
复制代码


Promise 是可以链式调用的,由于每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般任务的链式调用一样 return this。


上面的输出结果之所以依次打印出 1 和 2,是因为resolve(1)之后走的是第一个 then 方法,并没有进 catch 里,所以第二个 then 中的 res 得到的实际上是第一个 then 的返回值。并且 return 2 会被包装成resolve(2),被最后的 then 打印输出 2。

字符串模板

function render(template, data) {    const reg = /\{\{(\w+)\}\}/; // 模板字符串正则    if (reg.test(template)) { // 判断模板里是否有模板字符串        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染        return render(template, data); // 递归的渲染并返回渲染后的结构    }    return template; // 如果模板没有模板字符串直接返回}
复制代码


测试:


let template = '我是{{name}},年龄{{age}},性别{{sex}}';let person = {    name: '布兰',    age: 12}render(template, person); // 我是布兰,年龄12,性别undefined
复制代码

Promise.reject

Promise.reject = function(reason) {    return new Promise((resolve, reject) => reject(reason));}
复制代码

在地址栏里输入一个地址回车会发生哪些事情

1、解析URL:首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。2、缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。3、DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。4、获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。5、TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向客户端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。6、HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。7、返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。8、页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。9、TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
复制代码


用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
前端必会面试题总结_JavaScript_loveX001_InfoQ写作社区