总括: 包含这三个月来碰到的一些以为比较好的面试题,三个月没怎么写博客着实有些手痒,哈哈哈。7000余字,不成敬意2333javascript
原文地址:个人前端进阶之路php
博主博客地址:Damonare的我的博客html
烈火试真金,逆境试强者前端
相同点:vue
数据驱动视图,提供响应式的视图组件html5
都有Virtual DOM,组件化开发,经过props参数进行父子组件数据的传递,都实现webComponents规范java
数据流动单向node
都支持服务端渲染react
都有支持native的方案,React的React native
,Vue的weex
不一样点:
社区:React社区仍是要比vue大不少;
开发模式:React在view层侵入性仍是要比Vue大不少的,React严格上只针对MVC的view层,Vue则是MVVM模式的一种实现;
数据绑定:Vue有实现了双向数据绑定,React数据流动是单向的
数据渲染:对于大规模数据渲染,React要比Vue更快,渲染机制启动时候要作的工做比较多;
数据更新方面:Vue 因为采用依赖追踪,默认就是优化状态:你动了多少数据,就触发多少更新,很少也很多。React在复杂的应用里有两个选择:
(1). 手动添加 shouldComponentUpdate 来避免不须要的 vdom re-render。 (2).Components 尽量都用 pureRenderMixin,而后采用 redux 结构 + Immutable.js;
开发风格的偏好:React 推荐的作法是 JSX + inline style,也就是把 HTML 和 CSS 全都写进 JavaScript 了,即"all in js";Vue进阶以后推荐的是使用 webpack + vue-loader 的单文件组件格式,即html,css,js写在同一个文件;
使用场景:React配合Redux架构适合超大规模多人协做的复杂项目;Vue则适合小快灵的项目。对于须要对 DOM 进行不少自定义操做的项目,Vue 的灵活性优于 React;
Vue要比React更好上手,具体可能体如今不少人不熟悉React的JSX语法和函数式编程的思想,以及想要发挥出React的最大威力须要学习它一系列生态的缘故;
Vue着重提升开发效率,让前端程序员更快速方便的开发应用。React着重于变革开发思想,提高前端程序员编程的深度与创造力,让前端工程师成为真正的程序员而不是UI的构建者;
gulp是一种工具,咱们能够用它来优化前端的工做流程,好比自动刷新页面、combo、压缩css、js、编译less等等。具体体现为:在gulp的配置文件中书写一个个的task,webpack则是一种打包工具,或者说是一种模块化解决方案,实际上很大一部分人刚开始使用webpack的方式就是经过gulp-webpack这个插件,写好task来使用webpack对前端的一些文件进行打包;
gulp的处理任务须要本身去写,webpack则有现成的解决方案,只须要在webpack.config.js配置好便可;
用户点击以后按钮disabled;
函数节流
abort掉上一个请求。
事件捕获阶段(capturing phase)。事件从document一直向下传播到目标元素, 依次检查通过的节点是否绑定了事件监听函数,若是有则执行。
事件处理阶段(target phase)。事件到达目标元素, 触发目标元素的监听函数。
事件冒泡阶段(bubbling phase)。事件从目标元素冒泡到document, 依次检查通过的节点是否绑定了事件监听函数,若是有则执行。
Expires策略
Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过时时间前浏览器能够直接从浏览器缓存取数据,而无需再次请求。Expires 是HTTP 1.0的东西,如今默认浏览器均默认使用HTTP 1.1,因此它的做用基本忽略。
Cache-Control策略
Cache-Control与Expires的做用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓读取数据仍是从新发请求到服务器取数据。只不过Cache-Control的选择更多,设置更细致,若是同时设置的话,其优先级高于Expires。
以上是设置缓存时间的两种方法。那么当缓存时间过了咋整呢?有人确定说了,那就再次发起请求啊,这是对的。问题是若是服务器资源并无更新呢?好比说我有一个jQuery.js
文件已经缓存了,当它的缓存时间到了以后服务器的jQuery.js
文件也没有更新,那实际上咱们直接使用本地缓存的文件就能够啊!不必浪费带宽和时间去从新请求一个新的文件啊!这时候咱们就须要再进一步看一下HTTP协议里这几个参数的做用了。
Last-Modified/If-Modified-Since
首先Last-Modified/If-Modified-Since要配合Cache-Control使用。
Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间(这个参数是和Cache-Control一块儿过来的)。
If-Modified-Since:当资源过时时(使用Cache-Control标识的max-age),发现资源具备Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since ,则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。
ETag/If-None-Match
Etag/If-None-Match也要配合Cache-Control使用。
Etag:web服务器响应请求时,告诉浏览器当前资源在服务器的惟一标识(生成规则由服务器以为)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后获得的。
If-None-Match:当资源过时时(使用Cache-Control标识的max-age),发现资源具备Etage声明,则再次向web服务器请求时带上头If-None-Match(Etag的值)。web服务器收到请求后发现有头If-None-Match 则与被请求资源的相应校验串进行比对,决定返回200或304。
ETag和Last-Modified
HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
Last-Modified标注的最后修改只能精确到秒级,若是某些文件在1秒钟之内,被修改屡次的话,它将不能准确标注文件的修改时间
若是某些文件会被按期生成,当有时内容并无任何变化,但Last-Modified却改变了,致使文件无法使用缓存
有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的惟一标识符,可以更加准确的控制缓存。Last-Modified与ETag是能够一块儿使用的,服务器会优先验证ETag,一致的状况下,才会继续比对Last-Modified,最后才决定是否返回304。
Ajax的状态值
0: (未初始化)尚未调用send()方法。
1: (载入)已经调用send()方法,正在派发请求。
2: (载入完成)send()已经执行完成,已经接收到所有的响应内容。
3: (交互)正在解析响应内容。
4: (完成)响应内容已经解析完成,用户能够调用。
HTTP状态码
200 & OK: 请求成功;
204 & No Content: 请求处理成功,但没有资源能够返回;
206 & Partial Content: 对资源某一部分进行请求(好比对于只加载了通常的图片剩余部分的请求);
301 & Move Permanently: 永久性重定向;
302 & Found: 临时性重定向;
303 & See Other: 请求资源存在另外一个URI,应使用get方法请求;
304 & Not Modified: 服务器判断本地缓存未更新,能够直接使用本地的缓存;
307 & Temporary Redirect: 临时重定向;
400 & Bad Request: 请求报文存在语法错误;
401 & Unauthorized: 请求须要经过HTTP认证;
403 & Forbidden: 请求资源被服务器拒绝,访问权限的问题;
404 & Not Found: 服务器上没有请求的资源;
500 & Internal Server Error: 服务器执行请求时出现错误;
502 & Bad Gateway: 错误的网关;
503 & Service Unavailable: 服务器超载或正在维护,没法处理请求;
504 & Gateway timeout: 网关超时;
1.History
老浏览器的history: 主要经过hash来实现,对应createHashHistory
高版本浏览器: 经过html5里面的history,对应createBrowserHistory
node环境下: 主要存储在memeory里面,对应createMemoryHistory
内部createHistory
实现:
// 内部的抽象实现 function createHistory(options={}) { ... return { listenBefore, // 内部的hook机制,能够在location发生变化前执行某些行为,AOP的实现 listen, // location发生改变时触发回调 transitionTo, // 执行location的改变 push, // 改变location replace, go, goBack, goForward, createKey, // 建立location的key,用于惟一标示该location,是随机生成的 createPath, createHref, createLocation, // 建立location } }
createLocation
方法:
function createLocation() { return { pathname, // url的基本路径 search, // 查询字段 hash, // url中的hash值 state, // url对应的state字段 action, // 分为push、replace、pop三种 key // 生成方法为: Math.random().toString(36).substr(2, length) } }
三种方法各自执行URL
前进的方式:
createBrowserHistory
: pushState、replaceState
createHashHistory
: location.hash=***
location.replace()
createMemoryHistory
: 在内存中进行历史记录的存储
伪代码实现:
// createBrowserHistory(HTML5)中的前进实现 function finishTransition(location) { ... const historyState = { key }; ... if (location.action === 'PUSH') ) { window.history.pushState(historyState, null, path); } else { window.history.replaceState(historyState, null, path) } } // createHashHistory的内部实现 function finishTransition(location) { ... if (location.action === 'PUSH') ) { window.location.hash = path; } else { window.location.replace( window.location.pathname + window.location.search + '#' + path ); } } // createMemoryHistory的内部实现 entries = []; function finishTransition(location) { ... switch (location.action) { case 'PUSH': entries.push(location); break; case 'REPLACE': entries[current] = location; break; } }
React-router的基本原理
URL
对应Location
对象,而UI
是由react的 components
来决定的,这样就转变成location
与components
之间的同步问题。
每个对象都会在内部连接到另外一个对象(该对象的原型对象),该对象有一个原型prototype
,当访问对象的属性或是方法的时候,不只仅会在原对象上查找,还会顺着原型链在原型对象的原型链上查找,直到查到null
(全部原型链的顶层)为止。原型是JavaScript实现继承的基础,new
关键字作的主要的事情就是将实例对象的__proto__
属性指向原型对象的prototype。
闭包是javascript支持头等函数的一种方式,它是一个可以引用其内部做用域变量(在本做用域第一次声明的变量)的表达式,这个表达式能够赋值给某个变量,能够做为参数传递给函数,也能够做为一个函数返回值返回。
闭包是函数开始执行的时候被分配的一个栈帧,在函数执行结束返回后仍不会被释放(就好像一个栈帧被分配在堆里而不是栈里!)
闭包的应用:
好比写柯里化函数的时候利用闭包,保存参数在内存中;
var currying = function(fun) { //格式化arguments var args = Array.prototype.slice.call(arguments, 1); return function() { //收集全部的参数在同一个数组中,进行计算 var _args = args.concat(Array.prototype.slice.call(arguments)); return fun.apply(null, _args); }; }
模拟私有变量或是私有方法;
people = (num) => {
var num = num;
return {
increase: () => {
num++;
},
get: () => { return num; } }
}
man = people(4);
man.increase();
man.get();
避免引用错误
(var i = 0; i < 4; i++) {
(function(_i) {
setTimeout(function() { console.log(_i)
}, 1000)
})(i)
}
图片懒加载的原理就是暂时不设置图片的src
属性,而是将图片的url
隐藏起来,好比先写在data-src
里面,等某些事件触发的时候(好比滚动到底部,点击加载图片)再将图片真实的url
放进src
属性里面,从而实现图片的延迟加载
图片预加载,是指在一些须要展现大量图片的网站,实现图片的提早加载。从而提高用户体验。经常使用的方式有两种,一种是隐藏在css的background的url属性里面,一种是经过javascript的Image对象设置实例对象的src属性实现图片的预加载。相关代码以下:
CSS预加载图片方式:
#preload-01 { background: url(http://domain.tld/image-01.png) no-repeat -9999px -9999px; } #preload-02 { background: url(http://domain.tld/image-02.png) no-repeat -9999px -9999px; } #preload-03 { background: url(http://domain.tld/image-03.png) no-repeat -9999px -9999px; }
Javascript预加载图片的方式:
function preloadImg(url) { var img = new Image(); img.src = url; if(img.complete) { //接下来可使用图片了 //do something here } else { img.onload = function() { //接下来可使用图片了 //do something here }; } }
跨域的方式有不少种,最经常使用的是jsonp
主要利用了script
的开放策略:经过script标签引入一个js或者是一个其余后缀形式(如php,jsp等)的文件,此文件返回一个js函数的调用。缺点在于只支持get请求并且存在安全问题。
CORS跨域,关键在于服务器,若是服务器实现了CORS跨域的接口,那么就可使用ajax(请求路径为绝对路径)进行跨域请求。CORS请求分为两种,一种是简单请求,一种是非简单请求。简单请求是指请求方法在HEAD
,GET
,POST
三者之间而且请求头信息局限在
Accept
Accept-Language
Content-Language
Content-Type:只限于三个值application/x-www-form-urlencoded
、multipart/form-data
、text/plain
非简单请求请求头:
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段
执行简单请求的时候,浏览器会在请求头信息增长origin
字段,服务器据此来判断请求域名是否在许可范围以内,来决定是否返回Access-Control-Allow-Origin
字段。响应头有如下几种:
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否容许发送Cookie。默认状况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie能够包含在请求中,一块儿发给服务器。这个值也只能设为true
,若是服务器不要浏览器发送Cookie,删除该字段便可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。若是想拿到其余字段,就必须在Access-Control-Expose-Headers
里面指定。
(4)Access-Control-Max-Age
Access-Control-Max-Age
首部字段指明了预检请求的响应的有效时间。
(5)Access-Control-Allow-Methods
Access-Control-Allow-Methods
首部字段用于预检请求的响应。其指明了实际请求所容许使用的 HTTP 方法。
(6)Access-Control-Allow-Headers
Access-Control-Allow-Headers
首部字段用于预检请求的响应。其指明了实际请求中容许携带的首部字段。
其余方法:document.domin
,html5的postMessage
,window.name
等
函数节流让指函数有规律的进行调用,应用场景:window.resize,游戏中子弹发射(1s只能发射一颗子弹)等;
函数防抖让函数在"调用''以后的一段时间后生效,应用场景:输入框(例:在用户中止输入的500ms后再处理用户数据)。
//函数节流 /* * @params {Function} fun 调用函数 * @params {delay} number 延迟时间 */ const throttle = (fun, delay, ...rest) => { let last = null; return () => { const now = + new Date(); if (now - last > delay) { fun(rest); last = now; } } } //实例 const throttleExample = throttle(() => console.log(1), 1000); //调用 throttleExample(); throttleExample(); throttleExample(); //函数防抖 const debouce = (fun, delay, ...rest) => { let timer = null; return () => { clearTimeout(timer); timer = setTimeout(() => { fun(rest); }, delay); } } //实例 const debouceExample = debouce(() => console.log(1), 1000); //调用 debouceExample(); debouceExample(); debouceExample();
从数列中挑出一个元素,称为"基准"(pivot),
从新排序数列,全部比基准值小的元素摆放在基准前面,全部比基准值大的元素摆在基准后面(相同的数能够到任一边)。在这个分区结束以后,该基准就处于数列的中间位置。这个称为分区(partition)操做。
递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
时间复杂度平均状况:O(nlog n) 最快:O(n^{2}) 空间复杂度: O(log n)
var quickSort = function(arr) { console.time('2.快速排序耗时'); if (arr.length <= 1) { return arr; } var pivot = arr.splice(0, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } console.timeEnd('2.快速排序耗时'); return quickSort(left).concat([pivot], quickSort(right)); }; var arr=[3,44,38,5,47,15,36,26,27,2,46,4,19,50,48]; console.log(quickSort(arr));//[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
AMD 是 RequireJS 在推广过程当中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程当中对模块定义的规范化产出。
对于依赖的模块,AMD 是提早执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改为能够延迟执行(根据写法不一样,处理方式不一样)。CMD 推崇 as lazy as possible.
CMD 推崇依赖就近,AMD 推崇依赖前置。
AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。好比 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每一个 API 都简单纯粹。
易出现泄露的场景
XMLHttpRequest 泄漏发生在IE7-8,释放方法,将XMLHttpRequest实例对象设置为Null;
DOM&BOM等COM对象循环绑定 泄漏发生在IE6-8,释放方法,切断循环引用,将对对象的应用设置为Null;
定时器(严格上说不能算是泄露,是被闭包持有了,是正常的表现),对于闭包中无用的变量可使用delete操做符进行释放;
JavaScript垃圾回收机制
引用计数
此算法把“对象是否再也不须要”简化定义为“对象有没有其余对象引用到它”。若是没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
限制:没法处理循环引用。在下面的例子中,两个对象被建立,并互相引用,造成了一个循环。它们被调用以后不会离开函数做用域,因此它们已经没有用了,能够被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,因此它们不会被回收。
标记清除
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,由于只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
垃圾回收器在运行的时候会给存储在内存中的全部变量都加上标记(固然,可使用任何标记方式)。而后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此以后再被加上标记的变量将被视为准备删除的变量,缘由是环境中的变量已经没法访问到这些变量了。最后,垃圾回收器完成内存清除工做,销毁那些带标记的值并回收它们所占用的内存空间。
所谓的柯里化函数简单的说就是将原本接受多个参数的函数变为只接受一个参数的函数。柯里化函数的模板和实例以下:
var currying = function(fun) { //格式化arguments var args = Array.prototype.slice.call(arguments, 1); return function() { //收集全部的参数在同一个数组中,进行计算 var _args = args.concat(Array.prototype.slice.call(arguments)); return fun.apply(null, _args); }; } var add = currying(function() { var args = Array.prototype.slice.call(arguments); return args.reduce(function(a, b) { return a + b; }); }) add(1, 2, 4) /* * 经典面试题 * 函数参数不定回调函数数目不定 * 编写函数实现: * add(1,2,3,4,5)==15 * add(1,2)(3,4)(5)==15 */ function add() { // 第一次执行时,定义一个数组专门用来存储全部的参数 var _args = [].slice.call(arguments); // 在内部声明一个函数,利用闭包的特性保存_args并收集全部的参数值 var adder = function () { var _adder = function() { [].push.apply(_args, [].slice.call(arguments)); return _adder; }; // 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } return adder.apply(null, _args); } // 输出结果,可自由组合的参数 console.log(add(1, 2, 3, 4, 5)); // 15 console.log(add(1, 2, 3, 4)(5)); // 15 console.log(add(1)(2)(3)(4)(5)); // 15
变量(@color = #fff)
混合(Mixin)
内置函数(颜色,字符串,类型判断,数学)
循环
嵌套
运算
导入(@import)
变量定义(let和const,可变与不可变,const定义对象的特殊状况)
解构赋值
模板字符串
数组新API(例:Array.from(),entries(),values(),keys())
箭头函数(rest参数,扩展运算符,::绑定this)
Set和Map数据结构(set实例成员值惟一存储key值,map实例存储键值对(key-value))
Promise对象(前端异步解决方案进化史,generator函数,async函数)
Class语法糖(super关键字)
题目:
import React from 'react' class App extends React.Component { constructor() { super(); this.state = { value: 0 } } componentDidMount() { this.setState({value: this.state.value + 1}); console.log(this.state.value); this.setState({value: this.state.value + 1}); console.log(this.state.value); this.setState({value: this.state.value + 1}); console.log(this.state.value); setTimeout(() => { this.setState({value: this.state.value + 1}); console.log(this.state.value); this.setState({value: this.state.value + 1}); console.log(this.state.value); }, 0) } }
答案: 0、0、0、二、3;
分析:
当setState
方法调用的时候React
就会从新调用render
方法来从新渲染组件;setState
经过一个队列来更新state
,当调用setState
方法的时候会将须要更新的state放入这个状态队列中,这个队列会高效的批量更新state
;
源码地址:enqueueUpdate
function enqueueUpdate(component) { ensureInjected(); //判断是否处于批量更新模式 if (!batchingStrategy.isBatchingUpdates) { //关键!下面的代码片断是这个方法的源码 batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } //若是处于批量更新模式,则将这个组件保存在dirtyComponents dirtyComponents.push(component); }
源码地址:ReactDefaultBatchingStrategy
//batchingStrategy对象 var ReactDefaultBatchingStrategy = { //注意默认为false isBatchingUpdates: false, batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; if (alreadyBatchingUpdates) { callback(a, b, c, d, e); } else { //关键!!!事务的理解 transaction.perform(callback, null, a, b, c, d, e); } }, };
源码地址:Transaction
如图:事务会将所须要执行的方法(图中的anyMethod)使用wrapper
封装起来,再经过perform
方法执行该方法,但在perform
执行以前会先执行全部wrapper
中的initialize
方法,perform
方法执行结束后,再执行全部的close
方法;
var Transaction = require('./Transaction'); // 咱们本身定义的 var MyTransaction = function() { //do something }; Object.assign(MyTransaction.prototype, Transaction.Mixin, { //须要自定义一个getTransactionWrappers对象,获取全部须要封装的initialize方法和close方法 getTransactionWrappers: function() { return [{ initialize: function() { console.log('before method perform'); }, close: function() { console.log('after method perform'); } }]; }; }); //实例化一个transaction var transaction = new MyTransaction(); //须要调用的方法 var testMethod = function() { console.log('test'); } transaction.perform(testMethod); //before method perform //test //after method perform
理解题目的关键是,整个组件渲染到DOM中的过程就已经处于一次大的事务中了,所以在componentDidMount
方法中调用setState
的时候 ReactDefaultBatchingStrategy.isBatchingUpdates = true;
这句代码已经执行过了,因此setState
的结果并无当即生效,而是扔进了dirtyComponent
;所以执行三次setState的结果this.state.value的值依然是0,而setTimeout中的两次setState因为没有调用过batchedUpdates
方法(isBatchingUpdates
默认为false
),因此setState
方法当即生效,第二次setSState
同理
XSS是一种跨站脚本攻击,是属于代码注入的一种,攻击者经过将代码注入网页中,其余用户看到会受到影响(代码内容有请求外部服务器);
CSRF是一种跨站请求伪造,冒充用户发起请求,完成一些违背用户请求的行为(删帖,改密码,发邮件,发帖等)
防护方法举例:
对一些关键字和特殊字符进行过滤(<>,?,script等),或对用户输入内容进行URL编码(encodeURIComponent);
Cookie不要存放用户名和密码,对cookie信息进行MD5等算法散列存放,必要时能够将IP和cookie绑定;
时隔三个月,终于迎来了博文的更新,有看到博友在评论留言:内心很温暖,这篇不算博文的博文就当是回归之做吧,接下来的时间会尽可能保持在一周一更,实习结束有的是时间了,哈哈哈。