写点什么

那些高级前端是如何回答面试题的

作者:Geek_02d948
  • 2023-02-24
    浙江
  • 本文字数:19210 字

    阅读完需:约 63 分钟

防抖节流

题目描述:手写防抖节流


实现代码如下:


// 防抖function debounce(fn, delay = 300) {  //默认300毫秒  let timer;  return function () {    const args = arguments;    if (timer) {      clearTimeout(timer);    }    timer = setTimeout(() => {      fn.apply(this, args); // 改变this指向为调用debounce所指的对象    }, delay);  };}
window.addEventListener( "scroll", debounce(() => { console.log(111); }, 1000));
// 节流// 设置一个标志function throttle(fn, delay) { let flag = true; return () => { if (!flag) return; flag = false; timer = setTimeout(() => { fn(); flag = true; }, delay); };}
window.addEventListener( "scroll", throttle(() => { console.log(111); }, 1000));
复制代码

你在工作终于到那些问题,解决方法是什么

经常遇到的问题就是Cannot read property ‘prototype’ of undefined解决办法通过浏览器报错提示代码定位问题,解决问题
Vue项目中遇到视图不更新,方法不执行,埋点不触发等问题一般解决方案查看浏览器报错,查看代码运行到那个阶段未之行结束,阅读源码以及相关文档等然后举出来最近开发的项目中遇到的算是两个比较大的问题。
复制代码

数组扁平化

题目描述:实现一个方法使多维数组变成一维数组


最常见的递归版本如下:


function flatter(arr) {  if (!arr.length) return;  return arr.reduce(    (pre, cur) =>      Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],    []  );}// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
复制代码


扩展思考:能用迭代的思路去实现吗?


实现代码如下:


function flatter(arr) {  if (!arr.length) return;  while (arr.some((item) => Array.isArray(item))) {    arr = [].concat(...arr);  }  return arr;}// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
复制代码

instance 如何使用

左边可以是任意值,右边只能是函数


'hello tuture' instanceof String // false
复制代码

10 个 Ajax 同时发起请求,全部返回展示结果,并且至多允许三次失败,说出设计思路

这个问题相信很多人会第一时间想到 Promise.all ,但是这个函数有一个局限在于如果失败一次就返回了,直接这样实现会有点问题,需要变通下。以下是两种实现思路


// 以下是不完整代码,着重于思路 非 Promise 写法let successCount = 0let errorCount = 0let datas = []ajax(url, (res) => {     if (success) {         success++         if (success + errorCount === 10) {             console.log(datas)         } else {             datas.push(res.data)         }     } else {         errorCount++         if (errorCount > 3) {            // 失败次数大于3次就应该报错了             throw Error('失败三次')         }     }})// Promise 写法let errorCount = 0let p = new Promise((resolve, reject) => {    if (success) {         resolve(res.data)     } else {         errorCount++         if (errorCount > 3) {            // 失败次数大于3次就应该报错了            reject(error)         } else {             resolve(error)         }     }})Promise.all([p]).then(v => {  console.log(v);});
复制代码

函数防抖

触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。


简单版:函数内部支持使用 this 和 event 对象;


function debounce(func, wait) {    var timeout;    return function () {        var context = this;        var args = arguments;        clearTimeout(timeout)        timeout = setTimeout(function(){            func.apply(context, args)        }, wait);    }}
复制代码


使用:


var node = document.getElementById('layout')function getUserAction(e) {    console.log(this, e)  // 分别打印:node 这个节点 和 MouseEvent    node.innerHTML = count++;};node.onmousemove = debounce(getUserAction, 1000)
复制代码


最终版:除了支持 this 和 event 外,还支持以下功能:


  • 支持立即执行;

  • 函数可能有返回值;

  • 支持取消功能;


function debounce(func, wait, immediate) {    var timeout, result;
var debounced = function () { var context = this; var args = arguments;
if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; };
debounced.cancel = function() { clearTimeout(timeout); timeout = null; };
return debounced;}
复制代码


使用:


var setUseAction = debounce(getUserAction, 10000, true);// 使用防抖node.onmousemove = setUseAction
// 取消防抖setUseAction.cancel()
复制代码


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

为什么需要清除浮动?清除浮动的方式

浮动的定义: 非 IE 浏览器下,容器不设高度且子元素浮动时,容器高度不能被内容撑开。 此时,内容会溢出到容器外面而影响布局。这种现象被称为浮动(溢出)。


浮动的工作原理:


  • 浮动元素脱离文档流,不占据空间(引起“高度塌陷”现象)

  • 浮动元素碰到包含它的边框或者其他浮动元素的边框停留


浮动元素可以左右移动,直到遇到另一个浮动元素或者遇到它外边缘的包含框。浮动框不属于文档流中的普通流,当元素浮动之后,不会影响块级元素的布局,只会影响内联元素布局。此时文档流中的普通流就会表现得该浮动框不存在一样的布局模式。当包含框的高度小于浮动框的时候,此时就会出现“高度塌陷”。


浮动元素引起的问题?


  • 父元素的高度无法被撑开,影响与父元素同级的元素

  • 与浮动元素同级的非浮动元素会跟随其后

  • 若浮动的元素不是第一个元素,则该元素之前的元素也要浮动,否则会影响页面的显示结构


清除浮动的方式如下:


  • 给父级 div 定义height属性

  • 最后一个浮动元素之后添加一个空的 div 标签,并添加clear:both样式

  • 包含浮动元素的父级标签添加overflow:hidden或者overflow:auto

  • 使用 :after 伪元素。由于 IE6-7 不支持 :after,使用 zoom:1 触发 hasLayout**


.clearfix:after{    content: "\200B";    display: table;     height: 0;    clear: both;  }  .clearfix{    *zoom: 1;  }
复制代码

动态规划求解硬币找零问题

题目描述:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1


示例1:输入: coins = [1, 2, 5], amount = 11输出: 3解释: 11 = 5 + 5 + 1
示例2:输入: coins = [2], amount = 3输出: -1
复制代码


实现代码如下:


const coinChange = function (coins, amount) {  // 用于保存每个目标总额对应的最小硬币个数  const f = [];  // 提前定义已知情况  f[0] = 0;  // 遍历 [1, amount] 这个区间的硬币总额  for (let i = 1; i <= amount; i++) {    // 求的是最小值,因此我们预设为无穷大,确保它一定会被更小的数更新    f[i] = Infinity;    // 循环遍历每个可用硬币的面额    for (let j = 0; j < coins.length; j++) {      // 若硬币面额小于目标总额,则问题成立      if (i - coins[j] >= 0) {        // 状态转移方程        f[i] = Math.min(f[i], f[i - coins[j]] + 1);      }    }  }  // 若目标总额对应的解为无穷大,则意味着没有一个符合条件的硬币总数来更新它,本题无解,返回-1  if (f[amount] === Infinity) {    return -1;  }  // 若有解,直接返回解的内容  return f[amount];};
复制代码

实现函数原型方法

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')) } }) }) })}
复制代码

虚拟 DOM 转换成真实 DOM

描述:将如下 JSON 格式的虚拟 DOM 结构转换成真实 DOM 结构。


// vnode 结构{    tag: 'DIV',    attrs: {        id: "app"    },    children: [        {            tag: 'SPAN',            children: [                {                    tag: 'A',                    children: []                }            ]        }    ]}// 真实DOM 结构<div id="app">    <span>        <a></a>    </span></div>
复制代码


实现


function _render(vnode) {    // 如果是数字类型转化为字符串;    if(typeof vnode === "number") {        vnode = String(vnode);    }    // 字符串类型直接就是文本节点    if(typeof vnode === "string") {        return document.createTextNode(vnode);    }    // 普通 DOM    const dom = document.createElement(vnode.tag);    if(vnode.attrs) {        // 遍历属性        Object.keys(vnode.attrs).forEach((key) => {            dom.setAttribute(key, vnode.attrs[key]);        });    }    // 子数组进行递归操作    vnode.children.forEach((child) => dom.appendChild(_render(child)));    return dom;}
// 测试let vnode = { tag: "DIV", attrs: { id: "app", }, children: [ { tag: "SPAN", children: [ { tag: "A", children: [], }, ], }, ],};console.log(_render(vnode)); // <div id="app"><span><a></a></span></div>
复制代码

Set 和 Map 有什么区别?

1、Map是键值对,Set是值得集合,当然键和值可以是任何得值2、Map可以通过get方法获取值,而set不能因为它只有值3、都能通过迭代器进行for...of 遍历4、Set的值是唯一的可以做数组去重,而Map由于没有格式限制,可以做数据存储
复制代码

说一下 for...in 和 for...of 的区别?

for...of遍历获取的是对象的键值, for...in获取的是对象的键名;for...in会遍历对象的整个原型链, 性能非常差不推荐使用,而for...of只遍历当前对象不会遍历原型链;对于数组的遍历,for...in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for...of只返回数组的下标对应的属性值;总结:for...in循环主要是为了遍历对象而生,不适用遍历数组; for....of循环可以用来遍历数组、类数组对象、字符串、Set、Map以及Generator对象
复制代码

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

描述:实现一个发布订阅模式,拥有 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);            }        }    }}
复制代码

PWA 使用过吗?serviceWorker 的使用原理是啥?

渐进式网络应用(PWA)是谷歌在 2015 年底提出的概念。基本上算是 web 应用程序,但在外观和感觉上与原生app类似。支持PWA的网站可以提供脱机工作、推送通知和设备硬件访问等功能。


Service Worker是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 现在,它们已包括如推送通知和后台同步等功能。 将来,Service Worker将会支持如定期同步或地理围栏等其他功能。 本教程讨论的核心功能是拦截和处理网络请求,包括通过程序来管理缓存中的响应。

原函数形参不定长(此时 fn.length 为 0)

function curry(fn) {    // 保存参数,除去第一个函数参数    let args = [].slice.call(arguments, 1);    // 返回一个新函数    let curried = function () {        // 新函数调用时会继续传参        let allArgs = [...args, ...arguments];        return curry(fn, ...allArgs);    };    // 利用toString隐式转换的特性,当最后执行函数时,会隐式转换    curried.toString = function () {        return fn(...args);    };    return curried;}
// 测试function add(...args) { return args.reduce((pre, cur) => pre + cur, 0);}console.log(add(1, 2, 3, 4));let addCurry = curry(add);console.log(addCurry(1)(2)(3) == 6); // trueconsole.log(addCurry(1, 2, 3)(4) == 10); // trueconsole.log(addCurry(2, 6)(1).toString()); // 9console.log(addCurry(2, 6)(1, 8)); // 打印 curried 函数
复制代码

说一下常见的 HTTP 状态码?说一下状态码是 302 和 304 是什么意思?你在项目中出现过么?你是怎么解决的?

    <!-- 状态码:由3位数字组成,第一个数字定义了响应的类别 -->    <!-- 1xx:指示消息,表示请求已接收,继续处理 -->    <!-- 2xx:成功,表示请求已被成功接收,处理 -->    <!-- 200 OK:客户端请求成功         204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。         206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容 -->    <!-- 3xx 重定向 -->    <!-- 301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。         302 Found:临时重定向,表示请求的资源临时搬到了其他位置         303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问         307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET         304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有 -->    <!-- 4xx:客户端错误 -->    <!-- 400 Bad Request:客户端请求有语法错误,服务器无法理解。         401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。         403 Forbidden:服务器收到请求,但是拒绝提供服务         404 Not Found:请求资源不存在。比如,输入了错误的url         415 Unsupported media type:不支持的媒体类型 -->    <!-- 5xx:服务器端错误,服务器未能实现合法的请求。 -->    <!-- 500 Internal Server Error:服务器发生不可预期的错误。         503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常, -->
复制代码

说一下常见的 git 操作

git branch 查看本地所有分支git status 查看当前状态 git commit 提交 git branch -a 查看所有的分支git branch -r 查看远程所有分支git commit -am "nit" 提交并且加注释 git remote add origin git@192.168.1.119:ndshowgit push origin master 将文件给推到服务器上 git remote show origin 显示远程库origin里的资源 git push origin master:developgit push origin master:hb-dev 将本地库与服务器上的库进行关联 git checkout --track origin/dev 切换到远程dev分支git branch -D master develop 删除本地库developgit checkout -b dev 建立一个新的本地分支devgit merge origin/dev 将分支dev与当前分支进行合并git checkout dev 切换到本地dev分支git remote show 查看远程库git add .git rm 文件名(包括路径) 从git中删除指定文件git clone git://github.com/schacon/grit.git 从服务器上将代码给拉下来git config --list 看所有用户git ls-files 看已经被提交的git rm [file name] 删除一个文件git commit -a 提交当前repos的所有的改变git add [file name] 添加一个文件到git indexgit commit -v 当你用-v参数的时候可以看commit的差异git commit -m "This is the message describing the commit" 添加commit信息git commit -a -a是代表add,把所有的change加到git index里然后再commitgit commit -a -v 一般提交命令git log 看你commit的日志git diff 查看尚未暂存的更新git rm a.a 移除文件(从暂存区和工作区中删除)git rm --cached a.a 移除文件(只从暂存区中删除)git commit -m "remove" 移除文件(从Git中删除)git rm -f a.a 强行移除修改后文件(从暂存区和工作区中删除)git diff --cached 或 $ git diff --staged 查看尚未提交的更新git stash push 将文件给push到一个临时空间中git stash pop 将文件从临时空间pop下来

复制代码

Promise.reject

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

Vuex 有哪些基本属性?为什么 Vuex 的 mutation 中不能做异步操作?

有五种,分别是 State、 Getter、Mutation 、Action、 Module1、state => 基本数据(数据源存放地)2、getters => 从基本数据派生出来的数据3、mutations => 提交更改数据的方法,同步4、actions => 像一个装饰器,包裹mutations,使之可以异步。5、modules => 模块化Vuex
1、Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。2、每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
复制代码

详细说明 Event loop

众所周知 JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。


JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。


console.log('script start');
setTimeout(function() { console.log('setTimeout');}, 0);
console.log('script end');
复制代码


以上代码虽然 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增加。所以 setTimeout 还是会在 script end 之后打印。


不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task


console.log('script start');
setTimeout(function() { console.log('setTimeout');}, 0);
new Promise((resolve) => { console.log('Promise') resolve()}).then(function() { console.log('promise1');}).then(function() { console.log('promise2');});
console.log('script end');// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码


以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。


微任务包括 process.nextTickpromiseObject.observeMutationObserver


宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering


很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。


所以正确的一次 Event loop 顺序是这样的


  1. 执行同步代码,这属于宏任务

  2. 执行栈为空,查询是否有微任务需要执行

  3. 执行所有微任务

  4. 必要的话渲染 UI

  5. 然后开始下一轮 Event loop,执行宏任务中的异步代码


通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不相同。


Node 的 Event loop 分为 6 个阶段,它们会按照顺序反复运行


┌───────────────────────┐┌─>│        timers         ││  └──────────┬────────────┘│  ┌──────────┴────────────┐│  │     I/O callbacks     ││  └──────────┬────────────┘│  ┌──────────┴────────────┐│  │     idle, prepare     ││  └──────────┬────────────┘      ┌───────────────┐│  ┌──────────┴────────────┐      │   incoming:   ││  │         poll          │<──connections───     ││  └──────────┬────────────┘      │   data, etc.  ││  ┌──────────┴────────────┐      └───────────────┘│  │        check          ││  └──────────┬────────────┘│  ┌──────────┴────────────┐└──┤    close callbacks    │   └───────────────────────┘
复制代码


timer


timers 阶段会执行 setTimeoutsetInterval


一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。


下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为 1。


I/O


I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调


idle, prepare


idle, prepare 阶段内部实现


poll


poll 阶段很重要,这一阶段中,系统会做两件事情


  1. 执行到点的定时器

  2. 执行 poll 队列中的事件


并且当 poll 中没有定时器的情况下,会发现以下两件事情


  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制

  • 如果 poll 队列为空,会有两件事发生

  • 如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate

  • 如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调


如果有别的定时器需要被执行,会回到 timer 阶段执行回调。


check


check 阶段执行 setImmediate


close callbacks


close callbacks 阶段执行 close 事件


并且在 Node 中,有些情况下的定时器执行顺序是随机的


setTimeout(() => {    console.log('setTimeout');}, 0);setImmediate(() => {    console.log('setImmediate');})// 这里可能会输出 setTimeout,setImmediate// 可能也会相反的输出,这取决于性能// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate// 否则会执行 setTimeout
复制代码


当然在这种情况下,执行顺序是相同的


var fs = require('fs')
fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });});// 因为 readFile 的回调在 poll 中执行// 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调// 再去 timer 阶段执行 setTimeout// 所以以上输出一定是 setImmediate,setTimeout
复制代码


上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。


setTimeout(()=>{    console.log('timer1')
Promise.resolve().then(function() { console.log('promise1') })}, 0)
setTimeout(()=>{ console.log('timer2')
Promise.resolve().then(function() { console.log('promise2') })}, 0)
// 以上代码在浏览器和 node 中打印情况是不同的// 浏览器中打印 timer1, promise1, timer2, promise2// node 中打印 timer1, timer2, promise1, promise2
复制代码


Node 中的 process.nextTick 会先于其他 microtask 执行。


setTimeout(() => {  console.log("timer1");
Promise.resolve().then(function() { console.log("promise1"); });}, 0);
process.nextTick(() => { console.log("nextTick");});// nextTick, timer1, promise1
复制代码


用户头像

Geek_02d948

关注

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

还未添加个人简介

评论

发布
暂无评论
那些高级前端是如何回答面试题的_JavaScript_Geek_02d948_InfoQ写作社区