前端面试题总结(不断更新修改)

前言

因为一些特殊原因,刚入职半年又重新找工作,发现之前的面试题已经忘了,因此在此记录并不断更新。
面试题不是靠死记硬背,而是在短时间尽量多的备战可能问到的题目,答案则是需要理解更深层的原理,回答时才能够从容不迫,这便需要花费更多的时间。共勉!希望每个人面试都能有个好结果~

CSS部分

元素水平垂直居中

1、margin 已知高宽
1)margin为auto

	.box {
      background-color: #FF8C00;
      width: 300px;
      height: 300px;
      position: relative;
    }
    .content {
      background-color: #F00;
      width: 100px;
      height: 100px;
      position: absolute;
      left: 0;
      top: 0;
      bottom: 0;
      right: 0;
      margin: auto;
    }

2)margin为指定值

	.box {
      background-color: #FF8C00;
      width: 300px;
      height: 300px;
      position: relative;
    }
    .content {
      background-color: #F00;
      width: 100px;
      height: 100px;
      position: absolute;
      left: 50%;
      top: 50%;
      margin: -50px 0 0 -50px;
    }

2、transform 未知高宽

.box {
    background-color: #FF8C00;
    width: 300px;
    height: 300px;
    position: relative;
}
.content {
    background-color: #F00;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
}

3、flex布局 未知高宽

.box {
    background-color: #FF8C00;
    width: 300px;
    height: 300px;
    display: flex;
    justify-content: center;
    align-items: center;
}
.content {
    background-color: #F00;
}

4、table-cell布局

.box {
    background-color: #FF8C00;
    width: 300px;
    height: 300px;
    display: table;
}
.content {
    background-color: #F00;
    display: table-cell;
    vertical-align: middle;
    text-align: center;
}
.inner {
    background-color: #000;
    display: inline-block;
    width: 20%;
    height: 20%;
}

css优先级算法

css属于层叠样式,会来自于父组件的继承样式以及自己的特殊声明样式,那么如何判断其样式的优先级呢,就需要有一定的计算方式。
各选择器优先级为:!important>id选择器>class、属性选择器或伪类>元素和伪元素选择器>*
其权重通过0,0,0,0的方式进行计算,其中:
1、ID选择器的特殊性,其值0,1,0,0;
2、类选择器、属性选择器或伪类,加0,0,1,0。
3、元素和伪元素,加0,0,0,1。
4、通配选择器*对特殊性没有贡献,即0,0,0,0。
5、最后比较特殊的一个标志!important(权重),它没有特殊性值,但它的优先级是最高的,为了方便记忆,可以认为它的特殊性值为1,0,0,0,0。
例如以下规则的选择器优先级算法为:

a{color: yellow;} /*特殊性值:0,0,0,1*/
div a{color: green;} /*特殊性值:0,0,0,2*/
demo a{color: black;} /*特殊性值:0,0,1,1*/
demo input[type="text"]{color: blue;} /*特殊性值:0,0,2,1*/
demo *[type="text"]{color: grey;} /*特殊性值:0,0,2,0*/
#demo a{color: orange;} /*特殊性值:0,1,0,1*/
div#demo a{color: red;} /*特殊性值:0,1,0,2*/

当然,如果优先级计算结果一致,则后声明的覆盖先声明的样式。

清除浮动

1、额外标签法

clear:both;

缺点:需要新增多余的标签,语义化差。
2、父级添加overflow属性

overflow: hidden;

缺点:内容增多的时候容易造成不会自动换行导致内容被隐藏掉,无法显示要溢出的元素。
3、after伪元素清除浮动

	.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/
        content: "";
        display: block;
        height: 0;
        clear:both;
        visibility: hidden;
    }
    .clearfix{
        *zoom: 1;/*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/
    }

缺点:ie6-7不支持伪元素:after,使用zoom:1触发hasLayout。

全局reset

JS及ES6部分

new操作符的机制

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:

1、创建一个空的简单JavaScript对象(即{});
2、链接该对象(即设置该对象的构造函数)到另一个对象 ;
3、将步骤1新创建的对象作为this的上下文 ;
4、如果该函数没有返回对象,则返回this。
其代码原理如下:

function newCreate(Base, ...args){
	let obj = {}
	obj.__proto__ = Base.prototype
	let result = Base.call(obj, ...args)
	return result instanceof Object ? result : obj
}

闭包理解

闭包是大部分笔试面试都会问的问题。网上一堆关于闭包问题的解释,在此只记录我所学习到的内容。
闭包是函数和声明该函数的词法环境的组合,指有权访问另一函数作用域下的变量的函数。

词法环境

js没有块级作用域概念,函数作用域是在其函数原型的作用域中一层层往上检索是否有该变量,也就是检索词法环境。如下:

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        alert(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

init() 创建了一个局部变量 name 和一个名为 displayName() 的函数。displayName() 是定义在 init() 里的内部函数,仅在该函数体内可用。displayName() 内没有自己的局部变量,然而它可以访问到外部函数的变量,所以 displayName() 可以使用父函数 init() 中声明的变量 name 。但是,如果有同名变量 namedisplayName() 中被定义,则会使用 displayName() 中定义的 name

闭包

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

运行这段代码和之前的 init() 示例的效果完全一样。其中的不同 — 也是有意思的地方 — 在于内部函数 displayName() 在执行前,被外部函数返回。

第一眼看上去,也许不能直观的看出这段代码能够正常运行。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行完毕,我们会认为 name 变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。

这个谜题的答案是,JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成。**这个环境包含了这个闭包创建时所能访问的所有局部变量。**在我们的例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用,而 displayName 实例仍可访问其词法作用域中的变量,即可以访问到 name 。由此,当 myFunc 被调用时,name 仍可被访问,其值 Mozilla 就被传递到alert中。

闭包缺点

1、内存泄漏
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2、引用参数会修改父函数的值
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

Promise

Promise面试题内容较多,暂时不整理,独立一文整理。

finally

Promise.prototype.finally = function (callback) {
      let P = this.constructor;
     return this.then(function(value) {
         P.resolve(callback()).then(function(){
             return value;
         });
     }, 
     function (reason) {
         P.resolve(callback()).then(function() {
             throw reason;
         });
     });
 };

高阶函数

高阶函数源自于函数式编程,是函数式编程的基本技术。JS中函数可以被赋值给变量,被变量引用,因此可以作为参数传递给函数:

/** 
 * 数值转换
 * @param {Number} val 要被处理的数值
 * @param {Function} fn 处理输入的val
 * @return {Number || String}
 */
const toConvert = function(val, fn) {
    return fn(val);
};

const addUnitW = function(val) {
    return val + 'W';
};

toConvert(123.1, Math.ceil); // 124
toConvert(123.1, addUnitW); // "123.1W"

另外,JS的回调函数同样是以实参形式传入其他函数中,这也是高阶函数(在函数式编程中回调函数被称为 lambda表达式):

[1, 2, 3, 4, 5].map(d => d ** 2); // [1, 4, 9, 16, 25]

// 以上,等同于:
const square = d => d ** 2;
[1, 2, 3, 4, 5].map(square); // [1, 4, 9, 16, 25]

跨域请求

何为跨域

浏览器处于安全考虑,设计了同源策略,同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源,否则则为跨域。

解决方法

1、JSONP
js加载并没有跨域问题,因此可以添加script标签访问外部请求:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
      window.jsonpCb = function (res) {
        console.log(res)
      }
    </script>
    <script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

缺点:1)从外部加载代码很危险;2)确定请求是否成功并不容易
2、CORS
这个只需要后端配置就行:

ctx.set('Access-Control-Allow-Origin', '*')

3、Node中间件代理
通过Node的中间层代理,转发请求

var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer({
    target: 'http://localhost:9871/',   //接口地址
    //创建一个代理,从以上路径获取数据。
});

4、Nginx代理
通过nginx代理,转发前端的请求。

server{
    # 监听9099端口
    listen 9099;
    # 域名是localhost
    server_name localhost;
    #凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
    location ^~ /api {
        proxy_pass http://localhost:9871;
    }    
}

其中3、4两点,同源策略是浏览器的限制,http协议并不会有此限制,因此通过后端转发并不存在跨域的问题。
5、图像ping
缺点:只能获取Get请求,且只是单向请求,无法获取响应文本。

async原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

async等同于声明了个自执行的函数,参数为Generator函数。
spawn函数的实现

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) { // 若已完成,则返回结果
        return resolve(next.value);
      }
      // 若未执行结束,则继续执行之后的内容
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

其基本过程为:
返回一个Promise对象,对象内:
声明自执行函数step,若函数结果done未完成,则继续递归执行step函数。

bind的实现

实现原理:
1、判断自身是否为函数,不是则抛出异常;
2、指定this所属以及函数的参数;
3、返回一个函数,该函数返回调用this主函数的函数;
4、指定函数的原型链
MDN给出的polyfill方法:

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
          return fToBind.apply(this instanceof fBound
                 ? this
                 : oThis,
                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 维护原型关系
    if (this.prototype) {
      // 当执行Function.prototype.bind()时, this为Function.prototype 
      // this.prototype(即Function.prototype.prototype)为undefined
      fNOP.prototype = this.prototype; 
    }
    // 下行的代码使fBound.prototype是fNOP的实例,因此
    // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
    fBound.prototype = new fNOP();

    return fBound;
  };
}

Class原理

Class类

ES6使用class定义类:

class Parent {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    speakSomething(){
        console.log("I can speek chinese");
    }
}

经过babel转码之后为:

var _createClass = function () {
    function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
            var descriptor = props[i];
            descriptor.enumerable = descriptor.enumerable || false;
            descriptor.configurable = true;
            if ("value" in descriptor) descriptor.writable = true;
            Object.defineProperty(target, descriptor.key, descriptor);
        }
    }
 
    return function (Constructor, protoProps, staticProps) {
        if (protoProps) defineProperties(Constructor.prototype, protoProps);
        if (staticProps) defineProperties(Constructor, staticProps);
        return Constructor;
    };
}();

function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
        throw new TypeError("Cannot call a class as a function");
    }
}

var Parent = function () {
    function Parent(name, age) {
        _classCallCheck(this, Parent);

        this.name = name;
        this.age = age;
    }

    _createClass(Parent, [{
        key: "speakSomething",
        value: function speakSomething() {
            console.log("I can speek chinese");
        }
    }]);

    return Parent;
}();

通过ES6创建的类,是不允许你直接调用的。在ES5中,构造函数是可以直接运行的,比如Parent()。但是在ES6就不行。我们可以看到转码的构造函数中有_classCallCheck(this, Parent)语句,这句话是防止你通过构造函数直接运行的。你直接在ES6运行Parent(),这是不允许的,ES6中抛出Class constructor Parent cannot be invoked without 'new'错误。转码后的会抛出Cannot call a class as a function.能够规范化类的使用方式。
转码中的_createClass方法,它调用Object.defineProperty方法去给新创建的Parent添加各种属性。defineProperties(Constructor.prototype, protoProps)是给原型添加属性。如果你有静态属性,会直接添加到构造函数defineProperties(Constructor, staticProps)上。

ES6继承

require和import的区别

prototype和__proto__区别

isArray原理

isArray用于判断传递的值是否为数组。使用方式:

// 下面的函数调用都返回 true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
// 鲜为人知的事实:其实 Array.prototype 也是一个数组。
Array.isArray(Array.prototype); 

// 下面的函数调用都返回 false
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray('Array');
Array.isArray(true);
Array.isArray(false);
Array.isArray({ __proto__: Array.prototype });

其他判断数组方式

1、instanceof

let a = [];
a instanceof Array; //true
let b = {};
b instanceof Array; //false

缺点:不能够检验iframes对象里的数组
2、constructor

let a = [1,3,4];
a.constructor === Array;//true

缺点:与instanceof一样,无法检验iframes里的数组
3、Object.prototype.toString.call()

let a = [1,2,3]
Object.prototype.toString.call(a) === '[object Array]';//true

该方法对iframes里数组一样适用

isArray vs instanceof

检验数组时,isArray更优于instanceof,因为Array.isArray能够检测iframes。

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

isArray实现原理

若不存在改方法,可使用以下方式实现:

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

事件轮询机制

函数防抖

var timer = null;
document.onscroll = function () {
	window.clearTimeout(timer);
	timer = setTimeout(function() {
		console.log('防抖');	
	}, 300);
}

函数节流

var canRun = true;
document.onscroll = function () {
	if(!canRun) return ;
	canRun = false;
	setTimeout(function () {
		console.log('节流')
		canRun = true;
	}, 300);
}

数组去重

数组随机排序

Jsonp封装

Vue部分

MVVM原理

DIFF算法

详情可看文章:
详解vue的diff算法

虚拟DOM渲染过程

把模板编译为render函数
实例进行挂载, 根据根节点render函数的调用,递归的生成虚拟dom
利用DIFF 算法对比虚拟dom,渲染到真实dom
组件内部data发生变化,组件和子组件引用data作为props重新调用render函数,生成虚拟dom, 返回到步骤3

详情可看文章:
vue核心之虚拟DOM(vdom)

vue生成AST源码解析

Vue源码解析之Template转化为AST

computed和watch的区别

MVC和MVVM的区别

v-bind和v-model的区别

vue的生命周期

vue路由钩子

vue路由钩子理解

路由间跳转

路由间参数传递

vue组件间传递参数

vue中event bus的实现

keep-alive作用和原理

类似于缓存代理模式

vuex的理解

vuex数据响应原理

vuex工作原理详解

vue插槽slot原理解析

Vue源码解析-了解vue插槽slot篇

webpack部分

webpack打包原理

webpack热部署原理

搞懂webpack热更新原理

webpack和gulp的区别

其他内容

http请求

http优化

https工作原理

WebSocket

常见的web攻击方式

SQL注入
XSS攻击
CSRF攻击
DOS攻击

常见设计模式

TCP三次握手和四次挥手

TCP位码,有6种标示:
SYN(synchronous建立联机)
ACK(acknowledgement 确认)
PSH(push传送)
FIN(finish结束)
RST(reset重置)
URG(urgent紧急)
Sequence number(顺序号码)
Acknowledge number(确认号码)

三次握手

第一次握手:主机A发送位码为syn=1,随机产生seq number=x的数据包到服务器,客户端进入SYN_SEND状态,等待服务器的确认;主机B由SYN=1知道,A要求建立联机;

第二次握手:主机B收到请求后要确认联机信息,向A发送ack number(主机A的seq+1),syn=1,ack=1,随机产生seq=y的包,此时服务器进入SYN_RECV状态;

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
TCP三次握手

四次挥手

第一次挥手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;

第二次挥手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我也没有数据要发送了,可以进行关闭连接了;

第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入CLOSE_WAIT状态;

第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
TCP四次挥手

函数式编程

函数式编程思想

1、声明式编程
命令式主张告诉编译器“如何”做,声明式告诉编译器“做什么”,如何做的部分(获得数组长度,循环遍历每一项)被抽象到高阶函数中,forEach就是这样一个内置函数。

// 命令式方式
var array = [0, 1, 2, 3]
for(let i = 0; i < array.length; i++) {
    array[i] = Math.pow(array[i], 2)
}

array; // [0, 1, 4, 9]

// 声明式方式
[0, 1, 2, 3].map(num => Math.pow(num, 2))

2、纯函数
纯函数是对相同输入返回相同输出的函数,不依赖(包含)任何外部变量,所以也不会产生改变外部环境变量的副作用。
3、引用透明
所有函数对于相同的输入都将返回相同的值(函数只依赖参数的输入,不依赖于其他全局数据,即函数内部没有全局引用),这使并行代码和缓存(用值直接替换函数的结果)成为可能。

// 非引用透明
var counter = 0

function increment() {
    return ++counter
}

// 引用透明
var increment = (counter) => counter + 1

4、不可变性
不可变数据是指那些创建后不能更改的数据。与许多其他语言一样,JavaScript 里有一些基本类型(String,Number 等)从本质上是不可变的,但是对象就是在任意的地方可变。
考虑一个简单的数组排序代码:

var sortDesc = function(arr) {
    return arr.sort(function(a, b) {
        return a - b
    })
}

var arr = [1, 3, 2]
sortDesc(arr) // [1, 2, 3]
arr // [1, 2, 3]

重要的概念

1、 柯里化 curry

function currying(fn, ...args) {
    if (args.length >= fn.length) {
        return fn(...args)
    }
    return function (...args2) {
        return currying(fn, ...args, ...args2)
    }
}

2、组合函数 compose

const compose = (a, b)=>(c)=>a(b(c))

3、部分应用 partial

function partial(fn, ...args) {
    return fn.bind(null, ...args)
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章