跳到主要内容

72 篇博文 含有标签「面试题」

查看所有标签

为什么需要模块化?

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

  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 分钟

一、methods

methods中存放的是事件的回调函数,具有以下几个特点:

  1. 可以通过Vue实例访问方法,在方法中最好不要使用箭头函数,因为涉及到this指向的问题。
  2. 重新渲染的时候,methods总会执行该函数。
  3. methods是函数调用,filters和computed、watch是属性调用。
  4. methods是不具有缓存性的。

二、filters

filters一般用于格式化输出的场景,比如日期格式化,filters过滤器可以进行串联调用,所以可以定义一些基础的filters,然后按需在组件内使用。filters不具有缓存性。

{{msg | fliterA | filterB}}

三、computed

computed是计算属性,会监听一个依赖属性,如果这个属性发生变化,就会更新视图,适用于计算比较消耗性能的计算场景,因为其具有缓存功能,这意味着其监听的数据如果没有发生变化,多次调用计算属性会返回之间的计算结果,不必再次执行函数。

四、watch

watch可以监听某一个数据的变化,watch要想监听对象中某个属性是否发生了变化,需要将deep属性置为true。但是watch无法拿到对象中属性变化的旧值,只能拿到新的对象。

  watch: {
name(newValue, oldValue) {
console.log("name", newValue, oldValue);
},
info: {
handler: function(newValue,oldValue) {
console.log('info',newValue,oldValue);
},
deep: true
}
},

问题汇总

RQ1:watch和computed的区别

  1. watch是监听一个数据是否发生变化,当这个数据发生变化才执行函数。
  2. computed则是只要其函数内部的代码指向的数据发生了变化,其就会执行函数,然后返回。
  3. computed具有缓存特性,watch则没有。

Justin面试题阅读需 2 分钟

什么是函数柯里化?

函数柯里化主要用于给函数分步传递参数,每次传递参数进行处理,并返回一个更具体的函数来接受剩下的参数,这中间可以嵌套多层,直至返回最后的结果。

实现思路

  1. 接收一个处理数据的函数。
  2. 定义一个数组用于接收所有的参数。
  3. 返回一个函数,如果返回的函数接收的参数的长度是0,则返回fn执行的结果,如果不是0,则将参数push进数组中,并返回函数。

代码实现一(需要额外调用)

// 手写函数柯里化
const curring = function(fn) {
const args = [];
return function result(...rest) {
if (rest.length === 0) {
return fn(...args)
} else {
args.push(...rest);
return result;
}
}
}
const sum = (...arg) => {
return arg.reduce((pre,cur) => {
return pre + cur
},0)
}

curring(sum)(1)(2,5)(3)()

代码实现二(不需要额外调用)

// 手写函数科里化
// foo(1)(2)(3)
const sum = (arg) => {
return arg.reduce((pre,cur) => {
return pre + cur;
},0)
};

const foo = (...args1) => {
const sum1 = sum(args1);
const fn = (...args2) => {
const sum2 = sum(args2);
return foo(sum1 + sum2);
}
fn.toString = () => {
return sum1;
}
return fn;
}
// 这种方式只能使用== 不能使用===
foo(1)(2)(3) == 6

但是这种方式只能使用==,不能使用===


Justin面试题阅读需 2 分钟

方式一:通过constructor

通过constructor可以直接找到元素的构造函数类型,这种方法能够区分引用数据类型到底是哪种类型,请看下面的例子。

const arr = [1,2,3,5];
const date = new Date();
const num = 666;
const map = new Map();
const set = new Set();
const reg = new RegExp();
const str = '111';
const sym = Symbol(66);
const func = function(){}
// 需要注意的是null和undefined没有constructor

console.log(arr.constructor === Array); // true
console.log(date.constructor === Date); // true
console.log(num.constructor === Number); // true
console.log(map.constructor === Map); // true
console.log(set.constructor === Set); // true
console.log(reg.constructor === RegExp); // true
console.log(str.constructor === String); // true
console.log(sym.constructor === Symbol); // true
console.log(func.constructor === Function); // true

需要特别注意的是null和undefined没有constructor属性。

方式二:使用instanceof来判断引用类型到底属于哪种类型

const arr = [1,2,3,5];
const date = new Date();
const map = new Map();
const set = new Set();
const reg = new RegExp();
const func = function(){}

console.log(arr instanceof Array); // true
console.log(date instanceof Date); // true
console.log(map instanceof Map); // true
console.log(set instanceof Set); // true
console.log(reg instanceof RegExp); // true
console.log(func instanceof Function); // true

需要特别注意的是:instanceof 不适用于判断基本类型。

方式三:使用typeof来判断基本数据类型

需要注意的是typeof能够帮助我们判断出基本数据类型和函数,但是引用数据类型一般是object。

const arr = [1,2,3,5];
const date = new Date();
const num = Number(666);
const map = new Map();
const set = new Set();
const reg = new RegExp();
const str = '111';
const sym = Symbol(66);
const func = function(){}
// 需要注意的是null和undefined没有constructor

console.log(typeof arr); // object
console.log(typeof date); // object
console.log(typeof num); // number
console.log(typeof map); // object
console.log(typeof set); // object
console.log(typeof reg); // object
console.log(typeof str); // string
console.log(typeof sym); // symbol
console.log(typeof func); //function

方式四:通过Object.prototype.toString.call()精准确定类型(强烈推荐)

需要注意的是这个方法输出的是一个字符串,这个字符串object是小写的,后面的是大写的。

const arr = [1,2,3,5];
const date = new Date();
const num = Number(666);
const map = new Map();
const set = new Set();
const reg = new RegExp();
const str = '111';
const sym = Symbol(66);
const func = function(){}

console.log(Object.prototype.toString.call(arr)); // [object Array]
console.log(Object.prototype.toString.call(date)); // [object Date]
console.log(Object.prototype.toString.call(num)); // [object Number]
console.log(Object.prototype.toString.call(map)); // [object Map]
console.log(Object.prototype.toString.call(set)); // [object Set]
console.log(Object.prototype.toString.call(reg)); // [object RegExp]
console.log(Object.prototype.toString.call(str)); // [object String]
console.log(Object.prototype.toString.call(sym)); // [object Symbol]
console.log(Object.prototype.toString.call(func)); // [object Function]

问题汇总

RQ1:如何判断一个对象是一个空对象?

  1. 通过Reflect.ownKeys的长度为零。Reflect.ownKys()可以返回一个由目标对象自身的属性组成的数组。
const obj = {};

console.log(Reflect.ownKeys(obj).length === 0); // true
  1. 通过JSON.stringify()
const obj = {};

console.log(JSON.stringify(obj) === '{}'); // true

RQ2:有什么方法可以获取对象的key?

  1. 使用Object.keys()
  2. 使用Reflect.ownKeys()

RQ3:对象的中括号运算符和点运算符有什么区别?

中括号中可以用变量,但是点后面不能是一个变量。

const obj = {};

obj.name = '111';
const myName = 'name'
console.log(obj.myName); // undefined
console.log(obj[myName]); // 111

Justin面试题阅读需 3 分钟