跳到主要内容
你好! 我是Faith&Passion

长期写作最大的好处之一就是,写着写着,你的自我会变得越来越清晰。 你最终会明白自己是一个什么样的人,以及自己热爱的又是什么东西。——《为什么要写博客》

自我介绍
Layer 1

为什么需要模块化?

假如没有模块化,在前端开发时可能存在下面的问题:

  1. 变量和方法不容易维护,容易污染到全局作用域。
  2. 通过script标签进行大量引入资源,代码可读性和可维护性都比较差。
  3. 代码一多就比较复杂。
  4. 多人合作的场景下,资源的引入会带来比较大的困难。

JS模块化的演变史

1. CommonJS

我们熟知的Node.js在模块化方面就是遵守的CommonJS规范。CommonJS模块化具有下面几个特点:

  1. 模块内的代码运行在模拟作用域中,不会污染到全局作用域中。
  2. 模块可以多次引入,但只会在第一次加载的时候执行一次,后面的运行都是从缓存中获取值。
  3. 代码出现的顺序就是模块加载的顺序。

模块的导入导出方式:

通过module.exports或者exports进行导出,通过require进行导入。

module.exports = {age: 1,name: 'hello'}
const foo = require('./foo.js');

2. ES6 Module

CommonJS不适合浏览器等场景,于是ES6 Module诞生了,它是ES6之后新增的模块化规范。

模块的导入导出方式:

通过export导出模块,通过import导入模块。在导出的时候有两种方式,一种是默认暴露,一种是分别暴露。

问题汇总

RQ1:浏览器为什么不适用CommonJS?

因为CommonJS的require语法是同步的,在浏览器端文件一般存放在服务器上,一般通过网络请求来获取数据,如果使用CommonJS会导致时间很长,造成浏览器卡顿现象,NodeJS之所以采用CommonJS是因为NodeJS在服务端读取的是本地硬盘,因此速度比较快。

RQ2:CommonJS和ES6 Module之间的区别

  1. CommonJS输出的是值的拷贝,而ES6 Module输出的是值的引用。
  2. CommonJS模块是运行时加载,ES6 Moduke是编译时输出接口。
  3. CommonJS加载是同步的可能阻塞的,ES6 Module是异步加载。

RQ3:在Node.js中module.exports和exports有什么区别?

  1. 通过module.exports暴露的函数,在引入的时候可以不知道函数名,但是通过exports暴露的内容必须知道名字。
  2. exports对象是module对象的一个属性,初始时module.exports和exports指向的是同一块内存区域。
  3. 模块导出的是module.exports,exports只是和它指向的是同一片内存,在不改变exports内存的情况下,修改exports的值可以改变module.exports的值。
  4. 导出时尽量使用module.exports以避免赋值导致的混乱。

Justin面试题阅读需 3 分钟

在学习前端知识的时候,我们不仅要会用工具和API,更要知道其实现原理,因为只有知道原理,我们的理解才能更上一层楼,这次让我们来一起解决最常见的new的实现原理吧。

new在原型链中扮演什么样的角色?

在this的指向规则中,有一种this指向是new绑定,new绑定会让构造函数的this指向我们新创建的对象,请看下面的例子:

function sayHi(name){
this.name = name;

}
var Hi = new sayHi('zhangsan');
console.log('Hello,', Hi.name); // Hello, zhangsan

其实,除了上面我们提到的this指向之外,new还会让实例的隐式原型指向其显式原型,下面让我们来一起看看new的内部是如何实现的吧!

手写new的实现

  1. 创建一个空对象。
  2. 拿到构造函数。
  3. 让空对象的隐式原型指向构造函数的显示原型。
  4. 让构造函数的this指向我们创建的对象并执行。
  5. 如果执行的结果是引用类型则返回引用类型,否则返会创建的对象。
function myNew() {
// 1. 创建一个空对象
const obj = {};
// 2. 拿到构造函数,也就是第一个参数
const Constructor = Array.prototype.shift.call(arguments);
// 3. 让实例的隐式原型指向构造函数的显式原型
obj.__proto__ = Constructor.prototype;
// 4. 改变构造函数的this指向并执行
const res = Constructor.apply(obj, arguments);
// 5. 判断是否是引用类型,是则返回引用类型,不是则返回obj
return res instanceof Object ? res : obj;
}

总结

手写new不仅是一道常考题,其内部涉及到了this指向,原型链,如何通过原生方法获取arguments的第一个参数,apply执行的结果等知识,是一道必须掌握的题目!


Justin面试题阅读需 2 分钟

什么是Koa?

Koa是一个精简的node框架,被认为是第二代Node框架,其最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型,它的核心工作包括下面两个方面:

  1. 将node原生的req和res封装成为一个context对象。
  2. 基于async/await的中间件洋葱模型机制。

Koa1和Koa2在源码上的区别有何不同?

  1. Koa1是使用generator、yield的模式。
  2. Koa2使用的是async/await + Promise的模式。

什么是洋葱模型?

Koa的洋葱模型是以next()函数为分割点,先由外到内执行Request的逻辑,然后再由内到外执行Response的逻辑,这里的request的逻辑,我们可以理解为是next之前的内容,response的逻辑是next函数之后的内容,也可以说每一个中间件都有两次处理时机。洋葱模型的核心原理主要是借助compose方法。为了大家更好的理解什么是洋葱模型,这个图很好的给出了解释:

image.png

下面是洋葱模型的示例代码:

const Koa = require('koa');

//Applications
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(7000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

当我们访问指定路径的7000端口时,中间件的打印顺序是1 -> 3 -> 4 -> 2。

源码解析

  1. use方法

当我们使用中间件的时候,首先是使用use方法,use方法会将传入的中间件回调函数存储到middleware中间件数组中。

  1. listen方法

当执行app.listen去监听端口的时候,其实其内部调用了http模块的createServer方法,然后传入内置的callback方法,这个callback方法就会将use方法存储的middleware中间件数组传给compose函数。

  1. compose方法

compose方法是洋葱模型的核心,compose方法中有一个dispatch方法,第一次调用的时候,执行的是第一个中间件函数,中间件函数执行的时候就是再次调用dispatch函数,也就说形成了一个递归,这就是next函数执行的时候会执行下一个中间件的原因,因此形成了一个洋葱模型。

function compose (middleware) {
// ...
return function (context, next) {
// last called middleware #
let index = -1
// 一开始的时候传入为 0,后续会递增
return dispatch(0)
function dispatch (i) {
// 假如没有递增,则说明执行了多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 拿到当前的中间件
let fn = middleware[i]
if (i === middleware.length) fn = next
// 当 fn 为空的时候,就会开始执行 next() 后面部分的代码
if (!fn) return Promise.resolve()
try {
// 执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数
// 也就是说执行 next 的时候也就是调用 dispatch 函数的时候
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

为什么需要洋葱模型?

因为很多时候,在一个app里面有很多个中间件,有些中间件需要依赖其他中间件的结果,洋葱模型可以保证执行的顺序,如果没有洋葱模型,执行顺序可能出乎我们的预期。

参考文献

【Node】深入浅出 Koa 的洋葱模型


Justin面试题阅读需 3 分钟

OSI七层模型具体指的是哪七层?

从底到上分别是:

  • 物理层
  • 数据链路层
  • 网络层
  • 传输层
  • 会话层
  • 表示层
  • 应用层

每一层主要负责什么?都有什么协议?

  • 物理层

物理层主要规定通信设备的电气特性,用以建立物理链路连接,例如规定了设备的规格尺寸、引脚数量和排列情况等,电气特性规定了物理连接上传输bit流时信号电平的大小、传输速率等,核心作用就是负责传输0和1的电信号。

  • 数据链路层

数据链路层在物理层提供比特流的基础上,通过差错控制提供数据帧在信道上无差错的传输,这一层确定了0和1的分组方式。数据链路层将物理层的比特流转换成帧。

  • 网络层

计算机网络中进行通信的两个计算机之间可能会经过多个数据链路,或通信子网,网络层的任务就是选择合适的路由和交换节点,确保数据及时传送,网络层将数据链路层提供的帧组成数据包。这一层的代表协议是IP协议、RIP协议。网络层的核心功能是建立主机到主机的通信。RIP协议是一种动态路由选择协议。

  • 传输层

传输层的核心功能是建立端口到端口的通信,只要确定主机和端口就能实现程序之间的通信。传输层的代表协议有:TCP、UDP协议。

  • 会话层

会话层主要用于辅助何时建立连接、何时断开连接、以及保持多久的连接。代表的协议有DNS

  • 表示层

表示层和主要是将设备固有的数据格式和网络标准数据格式之间的转换。

  • 应用层

应用层主要是针对特定应用的协议,例如电子邮件Email、远程登陆协议SSH、文件传输协议FTP、网络请求协议HTTP。


Justin面试题阅读需 2 分钟

Promise根据异常出现的位置不同采取的方案也不同

Promise内部抛出异常

通过then的第二个函数来捕捉异常。

// Promise的异常捕获问题
const promise = new Promise((resolve,reject) => {
throw new Error('test')
})

// 通过then的第二个函数来进行捕捉
promise.then(res => {
console.log(res);
},err => {
console.log(err); // [Error test]
})

Promise.then的第一个函数出现了异常

通过catch来捕捉Promise.then的第一个函数。

const promise = new Promise((resolve,reject) => {
resolve(666)
})

// 通过.catch进行捕捉
promise.then(res => {
throw new Error('test2')
},err => {
console.log(err);
}).catch(err => {
console.log(err); //[Error test2]
})

Justin面试题阅读需 1 分钟

假如v-if和v-for一起使用会出现什么情况?

<ul>
<li v-for="(item,index) in arr " v-if="flag" :key="index" >
{{item}}
</li>
</ul>

一起使用,会出现下面的错误提示:

image.png

为什么不能一起使用?

因为v-for的优先级比v-if的优先级高,所以如果嵌套使用的话,每次v-for都会执行一次v-if,造成重复计算的问题,会影响性能,所以vue官方不推荐这样使用。


Justin面试题阅读需 1 分钟

核心区别:获取的内容不同

  • for...in主要获取对象的key和数组的下标,同时for...in还能够遍历原型链上的可枚举属性。
  • for...of主要获取对象的value值。
const obj = {'a' : '111','b':'222'};

for (let key in obj) {
console.log(key); // a b
}
// 没有部署原生的iterator接口的对象不能直接遍历
for (let value of Object.keys(obj)) {
console.log(obj[value]); // 111 222
}

常见问题汇总

RQ1:for...in或者for...of能遍历Symbol类型的值吗?

无论是for...in还是for...of都不能遍历Symbol类型的值,遍历Symbol类型的值需要使用Object.getOwnPropertySymbols()方法。

RQ2:for...in遍历出原型上的属性怎么办?

如果只想要遍历实例对象身上的属性,不遍历原型链上的属性可以使用hasOwnProperty方法来过滤。

RQ3:for in 和 Object.keys()的区别?

for...in循环会遍历原型链上的可枚举属性,但是Object.keys()则不会涉及到原型链上的属性。


Justin面试题阅读需 1 分钟

keep-alive的使用场景及其特点

  1. 用于Vue性能优化。
  2. 缓存组件。
  3. 频繁切换,不需要重复渲染。
  4. keep-alive有include和exclude属性,这两个属性决定了哪些组件可以进入缓存。
  5. keep-alive还有一个max属性,通过它可以设置最大缓存数,当缓存的实例超过max的时候,vue会删除最久没有使用的缓存,属于LRU缓存策略。
  6. keep-alive其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated和deactivated,它们分别在组件激活和失活的时候触发。

将组件放入keep-alive中即可实现组件的缓存。

<keep-alive>
<KeepaliveA v-if="state === 'A' " />
<KeepaliveB v-if="state === 'B' "/>
<KeepaliveC v-if="state === 'C' "/>
</keep-alive>

keep-alive的原理?

keep-alive在实现上,维护了一个key数组和一个缓存对象,这个key数组记录目前缓存的组件的key值,如果这个组件没有指定key值,会自动生成一个唯一的key值,缓存对象会以key值为键,vnode为值,用于缓存组件对应的虚拟DOM,在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有则从缓存中读取到对应的组件实例,没有就把它缓存。

keep-alive如何根据不同场景来更新数据?

可以利用keep-alive提供的include和exclude指定缓存哪些组件不缓存哪些组件,然后配合vuex等状态管理工具实现动态控制。


JustinVue阅读需 2 分钟

题目描述

image.png

image.png

解题思路

核心的解题思路就是借助一个栈来辅助我们,遇到空字符串和一个点的则跳过,遇到两个点的则出栈。

image.png

AC代码

var simplifyPath = function(path) {
// 简化路径的核心就是借助辅助栈
const stack = [];
// 分割字符串
const strArr = path.split('/');
// 遇到空字符串和一个点的直接跳过
// 遇到两个点则出栈
const res = [];
for (let v of strArr) {
if (v.length === 0 || v === '.') {
continue;
}
if (v === '..') {
stack.pop();
continue;
}
stack.push(v);
}
for (let i = 0; i < stack.length; i++) {
res.push('/');
res.push(stack[i]);
}
return res.length > 0 ? res.join('') : '/'
};

总结

简化路径的本质就是借助栈这个数据结构的特点对不同的情况进行出栈和入栈,最后返回的就是路径的简化结果,遇到空字符串和一个点的都跳过,遇到两个点的则出栈是本题的核心。


JustinLeetCode阅读需 1 分钟