关于前端:2023秋招前端面试必会的面试题

3次阅读

共计 10649 个字符,预计需要花费 27 分钟才能阅读完成。

判断数组的形式有哪些

  • 通过 Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通过原型链做判断
obj.__proto__ === Array.prototype;
  • 通过 ES6 的 Array.isArray()做判断
Array.isArrray(obj);
  • 通过 instanceof 做判断
obj instanceof Array
  • 通过 Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

JS 整数是怎么示意的?

  • 通过 Number 类型来示意,遵循 IEEE754 规范,通过 64 位来示意一个数字,(1 + 11 + 52),最大平安数字是 Math.pow(2, 53) – 1,对于 16 位十进制。(符号位 + 指数位 + 小数局部无效位)

实现 LazyMan

题目形容:

实现一个 LazyMan,能够依照以下形式调用:
LazyMan(“Hank”)输入:
Hi! This is Hank!

LazyMan(“Hank”).sleep(10).eat(“dinner”)输入
Hi! This is Hank!
// 期待 10 秒..
Wake up after 10
Eat dinner~

LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输入
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).eat(“supper”).sleepFirst(5)输入
// 期待 5 秒
Wake up after 5
Hi This is Hank!
Eat supper

实现代码如下:

class _LazyMan {constructor(name) {this.tasks = [];
    const task = () => {console.log(`Hi! This is ${name}`);
      this.next();};
    this.tasks.push(task);
    setTimeout(() => {// 把 this.next() 放到调用栈清空之后执行
      this.next();}, 0);
  }
  next() {const task = this.tasks.shift(); // 取第一个工作执行
    task && task();}
  sleep(time) {this._sleepWrapper(time, false);
    return this; // 链式调用
  }
  sleepFirst(time) {this._sleepWrapper(time, true);
    return this;
  }
  _sleepWrapper(time, first) {const task = () => {setTimeout(() => {console.log(`Wake up after ${time}`);
        this.next();}, time * 1000);
    };
    if (first) {this.tasks.unshift(task); // 放到工作队列顶部
    } else {this.tasks.push(task); // 放到工作队列尾部
    }
  }
  eat(name) {const task = () => {console.log(`Eat ${name}`);
      this.next();};
    this.tasks.push(task);
    return this;
  }
}
function LazyMan(name) {return new _LazyMan(name);
}

手写公布订阅

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))
        }
    }
}

异步任务调度器

形容:实现一个带并发限度的异步调度器 Scheduler,保障同时运行的工作最多有 limit 个。

实现

class Scheduler {queue = [];  // 用队列保留正在执行的工作
    runCount = 0;  // 计数正在执行的工作个数
    constructor(limit) {this.maxCount = limit;  // 容许并发的最大个数}
    add(time, data){const promiseCreator = () => {return new Promise((resolve, reject) => {setTimeout(() => {console.log(data);
                    resolve();}, time);
            });
        }
        this.queue.push(promiseCreator);
        // 每次增加的时候都会尝试去执行工作
        this.request();}
    request() {
        // 队列中还有工作才会被执行
        if(this.queue.length && this.runCount < this.maxCount) {
            this.runCount++;
            // 执行先退出队列的函数
            this.queue.shift()().then(() => {
                this.runCount--;
                // 尝试进行下一次工作
                this.request();});
        }
    }
}

// 测试
const scheduler = new Scheduler(2);
const addTask = (time, data) => {scheduler.add(time, data);
}

addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输入后果 2 3 1 4

首屏和白屏工夫如何计算

首屏工夫的计算,能够由 Native WebView 提供的相似 onload 的办法实现,在 ios 下对应的是 webViewDidFinishLoad,在 android 下对应的是 onPageFinished 事件。

白屏的定义有多种。能够认为“没有任何内容”是白屏,能够认为“网络或服务异样”是白屏,能够认为“数据加载中”是白屏,能够认为“图片加载不进去”是白屏。场景不同,白屏的计算形式就不雷同。

办法 1:当页面的元素数小于 x 时,则认为页面白屏。比方“没有任何内容”,能够获取页面的 DOM 节点数,判断 DOM 节点数少于某个阈值 X,则认为白屏。办法 2:当页面呈现业务定义的错误码时,则认为是白屏。比方“网络或服务异样”。办法 3:当页面呈现业务定义的特征值时,则认为是白屏。比方“数据加载中”。

参考 前端进阶面试题具体解答

列表转成树形构造

题目形容:

[
    {
        id: 1,
        text: '节点 1',
        parentId: 0 // 这里用 0 示意为顶级节点
    },
    {
        id: 2,
        text: '节点 1_1',
        parentId: 1 // 通过这个字段来确定子父级
    }
    ...
]

转成
[
    {
        id: 1,
        text: '节点 1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '节点 1_1',
                parentId:1
            }
        ]
    }
]

实现代码如下:

function listToTree(data) {let temp = {};
  let treeData = [];
  for (let i = 0; i < data.length; i++) {temp[data[i].id] = data[i];
  }
  for (let i in temp) {if (+temp[i].parentId != 0) {if (!temp[temp[i].parentId].children) {temp[temp[i].parentId].children = [];}
      temp[temp[i].parentId].children.push(temp[i]);
    } else {treeData.push(temp[i]);
    }
  }
  return treeData;
}

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

形容:实现一个公布订阅模式,领有 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);
            }
        }
    }
}

其余值到字符串的转换规则?

  • Null 和 Undefined 类型,null 转换为 “null”,undefined 转换为 “undefined”,
  • Boolean 类型,true 转换为 “true”,false 转换为 “false”。
  • Number 类型的值间接转换,不过那些极小和极大的数字会应用指数模式。
  • Symbol 类型的值间接转换,然而只容许显式强制类型转换,应用隐式强制类型转换会产生谬误。
  • 对一般对象来说,除非自行定义 toString() 办法,否则会调用 toString()(Object.prototype.toString())来返回外部属性 [[Class]] 的值,如 ”[object Object]”。如果对象有本人的 toString() 办法,字符串化时就会调用该办法并应用其返回值。

HTTP 状态码

  • 1xx 信息性状态码 websocket upgrade
  • 2xx 胜利状态码

    • 200 服务器已胜利解决了申请
    • 204(没有响应体)
    • 206(范畴申请 暂停持续下载)
  • 3xx 重定向状态码

    • 301(永恒):申请的页面已永恒跳转到新的 url
    • 302(长期):容许各种各样的重定向,个别状况下都会实现为到 GET 的重定向,然而不能确保 POST 会重定向为 POST
    • 303 只容许任意申请到 GET 的重定向
    • 304 未修改:自从上次申请后,申请的网页未修改过
    • 307:307302 一样,除了不容许 POSTGET 的重定向
  • 4xx 客户端谬误状态码

    • 400 客户端参数谬误
    • 401 没有登录
    • 403 登录了没权限 比方管理系统
    • 404 页面不存在
    • 405 禁用申请中指定的办法
  • 5xx 服务端谬误状态码

    • 500 服务器谬误:服务器外部谬误,无奈实现申请
    • 502 谬误网关:服务器作为网关或代理呈现谬误
    • 503 服务不可用:服务器目前无奈应用
    • 504 网关超时:网关或代理服务器,未及时获取申请

大数相加

题目形容: 实现一个 add 办法实现两个大数相加

let a = "9007199254740991";
let b = "1234567899999999999";

function add(a ,b){//...}

实现代码如下:

function add(a ,b){
   // 取两个数字的最大长度
   let maxLength = Math.max(a.length, b.length);
   // 用 0 去补齐长度
   a = a.padStart(maxLength , 0);//"0009007199254740991"
   b = b.padStart(maxLength , 0);//"1234567899999999999"
   // 定义加法过程中须要用到的变量
   let t = 0;
   let f = 0;   //"进位"
   let sum = "";
   for(let i=maxLength-1 ; i>=0 ; i--){t = parseInt(a[i]) + parseInt(b[i]) + f;
      f = Math.floor(t/10);
      sum = t%10 + sum;
   }
   if(f!==0){sum = '' + f + sum;}
   return sum;
}

对 Service Worker 的了解

Service Worker 是运行在浏览器背地的 独立线程,个别能够用来实现缓存性能。应用 Service Worker 的话,传输协定必须为 HTTPS。因为 Service Worker 中波及到申请拦挡,所以必须应用 HTTPS 协定来保障平安。

Service Worker 实现缓存性能个别分为三个步骤:首先须要先注册 Service Worker,而后监听到 install 事件当前就能够缓存须要的文件,那么在下次用户拜访的时候就能够通过拦挡申请的形式查问是否存在缓存,存在缓存的话就能够间接读取缓存文件,否则就去申请数据。以下是这个步骤的实现:

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register('sw.js')
    .then(function(registration) {console.log('service worker 注册胜利')
    })
    .catch(function(err) {console.log('servcie worker 注册失败')
    })
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
  e.waitUntil(caches.open('my-cache').then(function(cache) {return cache.addAll(['./index.html', './index.js'])
    })
  )
})
// 拦挡所有申请事件
// 如果缓存中曾经有申请的数据就间接用缓存,否则去申请数据
self.addEventListener('fetch', e => {
  e.respondWith(caches.match(e.request).then(function(response) {if (response) {return response}
      console.log('fetch source')
    })
  )
})

关上页面,能够在开发者工具中的 Application 看到 Service Worker 曾经启动了:在 Cache 中也能够发现所需的文件已被缓存:

如何防止 React 生命周期中的坑

16.3 版本

>=16.4 版本

在线查看:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram(opens new window)

  • 防止生命周期中的坑须要做好两件事:不在失当的时候调用了不该调用的代码;在须要调用时,不要忘了调用。
  • 那么次要有这么 7 种状况容易造成生命周期的坑

    • getDerivedStateFromProps 容易编写反模式代码,使受控组件与非受控组件辨别含糊
    • componentWillMount 在 React 中已被标记弃用,不举荐应用,次要起因是 新的异步渲染架构会导致它被屡次调用。所以网络申请及事件绑定代码应移至 componentDidMount 中。
    • componentWillReceiveProps 同样被标记弃用,被 getDerivedStateFromProps 所取代,次要起因是性能问题
    • shouldComponentUpdate 通过返回 true 或者 false 来确定是否须要触发新的渲染。次要用于性能优化
    • componentWillUpdate 同样是因为新的异步渲染机制,而被标记废除,不举荐应用,原先的逻辑可联合 getSnapshotBeforeUpdatecomponentDidUpdate 革新应用。
    • 如果在 componentWillUnmount 函数中遗记解除事件绑定,勾销定时器等清理操作,容易引发 bug
    • 如果没有增加谬误边界解决,当渲染产生异样时,用户将会看到一个无奈操作的白屏,所以肯定要增加

“React 的申请应该放在哪里,为什么?”这也是常常会被诘问的问题。你能够这样答复。

对于异步申请,应该放在 componentDidMount 中去操作。从工夫程序来看,除了 componentDidMount 还能够有以下抉择:

  • constructor:能够放,但从设计上而言不举荐。constructor 次要用于初始化 state 与函数绑定,并不承载业务逻辑。而且随着类属性的风行,constructor 曾经很少应用了
  • componentWillMount:已被标记废除,在新的异步渲染架构下会触发屡次渲染,容易引发 Bug,不利于将来 React 降级后的代码保护。
  • 所以 React 的申请放在 componentDidMount 里是最好的抉择

透过景象看实质:React 16 缘何两次求变?

Fiber 架构简析

Fiber 是 React 16 对 React 外围算法的一次重写。你只须要 get 到这一个点:Fiber 会使本来同步的渲染过程变成异步的

在 React 16 之前,每当咱们触发一次组件的更新,React 都会构建一棵新的虚构 DOM 树,通过与上一次的虚构 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。上面这张图形象地展现了这个过程的特色:

如图所示,同步渲染的递归调用栈是十分深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回 。这个漫长且不可打断的更新过程,将会带来用户体验层面的微小危险: 同步渲染一旦开始,便会牢牢抓住主线程不放,直到递归彻底实现。在这个过程中,浏览器没有方法解决任何渲染之外的事件,会进入一种无奈解决用户交互的状态。因而若渲染工夫略微长一点,页面就会面临卡顿甚至卡死的危险。

而 React 16 引入的 Fiber 架构,恰好可能解决掉这个危险:Fiber 会将一个大的更新工作拆解为许多个小工作 每当执行完一个小工作时,渲染线程都会把主线程交回去,看看有没有优先级更高的工作要解决,确保不会呈现其余工作被“饿死”的状况,进而防止同步渲染带来的卡顿。在这个过程中,渲染线程不再“一去不回头”,而是能够被打断的,这就是所谓的“异步渲染”,它的执行过程如下图所示:

换个角度看生命周期工作流

Fiber 架构的重要特色就是能够被打断的异步渲染模式。但这个“打断”是有准则的,依据“是否被打断”这一规范,React 16 的生命周期被划分为了 render 和 commit 两个阶段,而 commit 阶段又被细分为了 pre-commit 和 commit。每个阶段所涵盖的生命周期如下图所示:

咱们先来看下三个阶段各自有哪些特色

  • render 阶段:污浊且没有副作用,可能会被 React 暂停、终止或重新启动。
  • pre-commit 阶段:能够读取 DOM。
  • commit 阶段:能够应用 DOM,运行副作用,安顿更新。

总的来说,render 阶段在执行过程中容许被打断,而 commit 阶段则总是同步执行的。

为什么这样设计呢?简略来说,因为 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知 。而 commit 阶段的操作则波及实在 DOM 的渲染,所以 这个过程必须用同步渲染来求稳

函数柯里化

什么叫函数柯里化?其实就是将应用多个参数的函数转换成一系列应用一个参数的函数的技术。还不懂?来举个例子。

function add(a, b, c) {return a + b + c}
add(1, 2, 3)
let addCurry = curry(add)
addCurry(1)(2)(3)

当初就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成屡次调用每次传一个参数。

function curry(fn) {let judge = (...args) => {if (args.length == fn.length) return fn(...args)
        return (...arg) => judge(...args, ...arg)
    }
    return judge
}

数组去重

实现代码如下:

function uniqueArr(arr) {return [...new Set(arr)];
}

对虚构 DOM 的了解

虚构 dom 从来不是用来和间接操作 dom 比照的 ,它们俩最终必由之路。 虚构 dom 只不过是部分更新的一个环节而已 ,整个环节的比照对象是全量更新。虚构 dom 对于 state=UI 的意义是,虚构 dom 使 diff 成为可能(实践上也能够间接用 dom 对象 diff,然而太臃肿),促成了新的开发思维,又不至于性能太差。然而性能再好也不可能好过间接操作 dom,人脑连 diff 都省了。还有一个很重要的意义是, 对视图形象,为跨平台助力

其实我最终心愿你明确的事件只有一件:虚构 DOM 的价值不在性能,而在别处。因而想要从性能角度来把握虚构 DOM 的劣势,无异于背道而驰。偏偏在面试场景下,10 集体外面有 9 个都走这条歧路,最初 9 集体外面天然没有一个能自圆其说,切实让人可惜。

为什么须要浏览器缓存?

对于浏览器的缓存,次要针对的是前端的动态资源,最好的成果就是,在发动申请之后,拉取相应的动态资源,并保留在本地。如果服务器的动态资源没有更新,那么在下次申请的时候,就间接从本地读取即可,如果服务器的动态资源曾经更新,那么咱们再次申请的时候,就到服务器拉取新的资源,并保留在本地。这样就大大的缩小了申请的次数,进步了网站的性能。这就要用到浏览器的缓存策略了。

所谓的 浏览器缓存 指的是浏览器将用户申请过的动态资源,存储到电脑本地磁盘中,当浏览器再次拜访时,就能够间接从本地加载,不须要再去服务端申请了。

应用浏览器缓存,有以下长处:

  • 缩小了服务器的累赘,进步了网站的性能
  • 放慢了客户端网页的加载速度
  • 缩小了多余网络数据传输

介绍一下 webpack scope hosting

作用域晋升,将扩散的模块划分到同一个作用域中,防止了代码的反复引入,无效缩小打包后的代码体积和运行时的内存损耗;

常见的浏览器内核比拟

  • 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。

数组扁平化

题目形容: 实现一个办法使多维数组变成一维数组

最常见的递归版本如下:

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]]]]]));
正文完
 0