关于javascript:2023我的前端面试小结

35次阅读

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

首屏和白屏工夫如何计算

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

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

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

具体阐明 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

new 一个函数产生了什么

结构调用:

  • 发明一个全新的对象
  • 这个对象会被执行 [[Prototype]] 连贯,将这个新对象的 [[Prototype]] 链接到这个构造函数.prototype 所指向的对象
  • 这个新对象会绑定到函数调用的 this
  • 如果函数没有返回其余对象,那么 new 表达式中的函数调用会主动返回这个新对象

代码输入后果

Promise.reject('err!!!')
  .then((res) => {console.log('success', res)
  }, (err) => {console.log('error', err)
  }).catch(err => {console.log('catch', err)
  })

输入后果如下:

error err!!!

咱们晓得,.then函数中的两个参数:

  • 第一个参数是用来解决 Promise 胜利的函数
  • 第二个则是解决失败的函数

也就是说 Promise.resolve('1') 的值会进入胜利的函数,Promise.reject('2')的值会进入失败的函数。

在这道题中,谬误间接被 then 的第二个参数捕捉了,所以就不会被 catch 捕捉了,输入后果为:error err!!!'

然而,如果是像上面这样:

Promise.resolve()
  .then(function success (res) {throw new Error('error!!!')
  }, function fail1 (err) {console.log('fail1', err)
  }).catch(function fail2 (err) {console.log('fail2', err)
  })

then 的第一参数中抛出了谬误,那么他就不会被第二个参数不活了,而是被前面的 catch 捕捉到。

事件是什么?事件模型?

事件是用户操作网页时产生的交互动作,比方 click/move,事件除了用户触发的动作外,还能够是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,蕴含了该事件产生时的所有相干信息(event 的属性)以及能够对事件进行的操作(event 的办法)。

事件是用户操作网页时产生的交互动作或者网页自身的一些操作,古代浏览器一共有三种事件模型:

  • DOM0 级事件模型,这种模型不会流传,所以没有事件流的概念,然而当初有的浏览器反对以冒泡的形式实现,它能够在网页中间接定义监听函数,也能够通过 js 属性来指定监听函数。所有浏览器都兼容这种形式。间接在 dom 对象上注册事件名称,就是 DOM0 写法。
  • IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行指标元素绑定的监听事件。而后是事件冒泡阶段,冒泡指的是事件从指标元素冒泡到 document,顺次查看通过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来增加监听函数,能够增加多个监听函数,会按程序顺次执行。
  • DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕捉阶段。捕捉指的是事件从 document 始终向下流传到指标元素,顺次查看通过的节点是否绑定了事件监听函数,如果有则执行。前面两个阶段和 IE 事件模型的两个阶段雷同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数能够指定事件是否在捕捉阶段执行。

Vue 通信

1.props 和 $emit
2. 地方事件总线 EventBus(根本不必)
3.vuex(官网举荐状态管理器)
4.$parent 和 $children
当然还有一些其余方法,但根本不罕用,或者用起来太简单来。介绍来通信的形式,还能够扩大说一下应用
场景,如何应用,注意事项之类的。

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

箭头函数和一般函数有啥区别?箭头函数能当构造函数吗?

  • 一般函数通过 function 关键字定义,this 无奈联合词法作用域应用,在运行时绑定,只取决于函数的调用形式,在哪里被调用,调用地位。(取决于调用者,和是否独立运行)
  • 箭头函数应用被称为“胖箭头”的操作 => 定义,箭头函数不利用一般函数 this 绑定的四种规定,而是依据外层(函数或全局)的作用域来决定 this,且箭头函数的绑定无奈被批改(new 也不行)。

    • 箭头函数罕用于回调函数中,包含事件处理器或定时器
    • 箭头函数和 var self = this,都试图取代传统的 this 运行机制,将 this 的绑定拉回到词法作用域
    • 没有原型、没有 this、没有 super,没有 arguments,没有 new.target
    • 不能通过 new 关键字调用

      • 一个函数外部有两个办法:[[Call]] 和 [[Construct]],在通过 new 进行函数调用时,会执行 [[construct]] 办法,创立一个实例对象,而后再执行这个函数体,将函数的 this 绑定在这个实例对象上
      • 当间接调用时,执行 [[Call]] 办法,间接执行函数体
      • 箭头函数没有 [[Construct]] 办法,不能被用作结构函数调用,当应用 new 进行函数调用时会报错。
function foo() {return (a) => {console.log(this.a);
  }
}

var obj1 = {a: 2}

var obj2 = {a: 3}

var bar = foo.call(obj1);
bar.call(obj2);

哪些状况会导致内存透露

以下四种状况会造成内存的透露:

  • 意外的全局变量: 因为应用未声明的变量,而意外的创立了一个全局变量,而使这个变量始终留在内存中无奈被回收。
  • 被忘记的计时器或回调函数: 设置了 setInterval 定时器,而遗记勾销它,如果循环函数有对外部变量的援用的话,那么这个变量会被始终留在内存中,而无奈被回收。
  • 脱离 DOM 的援用: 获取一个 DOM 元素的援用,而前面这个元素被删除,因为始终保留了对这个元素的援用,所以它也无奈被回收。
  • 闭包: 不合理的应用闭包,从而导致某些变量始终被留在内存当中。

代码输入后果

const promise = new Promise((resolve, reject) => {console.log(1);
  setTimeout(() => {console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});
promise.then((res) => {console.log(res);
});
console.log(4);

输入后果如下:

1
2
4
timerStart
timerEnd
success

代码执行过程如下:

  • 首先遇到 Promise 构造函数,会先执行外面的内容,打印1
  • 遇到定时器steTimeout,它是一个宏工作,放入宏工作队列;
  • 持续向下执行,打印出 2;
  • 因为 Promise 的状态此时还是 pending,所以promise.then 先不执行;
  • 继续执行上面的同步工作,打印出 4;
  • 此时微工作队列没有工作,继续执行下一轮宏工作,执行steTimeout
  • 首先执行 timerStart,而后遇到了resolve,将promise 的状态改为 resolved 且保留后果并将之前的 promise.then 推入微工作队列,再执行timerEnd
  • 执行完这个宏工作,就去执行微工作 promise.then,打印出resolve 的后果。

快排 – 工夫复杂度 nlogn~ n^2 之间

题目形容: 实现一个快排

实现代码如下:

function quickSort(arr) {if (arr.length < 2) {return arr;}
  const cur = arr[arr.length - 1];
  const left = arr.filter((v, i) => v <= cur && i !== arr.length - 1);
  const right = arr.filter((v) => v > cur);
  return [...quickSort(left), cur, ...quickSort(right)];
}
// console.log(quickSort([3, 6, 2, 4, 1]));

代码输入后果

var a = 1;
function printA(){console.log(this.a);
}
var obj={
  a:2,
  foo:printA,
  bar:function(){printA();
  }
}

obj.foo(); // 2
obj.bar(); // 1
var foo = obj.foo;
foo(); // 1

输入后果:2 1 1

解析:

  1. obj.foo(),foo 的 this 指向 obj 对象,所以 a 会输入 2;
  2. obj.bar(),printA 在 bar 办法中执行,所以此时 printA 的 this 指向的是 window,所以会输入 1;
  3. foo(),foo 是在全局对象中执行的,所以其 this 指向的是 window,所以会输入 1;

同步和异步的区别

  • 同步 指的是当一个过程在执行某个申请时,如果这个申请须要期待一段时间能力返回,那么这个过程会始终期待上来,直到音讯返回为止再持续向下执行。
  • 异步 指的是当一个过程在执行某个申请时,如果这个申请须要期待一段时间能力返回,这个时候过程会持续往下执行,不会阻塞期待音讯的返回,当音讯返回时零碎再告诉过程进行解决。

懒加载与预加载的区别

这两种形式都是进步网页性能的形式,两者次要区别是一个是提前加载,一个是缓慢甚至不加载。懒加载对服务器前端有肯定的缓解压力作用,预加载则会减少服务器前端压力。

  • 懒加载也叫提早加载,指的是在长网页中提早加载图片的机会,当用户须要拜访时,再去加载,这样能够进步网站的首屏加载速度,晋升用户的体验,并且能够缩小服务器的压力。它实用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的实在门路保留在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出实在门路赋值给图片的 src 属性,以此来实现图片的提早加载。
  • 预加载指的是将所需的资源提前申请加载到本地,这样前面在须要用到时就间接从缓存取资源。 通过预加载可能缩小用户的等待时间,进步用户的体验。我理解的预加载的最罕用的形式是应用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。

继承

原型链继承

function Animal() {this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {return this.colors}
function Dog() {}
Dog.prototype =  new Animal()

let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors)  // ['black', 'white', 'brown']

原型链继承存在的问题:

  • 问题 1:原型中蕴含的援用类型属性将被所有实例共享;
  • 问题 2:子类在实例化的时候不能给父类构造函数传参;

借用构造函数实现继承

function Animal(name) {
    this.name = name
    this.getName = function() {return this.name}
}
function Dog(name) {Animal.call(this, name)
}
Dog.prototype =  new Animal()

借用构造函数实现继承解决了原型链继承的 2 个问题:援用类型共享问题以及传参问题。然而因为办法必须定义在构造函数中,所以会导致每次创立子类实例都会创立一遍办法。

组合继承

组合继承联合了原型链和盗用构造函数,将两者的长处集中了起来。根本的思路是应用原型链继承原型上的属性和办法,而通过盗用构造函数继承实例属性。这样既能够把办法定义在原型上以实现重用,又能够让每个实例都有本人的属性。

function Animal(name) {
    this.name = name
    this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {return this.name}
function Dog(name, age) {Animal.call(this, name)
    this.age = age
}
Dog.prototype =  new Animal()
Dog.prototype.constructor = Dog

let dog1 = new Dog('奶昔', 2)
dog1.colors.push('brown')
let dog2 = new Dog('哈赤', 1)
console.log(dog2) 
// {name: "哈赤", colors: ["black", "white"], age: 1 }

寄生式组合继承

组合继承曾经绝对欠缺了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。

所以解决方案就是不间接调用父类构造函数给子类原型赋值,而是通过创立空函数 F 获取父类原型的正本。

寄生式组合继承写法上和组合继承根本相似,区别是如下这里:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f

略微封装下下面增加的代码后:

function object(o) {function F() {}
    F.prototype = o
    return new F()}
function inheritPrototype(child, parent) {let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Dog, Animal)

如果你厌弃下面的代码太多了,还能够基于组合继承的代码改成最简略的寄生式组合继承:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ Dog.prototype =  Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog

class 实现继承

class Animal {constructor(name) {this.name = name} 
    getName() {return this.name}
}
class Dog extends Animal {constructor(name, age) {super(name)
        this.age = age
    }
}

如果一个构造函数,bind 了一个对象,用这个构造函数创立出的实例会继承这个对象的属性吗?为什么?

不会继承,因为依据 this 绑定四大规定,new 绑定的优先级高于 bind 显示绑定,通过 new 进行结构函数调用时,会创立一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的状况下,返回这个新建的对象

CDN 的概念

CDN(Content Delivery Network,内容散发网络)是指一种通过互联网相互连贯的电脑网络零碎,利用最靠近每位用户的服务器,更快、更牢靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

典型的 CDN 零碎由上面三个局部组成:

  • 散发服务零碎: 最根本的工作单元就是 Cache 设施,cache(边缘 cache)负责间接响应最终用户的拜访申请,把缓存在本地的内容疾速地提供给用户。同时 cache 还负责与源站点进行内容同步,把更新的内容以及本地没有的内容从源站点获取并保留在本地。Cache 设施的数量、规模、总服务能力是掂量一个 CDN 零碎服务能力的最根本的指标。
  • 负载平衡零碎: 次要性能是负责对所有发动服务申请的用户进行拜访调度,确定提供给用户的最终理论拜访地址。两级调度体系分为全局负载平衡(GSLB)和本地负载平衡(SLB)。全局负载平衡 次要依据用户就近性准则,通过对每个服务节点进行“最优”判断,确定向用户提供服务的 cache 的物理地位。本地负载平衡 次要负责节点外部的设施负载平衡
  • 经营管理系统: 经营管理系统分为经营治理和网络管理子系统,负责解决业务层面的与外界零碎交互所必须的收集、整顿、交付工作,蕴含客户治理、产品治理、计费治理、统计分析等性能。

事件循环机制(Event Loop)

事件循环机制从整体上通知了咱们 JavaScript 代码的执行程序 Event Loop即事件循环,是指浏览器或 Node 的一种解决 javaScript 单线程运行时不会阻塞的一种机制,也就是咱们常常应用 异步 的原理。

先执行 Script 脚本,而后清空微工作队列,而后开始下一轮事件循环,持续先执行宏工作,再清空微工作队列,如此往返。

  • 宏工作:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
  • 微工作:process.nextTick()/Promise

上诉的 setTimeout 和 setInterval 等都是工作源,真正进入工作队列的是他们散发的工作。

优先级

  • setTimeout = setInterval 一个队列
  • setTimeout > setImmediate
  • process.nextTick > Promise
for (const macroTask of macroTaskQueue) {handleMacroTask();    
  for (const microTask of microTaskQueue) {handleMicroTask(microTask);  
  }
}

字符串模板

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

变量晋升

函数在运行的时候,会首先创立执行上下文,而后将执行上下文入栈,而后当此执行上下文处于栈顶时,开始运行执行上下文。

在创立执行上下文的过程中会做三件事:创立变量对象,创立作用域链,确定 this 指向,其中创立变量对象的过程中,首先会为 arguments 创立一个属性,值为 arguments,而后会扫码 function 函数申明,创立一个同名属性,值为函数的援用,接着会扫码 var 变量申明,创立一个同名属性,值为 undefined,这就是变量晋升。

instance 如何应用

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

'hello tuture' instanceof String // false

正文完
 0