写点什么

前端八股文总结

作者:loveX001
  • 2022-11-01
    浙江
  • 本文字数:13282 字

    阅读完需:约 44 分钟

继承

原型继承

核心思想:子类的原型成为父类的实例


实现


function SuperType() {    this.colors = ['red', 'green'];}function SubType() {}// 原型继承关键: 子类的原型成为父类的实例SubType.prototype = new SuperType();
// 测试let instance1 = new SubType();instance1.colors.push('blue');
let instance2 = new SubType();console.log(instance2.colors); // ['red', 'green', 'blue']
复制代码


原型继承存在的问题


  1. 原型中包含的引用类型属性将被所有实例对象共享

  2. 子类在实例化时不能给父类构造函数传参

构造函数继承

核心思想:在子类构造函数中调用父类构造函数


实现


function SuperType(name) {    this.name = name;    this.colors = ['red', 'green'];    this.getName = function() {        return this.name;    }}function SubType(name) {    // 继承 SuperType 并传参    SuperType.call(this, name);}
// 测试let instance1 = new SubType('instance1');instance1.colors.push('blue');console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2');console.log(instance2.colors); // ['red', 'green']
复制代码


构造函数继承的出现是为了解决了原型继承的引用值共享问题。优点是可以在子类构造函数中向父类构造函数传参。它存在的问题是:1)由于方法必须在构造函数中定义,因此方法不能重用。2)子类也不能访问父类原型上定义的方法。

组合继承

核心思想:综合了原型链和构造函数,即,使用原型链继承原型上的方法,而通过构造函数继承实例属性。


实现


function SuperType(name) {    this.name = name;    this.colors = ['red', 'green'];}Super.prototype.sayName = function() {    console.log(this.name);}function SubType(name, age) {    // 继承属性    SuperType.call(this, name);    // 实例属性    this.age = age;}// 继承方法SubType.prototype = new SuperType();
// 测试let instance1 = new SubType('instance1', 1);instance1.sayName(); // "instance1"instance1.colors.push("blue");console.log(instance1.colors); // ['red','green','blue']
let instance2 = new SubType('instance2', 2);instance2.sayName(); // "instance2"console.log(instance2.colors); // ['red','green']
复制代码


组合继承存在的问题是:父类构造函数始终会被调用两次:一次是在创建子类原型时new SuperType()调用,另一次是在子类构造函数中SuperType.call()调用。

寄生式组合继承(最佳)

核心思想:通过构造函数继承属性,但使用混合式原型继承方法,即,不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。


实现


function SuperType(name) {    this.name = name;    this.colors = ['red', 'green'];}Super.prototype.sayName = function() {    console.log(this.name);}function SubType(name, age) {    // 继承属性    SuperType.call(this, name);    this.age = age;}// 继承方法SubType.prototype = Object.create(SuperType.prototype);// 重写原型导致默认 constructor 丢失,手动将 constructor 指回 SubTypeSubType.prototype.constructor = SubType;
复制代码

class 实现继承(ES6)

核心思想:通过 extends 来实现类的继承(相当于 ES5 的原型继承)。通过 super 调用父类的构造方法 (相当于 ES5 的构造函数继承)。


实现


class SuperType {    constructor(name) {        this.name = name;    }    sayName() {        console.log(this.name);    }}class SubType extends SuperType {    constructor(name, age) {        super(name);  // 继承属性        this.age = age;    }}
// 测试let instance = new SubType('instance', 0);instance.sayName(); // "instance"
复制代码


虽然类继承使用的是新语法,但背后依旧使用的是原型链。

OPTIONS 请求方法及使用场景

OPTIONS 是除了 GET 和 POST 之外的其中一种 HTTP 请求方法。


OPTIONS 方法是用于请求获得由Request-URI标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。该请求方法的响应不能缓存。


OPTIONS 请求方法的主要用途有两个:


  • 获取服务器支持的所有 HTTP 请求方法;

  • 用来检查访问权限。例如:在进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。

浏览器渲染进程的线程有哪些

浏览器的渲染进程的线程总共有五种: (1)GUI 渲染线程 负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树、构建 CSSOM 树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。


注意:GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。


(2)JS 引擎线程 JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个 JS 引擎线程在运行 JS 程序;


注意:GUI 渲染线程与 JS 引擎线程的互斥关系,所以如果 JS 执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。


(3)时间触发线程 时间触发线程属于浏览器而不是 JS 引擎,用来控制事件循环;当 JS 引擎执行代码块如 setTimeOut 时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理;


注意:由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行);


(4)定时器触发进程 定时器触发进程即 setInterval 与 setTimeout 所在线程;浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;


注意:W3C 在 HTML 标准中规定,定时器的定时时间不能小于 4ms,如果是小于 4ms,则默认为 4ms。


(5)异步 http 请求线程


  • XMLHttpRequest 连接后通过浏览器新开一个线程请求;

  • 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待 JS 引擎空闲后执行;

虚拟 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>
复制代码

如何判断一个对象是不是空对象?

Object.keys(obj).length === 0


手写题:在线编程,getUrlParams(url,key); 就是很简单的获取 url 的某个参数的问题,但要考虑边界情况,多个返回值等等

代码输出结果

function a() {  console.log(this);}a.call(null);
复制代码


打印结果:window 对象


根据 ECMAScript262 规范规定:如果第一个参数传入的对象调用者是 null 或者 undefined,call 方法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不管传入 null 还是 undefined,其 this 都是全局对象 window。所以,在浏览器上答案是输出 window 对象。


要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined:


'use strict';
function a() { console.log(this);}a.call(null); // nulla.call(undefined); // undefined
复制代码

代码输出结果

Promise.resolve(1)  .then(2)  .then(Promise.resolve(3))  .then(console.log)
复制代码


输出结果如下:


1
复制代码


看到这个题目,好多的 then,实际上只需要记住一个原则:.then.catch 的参数期望是函数,传入非函数则会发生值透传


第一个 then 和第二个 then 中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个 then 里,直接打印出 1。


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

JS 闭包,你了解多少?

应该有面试官问过你:


  1. 什么是闭包?

  2. 闭包有哪些实际运用场景?

  3. 闭包是如何产生的?

  4. 闭包产生的变量如何被回收?


这些问题其实都可以被看作是同一个问题,那就是面试官在问你:**你对JS闭包了解多少?**


来总结一下我听到过的答案,尽量完全复原候选人面试的时候说的原话。


答案1: 就是一个function里面return了一个子函数,子函数访问了外面那个函数的变量。


答案2: for 循环里面可以用闭包来解决问题。


for(var i = 0; i < 10; i++){    setTimeout(()=>console.log(i),0)}// 控制台输出10遍10.for(var i = 0; i < 10; i++){    (function(a){    setTimeout(()=>console.log(a),0)    })(i)} // 控制台输出0-9
复制代码


答案3: 当前作用域产产生了对父作用域的引用。


答案4: 不知道。是跟浏览器的垃圾回收机制有关吗?


开杠了。请问,小伙伴的答案和以上的内容有多少相似程度?


其实,拿着这些问题好好想想,你就会发现这些问题都只是为了最终那一个问题。


闭包的底层实现原理


1. JS执行上下文


我们都知道,我们手写的 js 代码是要经过浏览器 V8 进行预编译后才能真正的被执行。例如变量提升、函数提升。举个栗子。


// 栗子:var d = 'abc';function a(){    console.log("函数a");};console.log(a);   // ƒ a(){ console.log("函数a"); }a();              // '函数a'var a = "变量a";  console.log(a);   // '变量a'a();              // a is not a functionvar c = 123;
// 输出结果及顺序:// ƒ a(){ console.log("函数a"); }// '函数a'// '变量a'// a is not a function
// 栗子预编后相当于:function a(){ console.log("函数a");};var d;console.log(a); // ƒ a(){ console.log("函数a"); }a(); // '函数a'
a = "变量a"; // 此时变量a赋值,函数声明被覆盖
console.log(a); // "变量a"a(); // a is not a function

复制代码


那么问题来了。 请问是谁来执行预编译操作的?那这个谁又是在哪里进行预编译的?


是的,你的疑惑没有错。js 代码运行需要一个运行环境,那这个环境就是执行上下文。 是的,js 运行前的预编译也是在这个环境中进行。


js 执行上下文分三种:


  • 全局执行上下文: 代码开始执行时首先进入的环境。

  • 函数执行上下文:函数调用时,会开始执行函数中的代码。

  • eval执行上下文:不建议使用,可忽略。


那么,执行上下文的周期,分为两个阶段:


  • 创建阶段

  • 创建词法环境

  • 生成变量对象(VO),建立作用域链作用域链作用域链(重要的事说三遍)

  • 确认this指向,并绑定this

  • 执行阶段。这个阶段进行变量赋值,函数引用及执行代码。


你现在猜猜看,预编译是发生在什么时候?


噢,我忘记说了,其实与编译还有另一个称呼:**执行期上下文**。


预编译发生在函数执行之前。预编译四部曲为:


  1. 创建AO对象

  2. 找形参和变量声明,将变量和形参作为 AO 属性名,值为undefined

  3. 将实参和形参相统一

  4. 在函数体里找到函数声明,值赋予函数体。最后程序输出变量值的时候,就是从AO对象中拿。


所以,预编译真正的结果是:


var AO = {    a = function a(){console.log("函数a");};    d = 'abc'}
复制代码


我们重新来。

1. 什么叫变量对象?

变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。

2. 变量对象(VO)的创建过程

变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:


  • 创建 arguments 对象。

  • 对于函数执行环境,首先查询是否有传入的实参,如果有,则会将参数名是实参值组成的键值对放入arguments 对象中。否则,将参数名和 undefined组成的键值对放入 arguments 对象中。


//举个栗子 function bar(a, b, c) {    console.log(arguments);  // [1, 2]    console.log(arguments[2]); // undefined}bar(1,2)
复制代码


  • 当遇到同名的函数时,后面的会覆盖前面的。


console.log(a); // function a() {console.log('Is a ?') }function a() {    console.log('Is a');}function a() {  console.log('Is a ?')}
/**ps: 在执行第一行代码之前,函数声明已经创建完成.后面的对之前的声明进行了覆盖。**/
复制代码


  • 检查当前环境中的变量声明并赋值为undefined。当遇到同名的函数声明,为了避免函数被赋值为 undefined ,会忽略此声明


console.log(a); // function a() {console.log('Is a ?') }console.log(b); // undefinedfunction a() {  console.log('Is a ');}function a() {console.log('Is a ?');}var b = 'Is b';var a = 10086;
/**这段代码执行一下,你会发现 a 打印结果仍旧是一个函数,而 b 则是 undefined。**/
复制代码


根据以上三个步骤,对于变量提升也就知道是怎么回事了。

3. 变量对象变为活动对象

执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象。


我们还是举上面的例子:


console.log(a); // function a() {console.log('fjdsfs') }console.log(b); // undefinedfunction a() {   console.log('Is a');}function a() { console.log('Is a?');}var b = 'Is b';console.log(b); // 'Is b'var a = 10086; console.log(a);  // 10086var b = 'Is b?';console.log(b); // 'Is b?'
复制代码


在上面的代码中,代码真正开始执行是从第一行 console.log() 开始的,自这之前,执行上下文是这样的:


// 创建过程EC= {  VO: {}; // 创建变量对象  scopeChain: {}; // 作用域链}VO = {  argument: {...}; // 当前为全局上下文,所以这个属性值是空的  a: <a reference> // 函数 a  的引用地址  b: undefiend  // 见上文创建变量对象的第三步}

复制代码
词法作用域(Lexical scope

这里想说明,我们在函数执行上下文中有变量,在全局执行上下文中有变量。JavaScript的一个复杂之处在于它如何查找变量,如果在函数执行上下文中找不到变量,它将在调用上下文中寻找它,如果在它的调用上下文中没有找到,就一直往上一级,直到它在全局执行上下文中查找为止。(如果最后找不到,它就是 undefined)。


再来举个栗子:


 1: let top = 0; //  2: function createWarp() { 3:   function add(a, b) { 4:     let ret = a + b 5:     return ret 6:   } 7:   return add 8: } 9: let sum = createWarp()10: let result = sum(top, 8)11: console.log('result:',result)


复制代码


分析过程如下:


  • 在全局上下文中声明变量top 并赋值为 0.

  • 2 - 8 行。在全局执行上下文中声明了一个名为 createWarp 的变量,并为其分配了一个函数定义。其中第 3-7 行描述了其函数定义,并将函数定义存储到那个变量(createWarp)中。

  • 第 9 行。我们在全局执行上下文中声明了一个名为 sum 的新变量,暂时,值为 undefined

  • 第 9 行。遇到(),表明需要执行或调用一个函数。那么查找全局执行上下文的内存并查找名为 createWarp 的变量。 明显,已经在步骤 2 中创建完毕。接着,调用它。

  • 调用函数时,回到第 2 行。创建一个新的createWarp执行上下文。我们可以在 createWarp 的执行上下文中创建自有变量。js 引擎createWarp 的上下文添加到调用堆栈(call stack)。因为这个函数没有参数,直接跳到它的主体部分.

  • 3 - 6 行。我们有一个新的函数声明,createWarp执行上下文中创建一个变量 addadd 只存在于 createWarp 执行上下文中, 其函数定义存储在名为 add 的自有变量中。

  • 第 7 行,我们返回变量 add 的内容。js 引擎查找一个名为 add 的变量并找到它. 第 4 行和第 5 行括号之间的内容构成该函数定义。

  • createWarp 调用完毕,createWarp 执行上下文将被销毁。add 变量也跟着被销毁。add 函数定义仍然存在,因为它返回并赋值给了 sum 变量。 (ps: 这才是闭包产生的变量存于内存当中的真相

  • 接下来就是简单的执行过程,不再赘述。。

  • ……

  • 代码执行完毕,全局执行上下文被销毁。sum 和 result 也跟着被销毁。


小结一下


现在,如果再让你回答什么是闭包,你能答出多少?


其实,大家说的都对。不管是函数返回一个函数,还是产生了外部作用域的引用,都是有道理的。


所以,什么是闭包?


  • 解释一下作用域链是如何产生的。

  • 解释一下 js 执行上下文的创建、执行过程。

  • 解释一下闭包所产生的变量放在哪了。

  • 最后请把以上 3 点结合起来说给面试官听。

图片懒加载

与普通的图片懒加载不同,如下这个多做了 2 个精心处理:


  • 图片全部加载完成后移除事件监听;

  • 加载完的图片,从 imgList 移除;


let imgList = [...document.querySelectorAll('img')]let length = imgList.length
// 修正错误,需要加上自执行- const imgLazyLoad = function() {+ const imgLazyLoad = (function() { let count = 0
return function() { let deleteIndexList = [] imgList.forEach((img, index) => { let rect = img.getBoundingClientRect() if (rect.top < window.innerHeight) { img.src = img.dataset.src deleteIndexList.push(index) count++ if (count === length) { document.removeEventListener('scroll', imgLazyLoad) } } }) imgList = imgList.filter((img, index) => !deleteIndexList.includes(index)) }- }+ })()
// 这里最好加上防抖处理document.addEventListener('scroll', imgLazyLoad)
复制代码

手写题:数组扁平化

function flatten(arr) {  let result = [];
for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])); } else { result = result.concat(arr[i]); } }
return result;}
const a = [1, [2, [3, 4]]];console.log(flatten(a));

复制代码

说一说 SessionStorage 和 localStorage 还有 cookie

共同点:都是保存在浏览器端、且同源的不同点:    1.cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。    cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下    sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。    2.存储大小限制也不同,cookie数据不能超过4K,sessionStorage和localStorage可以达到5M    3.sessionStorage:仅在当前浏览器窗口关闭之前有效;    localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;    cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭    4.作用域不同    sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面;    localstorage:在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在    cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在
复制代码

vuex

vuex是一个专为vue.js应用程序开发的状态管理器,它采用集中式存储管理应用的所有组件的状态,并且以相应的规则保证状态以一种可以预测的方式发生变化。
state: vuex使用单一状态树,用一个对象就包含来全部的应用层级状态
mutation: 更改vuex中state的状态的唯一方法就是提交mutation
action: action提交的是mutation,而不是直接变更状态,action可以包含任意异步操作
getter: 相当于vue中的computed计算属性
复制代码

说说 Vue 原理

Vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty来()来劫持各个属性的getter和setter在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。
具体就是:MVVM作为绑定的入口,整合Observe,Compil和Watcher三者,通过Observe来监听model的变化通过Compil来解析编译模版指令,最终利用Watcher搭起Observe和Compil之前的通信桥梁从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。
复制代码

说一说前端性能优化方案

三个方面来说明前端性能优化一: webapck优化与开启gzip压缩    1.babel-loader用 include 或 exclude 来帮我们避免不必要的转译,不转译node_moudules中的js文件    其次在缓存当前转译的js文件,设置loader: 'babel-loader?cacheDirectory=true'    2.文件采用按需加载等等    3.具体的做法非常简单,只需要你在你的 request headers 中加上这么一句:    accept-encoding:gzip    4.图片优化,采用svg图片或者字体图标    5.浏览器缓存机制,它又分为强缓存和协商缓存二:本地存储——从 Cookie 到 Web Storage、IndexedDB    说明一下SessionStorage和localStorage还有cookie的区别和优缺点三:代码优化    1.事件代理    2.事件的节流和防抖    3.页面的回流和重绘    4.EventLoop事件循环机制    5.代码优化等等

复制代码

什么是同源策略

跨域问题其实就是浏览器的同源策略造成的。


同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议端口号域名必须一致。


同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。


同源政策主要限制了三个方面:


  • 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。

  • 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。

  • 当前域下 ajax 无法发送跨域请求。


同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者 script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。

网络劫持有哪几种,如何防范?

⽹络劫持分为两种:


(1)DNS 劫持: (输⼊京东被强制跳转到淘宝这就属于 dns 劫持)


  • DNS 强制解析: 通过修改运营商的本地 DNS 记录,来引导⽤户流量到缓存服务器

  • 302 跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起 302 跳转的回复,引导⽤户获取内容


(2)HTTP 劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于 http 明⽂传输,运营商会修改你的 http 响应内容(即加⼴告)


DNS 劫持由于涉嫌违法,已经被监管起来,现在很少会有 DNS 劫持,⽽http 劫持依然⾮常盛⾏,最有效的办法就是全站 HTTPS,将 HTTP 加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。

Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。


Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。


(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。


(2)Pending callbacks:执行推迟到下一个循环迭代的 I / O 回调(系统调用相关的回调)。


(3)Idle/Prepare:仅供内部使用。


(4)Poll(轮询阶段)


  • 当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。

  • 当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval 和 setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的 I/O 操作完成,并马上执行相应的回调,直到所有回调执行完毕。


(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。


(6)Close callbacks:执行一些关闭回调,比如 socket.on('close', ...)等。


下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机


setTimeout(() => {    console.log('setTimeout')}, 0)setImmediate(() => {    console.log('setImmediate')})
复制代码


对于以上代码来说,setTimeout 可能执行在前,也可能执行在后


  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的

  • 进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调

  • 那么如果准备时间花费小于 1ms,那么就是 setImmediate 回调先执行了


当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:


const fs = require('fs')fs.readFile(__filename, () => {    setTimeout(() => {        console.log('timeout');    }, 0)    setImmediate(() => {        console.log('immediate')    })})
复制代码


在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。


上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,


setTimeout(() => {  console.log('timer21')}, 0)Promise.resolve().then(function() {  console.log('promise1')})
复制代码


对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。


最后来看 Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。


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


对于以上代码,永远都是先把 nextTick 全部打印出来。

call apply bind

题目描述:手写 call apply bind 实现


实现代码如下:


Function.prototype.myCall = function (context, ...args) {  if (!context || context === null) {    context = window;  }  // 创造唯一的key值  作为我们构造的context内部方法名  let fn = Symbol();  context[fn] = this; //this指向调用call的函数  // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了  return context[fn](...args);};
// apply原理一致 只是第二个参数是传入的数组Function.prototype.myApply = function (context, args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; // 执行函数并返回结果 return context[fn](...args);};
//bind实现要复杂一点 因为他考虑的情况比较多 还要涉及到参数合并(类似函数柯里化)
Function.prototype.myBind = function (context, ...args) { if (!context || context === null) { context = window; } // 创造唯一的key值 作为我们构造的context内部方法名 let fn = Symbol(); context[fn] = this; let _this = this; // bind情况要复杂一点 const result = function (...innerArgs) { // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象 // 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论 // this.__proto__ === result.prototype //this instanceof result =>true // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true if (this instanceof _this === true) { // 此时this指向指向result的实例 这时候不需要改变this指向 this[fn] = _this; this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并 } else { // 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context context[fn](...[...args, ...innerArgs]); } }; // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法 // 实现继承的方式: 使用Object.create result.prototype = Object.create(this.prototype); return result;};
//用法如下
// function Person(name, age) {// console.log(name); //'我是参数传进来的name'// console.log(age); //'我是参数传进来的age'// console.log(this); //构造函数this指向实例对象// }// // 构造函数原型的方法// Person.prototype.say = function() {// console.log(123);// }// let obj = {// objName: '我是obj传进来的name',// objAge: '我是obj传进来的age'// }// // 普通函数// function normalFun(name, age) {// console.log(name); //'我是参数传进来的name'// console.log(age); //'我是参数传进来的age'// console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj// console.log(this.objName); //'我是obj传进来的name'// console.log(this.objAge); //'我是obj传进来的age'// }
// 先测试作为构造函数调用// let bindFun = Person.myBind(obj, '我是参数传进来的name')// let a = new bindFun('我是参数传进来的age')// a.say() //123
// 再测试作为普通函数调用// let bindFun = normalFun.myBind(obj, '我是参数传进来的name')// bindFun('我是参数传进来的age')
复制代码

什么情况会阻塞渲染?

首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。


当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。

NaN 是什么,用 typeof 会输出什么?

Not a Number,表示非数字,typeof NaN === 'number'


用户头像

loveX001

关注

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

还未添加个人简介

评论

发布
暂无评论
前端八股文总结_JavaScript_loveX001_InfoQ写作社区