上篇文章 主要整理了一些vue 的面试题 ,但是那些还远远不够 这篇文章继续整理前端常被面试官问到的问题:
一:什么是BFC?什么时候触发BFC?
BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有Block-level box参与, 它规定了内部的Block-level Box如何布局,并且与这个区域外部毫不相干。
** BFC布局规则:**
内部的Box会在垂直方向,一个接一个地放置。
Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠
每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
BFC的区域不会与float box重叠。
BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
计算BFC的高度时,浮动元素也参与计算
哪些元素会生成BFC:
1.根元素
2.float属性不为none
3.position为absolute或fixed
4.display为inline-block, table-cell, table-caption, flex, inline-flex
5.overflow不为visible
二:在地址栏输入网址敲回车发生了什么?
总的来说分为以下几个过程:
1、DNS解析:将域名解析为IP地址;根据IP地址找到对应的服务器;
2、TCP连接:TCP三次握手;
3、发生HTTP请求;
4、服务器处理请求并返回HTTP报文;
5、浏览器解析渲染页面;
6、断开连接:TCP四次挥手;
TCP三次握手的过程:
TCP就是通信协议
1、第一次握手由浏览器发起,告诉服务器我要发送请求;
2、第二次握手由服务器发起,告诉浏览器我准备接收了,你发送吧;
3、第三次握手由浏览器发起,告诉服务器,我马上发送,准备接收;
为什么要发起三次握手:
为了防止已失效的连接请求报文段突然又传送到服务端,因而产生错误
在TCP三次握手结束后,开始发送HTTP请求报文
请求报文由请求行、请求头、请求体三部分组成:
1.请求行包含请求方法、URL、协议版本
请求方法包含 8 种:
GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、TRACE。
URL 即请求地址,由 <协议>://<主机>:<端口>/<路径>?<参数> 组成
协议版本即 http 版本号
2.请求头包含请求的附加信息,由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。
请求头部通知服务器有关于客户端请求的信息。它包含许多有关的客户端环境和请求正文的有用信息。其中比如:Host,表示主机名,虚拟主机;Connection,HTTP/1.1 增加的,使用 keepalive,即持久连接,一个连接可以发多个请求;User-Agent,请求发出者,兼容性以及定制化需求。
3.请求体,可以承载多个请求参数的数据,包含回车符、换行符和请求数据,并不是所有请求都具有请求数据。
断开连接:TCP四次挥手;
当数据传送完毕,需要断开 TCP连接,此时发起 TCP 四次挥手。
1、第一次挥手:由浏览器发起,发送给服务器,我请求报文发送完了,你准备关闭吧;
2、第二次挥手:由服务器发起,告诉浏览器,我接收完请求报文,我准备关闭,你也准备吧;
3、第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完毕,你准备关闭吧;
4、第四次挥手:由浏览器发起,告诉服务器,我响应报文接收完毕,我准备关闭,你也准备吧;
大概就是这样吧
三:什么是重绘什么是重流
重流: reflow, 重绘: repaint
重流必定导致重绘, 重绘不一定重流
布局改变,浏览器的尺寸,字体大小,元素的尺寸,元素的位置变化,CSS伪类激活,DOM操作会发生重流, 元素颜色等改变只会发生重绘
**下面是通过减少重流/重绘次数而优化页面性能的一些手段: **
减少js中的dom操作, 若必须, 则尽量将读取dom和写入dom的操作放在一起, 便于浏览器累积dom变动, 一次执行, 减少重绘.
缓存dom信息, 多次使用, 不要重复读取dom节点.
不要一项一项地改变样式, 尽量用 css class一次性改变样式.
使用documentFragment操作DOM
动画使用absolute或fixed定位. 不然重流会很严重.
只有在必要时才显示或隐藏元素, 这个操作必定会导致重流
使用window.requestAnimationFrame()可以把代码推迟到下一次重流时执行.
使用Virtual DOM(如 React / Vue)
四:函数防抖和函数节流
概念
函数防抖
当调用动作过n毫秒后,才会执行该动作,若在这n毫秒内又调用此动作则将重新计算执行时间
函数节流
预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期
函数节流(throttle)与 函数防抖(debounce)都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。
比如如下的情况:
window对象的resize、scroll事件
拖拽时的mousemove事件
文字输入、自动完成的keyup事件
**区别举个列子 **
函数防抖:如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。
函数节流 :保证如果电梯第一个人进来后,10秒后准时运送一次,这个时间从第一个人上电梯开始计时,不等待,如果没有人,则不运行
五:Vue中assets和static的区别
相同点:
assets和static两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点:
assets中存放的静态资源文件在项目打包时,也就是运行npm run build时会将assets中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html一同上传至服务器
static中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是static中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于assets中打包后的文件提交较大点。在服务器中就会占据更大的空间。所以简单点使用建议如下:
将项目中template需要的样式文件js文件等都可以放置在assets中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在static中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。
当然具体情况,具体分析,在不同的开发环境,不同的需求下,大家应针对不同具体情况采用合适方式。对两者的理解就简单总结这些。记录这些,只为记录自己的开发点击,望对大家有帮助。
六:computed与method的区别
上篇文章说了 computed与watch的区别 这里在补充一下computed与method的区别
computed是属性调用,而methods是函数调用
computed带有缓存功能,而methods不是
六:mvc,mvp和mvvm介绍及三大框架对比
上篇文章详细介绍了 MVVM 这里补充下mvc,mvp和mvvm的区别
mvc
View 传送指令到 Controller
Controller 完成业务逻辑后,要求 Model 改变状态
Model 将新的数据发送到 View,用户得到反馈
所有通信都是单向的。
mvp
MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。
各部分之间的通信,都是双向的。
View 与 Model 不发生联系,都通过 Presenter 传递。
View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
mvvm
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。
唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。
这里面使用的设计模式有:观察者模式(发布订阅模式)、代理模式、工厂模式、单例模式。
七:SPA路由为history 刷新404 为什么 ?如何解决
原因:当路由模式为history的时候,服务器端会根据浏览器中的请求地址去匹配资源,此时服务器端没有对应的接口地址,因此返回404
解决办法:第一种:使用connect-history-api-fallback中间件
安装connect-history-api-fallback中间件
npm install --save connect-history-api-fallback
在app.js文件中增加以下代码:
const history = require('connect-history-api-fallback')
app.use('/', history());
重新启动服务
第二种方法:Internet Information Services (IIS)
1.安装 IIS UrlRewrite
在你的网站根目录中创建一个 web.config 文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Handle History Mode and custom 404/500" stopProcessing="true">
<match url="(.*)" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="/" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
八:手写promise封装ajax请求
上代码:
// 使用promise实现一个简单的ajax
/**
* 首先,可能会使用到的xhr方法或者说属性
* onloadstart // 开始发送时触发
* onloadend // 发送结束时触发,无论成功不成功
* onload // 得到响应
* onprogress // 从服务器上下载数据,每50ms触发一次
* onuploadprogress // 上传到服务器的回调
* onerror // 请求错误时触发
* onabort // 调用abort时候触发
* status // 返回状态码
* setRequestHeader // 设置请求头
* responseType // 请求传入的数据
*/
// 默认的ajax参数
let ajaxDefaultOptions = {
url: '#', // 请求地址,默认为空
method: 'GET', // 请求方式,默认为GET请求
async: true, // 请求同步还是异步,默认异步
timeout: 0, // 请求的超时时间
dataType: 'text', // 请求的数据格式,默认为text
data: null, // 请求的参数,默认为空
headers: {
}, // 请求头,默认为空
onprogress: function () {
}, // 从服务器下载数据的回调
onuploadprogress: function () {
}, // 处理上传文件到服务器的回调
xhr: null // 允许函数外部创建xhr传入,但是必须不能是使用过的
};
function _ajax(paramOptions) {
let options = {
};
for (const key in ajaxDefaultOptions) {
options[key] = ajaxDefaultOptions[key];
}
// 如果传入的是否异步与默认值相同,就使用默认值,否则使用传入的参数
options.async = paramOptions.async === ajaxDefaultOptions.async ? ajaxDefaultOptions.async : paramOptions.async;
// 判断传入的method是否为GET或者POST,否则传入GET 或者可将判断写在promise内部,reject出去
options.method = paramOptions.method ? ("GET" || "POST") : "GET";
// 如果外部传入xhr,否则创建一个
let xhr = options.xhr || new XMLHttpRequest();
// return promise对象
return new Promise(function (resolve, reject) {
xhr.open(options.method, options.url, options.async);
xhr.timeout = options.timeout;
// 设置请求头
for (const key in options.headers) {
xhr.setRequestHeader(key, options.headers[key]);
}
// 注册xhr对象事件
xhr.responseType = options.dataType;
xhr.onprogress = options.onprogress;
xhr.onuploadprogress = options.onuploadprogress;
// 开始注册事件
// 请求成功
xhr.onloadend = function () {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
resolve(xhr);
} else {
reject({
errorType: "status_error",
xhr: xhr
});
}
};
// 请求超时
xhr.ontimeout = function () {
reject({
errorType: "timeout_error",
xhr: xhr
});
}
// 请求错误
xhr.onerror = function () {
reject({
errorType: "onerror",
xhr: xhr
});
}
// abort错误(未明白,只知道是三种异常中的一种)
xhr.onabort = function () {
reject({
errorType: "onabort",
xhr: xhr
});
}
// 捕获异常
try {
xhr.send(options.data);
} catch (error) {
reject({
errorType: "send_error",
error: error
});
}
});
}
// 调用示例
_ajax({
url: 'http://localhost:3000/suc',
async: true,
onprogress: function (evt) {
console.log(evt.position / evt.total);
},
dataType: 'text/json'
}).then(
function (xhr) {
console.log(xhr.response);
},
function (e) {
console.log(JSON.stringify(e))
});
九:什么是事件委托 为什么要用事件委托
事件委托
事件委托就是利用事件冒泡机制指定一个事件处理程序,来管理某一类型的所有事件。
即:利用冒泡的原理,把事件加到父级上,触发执行效果。
好处:
只在内存中开辟了一块空间,节省资源同时减少了dom操作,提高性能
对于新添加的元素也会有之前的事件
为什么要用事件委托:
一般来说,dom需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的dom需要添加事件处理呢?比如我们有100个li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的li,然后给它们添加事件,那这么做会存在什么影响呢?
在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;
每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够用,是硬伤,
举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。
原生js的 window.onload与jq的$(document).ready(function(){})的区别
1.执行时间 window.onload必须等到页面内包括图片的所有元素加载完毕后才能执行。 $(document).ready()是 DOM 结构绘制完毕后就执行,不必等到加载完毕。
2.编写个数不同 window.onload不能同时编写多个,如果有多个 window.onload 方法,只会执 行一个 $(document).ready()可以同时编写多个,并且都可以得到执行
3.简化写法 window.onload没有简化写法 ( d o c u m e n t ) . r e a d y ( f u n c t i o n ( ) ) 可 以 简 写 成 (document).ready(function(){})可以简写成 (document).ready(function())可以简写成(function(){});
十:positon有几种取值,分别是什么?
static:静态定位,是position属性的默认值,表示无论怎么设置top、bottom、right、left属性元素的位置(与外部位置)都不会发生改变。
relative:相对定位,表示用top、bottom、right、left属性可以设置元素相对与其相对于初始位置的相对位置。
absolute:绝对定位,表示用top、bottom、right、left属性可以设置元素相对于其父元素(除了设置了static的父元素以外)左上角的位置,如果父元素设置了static,子元素会继续追溯到祖辈元素一直到body。
fixed:绝对定位,相对于浏览器窗口进行定位,同样是使用top、bottom、right、left。
四种取值中,除了static之外,其他属性都可通过z-index进行层次分级
十一:px,em,rem的区别
PX
px像素(Pixel)。相对长度单位。像素px是相对于显示器屏幕分辨率而言的。
PX特点
EM特点
clear : none | left | right | both
2、增加一个清除浮动的子元素
3、用:after 伪元素
4、父元素设置 overflow:hidden
5、父元素也设成 float
6、父元素设置 display:table。
第一种方法只适合相邻浮动元素清除浮动,后面三种是触发了 BFC,推荐使用第三种方法。
十一:css readonly和disabled的区别
disabled属性阻止对元素的一切操作,例如获取焦点,点击事件等等。disabled属性可以让表单元素的值无法被提交。
readonly属性只是将元素设置为只读,其他操作正常。readonly属性则不影响提交问题。
这个需要进行测试。。。
readonly 属性规定输入字段为只读。
只读字段是不能修改的。不过,用户仍然可以使用 tab 键切换到该字段,还可以选中或拷贝其文本。
readonly 属性可以防止用户对值进行修改,直到满足某些条件为止(比如选中了一个复选框)。然后,需要使用 JavaScript 消除 readonly 值,将输入字段切换到可编辑状态。
readonly 属性可与 或 配合使用。
disabled 属性规定应该禁用 input 元素。
被禁用的 input 元素既不可用,也不可点击。可以设置 disabled 属性,直到满足某些其他的条件为止(比如选择了一个复选框等等)。然后,就需要通过 JavaScript 来删除 disabled 值,将 input 元素的值切换为可用。
disabled 属性无法与 一起使用。
十二:css优先级算法如何计算?
!important 特殊性最高
1、ID #id
2、class .class
3、标签 p
4、通用 *
5、属性 [type=“text”]
6、伪类 :hover
7、伪元素 ::first-line
8、子选择器、相邻选择器
十三:手写数组去重,多种方法
一、利用ES6中的 Set 方法去重
let arr = [1,0,0,2,9,8,3,1];
2 function unique(arr) {
3 return Array.from(new Set(arr))
4 }
5 console.log(unique(arr)); // [1,0,2,9,8,3]
6 console.log(...new Set(arr)); // [1,0,2,9,8,3]
二、使用双重for循环,再利用数组的splice方法去重(ES5常用)
var arr = [1, 5, 6, 0, 7, 3, 0, 5, 9,5,5];
function unique(arr) {
for (var i = 0, len = arr.length; i < len; i++) {
for (var j = i + 1, len = arr.length; j < len; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
j--; // 每删除一个数j的值就减1
len--; // j值减小时len也要相应减1(减少循环次数,节省性能)
// console.log(j,len)
}
}
}
return arr;
}
console.log(unique(arr)); // 1, 5, 6, 0, 7, 3, 9
三、利用数组的indexOf方法去重
注:array.indexOf(item,statt) 返回数组中某个指定的元素的位置,没有则返回-1
var arr =[1,-5,-4,0,-4,7,7,3];
2 function unique(arr){
3 var arr1 = []; // 新建一个数组来存放arr中的值
4 for(var i=0,len=arr.length;i<len;i++){
5 if(arr1.indexOf(arr[i]) === -1){
6 arr1.push(arr[i]);
7 }
8 }
9 return arr1;
10 }
11 console.log(unique(arr)); // 1, -5, -4, 0, 7, 3
四、利用数组的sort方法去重(相邻元素对比法)
注:array.sort( function ) 参数必须是函数,可选,默认升序
var arr = [5,7,1,8,1,8,3,4,9,7];
function unique( arr ){
arr = arr.sort();
console.log(arr);
var arr1 = [arr[0]];
for(var i=1,len=arr.length;i<len;i++){
if(arr[i] !== arr[i-1]){
arr1.push(arr[i]);
}
}
return arr1;
}
console.log(unique(arr))l; // 1, 1, 3, 4, 5, 7, 7, 8, 8, 9
五、利用数组的includes去重
var arr = [-1,0,8,-3,-1,5,5,7];
2 function unique( arr ){
3 var arr1 = [];
4 for(var i=0,len=arr.length;i<len;i++){
5 if( !arr1.includes( arr[i] ) ){
// 检索arr1中是否含有arr中的值
6 arr1.push(arr[i]);
7 }
8 }
9 return arr1;
10 }
11 console.log(unique(arr)); // -1, 0, 8, -3, 5, 7
十四:实现一个clone函数
实现一个函数clone,可以对JavaScript中的5种主要的数据类型(包括Number、String、Object、Array、Boolean)进行值复制
方法一
function clone(obj){
var o;
switch(typeof obj){
case 'undefined': break;
case 'string' : o = obj + '';break;
case 'number' : o = obj - 0;break;
case 'boolean' : o = obj;break;
case 'object' :
if(obj === null){
o = null;
}else{
if(obj instanceof Array){
o = [];
for(var i = 0, len = obj.length; i < len; i++){
o.push(clone(obj[i]));
}
}else{
o = {
};
for(var k in obj){
o[k] = clone(obj[k]);
}
}
}
break;
default:
o = obj;break;
}
return o;
}
方法二:
function clone2(obj){
var o, obj;
if (obj.constructor == Object){
o = new obj.constructor();
}else{
o = new obj.constructor(obj.valueOf());
}
for(var key in obj){
if ( o[key] != obj[key] ){
if ( typeof(obj[key]) == 'object' ){
o[key] = clone2(obj[key]);
}else{
o[key] = obj[key];
}
}
}
o.toString = obj.toString;
o.valueOf = obj.valueOf;
return o;
}
方法三:
function clone3(obj){
function Clone(){
}
Clone.prototype = obj;
var o = new Clone();
for(var a in o){
if(typeof o[a] == "object") {
o[a] = clone3(o[a]);
}
}
return o;
}
十五:浏览器是如何渲染页面的:
1.处理HTML标记并构建DOM树
2.处理CSS标记并构建CSSOM树
3.将DOM与CSSOM合并成一个渲染树
4.根据渲染树来布局,计算每个节点的布局信息
5.将各个节点绘制到屏幕上
渲染树构建、布局及绘制
CSSOM树和DOM树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。
构建渲染树的步骤:
从DOM树的根节点开始遍历每个可见节点(display:none与visibility:hidden的区别)
对于每个可见节点,为其找到适配的CSSOM规则并应用它
1.生成渲染树
2.布局阶段:输出盒模型
3.绘制:输出到屏幕上的像素
CSS阻塞渲染
CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
这就是为什么我们将外部样式的引入放在head标签中的原因,在body渲染前先把相对完整CSSOM Tree构建好。
对于某些CSS样式只在特定条件下叉用,添加媒体查询解决。
请注意“阻塞渲染”仅是指浏览器是否需要暂停网页的首次渲染,直至该资源准备就绪。无论哪一种情况,浏览器仍会下载 CSS 资产,只不过不阻塞渲染的资源优先级较低罢了。
JavaScript阻塞渲染
JavaScript 会阻止 DOM 构建和延缓网页渲染。 为了实现最佳性能,可以让您的 JavaScript 异步执行,并去除关键渲染路径中任何不必要的 JavaScript。
JavaScript 可以查询和修改 DOM 与 CSSOM。
JavaScript 执行会阻止 CSSOM。
除非将 JavaScript 显式声明为异步,否则它会阻止构建 DOM。
如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。
简言之,JavaScript 在 DOM、CSSOM 和 JavaScript 执行之间引入了大量新的依赖关系,从而可能导致浏览器在处理以及在屏幕上渲染网页时出现大幅延迟:
脚本在文档中的位置很重要
当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
JavaScript 可以查询和修改 DOM 与 CSSOM。
JavaScript 执行将暂停,直至 CSSOM 就绪。
async属性:加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。无顺序
defer属性: 加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。按顺序
十六:call ,apply,bind方法的作用分别是什么?
每个函数都包含两个非继承而来的方法: apply()和call(), 这两个方法的用途就是在特定的作用域中调用函数,实际上就是设置函数体内this对象的值。
apply()
apply()接受两个参数,
第一个就是指定运行函数的作用域(就是this的指向)
第二个是参数数组,
Array实例
arguments对象
function sum(sum1, sum2) {
return sum1 + sum2
}
function sumApply1(sum1, sum2) {
return sum.apply(this, arguments) // 传如arguments对象
}
function sumApply2(sum1, sum2) {
return sum.apply(this, [sum1, sum2]) // 传入数组
}
console.log(sumApply1(10,20)) // => 30
console.log(sumApply2(10,20)) // => 30
上面的例子, sumApply1()执行sum()的时候传入了this作为this值(因为是在全局作用域中调用的, 所以this指向window对象)和arguments对象, 而sumApply2()传入了this和一个参数数组, 返回的结果是相同的;
call()
call() 方法和apply() 方法作用相同, 区别在于接收参数的方式不同, call() 需要列举所有传入的所有参数
function sum(sum1, sum2) {
return sum1 + sum2
}
function sumCall1(sum1, sum2) {
return sum.call(this, sum1, sum2)
}
console.log(sumCall1(10,20)); // => 30
apply()和call() 的真正强大的地方是能扩充作用域
var color = 'red';
var o = {
color: 'blue'
}
function sayColor() {
console.log(this.color);
}
sayColor.call(this) // => red
sayColor.call(window) // => red
sayColor.call(o); // => blue
定义一个全局函数sayColor(), 一个全局变量color='red’和一个对象o, 第一个sayColor.claa(this)由于是在全局作用域调用的, 所以this指向window, this.color就转换成了window.color 所以就是red, 第二个同第一个, sayColor.call(o)把执行环境改成了o, 因此函数内的this就指向了对象o, 所以结果是blue;
使用call()或apply()扩充作用域最大的好处厹, 对象不需要与方法有任何耦合关系,
bind()
这个方法会创建一个函数实例, 其this的值指向传给bind()函数的值
window.color = 'red'
var o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
var bindSayColor = sayColor.bind(o);
bindSayColor() // => blue
这里sayColor()调用了bind()并传入了参数o, 创建了bindSayColor函数, bindSayColor() 的this就指向了o, 因此在全局作用域中调用这个函数, 也会指向o
十七:线程和进程的区别
首先来一句概括的总论:进程和线程都是一个时间段的描述,是CPU工作时间段的描述。
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
十八:eval是做什么的
eval()的作用
把字符串参数解析成JS代码并运行,并返回执行的结果;
例如:
eval("2+3");//执行加运算,并返回运算值。
eval("varage=10");//声明一个age变量
eval的作用域:
functiona(){
eval("var x=1"); //等效于 var x=1;
console.log(x); //输出1
}
a();
console.log(x);//错误 x没有定义
说明作用域在它所有的范围内容有效
注意事项
应该避免使用eval,不安全,非常耗性能(2次,一次解析成js语句,一次执行)。
其它作用
由JSON字符串转换为JSON对象的时候可以用eval,例如:
varjson="{name:'Mr.CAO',age:30}";
varjsonObj=eval("("+json+")");
console.log(jsonObj);
十九:那些操作会造成内存泄漏
1)意外的全局变量引起的内存泄露
function leak(){
leak="xxx";//leak成为一个全局变量,不会被回收
}
2)闭包引起的内存泄露
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
//Even if it's a empty function
}
}
闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时,由于是函数内定义函数,并且内部函数–事件回调的引用外暴了,形成了闭包。
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
//将事件处理函数定义在外部
function onclickHandler(){
//do something
}
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=onclickHandler;
}
//在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent(){
var obj=document.createElement("XXX");
obj.οnclick=function(){
//Even if it's a empty function
}
obj=null;
}
3)没有清理的DOM元素引用
var elements={
button: document.getElementById("button"),
image: document.getElementById("image"),
text: document.getElementById("text")
};
function doStuff(){
image.src="http://some.url/image";
button.click():
console.log(text.innerHTML)
}
function removeButton(){
document.body.removeChild(document.getElementById('button'))
}
4)被遗忘的定时器或者回调
var someResouce=getData();
setInterval(function(){
var node=document.getElementById('Node');
if(node){
node.innerHTML=JSON.stringify(someResouce)
}
},1000)
这样的代码很常见, 如果 id 为 Node 的元素从 DOM 中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对 someResource 的引用, 定时器外面的 someResource 也不会被释放。
5)IE7/8引用计数使用循环引用产生的问题
function fn(){
var a={
};
var b={
};
a.pro=b;
b.pro=a;
}
fn();
fn()执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为a和b的引用次数不为0,所以不会被垃圾回收器回收内存,如果fn函数被大量调用,就会造成内存泄漏。在IE7与IE8上,内存直线上升。
IE中有一部分对象并不是原生js对象。例如,其内存泄漏DOM和BOM中的对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的js引擎采用标记清除策略来实现,但js访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。
怎样避免内存泄露
1)减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
2)注意程序逻辑,避免“死循环”之类的 ;
3)避免创建过多的对象 原则:不用了的东西要及时归还。
二十:什么是函数柯里化及使用场景
定义
柯里化指的是从一个多参数函数变成一连串单参数函数的变换。它描述的是变换的过程,不涉及变换之后对函数的调用。调用者可以决定对多少个参数实施变换,余下的部分将衍生为一个参数数目较少的新函数。这个新的函数接收剩下的参数,其内部则指向原始函数。当提供的参数完整了才会最终执行原始函数。
语法
//普通函数定义
func mutiply(x:Int,y:Int)->Int{
return x*y
}
//柯里化形式
func mutiply(x:Int)(y:Int)->Int{
return x*y
}
使用如下:
let twice=mutiply(2)
let result=twice(y: 5) //result等于10
//如果直接在一行里调用就这样写
let result2=mutiply(2)(y: 6)
例子里的twice的类型是一个闭包,可以粗暴的理解为mutiply的两个参数第一个参数x已经有了个默认值2,twice的参数就是剩下的另一个参数y。
两个细节
只有一个参数,并且这个参数是该函数的第一个参数。必须按照参数的定义顺序来调用柯里化函数。
柯里化函数的函数体只会执行一次,只会在调用完最后一个参数的时候执行柯里化函数体
js单线程和浏览器多线程
js单线程
js运作在浏览器中,是单线程的,js代码始终在一个线程上执行,此线程被称为js引擎线程。
ps:web worker也只是允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。
但是如果单线程,任务都需要排队。排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
浏览器多线程
1.js引擎线程(js引擎有多个线程,一个主线程,其它的后台配合主线程)
作用:执行js任务(执行js代码,用户输入,网络请求)
2.ui渲染线程
作用:渲染页面(js可以操作dom,影响渲染,所以js引擎线程和UI线程是互斥的。js执行时会阻塞页面的渲染。)
3.浏览器事件触发线程
作用:控制交互,响应用户
4.http请求线程
作用:ajax请求等
5.定时触发器线程
作用:setTimeout和setInteval
6.事件轮询处理线程
作用:轮询消息队列,event loop
所以异步是浏览器的两个或者两个以上线程共同完成的。比如ajax异步请求和setTimeout
同步任务和异步任务
同步任务:在主线程排队支持的任务,前一个任务执行完毕后,执行后一个任务,形成一个执行栈,线程执行时在内存形成的空间为栈,进程形成堆结构,这是内存的结构。执行栈可以实现函数的层层调用。注意不要理解成同步代码进入栈中,按栈的出栈顺序来执行。
异步任务会被主线程挂起,不会进入主线程,而是进入消息队列,而且必须指定回调函数,只有消息队列通知主线程,并且执行栈为空时,该消息对应的任务才会进入执行栈获得执行的机会。
主线程执行的说明: 【js的运行机制】
(1)所有同步任务都在主线程上执行,形成一个执行栈。
(2)主线程之外,还存在一个”任务队列”。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
二十一:微任务与宏任务
首先,JavaScript是一个单线程的脚本语言。
所以就是说在一行代码执行的过程中,必然不会存在同时执行的另一行代码,就像使用alert()以后进行疯狂console.log,如果没有关闭弹框,控制台是不会显示出一条log信息的。
亦或者有些代码执行了大量计算,比方说在前端暴力破解密码之类的鬼操作,这就会导致后续代码一直在等待,页面处于假死状态,因为前边的代码并没有执行完。
所以如果全部代码都是同步执行的,这会引发很严重的问题,比方说我们要从远端获取一些数据,难道要一直循环代码去判断是否拿到了返回结果么?就像去饭店点餐,肯定不能说点完了以后就去后厨催着人炒菜的,会被揍的。
于是就有了异步事件的概念,注册一个回调函数,比如说发一个网络请求,我们告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。
然后在异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,可以去执行。
比如说打了个车,如果司机先到了,但是你手头还有点儿事情要处理,这时司机是不可能自己先开着车走的,一定要等到你处理完事情上了车才能走。
微任务与宏任务的区别
这个就像去银行办业务一样,先要取号进行排号。
一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。
因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。
所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。
任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中,就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号
而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。
所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。
也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币?
无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。
这就说明:你大爷永远是你大爷
在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
所以就有了那个经常在面试题、各种博客中的代码片段:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。
在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
所以就得到了上述的输出结论1、2、3、4。
+部分表示同步执行的代码
+setTimeout(_ => {
- console.log(4)
+})
+new Promise(resolve => {
+ resolve()
+ console.log(1)
+}).then(_ => {
- console.log(3)
+})
+console.log(2)
本来setTimeout已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise的处理(临时添加业务)。
所以进阶的,即便我们继续在Promise中实例化Promise,其输出依然会早于setTimeout的宏任务:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
当然了,实际情况下很少会有简单的这么调用Promise的,一般都会在里边有其他的异步操作,比如fetch、fs.readFile之类的操作。
而这些其实就相当于注册了一个宏任务,而非是微任务。
二十二:Event-Loop是个啥
上边一直在讨论 宏任务、微任务,各种任务的执行。
但是回到现实,JavaScript是一个单进程的语言,同一时间不能处理多个任务,所以何时执行宏任务,何时执行微任务?我们需要有这样的一个判断逻辑存在。
每办理完一个业务,柜员就会问当前的客户,是否还有其他需要办理的业务。(检查还有没有微任务需要处理)
而客户明确告知说没有事情以后,柜员就去查看后边还有没有等着办理业务的人。(结束本次宏任务、检查还有没有宏任务需要处理)
这个检查的过程是持续进行的,每完成一个任务都会进行一次,而这样的操作就被称为Event Loop。(这是个非常简易的描述了,实际上会复杂很多)
而且就如同上边所说的,一个柜员同一时间只能处理一件事情,即便这些事情是一个客户所提出的,所以可以认为微任务也存在一个队列,大致是这样的一个逻辑:
const macroTaskList = [
['task1'],
['task2', 'task3'],
['task4'],
]
for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) {
const microTaskList = macroTaskList[macroIndex]
for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) {
const microTask = microTaskList[microIndex]
// 添加一个微任务
if (microIndex === 1) microTaskList.push('special micro task')
// 执行任务
console.log(microTask)
}
// 添加一个宏任务
if (macroIndex === 2) macroTaskList.push(['special macro task'])
}
// > task1
// > task2
// > task3
// > special micro task
// > task4
// > special macro task
之所以使用两个for循环来表示,是因为在循环内部可以很方便的进行push之类的操作(添加一些任务),从而使迭代的次数动态的增加。
以及还要明确的是,Event Loop只是负责告诉你该执行那些任务,或者说哪些回调被触发了,真正的逻辑还是在进程中执行的。
**二十三:移动端1px问题 **
原因
由于不同的手机有不同的像素密度导致的。如果移动显示屏的分辨率始终是普通屏幕的2倍,1px的边框在devicePixelRatio=2的移动显示屏下会显示成2px,所以在高清瓶下看着1px总是感觉变胖了
方案一:
在ios8+中当devicePixelRatio=2的时候使用0.5px
p{
border:1px solid #000;
}
@media (-webkit-min-device-pixel-ratio: 2) {
p{
border:0.5px solid #000;
}
}
二,伪类 + transform 实现:
对于老项目伪类+transform是比较完美的方法了。
原理是把原先元素的 border 去掉,然后利用 :before 或者 :after 重做 border ,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位。
单条border样式设置:
.scale-1px{
position: relative; border:none; }
.scale-1px:after{
content: '';
position: absolute; bottom: 0;
background: #000;
width: 100%; height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
优点:所有场景都能满足,支持圆角(伪类和本体类都需要加border-radius)
缺点:对于已经使用伪类的元素(例如clearfix),可能需要多层嵌套
三,viewport + rem 实现:
这种兼容方案相对比较完美,适合新的项目,老的项目修改成本过大。
在devicePixelRatio = 2 时,输出viewport:
在devicePixelRatio = 3 时,输出viewport:
优点:所有场景都能满足,一套代码,可以兼容基本所有布局
缺点:老项目修改代价过大,只适用于新项目
四,使用box-shadow模拟边框:
利用css 对阴影处理的方式实现0.5px的效果
样式设置:
box-shadow-1px {
box-shadow: inset 0px -1px 1px -1px #c8c7cc;
}
优点:代码量少,可以满足所有场景
缺点:边框有阴影,颜色变浅
五:使用css3的媒体查询+transform的scale
多边框实现,目前较为常见的解决方案
把需要使用到的边框全部提前定义好,使用时只需要写对应的class
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>边框1px</title>
<meta name="viewport" content="width=device-width,initial-scale=1.0,maixmum-scale=1.0,user-scalable=no">
<style>
*{
margin:0;
padding:0;
}
html,body{
width:100%;
height:100%;
}
.box{
width: 200px;
height: 200px;
background: #ccc;
margin-left: 10px;
margin-top: 10px;
margin-bottom: 20px;
}
.border,.border-left,.border-right,.border-top,.border-bottom{
position: relative;
border:none;
}
.border:after{
content: '';
position: absolute;
top: 0;
left: 0;
border: 1px solid #f00;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.border-left:after{
content: '';
position: absolute;
top: 0;
left: 0;
border-left: 1px solid #f00;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.border-right:after{
content: '';
position: absolute;
top: 0;
left: 0;
border-right: 1px solid #f00;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.border-top:after{
content: '';
position: absolute;
top: 0;
left: 0;
border-top: 1px solid #f00;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.border-bottom:after{
content: '';
position: absolute;
top: 0;
left: 0;
border-bottom: 1px solid #f00;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
height: 100%;
}
@media only screen and (-webkit-device-pixel-ratio:2){
.border:after{
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-left:after{
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-right:after{
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-top:after{
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-bottom:after{
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}
}
@media only screen and (-webkit-device-pixel-ratio:3){
.border:after{
width: 300%;
height: 300%;
-webkit-transform: scale(0.33333);
transform: scale(0.33333);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-left:after{
width: 300%;
height: 300%;
-webkit-transform: scale(0.33333);
transform: scale(0.33333);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-right:after{
width: 300%;
height: 300%;
-webkit-transform: scale(0.33333);
transform: scale(0.33333);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-top:after{
width: 300%;
height: 300%;
-webkit-transform: scale(0.33333);
transform: scale(0.33333);
-webkit-transform-origin: left top;
transform-origin: left top;
}
.border-bottom:after{
width: 300%;
height: 300%;
-webkit-transform: scale(0.33333);
transform: scale(0.33333);
-webkit-transform-origin: left top;
transform-origin: left top;
}
}
</style>
</head>
<body>
<div class="box border">div</div>
<div class="box border-left">div</div>
<div class="box border-right">div</div>
<div class="box border-bottom">div</div>
<!-- 单线 -->
<div class="border-bottom" style="width:100%;height: 1px;"></div>
</body>
</html>
二十四:JS中typeof与instanceof的区别
typeof
typeof 是判断参数是什么类型的实例,返回值为说明运算数类型的字符串。
返回值结果:“number”、“string”、“boolean”、“object”、“function”、“undefined”
若参数为引用类型,始终返回“object”,对于Array、null始终返回“object”,所以用typeof来判断参数类型有很大的局限性。
instanceof
instanceof是用来判断一个对象在其原型链中是否存在一个构造函数的prototype属性
a instanceof b:判断a是否为b的实例,可以用于继承关系中
b是c的父对象,a是c的实例,a instanceof b 与 a instanceof c 结果均为true
对于所有的引用类型,均为Object的实例
二十五:Cookie、session和token的区别
Cookie的内容是保存一小段文本信息,这些文本信息组成一份通行证。它是客户端对于无状态协议的一种解决方案。
Cookie的原理
(1)客户端第一次请求时,发送数据到服务器。
(2)服务器返回响应信息的同时,还会传回一个cookie(cookie S-001)
(3)客户端接收服务器的响应之后,浏览器会将cookie存放在一个统一的位置。
(4)客户端再次向服务器发送请求的时候,会把Cookie S-001再次发挥服务器。
cookie的生命周期
cookoe的生存时间是整个会话期间:浏览器会将cookie保存在内存中,浏览器关闭时自动删除这个cookie
cookie的生存时间是长久有效的:手动将cookie报存在客户端的硬盘中,浏览器关闭的话,cookie页不会清除;下次在打开浏览器访问对应网站内容,这个cookie就会自动再次发送到服务器。
session的原理:
(1)服务器在处理客户端请求过程中会创建session,并且为该session生存唯一的session ID。(这个session ID在随后的请求中会被用来重新获得已经创建的session。在session被创建后,就可以调用session相关的方法向session中新增内容,这些内容只会保存在服务器中)
(2)服务器将session ID发送到客户端
(3)当客户端再次请求时,就会带上这个session ID
(4)服务器接收到请求之后就会一句Session ID 找到相应的Session ,完成请求
ps:1、虽然session保存在服务器,但它还是需要客户端浏览器的支持,因为session需要使用cookie作为识别标志。服务器会向客户端发送一个名为JSEDDIONID的cookie,它的值为session ID。
2、当cookie被禁用时,可以使用url重写的方法:将session写在URL中,服务器在进行解析
**cookie和session的区别**
1、存储位置不同:session存储在服务器,cookie存储在客户端
2、存储容量不同:单个cookie保存数据小于等于4kb,一个站点最多保存20个cookie;session没有上限,但是由于服务器内存性能考虑,session不要存太多东西,并有删除机制
3、存取方式不同:cookie只能保存ASCII字符串;session能存取任何类型的数据
4、隐私策略不同:cookie是对客户端是可见的,可以分析存放在本地的cookie并进去cookie欺骗;session存储在服务器上,对于客户端是透明的,不存在敏感信息泄露的风险
5、服务器压力不同:session是保存在服务端,每隔用户都会产生一个session。加入并发访问的用户太多,会产生很多的session,对服务器是一个很大的负担,耗费大量内存cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,session是一个很好的选择。
6、浏览器的支持不同:session不支持新建窗口,只支持字窗口。而cookie都支持。 假设浏览器禁用cookie,session可以通过URL重写的方法实现。COOKIE就派不上用场。
Token的原理:
(1)客户端第一次请求时,发送用户信息到服务器。服务器对用户信息使用HSA256算法及密钥进行签名,再将这个签名和数据一起作为token返回给客户户端。
(2)服务端不再保存token,客户端保存token。
(3)当客户端再次发送请求时,在请求信息中将token一起发送给服务器。
(4)服务器用同样的HSA256算法和密钥,对数据再计算一次签名,和token的签名做比较
(5)如果相同,服务器就知道客户端登录过,则反之。
二十六:详述后台管理系统权限如何实现的?
然后服务端获取到这个Token后进行比对,如果通过则可以访问。
现有的做法是在登录成功的回调中将后台返回的Token直接存储到sessionStorage,然在请求时将Token取出放入headers中传给后台,代码如下:
this.$http({
method: 'get',
url: 'test/query?id=20',
withCredentials: true,
headers: {
token: sessionStorage.getItem('token'),
name: sessionStorage.getItem('name') //应后台需求传的用户名
}
}).then(response => {
//请求成功后的操作
})
后来在一些文章中发现axios可以在拦截器中直接将Token塞入config.headers.Authorization中,作为全局传入。下面是代码部分:
//main.js
import axios from 'axios'
// 实例化Axios,并进行超时设置
const service = axios.create({
timeout: 5000
})
// baseURL
// axios.defaults.baseURL = 'https://api.github.com';
// http request 拦截器
// 每次请求都为http头增加Authorization字段,其内容为token
service.interceptors.request.use(
config => {
if (store.state.user.token) {
config.headers.Authorization = `token ${
store.state.user.token}`;
}
return config
},
err => {
return Promise.reject(err)
}
);
export default service
页面权限控制
在前面已经说到,页面权限控制又分为两种:
1.菜单中的页面是否能被访问
2.页面中的按钮(增、删、改)的权限控制是否显示
这些权限一般是在固定页面进行配置,保存后记录到数据库中。
按钮权限暂且不提,页面访问权限在实现中又可以分为两种方式:
1.显示所有菜单,当用户访问不在自己权限内的菜单时,提示权限不足
2.只显示当前用户能访问的权限内菜单,如果用户通过URL进行强制访问,则会直接进入404
对流程梳理完成后我们开始进行详细的编写。
1、创建路由表
创建路由表实际上没有什么难度,照着vue-router官方文档给的示例直接写就行了。但是因为有部分页面是不需要访问权限的,
所以需要将登录、404、维护等页面写到默认的路由中,而将其它的需要权限的页面写到一个变量或者一个文件中,这样可
以有效的减轻后续的维护压力。
下面将index.js的代码贴上,异步路由将适量减少,以免占过多篇幅。
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import App from '@/App'
import store from '../store/index'
Vue.use(Router);
//手动跳转的页面白名单
const whiteList = [
'/'
];
//默认不需要权限的页面
const constantRouterMap = [
{
path: '/',
name: '登录',
component: (resolve) => require(['@/components/login'], resolve)
},
{
path: '/index',
name: 'nav.Home',
component: (resolve) => require(['@/components/index'], resolve)
},
{
path: '/templateMake',
name: '模板制作',
component: (resolve) => require(['@/components/Template/templateMake'], resolve)
},
{
path: '/programMack',
name: '节目制作',
component: (resolve) => require(['@/components/Template/programMack'], resolve)
},
{
path: '/release',
name: '节目发布',
component: (resolve) => require(['@/components/Program/release'], resolve)
}
]
//注册路由
export const router = new Router({
routes: constantRouterMap
});
//异步路由(需要权限的页面)
export const asyncRouterMap = [
{
path: '/resource',
name: 'nav.Resource',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Resource/resource'], resolve)
},
{
path: '/template',
name: 'nav.Template',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Template/template'], resolve)
},
{
path: '/generalSet',
name: 'nav.System',
meta: {
permission: []
},
component: (resolve) => require(['@/components/SystemSet/generalSet'], resolve)
},
{
path: '',
name: 'nav.Log',
component: App,
children: [
{
path: '/userLog',
name: 'nav.UserLog',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Log/userLog'], resolve),
},
{
path: '/operatingLog',
name: 'nav.SystemLog',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Log/operatingLog'], resolve),
},
]
}
]
];
注意事项:这里有一个需要非常注意的地方就是 404 页面一定要最后加载,如果放在constantRouterMap一同声明了404,后面的所以页面都会被拦截到404
页面访问权限
在开始时我们梳理了一个大致的页面访问权限流程。下面我们先实现最核心的部分:
我们首先获取用户权限列表,在这里我们将接触到vuex状态管理,官方文档有详细介绍,这里就不过多描述了,下面请看代码:
// store/index.js
import Axios from 'axios'
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const axios = Axios.create();
const state = {
mode: 'login',
list: []
};
const getters = {
};
const mutations = {
setMode: (state, data) => {
state.mode = data
},
setList: (state, data) => {
state.list = data
}
};
const actions = {
// 获取权限列表
getPermission({
commit}) {
return new Promise((resolve, reject) => {
axios({
url: '/privilege/queryPrivilege?id=' + sessionStorage.getItem('privId'),
methods: 'get',
headers: {
token: sessionStorage.getItem('token'),
name: sessionStorage.getItem('name')
}
}).then((res) => {
// 存储权限列表
commit('setList', res.data.cust.privileges[0].children);
resolve(res.data.cust.privileges[0].children)
}).catch(() => {
reject()
})
})
}
};
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
好了,我们现在请求后台拿到了权限数据,并将数据存放到了vuex中,下面我们需要利用返回数据匹配之前写的异步路由表,将匹配结果和静态路由表结合,开成最终的实际路由表。
其中最关键的是利用vue-router2.2.0版本新添加的一个addRoutes方法,我们看看官方文档如何解释此方法的:
动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。
那我们现在就可以开始使用addRoutes进行路由匹配了。下面看代码:
// router/index.js
/**
* 根据权限匹配路由
* @param {
array} permission 权限列表(菜单列表)
* @param {
array} asyncRouter 异步路由对象
*/
function routerMatch(permission, asyncRouter) {
return new Promise((resolve) => {
const routers = [];
// 创建路由
function createRouter(permission) {
// 根据路径匹配到的router对象添加到routers中即可
permission.forEach((item) => {
if (item.children && item.children.length) {
createRouter(item.children)
}
let path = item.path;
// 循环异步路由,将符合权限列表的路由加入到routers中
asyncRouter.find((s) => {
if (s.path === '') {
s.children.find((y) => {
if (y.path === path) {
y.meta.permission = item.permission;
routers.push(s);
}
})
}
if (s.path === path) {
s.meta.permission = item.permission;
routers.push(s);
}
})
})
}
createRouter(permission)
resolve([routers])
})
}
然后我们编写导航钩子:
// router/index.js
router.beforeEach((to, form, next) => {
if (sessionStorage.getItem('token')) {
if (to.path === '/') {
router.replace('/index')
} else {
console.log(store.state.list.length);
if (store.state.list.length === 0) {
//如果没有权限列表,将重新向后台请求一次
store.dispatch('getPermission').then(res => {
//调用权限匹配的方法
routerMatch(res, asyncRouterMap).then(res => {
//将匹配出来的权限列表进行addRoutes
router.addRoutes(res[0]);
next(to.path)
})
}).catch(() => {
router.replace('/')
})
} else {
if (to.matched.length) {
next()
} else {
router.replace('/')
}
}
}
} else {
if (whiteList.indexOf(to.path) >= 0) {
next()
} else {
router.replace('/')
}
}
});
到这里我们已经完成了对页面访问的权限控制,接下来我们来讲解一下操作按扭的权限部分:
数据操作权限
是否还记得前面的路由配置中我们多出来的一个代码,下面我们拿出来看看:
//异步路由(需要权限的页面)
export const asyncRouterMap = [
{
path: '/resource',
name: 'nav.Resource',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Resource/resource'], resolve)
},
{
path: '/template',
name: 'nav.Template',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Template/template'], resolve)
},
{
path: '/generalSet',
name: 'nav.System',
meta: {
permission: []
},
component: (resolve) => require(['@/components/SystemSet/generalSet'], resolve)
},
{
path: '',
name: 'nav.Log',
component: App,
children: [
{
path: '/userLog',
name: 'nav.UserLog',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Log/userLog'], resolve),
},
{
path: '/operatingLog',
name: 'nav.SystemLog',
meta: {
permission: []
},
component: (resolve) => require(['@/components/Log/operatingLog'], resolve),
},
]
}
]
];
为每个路由页面增加meta字段。在routerMatch函数中将匹配到的详细权限字段赋值到这里。这样在每个页面的route对象中就会得到这个字段。
asyncRouter.find((s) => {
if (s.path === '') {
s.children.find((y) => {
if (y.path === path) {
//赋值
y.meta.permission = item.permission;
routers.push(s);
}
})
}
if (s.path === path) {
s.meta.permission = item.permission;
routers.push(s);
}
})
接下来我们编写一个vue自定义指令对页面中需要进行鉴权的元素进行判断,比如类似这样的
<a @click="upload" v-allow="'3'"></a> /* 3代表一个上传权限的ID,权限中有3则显示按钮 */
我们直接注册一个全局指令,利用vnode来访问vue的方法。代码如下:
//main.js
//按扭权限指令
Vue.directive('allow', {
inserted: (el, binding, vnode) => {
let permissionList = vnode.context.$route.meta.permission;
if (!permissionList.includes(binding.value)) {
el.parentNode.removeChild(el)
}
}
})
至此为止,权限控制流程就已经完全结束了,在最后我们再看一下完整的权限控制流程图吧.
路由控制完整流程图
二十七:详述函数颗粒化是什么?及作用
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script>
/*----函数颗粒化就是函数参数截取,并合并参数-----*/
// 在这个函数中进行add函数所需参数的截取,用到的函数slice,call,apply
function curry(fn){
//截取第一个参数
var firstAgu = Array.prototype.slice.call(arguments,1);
//截取第二个参数
return function two(){
var lastTwoAgu = Array.prototype.slice.call(arguments);
var finalArguments = firstAgu.concat(lastTwoAgu);
return fn.apply(this,finalArguments);
}
}
//这里需要三个参数
function add(a,b,c){
var m = a + b +c;
console.log(m);
return m;
}
curry(add,5)(1,2);
</script>
</head>
<body>
</body>
</html>
请列举Object上挂载的方法:如Object.assign等:
1.Object.is() 比较两个值是否想等
2.Object.keys(),values()循环遍历一个对象
3.Object.setPrototypeOf() 设置一个对象的原型对象
4.Object.getPrototypeOf()读取一个对象的原型对象
es6的新特性有哪些:
1.set ,map 数据结构,
1.promise
3.箭头函数,
4.let const
5.forEach(),filter()
6.解构赋值
8.class 继承
如何理解javaScript的原型与原型链:
每一个javaScript 都有一个proptotype属性,函数的prototype指向了一个对象,那么这个对象就是调用该构造函数创建实例的原型
每一个javaScript (null除外) 在创建时都关联另一个对象,而这个对象就是原型,每个对象都会从原型上继承属性,
每一个javaScript(null除外) 都有一个__proto__,这个属性指向该对象的原型,原型链解决掉主要是继承问题,每个对象都有一个原型对象,通过proto指针,指向原型对象,并从中继承属性和方法,同时原型对象也有原型,这样一层一层最终指向null,Object.prototype.__proto__指向的是null,这种关系被称为原型链,通过原型链,一个对象可以拥有定义在其他对象中的属性和方法。
请列举你使用过的vue指令和修饰符:
1.v-text,2.v-show,3.v-if,4.v-html,5.v-for 6.v-bind,7v-on,8v-class 9.v-pre
事件修饰符:
Vue父子组件如何传值:
父组件传子组件: 子组件在props中创建一个属性,接收父组件传过来的值,父组件在子组件标签上添加子组件props中创建的属性
子组件传父组件:$emit触发一个事件,并传递一个参数,在父组件中子组件的标签上监听该自定义事件并添加一个响应该事件的处理方法
说说你对生命周期的理解:
vue 从创建到销毁的过程就是生命周期,从开始创建,初始化数据,编译模板,挂载dom->渲染,更新->渲染,销毁一系列过程称之为生命周期
vuex 有几个属性,怎么使用,如何修改state中的数据:
1.state :存放数据,里面的值不可以之间修改
2.mutatios:提交修改state里面的数据,必须是同步
3.actios:处理异步操作,actions提交的是mutatios,而不是直接修改state
4.getters: 相当于vue里的计算属性,用来过滤一些数据
5.models: 项目特别复杂时候,可以让每个模块都有自己的state,getters,mutatios,使结构清晰,方便管理
修改state 有两种方法,
1.在组件中this. s t o r e . s t a t e . 变 量 = ‘ x x x ’ 2. t h i s . store.state.变量=‘xxx’ 2.this. store.state.变量=‘xxx’2.this.store.dispath()
this.$store.commit()
怎么定义vue-router 的动态路由?怎么获取传过来的参数?路由之间如何跳转?
设置:在路由的path属性加上 /:id
获取:this. r o u t e . p a r a m s . i d 跳 转 方 式 : r o u t e r − l i n k 标 签 t h i s . route.params.id 跳转方式: router-link 标签 this. route.params.id跳转方式:router−link标签this.router.push()
this. r o u t e r . r e p l a c e ( ) t h i s . router.replace() this. router.replace()this.router.go()
请简单描述观察者模式:
观察者模式就是定义对象中一对多的依赖关系,每当数据发生改变,则依赖它的所有对象都会得到通知并自动更新
’