BAT 前端开发面经 —— 吐血总结
目录
更好阅读,请移步这里
聊之前
最近暑期实习招聘已经开始,个人目前参加了阿里的内推及腾讯和百度的实习生招聘,在此总结一下
一是备忘、总结提升,二是希望给大家一些参考
其他面试及基础相关可以参考其他博文:
每位面试官的面试时间基本都在 40-80 分钟,下面先简要介绍各个面试流程,问题详情见具体公司分类
腾讯内推&校招 offer got
首先腾讯分为三面,都为技术面:
- 初试一个面试官
-
复试两个面试官,包括电话远程 online coding
(我也不知道为什么几天内三个面试官面我,只算两个面试过程 ヾ(´A`)ノ゚)
-
终面一位面试官
这位应该是大 boss ,他说并不是前端开发的,面试的问题也不会很深入,主要看个人对这个领域的想法以及对常见问题在前端领域的解决思路,不按套路出牌,emmm 感觉回答的不理想
类似这种问题:- 说说最能体现你能力的工作
-
网络安全的实现
如何检测恶意脚本
如何屏蔽
...
腾讯二面 & online coding
腾讯的远程 online coding 时间较长,大概一个多小时,不仅要按要求编程,也要口述思路以及关键点,过程类似压力面,面试官说完题目要求会给你思考及coding 时间,他可以远程看到面试者的 coding 状态,主要考察应变能力,思维活跃度,编码习惯,调试能力,以及测试方法,本人在此过程中没有太注意测试过程,导致对于特殊情况考虑不全面,测试样例不完善,望小伙伴们注意 ヾ(´A`)ノ゚,不过,在口述代码时发现也可以自己提出来需要完善的地方。
coding 为一到两题,后续问题都是围绕它结合实际应用进行拓展,主要考察是否能灵活运用以及相似的思路转换,当时面试时间太长以及基础知识较差,进制转换,存储那些个基础被小学老师收回,一连串的炮轰简直爽歪歪,个人表示此过程自己的表现较差,也要重视基础基础啊老铁们 ( ̄_ ̄ )
经历了腾讯云的四个面试官,以及其他部门一个面试官的酱油面,腾讯的技术面试官普遍语速较快,思路转换很快,要跟上面试官的节奏,聊到自己比较熟悉的可以多说几句,他们也会顺着你回答的内容进行深入,也会引导面试者的回答方向,如果不太熟尽量坦白一些,不懂装懂很容易 gg
才接到通知另加两轮面试委员会现场技术面,简直爽歪歪 ヾ(´A`)ノ
腾讯校招现场
-
一面
面试官很 nice,基本看着简历问的,话题围绕项目(项目隔得时间太久,内心宁愿粗暴的技术面),基本为大致的了解,包括平时的学习习惯和关注的技术发展
- 问到了 ES5 ES6 ES7 甚至 ES8
- 项目中用过vue, 就问了 react vue 相关
- 实习中的工作方式,如何处理设计师的设计稿,回答了包括考虑响应式布局的常用方案
-
sass--定义function,使用rem
问是否有其他负面作用
想到了页面闪烁,由于初始化时需要计算页面尺寸,可能有缩放
如何避免:
<!-- 如果在页面加载完成后,页面是用js动态添加的,这个问题就不太明显, --> doc.addEventListener('DOMContentLoaded‘', function(e) { <!-- doc.body.style.fontSize = 12 * dpr + 'px'; 淘宝处理 --> }, false);
媒体查询
-
- 移动端和pc端的工作
-
二面
面试官不是前端的,所以没有问很深入的问题,主要模拟实际场景考察实现思路以及前后端的方案,后端没说出什么,基本上只是就前端层面进行了描述,包括实现思路、需要注意的问题、以及性能和安全方面的考虑
- 前端安全
- 场景考察
- 直播弹幕处理
- 后端返回大量数据且包含运算,如何实时显示
- 。。。记不清了
- HR 面
- 介绍经历
- 家庭情况
- 职业规划
- 对腾讯的认识及与其他实习单位对比
- 对比实习过的企业
- 是否还报了其他企业
- 男朋友工作找的怎么样了
阿里内推 二面 卒
我投的是蚂蚁金服的前端开发,投过简历硬生生排队等了12天,还被内推人提前告知蚂蚁的前端很严格 ( ̄_ ̄ )
阿里分为在线测试,初试 ......
-
首先是在线测试
投完简历后官网会有相关在线测试题
阿里前端的在线测试只有一道coding 题,限时 30 分,由于初次在线答题,看着倒计时紧张的思路不通,未能准确理解题意,但实际题目并不难,考察使用原生 js 实现类似css 的层级选择器的功能,具体题目记不太清,将仅存的记忆写了下来并附上个人实现,详情见本文后部分 - 一面
- 二面
挂掉了。。。
baidu 等通知
百度投的是核心搜索部门,但一面后估计不合适,没有了消息,后简历转到百度金融
-
一面
面试官语速很快,一般不给太多思考时间--------感觉自己说话都打着节拍 ( ̄_ ̄ )
-
二面
面试官很和蔼,是唯一一个认真看了我博客的面试官,很荣幸,也很紧张
基本偏向基础
但由于没安排好时间,面试时在户外,听不太清面试官的问题,在此提醒各位小伙伴提前选好面试场地,避免环境影响 -
三面
同样和蔼的面试官,开始考察基础知识,编译原理,网络协议等基础,后面考察个人素质,后续更注重个人经历,包括如何学习,找实习,实习中遇到的问题及如何解决,最骄傲的事情等等
关于结尾
-
百度 & 阿里面试结束后都有问:你觉得在面试过程中有什么没问到,但自己掌握比较好的技能么
面阿里时,头脑发晕,回答:没有,我感觉您面的很专业,问的知识点都是比较核心的 ( ̄_ ̄ )
百度经验: 回答自己掌握还比较好的知识后,面试官真的会问很多问题(菜鸟可以参考以上做法),如果面试官面试的较偏,倒可以补充 - 有什么要问的
-
腾讯二面教训
我:是否有导师带,实习的大致安排以及部门业务等
效果 差
面试官曰:
你那么关系有没有导师的问题,但学习中最重要的是靠自己,导师并不是负责回答你所有问题的
-
百度经验
我:部门是否有定期交流分享的机会;工作中是按照职位还是业务部门划分,如何交流;偏向页面还是业务逻辑
个人在自学过程中发现前端体系太大,不知对于前端学习,您有什么建议么
面试官:很多前端初学者都有类似的困惑,你最好自己从头开始做一个项目,不管是自己瞎想的还是模仿的,在项目过程中去发现需要学习什么技术,在遇到问题的时候才知道去哪个方向发展和思考,只有在项目中才能持续的学习和提高,前端知识很碎,没有项目很难有一连串的经历
-
总体
总体来说,面试中有按照面试题出的,也有直接聊的,一般也会结合实际工作中会遇到的场景以及技术中的一些坑,回答时结合自己的项目经验会更好,大厂的面试官更侧重于面试者对深层原理的理解,对于实习生来说一般面基础,如果有深查原理的习惯,个人的可塑造性也会较高
三厂体验对比:
- 腾讯阿里面试官一面开始都比较侧重实践,如果简历上有过实践项目和实习经历,会问更实际的问题,构造场景让解答
协议都会问到
相对来说,百度更多基础知识点,更多考察对更基本的知识掌握,不限于前端,还包括组成原理和编译原理的一些知识,当然前端偏多(比如选取一个class 标签元素有几种方式等小细节的问题来考察,细到要把每个表达说完整,把每个单词拼出来)
阿里腾讯更侧重应用中的注意事项(如:IE 和其他浏览器中的事件处理机制)不太揪细节
三厂都有问到算法,腾讯相对更注重对算法和逻辑,对面试者基础知识要求较高,甚至涉及更底层的。
另两厂对算法,数据结构的要求都是了解阶段
以下为面试中的一些知识点以及个人的一些补充,敲黑板啦啦啦
1. Tencent
1.1. js 的事件机制
事件阶段
一般的,事件分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
-
捕获阶段(Capture Phase)
- 事件的第一个阶段是捕获阶段。事件从文档的根节点流向目标对象节点。途中经过各个层次的DOM节点,并在各节点上触发捕获事件,直到到达事件的目标节点。捕获阶段的主要任务是建立传播路径,在冒泡阶段,事件会通过这个路径回溯到文档跟节点。
-
或这样描述:
任何事件产生时,如点击一个按钮,将从最顶端的容器开始(一般是html的根节点)。浏览器会向下遍历DOM树直到找到触发事件的元素,一旦浏览器找到该元素,事件流就进入事件目标阶段
-
目标阶段(Target Phase)
- 当事件到达目标节点的,事件就进入了目标阶段。事件在目标节点上被触发,然后会逆向回流,直到传播至最外层的文档节点。
- 冒泡阶段(Bubble Phase)
- 事件在目标元素上触发后,并不在这个元素上终止。它会随着DOM树一层层向上冒泡,回溯到根节点。
- 冒泡过程非常有用。它将我们从对特定元素的事件监听中释放出来,如果没有事件冒泡,我们需要监听很多不同的元素来确保捕获到想要的事件
事件处理程序
-
DOM0 级事件处理程序
var btn5 = document.getElementById('btn5'); btn5.onclick=function(){ console.log(this.id);//btn5 };
-
基于 DOM0 的事件,对于同一个 dom 节点而言,只能注册一个,后边注册的 同种事件 会覆盖之前注册的。
利用这个原理我们可以解除事件,
btn5.onclick=null;
其中this就是绑定事件的那个元素; 以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理;
-
- DOM2 级事件处理程序
- DOM2支持同一dom元素注册多个同种事件,事件发生的顺序按照添加的顺序依次触发(IE是相反的)。
-
DOM2事件通过
addEventListener
和removeEventListener
管理// addEventListener(eventName,handlers,boolean);removeEventListener() // 两个方法都一样接收三个参数,第一个是要处理的事件名,第二个是事件处理程序, // 第三个值为false时表示在事件冒泡阶段调用事件处理程序,一般建议在冒泡阶段使用, // 特殊情况才在捕获阶段; // 注意:通过addEventListener()添加的事件处理程序只能用removeEventListener()来移除 // 并且移除时传入的参数必须与添加时传入的参数一样;比如 var btn2 = document.getElementById('btn2'); var handlers = function () { console.log(this.id); }; btn2.addEventListener('click',handlers,false); btn2.removeEventListener('click',handlers.false);
-
ie 事件处理程序
//IE事件处理程序(IE和Opera支持) /* IE用了attachEvent(),detachEvent(),接收两个参数,事件名称和事件处理程序, * 通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段,所以平时为了兼容更多的浏览器最好将事件添加到事件冒泡阶段,IE8及以前只支持事件冒泡; */ var btn3 = document.getElementById('btn3'); var handlers2=function(){ console.log(this===window); // true,注意attachEvent()添加的事件处理程序运行在全局作用域; }; btn3.attachEvent('onclick',handlers2);
---ie 与其他浏览器的区别
总结
DOM事件模型中的事件对象常用属性:
- type用于获取事件类型
- target获取事件目标
- stopPropagation()阻止事件冒泡
- preventDefault()阻止事件默认行为
- 判断加载状态 —— onload 事件
IE事件模型中的事件对象常用属性:
- type用于获取事件类型
- srcElement获取事件目标
- cancelBubble 阻止事件冒泡
- returnValue 阻止事件默认行为
-
通过 readystate 属性值判断何时方法下载完毕可用
readystate共有以下几个值:
- uninitialized: 对象存在但未初始化;
- loading:对象正在加载;
- loaded:对象数据加载完毕;
- interactive:可以操作对象了,但还没加载完毕;
- complete:加载完毕。
注意上面5个值并不一定每个事件都全包含,并且不一定是什么顺序。
Document.readyState 属性
一个文档的 readyState 可以是以下之一:-
loading / 加载
document 仍在加载。
-
interactive / 互动
文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载。
-
complete / 完成
T文档和所有子资源已完成加载。状态表示 load 事件即将被触发。
当这个属性的值变化时,document 对象上的readystatechange 事件将被触发。
事件对象
-
IE
IE中事件对象是作为全局对象
window.event
存在的 -
Firefox
Firefox中则是做为句柄( handler )的第一个参数传入
-
通用
var evt = window.event || arguments[0];
事件监听
-
Chrome、FireFox、Opera、Safari、IE9.0及其以上版本
addEventListener(eventName,handler,boolean); removeEventListener() /* 两个方法都一样接收三个参数, * 事件名 * 事件处理程序 * boolean false时表示在事件冒泡阶段调用事件处理程序,一般建议在冒泡阶段使用 */
-
IE8.0及其以下版本
element.attachEvent(type, handler); element.detachEvent(type, handler); /* element 要绑定事件的对象,html 节点 * type 事件类型 +'on' 如: "onclick, onmouseover" * listener 事件处理程序(只写函数名,不带括号) */
早期浏览器
obj['on' + type] = handler
阻止冒泡
event.stopPropagation
event.cancelBubble = true //IE
阻止默认事件
event.preventDefault()
event.returnValue = false //IE
---通用的事件监听器
// event(事件)工具集,来源:github.com/markyun
markyun.Event = {
// 页面加载完成后
readyEvent: function (fn) {
if (fn == null) {
fn = document;
}
var oldonload = window.onload;
if (typeof window.onload != 'function') {
window.onload = fn;
} else {
window.onload = function () {
oldonload();
fn();
};
}
},
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 参数: 操作的元素,事件名称 ,事件处理程序
addEvent: function (element, type, handler) {
if (element.addEventListener) {
//事件类型、需要执行的函数、是否捕捉
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, function () {
handler.call(element);
});
} else {
element['on' + type] = handler;
}
},
// 移除事件
removeEvent: function (element, type, handler) {
if (element.removeEnentListener) {
element.removeEnentListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + type, handler);
} else {
element['on' + type] = null;
}
},
// 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
stopPropagation: function (ev) {
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true;
}
},
// 取消事件的默认行为
preventDefault: function (event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
// 获取事件目标
getTarget: function (event) {
return event.target || event.srcElement;
},
// 获取event对象的引用,取到事件的所有信息,确保随时能使用event;
getEvent: function (e) {
var ev = e || window.event;
if (!ev) {
var c = this.getEvent.caller;
while (c) {
ev = c.arguments[0];
if (ev && Event == ev.constructor) {
break;
}
c = c.caller;
}
}
return ev;
}
};
1.2. vue
---Vue 生命周期
---双向绑定原理 & 如何实现
---vuex 原理
---vue 数据更新后执行
1.3. 跨域
什么叫跨域
方案
1.4. 安全 && 怎样预防
1.5. session & cookie
1.6. 本地存储
1.7. 浏览器缓存
1.8. 页面从输入URL 到加载过程
1.9. HTTP
---content-type
-
application/x-www-form-urlencoded
最常见的 POST 提交数据的方式了。浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded方式提交数据。
传递的key/val会经过URL转码,所以如果传递的参数存在中文或者特殊字符需要注意。
//例子 //b=曹,a=1 POST HTTP/1.1(CRLF) Host: www.example.com(CRLF) Content-Type: application/x-www-form-urlencoded(CRLF) Cache-Control: no-cache(CRLF) (CRLF) b=%E6%9B%B9&a=1(CRLF) //这里b参数的值"曹"因为URL转码变成其他的字符串了
-
multipart/form-data
常见的 POST 数据提交的方式。我们使用表单上传文件时,必须让 form 的 enctyped 等于这个值
并且Http协议会使用boundary来分割上传的参数 -
text/xml
<!-- 例子 --> POST http://www.example.com HTTP/1.1(CRLF) Content-Type: text/xml(CRLF) (CRLF) <?xml version="1.0"?> <resource> <id>123</id> <params> <name> <value>homeway</value> </name> <age> <value>22</value> </age> </params> </resource>
-
application/json
用来告诉服务端消息主体是序列化后的 JSON 字符串
//例子 //传递json POST HTTP/1.1(CRLF) Host: www.example.com(CRLF) Content-Type: application/json(CRLF) Cache-Control: no-cache(CRLF) Content-Length: 24(CRLF) (CRLF) { "a":1, "b":"hello" }
(CRLF)指 \r\n
参考: HTTP常见Content-Type比较
1.10. get & post
1.11. TCP & UDP & 握手
---TCP (Transmission Control Protocol)
两个序号和三个标志位:
- 序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
- 确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。
标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
- URG:紧急指针(urgent poin* 有效。
- ACK:acknowledgement 确认序号有效。
- PSH:接收方应该尽快将这个报文交给应用层。
- RST:reset 重置连接。
- SYN:synchronous 建立联机,发起一个新连接。
- FIN:finish 释放一个连接。
需要注意的是:
- 不要将确认序号ack与标志位中的ACK搞混了。
- 确认方ack=发起方req+1,两端配对。
- 第一次握手:
Client将标志位SYN置为1,随机产生一个值 seq=J,并将该数据包发送给Server
Client进入SYN_SENT状态,等待Server确认。 - 第二次握手:
Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。 - 第三次握手:
Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了
为什么需要确认
---四次挥手
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN
首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
- 第一次挥手:
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。 - 第二次挥手:
Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。 - 第三次挥手:
Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。 - 第四次挥手:
Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
- 可靠的实现 TCP 全双工连接的终止
- 允许老的重复分节在网络中消失
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态
为什么连接的时候是三次握手,关闭的时候却是四次握手?
- 因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时
- 当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
参考: TCP三次握手详解及释放连接过程
1.12. HTTP 加密
---加密对象
对于HTTP协议来说,加密的对象有以下两个:
- 对通信的加密:
HTTP中没有加密功能,但是可以通过和SSL(Secure Socket Layer,安全套接层)组合使用,加密通信内容。使用SSL建立安全通信线路后,就可以在这条线路上进行HTTP通信了。与SSL组合使用的HTTP被称为HTTPS(HTTP Secure,超文本传输安全协议)。 - 对通信内容本身进行加密
即对HTTP报文里所包含的内容进行加密。这样,首先客户端要先对报文进行加密,然后再发给服务器。服务器在接受到请求时,需要对报文进行解密,再处理报文。该方式不同于SSL将整个通信线路进行加密处理,所以内容仍然有被篡改的风险。- A、任何人都可以发起请求
HTTP协议中,并未有确认通信方这一步骤,所以,任何人都可以发送请求,而服务器在接受到任何请求时,都会做出相应的响应。-
解决方案:
查明对手的证书
虽然HTTP不能确认通信方,但SSL是可以的。SSL不仅提供了加密处理,还使用了"证书"的手段,可用于确认通信方。证书是由值得信赖的第三方机构颁布,可用于确定证明服务器和客户端是实际存在的。所以,只要能确认通信方持有的证书,即可判断通信方的真实意图。
-
-
B、无法判断报文是否完整(报文可能已遭篡改)
HTTP协议无法判断报文是否被篡改,在请求或者响应发出后,在对方接收之前,即使请求或者响应遭到篡改是无法得知的。
-
防止篡改:
常用的,确定报文完整性方法:MD5、SHA-1 等 散列值校验方法,以及用来确认文件的数字签名方法。但是,使用这些方法,也无法百分百确保结果正确,因为MD5本身被修改的话,用户是没办法意识到得。
-
为了有效防止这些弊端,可以采用HTTPS。
- A、任何人都可以发起请求
-
POST 用户安全登陆
在关系到用户隐私的时候,要时刻遵循两个原则:
- 任何应用程序都不能在本地存储与安全相关的用户信息
- 任何应用程序在向服务器传递数据的时候,都不能直接传递与安全相关的用户信息。
要想让用户信息安全,就必须对其进行加密,让别人即便是拿到了安全信息,摆在眼前的也是一串乱码,没有半点用处
MD5是一种常用的加密方法,它是一种散列函数,利用MD5对用户信息进行加密,会增加用户信息安全性。
网上有关于MD5的第三方框架Category
利用这个第三方框架可以实现对密码进行MD5加密
+随机乱码字符防止被破解
---加密算法
对称加密
非对称加密
---HTTPS 加密原理
HTTPS简介
HTTPS其实是有两部分组成:HTTP + SSL / TLS,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据
SSL协议是通过非对称密钥机制保证双方身份认证,并完成建立连接,在实际数据通信时通过对称密钥机制保障数据安全性
服务器 用RSA生成公钥和私钥
把公钥放在证书里发送给客户端,私钥自己保存
客户端首先向一个权威的服务器检查证书的合法性,如果证书合法,客户端产生一段随机数,这个随机数就作为通信的密钥,我们称之为对称密钥,用公钥加密这段随机数,然后发送到服务器
服务器用密钥解密获取对称密钥,然后,双方就已对称密钥进行加密解密通信了
HTTPS 在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL 协议不仅仅是一套加密传输的协议,更是一件经过艺术家精心设计的艺术品,TLS/SSL 中使用了非对称加密,对称加密以及 HASH 算法。握手过程的具体描述如下:
- 浏览器将自己支持的一套加密规则发送给网站。
- 网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
-
浏览器获得网站证书之后浏览器要做以下工作:
a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。
b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
c) 使用约定好的HASH算法计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。
-
网站接收浏览器发来的数据之后要做以下的操作:
a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
b) 使用密码加密一段握手消息,发送给浏览器。
浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。
参考:图解HTTPS
如何保证 HTTP 传输安全性
-
重要的数据,要加密
比如用户名密码(如果简单的md5,是可以暴力破解),常见的是 md5(不可逆),aes(可逆),*组合,还可以加一些特殊字符
举例:
username = aes(username), pwd = MD5(pwd + username)
-
非重要数据,要签名
签名的目的是为了防止篡改,比如
http://www.xxx.com/getnews?id=1
,获取id为1的新闻,如果不签名那么通过id=2,就可以获取2的内容等等。怎样签名呢?通常使用sign,比如原链接请求的时候加一个 sign 参数,sign=md5(id=1),服务器接受到请求,验证sign是否等于 md5(id=1) ,如果等于说明正常请求。
这会有个弊端,假如规则被发现,那么就会被伪造,所以适当复杂一些,还是能够提高安全性的。
-
登录态怎么做
http是无状态的,也就是服务器没法自己判断两个请求是否有联系,那么登录之后,以后的接口怎么判定是否登录呢
简单的做法,在数据库中存一个token字段(名字随意),当用户调用登陆接口成功的时候,就将该字段设一个值,(比如aes(过期时间)),同时返回给前端,以后每次前端请求带上该值,服务器首先校验是否过期,其次校验是否正确,不通过就让其登陆。(redis 做这个很方便哦,key有过期时间)
1.13. 排序:冒泡,选择,快速
1.14. 数据库
---触发器
一种特殊的存储过程,存储过程一般通过定义的名字直接调用,而触发器是通过增、删、改进行触发执行的。会在事件发生时自动强制执行
触发器是一种特殊的存储过程,主要是通过事件来触发而被执行的。它可以强化约束,来维护数据的完整性和一致性,可以跟踪数据库内的操作从而不允许未经许可的更新和变化。可以联级运算。如,某表上的触发器上包含对另一个表的数据操作,而该操作又会导致该表触发器被触发。
---事务 & 锁
-
事务
就是被绑定在一起作为一个逻辑工作单元的SQL语句分组,如果任何一个语句操作失败那么整个操作就被失败,以后操作就会回滚到操作前状态,或者是上有个节点。
为了确保要么执行,要么不执行,就可以使用事务。要将一组语句作为事务考虑,就需要通过ACID测试,即原子性,一致性,隔离性和持久性。
-
锁:
在所有的DBMS中,锁是实现事务的关键,锁可以保证事务的完整性和并发性。与现实生活中锁一样,它可以使某些数据的拥有者,在某段时间内不能使用某些数据或数据结构。当然锁还分级别的。共享锁(只读不写)、排他锁(可读可写)
1.15. 软件设计模式
设计原则
- 对接口编程而不是对实现编程
- 优先使用对象组合而不是继承
---单例模式
单体是一个用来划分命名空间并将一批相关的属性和方法组织在一起的对象,如果他可以被实例化,那么他只能被实例化一次
// 对象字面量
var Singleton = {
attr1: 1,
attr2: 2,
method1: function(){
return this.attr1;
},
method2: function(){
return this.attr2;
}
};
// 上面的所有成员变量都是通过Singleton来访问的,但是它并不是单体模式;
// 因为单体模式还有一个更重要的特点,就是可以仅被实例化一次,上面的只是不能被实例化的一个类,因此不是单体模式;对象字面量是用来创建单体模式的方法之一;
/*要实现一个单体模式的话,我们无非就是使用一个变量来标识该类是否被实例化
如果未被实例化的话,那么我们可以实例化一次,否则的话,直接返回已经被实例化的对象
*/
// 单体模式
var Singleton = function(name){
this.name = name;
this.instance = null;
};
Singleton.prototype.getName = function(){
return this.name;
}
// 获取实例对象
function getInstance(name) {
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance;
}
// 测试单体模式的实例
var a = getInstance("aa");
var b = getInstance("bb");
console.log(a === b) // true
console.log(a.getName()) // aa
console.log(b.getName()) // aa
应用案例
-
弹窗
传统创建:比如我点击一个元素需要创建一个div,我点击第二个元素又会创建一次div,我们频繁的点击某某元素,他们会频繁的创建div的元素,虽然当我们点击关闭的时候可以移除弹出代码,但是呢我们频繁的创建和删除并不好,特别对于性能会有很大的影响,对DOM频繁的操作会引起重绘等,从而影响性能;因此这是非常不好的习惯;我们现在可以使用单体模式来实现弹窗效果,我们只实例化一次就可以
编写通用的单体模式
我们使用一个参数fn传递进去,如果有result这个实例的话,直接返回,否则的话,当前的getInstance函数调用fn这个函数,是this指针指向与这个fn这个函数;之后返回被保存在result里面;现在我们可以传递一个函数进去,不管他是创建div也好,还是创建iframe也好,总之如果是这种的话,都可以使用getInstance来获取他们的实例对象;
// 创建div
var createWindow = function(){
var div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild(div);
return div;
};
// 创建iframe
var createIframe = function(){
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
return iframe;
};
// 获取实例的封装代码
var getInstance = function(fn) {
var result;
return function(){
return result || (result = fn.call(this,arguments));
}
};
// 测试创建div
var createSingleDiv = getInstance(createWindow);
document.getElementById("Id").onclick = function(){
var win = createSingleDiv();
win.style.display = "block";
};
// 测试创建iframe
var createSingleIframe = getInstance(createIframe);
document.getElementById("Id").onclick = function(){
var win = createSingleIframe();
win.src = "http://cnblogs.com";
};
---以下为补充
---工厂模式
客户类和工厂类分开。消费者任何时候需要某种产品,只需向工厂请求即可。消费者无须修改就可以接纳新产品。
工厂模式是为了解决多个类似对象声明的问题;也就是为了解决实列化对象产生重复的问题。
- 优点:能解决多个相似的问题。
- 缺点:
- 不能知道对象识别的问题(对象的类型不知道)。
- 当产品修改时,工厂类也要做相应的修改。
function CreatePerson(name,age,sex) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sex = sex;
obj.sayName = function(){
return this.name;
}
return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
---模块模式
模块模式的思路是为单体模式添加私有变量和私有方法能够减少全局变量的使用
prototype + constructor
---装饰者模式
装饰者(decorator)模式能够在不改变对象自身的基础上,在程序运行期间给对像动态的添加职责(方法或属性)。
与继承相比,装饰者是一种更轻便灵活的做法。
可以动态的给某个对象添加额外的职责,而不会影响从这个类中派生的其它对象。
// ES7装饰器
function isAnimal(target) {
target.isAnimal = true
return target
}
// 装饰器
@isAnimal
class Cat {
// ...
}
console.log(Cat.isAnimal) // true
// 作用于类属性的装饰器:
function readonly(target, name, descriptor) {
discriptor.writable = false
return discriptor
}
class Cat {
@readonly
say() {
console.log("meow ~")
}
}
var kitty = new Cat()
kitty.say = function() {
console.log("woof !")
}
kitty.say() // meow ~
---观察者模式(发布-订阅)
发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知
发布订阅模式的流程如下:
- 确定谁是发布者(比如我的博客)。
- 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者。
- 发布消息,发布者需要遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
- 退订(比如不想再接收到这些订阅的信息了,就可以取消掉)
【实现事件模型】
即写一个类或是一个模块,有两个函数,一个bind一个trigger,分别实现绑定事件和触发事件,核心需求就是可以对某一个事件名称绑定多个事件响应函数,然后触发这个事件名称时,依次按绑定顺序触发相应的响应函数。
大致实现思路就是创建一个类或是匿名函数,在bind和trigger函数外层作用域创建一个字典对象,用于存储注册的事件及响应函数列表,bind时,如果字典没有则创建一个,key是事件名称,value是数组,里面放着当前注册的响应函数,如果字段中有,那么就直接push到数组即可。trigger时调出来依次触发事件响应函数即可
var Event = (function(){
var list = {},
listen,
trigger,
remove;
listen = function(key,fn){
if(!list[key]) {
list[key] = [];
}
list[key].push(fn);
};
trigger = function(){
var key = Array.prototype.shift.call(arguments),
fns = list[key];
if(!fns || fns.length === 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];) {
fn.apply(this,arguments);
}
};
remove = function(key,fn){
var fns = list[key];
if(!fns) {
return false;
}
if(!fn) {
fns && (fns.length = 0);
}else {
for(var i = fns.length - 1; i >= 0; i--){
var _fn = fns[i];
if(_fn === fn) {
fns.splice(i,1);
}
}
}
};
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
// 测试代码如下:
Event.listen("color",function(size) {
console.log("尺码为:"+size); // 打印出尺码为42
});
Event.trigger("color",42);
---代理模式
代理是一个对象,它可以用来控制对本体对象的访问,它与本体对象实现了同样的接口,代理对象会把所有的调用方法传递给本体对象的
本地对象注重的去执行页面上的代码,代理则控制本地对象何时被实例化,何时被使用
优点:
- 代理对象可以代替本体被实例化,并使其可以被远程访问;
- 它还可以把本体实例化推迟到真正需要的时候;对于实例化比较费时的本体对象,或者因为尺寸比较大以至于不用时不适于保存在内存中的本体,我们可以推迟实例化该对象;
// 先申明一个奶茶妹对象
var TeaAndMilkGirl = function(name) {
this.name = name;
};
// 这是京东ceo先生
var Ceo = function(girl) {
this.girl = girl;
// 送结婚礼物 给奶茶妹
this.sendMarriageRing = function(ring) {
console.log("Hi " + this.girl.name + ", ceo送你一个礼物:" + ring);
}
};
// 京东ceo的经纪人是代理,来代替送
var ProxyObj = function(girl){
this.girl = girl;
// 经纪人代理送礼物给奶茶妹
this.sendGift = function(gift) {
// 代理模式负责本体对象实例化
(new Ceo(this.girl)).sendMarriageRing(gift);
}
};
// 初始化
var proxy = new ProxyObj(new TeaAndMilkGirl("奶茶妹"));
proxy.sendGift("结婚戒"); // Hi 奶茶妹, ceo送你一个礼物:结婚戒
- TeaAndMilkGirl 是一个被送的对象(这里是奶茶妹);
- Ceo 是送礼物的对象,他保存了奶茶妹这个属性,及有一个自己的特权方法sendMarriageRing 就是送礼物给奶茶妹这么一个方法;
- 然后呢他是想通过他的经纪人去把这件事完成,于是需要创建一个经济人的代理模式,名字叫ProxyObj ;
- 他的主要做的事情是,把ceo交给他的礼物送给ceo的情人,因此该对象同样需要保存ceo情人的对象作为自己的属性,同时也需要一个特权方法sendGift ,该方法是送礼物,因此在该方法内可以实例化本体对象,这里的本体对象是ceo送花这件事情,因此需要实例化该本体对象后及调用本体对象的方法(sendMarriageRing).
理解使用虚拟代理实现图片的预加载
在网页开发中,图片的预加载是一种比较常用的技术,如果直接给img标签节点设置src属性的话,如果图片比较大的话,或者网速相对比较慢的话,那么在图片未加载完之前,图片会有一段时间是空白的场景,这样对于用户体验来讲并不好,那么这个时候我们可以在图片未加载完之前我们可以使用一个loading加载图片来作为一个占位符,来提示用户该图片正在加载,等图片加载完后我们可以对该图片直接进行赋值即可;下面我们先不用代理模式来实现图片的预加载的情况下代码如下:
// 不使用代理的预加载图片函数如下
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
var img = new Image();
img.onload = function(){
imgNode.src = this.src;
};
return {
setSrc: function(src) {
imgNode.src = "http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif";
img.src = src;
}
}
})();
// 调用方式
myImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");
//利用代理模式来编写预加载图片
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
// 代理模式
var ProxyImage = (function(){
var img = new Image();
img.onload = function(){
myImage.setSrc(this.src);
};
return {
setSrc: function(src) {
myImage.setSrc("http://img.lanrentuku.com/img/allimg/1212/5-121204193Q9-50.gif");
img.src = src;
}
}
})();
// 调用方式
ProxyImage.setSrc("https://img.alicdn.com/tps/i4/TB1b_neLXXXXXcoXFXXc8PZ9XXX-130-200.png");
这种懒加载方法不用代理模式也是可以实现的,只是用代理模式。我们可以让 myImage 只做一件事,只负责将实际图片加入到页面中,而loading图片交给ProxyImage去做。从而降低代码的耦合度。因为当我不想用loading的时候,可以直接调用myImage 方法。也即是说假如我门不需要代理对象的话,直接可以换成本体对象调用该方法即可
对比
- 不代理:不满足单一职责原则,代码耦合度高
- myimage 函数只负责一件事,其他交给代理
优点
- 用户可以放心地请求代理,他们只关心是否能得到想要的结果。假如我门不需要代理对象的话,直接可以换成本体对象调用该方法即可。
- 在任何使用本体对象的地方都可以替换成使用代理。
1.2. Online Coding
js 实现两个超大数相加
基础:
- JS 中所有的数字类型,实际存储都是通过 8 字节 double 浮点型 表示的,并不是能够精确表示范围内的所有数
- 大整数存储(安全使用范围)
- 其他语言
2^63 - 1
-
js
Math.pow(2, 53) - 1
//js 最大和最小安全值 Number.MAX_SAFE_INTEGER //9007199254740991 Number.MIN_SAFE_INTEGER //-9007199254740991
- 其他语言
var largeNumberAdd = function(num1, num2) {
var arr1 = num1.split(''),
arr2 = num2.split(''),
tem = '',
num3 = 0,
result = []
var longDiff = arr1.length - arr2.length
if (longDiff > 0) {
for (let i = 0; i < longDiff; i++) {
arr2.unshift('0')
}
}else if (longDiff < 0) {
for (let i = 0; i < Math.abs(longDiff); i++) {
arr1.unshift('0')
}
}
for (let i = arr1.length - 1; i >= 0; i--) {
tem = parseInt(arr1[i]) + parseInt(arr2[i]) + num3
// check if tem > 10
if (tem >= 10) {
num3 = 1
result.push((tem + '')[1])
}else {
num3 = 0
result.push(tem)
}
}
return result.reverse().join('')
}
// console.log(largeNumberAdd('11111','11111'))
console.log(largeNumberAdd('00000000000000000000011111','333331999'))
console.log(11111+333331999)
// console.log(largeNumberAdd('3333333333333333333333333333333311111111111111111111111111111111111111','333333333333333331111111111111111111111111111166666666666666'))
js 每秒钟的计算量
js 如何解析后台返回的超大数据
前提:
-
js 用浮点数表示所有64位数字,所有达到 2^53 的可以被精确表示,更大的数字都会被裁剪,——如何表示64位数字
虽然js 能够解析进制数字表示64位数字,但底层的数字表示不支持 64 位
在浏览器中执行以下代码<html> <head> <script language="javascript"> function showPrecisionLimits() { document.getElementById("r50").innerHTML = 0x0004000000000001 - 0x0004000000000000; document.getElementById("r51").innerHTML = 0x0008000000000001 - 0x0008000000000000; document.getElementById("r52").innerHTML = 0x0010000000000001 - 0x0010000000000000; document.getElementById("r53").innerHTML = 0x0020000000000001 - 0x0020000000000000; document.getElementById("r54").innerHTML = 0x0040000000000001 - 0x0040000000000000; } </script> </head> <body onload="showPrecisionLimits()"> <p>(2^50+1) - (2^50) = <span id="r50"></span></p> <p>(2^51+1) - (2^51) = <span id="r51"></span></p> <p>(2^52+1) - (2^52) = <span id="r52"></span></p> <p>(2^53+1) - (2^53) = <span id="r53"></span></p> <p>(2^54+1) - (2^54) = <span id="r54"></span></p> </body> </html>
在Firefox,Chrome和IE浏览器中,可以看到,如果能够存储64位数字,则以下减法结果皆为1。而结果相反,可以看到2 ^ 53 + 1和2 ^ 53 间的差异丢失
(2 ^ 50 + 1) - (2 ^ 50)= 1 (2 ^ 51 + 1) - (2 ^ 51)= 1 (2 ^ 52 + 1) - (2 ^ 52)= 1 (2 ^ 53 + 1) - (2 ^ 53)= 0 (2 ^ 54 + 1) - (2 ^ 54)= 0
位运算
因此,我们可以选择用两个 32 位的数字表示 64 位整数,然后进行按位与var a = [ 0x0000ffff, 0xffff0000 ]; var b = [ 0x00ffff00, 0x00ffff00 ]; var c = [ a[0] & b[0], a[1] & b[1] ]; document.body.innerHTML = c[0].toString(16) + ":" + c[1].toString(16); //结果 ff00:ff0000
网络安全
前端网络安全的实现
- 保证http传输安全性
如何检测恶意脚本
如何屏蔽
2. 阿里
一面
- 简单介绍一下自己
- 问题
- HTTP 相关
HTTP 与 HTTPS 区别,HTTPS 原理,如何加密 - 谈谈闭包
- 谈谈作用域链
- 常用的跨域方式
- vue
- 特点
- 生命周期
- vuex
实现原理(事件绑定)-如何实现 - 与 react 的不同
应用场景 - 如何实现双向绑定
- 如何通信
- HTTP 相关
- 其他
- 还会哪些语言
- 还有什么感觉没问到但掌握很好的
- 是否还有其他的问题
- 有什么问题想要问的
二面
- react
多次调用 setstate 为什么不马上渲染 - 解析 json ,手写 parse 函数
- csrf 举例
- 获取dom节点方式
- 将给定节点子元素第一个和最后一个元素替换
- 端口号的作用
端口
IP地址让网络上的两个节点之间可以建立点对点的连接
端口号则为端到端的连接提供了可能 (程序间通讯的接口)
IP协议是由TCP、UDP、ARP、ICMP等一系列子协议组成的。其中
-
TCP和UDP协议
主要用来做传输数据使用的
在TCP和UDP协议中,都有端口号的概念存在
-
端口号的作用
主要是区分服务类别和在同一时间进行多个会话
-
服务类别
-
举例来说,有主机A需要对外提供FTP和WWW两种服务,如果没有端口号存在的话,这两种服务是无法区分的。
实际上,当网络上某主机B需要访问A的FTP服务时,就要指定目的端口号为21;
当需要访问A的WWW服务时,则需要将目的端口号设为80,这时A根据B访问的端口号,就可以区分B的两种不同请求
-
- 多个会话
- 主机A需要同时下载网络上某FTP服务器B上的两个文件,那么A需要 与B同时建立两个会话,而这两个传输会话就是靠源端口号来区分的。在这种情况下如果没有源端口号的概念,那么A就无法区分B传回的数据究竟是属于哪个会话,属于哪个文件
- 通信过程是,A使用本机的1025号端口请求B的21号端口上的文件1,同时又使用1026号端口请求文件2。对于返回的数据,发现是传回给1025号端口的,就认为是属于文件1;传回给1026号端口的,则认为是属于文件2。这就是端口号区分多个会话的作用。
-
在线编程——编写一个 css 层级选择器
根据一个给定的元素生成一个css 选择器,函数名为genCssSelector ,
点击某元素弹出该元素及其父元素,类似 querySelector
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Document</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script language="javaScript">
// your code here
var genCssSelector = function (e) {
e = e || window.event
var tar = e.target || e.srcElement
var objArr = []
while (tar) {
if (tar.id) {
objArr.push('#' + tar.id)
console.log('id')
return objArr.reverse().join(' ') // 考虑 id 的唯一性,如果有 id,则停止查找
}else if (tar.className) {
objArr.push('.' + tar.className.split(' ')[0]) // 考虑如果有多个 class
}else {
objArr.push(tar.nodeName.toLowerCase())
}
tar = tar.parentNode
}
objArr.pop()
return objArr.reverse().join(' ')
}
document.addEventListener('click', function (e) {
//点击li时,返回:html body #page .content.main .refer ul li
console.log(genCssSelector(e));
})
</script>
</head>
<body>
<div id="page">
<div class="main" id="main">
<div class="reference refer">
<ul>
<li></li>
<li></li>
23333
</ul>
</div>
</div>
</div>
</body>
</html>
获取 dom 元素
JS获取DOM元素的方法(8种)
-
getElementById
只获取到一个元素,没有找到返回null
getElementsByName
getElementsByTagName
getElementsByClassName
-
document.documentElement
获取html
-
document.body
获取body
-
querySelector
获取一个元素
-
querySelectorAll
获取一组元素
获取子元素
- childNodes
dom.childNodes
返回一个nodeList(元素的所有子元素)- nodeType
- 元素节点的nodeType属性值为1
- 属性节点的nodeType属性值为2
- 文本节点的nodeType属性值为3
-
nodeValue属性
获得和改变文本节点的值
- nodeType
- firstChild 第一个子元素
- lastChild
获取父、兄
parentNode
nextSibling
previousSbiling
创建元素
-
createDocumentFragment
创建一个dom片段
-
createElement
创建一个具体的元素
-
createTextNode
创建一个文本节点
增删改元素
appendChild
removeChild
replaceChild
insertBefore
如何用 JS 实现 JSON.parse
---eval
直接调用eval
var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之后得到的对象
原理
JSON 脱胎于 JS,同时也是 JS 的子集,所以能够直接交给 eval 运行
-
加上圆括号的目的是迫使eval函数在处理JavaScript代码的时候强制将括号内的表达式(expression)转化为对象,而不是作为语句(statement)来执行
例如对象字面量{},如若不加外层的括号,那么eval会将大括号识别为JavaScript代码块的开始和结束标记,那么{}将会被认为是执行了一句空语句
缺点
-
XSS 漏洞
如:参数 json 并非真正的 JSON 数据,而是可执行的 JS 代码
-
对参数 json 做校验,只有真正符合 JSON 格式,才能调用 eval
// 1. 用 4 个正则表达式分为两个阶段解决(包容ie 和safari 的regexp 引擎) // 2. 将 json 反斜杠替换为 '@' (non-json字符) // 3. 用 ']' 替换所有简单标记 // 4. 删除所有跟随冒号,逗号或文本开始的方括号 // 5. 如果只剩下 '] , { }' 则是安全的 var rx_one = /^[\],:{}\s]*$/; var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; var rx_four = /(?:^|:|,)(?:\s*\[)+/g; if ( rx_one.test( json .replace(rx_two, "@") .replace(rx_three, "]") .replace(rx_four, "") ) ) { var obj = eval("(" +json + ")"); }
---递归
第一种 eval 的方法,相当于一股脑儿把 JSON 字符串塞进去。
其实我们还可以手动逐个字符地扫描,然后进行判断,这就是第二种方法:递归
// 所谓递归,就是重复调用value 函数
value = function () {
// Parse a JSON value. It could be an object, an array, a string, a number,
// or a word.
white();
// 根据当前字符是什么,我们便能推导出后面应该接的是什么类型
switch (ch) {
case "{":
return object();
case "[":
return array();
case "\"":
return string();
case "-":
return number();
default:
return (ch >= "0" && ch <= "9")
? number()
: word();
}
};
// 调用核心的 next 函数,逐个读取字符
var next = function (c) {
// If a c parameter is provided, verify that it matches the current character.
if (c && c !== ch) {
error("Expected '" + c + "' instead of '" + ch + "'");
}
// Get the next character. When there are no more characters,
// return the empty string.
ch = text.charAt(at);
at += 1;
return ch;
};
- 对于常量token
false true null
进行匹配,不匹配返回错误
以 {"a":"1", "b":2}
为例
程序大致逻辑是:启动 → 首次调用 value() → 发现是 { → 原来是对象,走 object() → 通过 string() 得到 key 值为 "a" → 读取到冒号,哦,后面可能是对象、数组、布尔值等等,具体是什么,还得再次调用 value() 才知道 → ……
xml 解析
-
close tag
使用一个 nodeStack 栈,在 opentag 时推入节点,close tag 时检查当前节点是否和栈尾节点是否匹配,匹配则推出末尾节点
comment
react setState
-
setState 不保证同步
- 可能会为了性能收益批量执行
-
setState()
不会立刻改变this.state
,而是创建一个即将处理的 state 转变。在调用该方法之后访问this.state
可能会返回现有的值。
解决方案
-
使用回调函数
setState 方法接收一个 function 作为回调函数。这个回掉函数会在 setState 完成以后直接调用,这样就可以获取最新的 state
this.setState({ selection: value }, this.fireOnSelect)
-
setTimeout
在 setState 使用 setTimeout 来让 setState 先完成以后再执行里面内容this.setState({ selection: value }); setTimeout(this.fireOnSelect, 0);
- 造成不必要的渲染
-
shouldComponentUpdate
解决setState()
将总是触发一次重绘,除非在shouldComponentUpdate()
中实现了条件渲染逻辑 -
和渲染无关的状态尽量不要放在 state 中来管理
通常 state 中只来管理和渲染有关的状态 ,从而保证 setState 改变的状态都是和渲染有关的状态。这样子就可以避免不必要的重复渲染。其他和渲染无关的状态,可以直接以属性的形式保存在组件中,在需要的时候调用和改变,不会造成渲染。
-
不能很有效的管理所有的组件状态
3. 百度
一面
jsonp cors
css
- 分栏布局哪些方式
- 详细说下flex
js & jq
- 如何获取一个元素(js jq)
- 异步事件是如何发送的,常用机制
- 有哪些接收后台数据的方法
- ajax
- fetch
- jsonp
- websocket
- SSE
- event loop
- 喜欢 es6 的哪些属性
- 箭头函数与普通函数的不同
- 闭包
- 简述
- 应用
- 在循环中如何用其他方式代替闭包
vue
- 和 react 区别
- 如何向服务器传递数据
操作系统
- 线程 && 进程
计算机网络
- 除了tcp 还用到哪些
- http 与 tcp 分别属于第几层
数据结构
- 有哪些线性存储空间(array 栈)
二面
-
如何做一个 css 选择器
见本文 阿里-在线编程
-
给定一组dom 节点,和一个css样式表,找出不含有样式的dom
面试官很耐心的解释了,还是没听明白题目
- 链表操作
- 单向链表反转
-
居中问题
没有详细问,我分了两个方面分别回答
- 水平居中
- 垂直水平
详细见 CSS 居中
-
get & post
回答了大多数应聘者的 “标准答案”, 但经面试官指点,顿悟,大概这就叫高手吧
三面
-
数字的存储
- 正负数在计算机中是如何存储的
-
无符号与有符号二进制存储
- 前端的编码问题
- 种类
- 乱码如何处理
- 有哪些协议,分别有什么作用
- 关于实习经历,找实习的过程,项目中的二三事及解决方案
- 大学中最有成就感的事
闲聊了一下,关于保研等等,总共 四十分钟
分栏布局
---等分布局
- float
原理:增大父框的实际宽度后,使用CSS3属性box-sizing进行布局的辅助。
-
用法:先将父框设置为
margin-left: -*px
,再设置子框float: left、width: 25%、padding-left、box-sizing: border-box
.parent{ margin-left: -20px; } .column{ float: left; width: 25%; padding-left: 20px; box-sizing: border-box; /*包含padding区域 w+g*/ }
- table
原理:通过增加一个父框的修正框,增大其宽度,并将父框转换为 table,将子框转换为 tabel-cell 进行布局。
-
用法:先将父框的修正框设置为
margin-left: -*px
,再设置父框display: table、width:100%、table-layout: fixed
,设置子框display: table-cell、padding-left
.parent-fix{ margin-left: -20px; } .parent{ display: table; width:100%; table-layout: fixed; } .column{ display: table-cell; padding-left: 20px; }
- flex
原理:通过设置CSS3布局利器flex中的flex属性以达到等分布局。
用法:将父框设置为display: flex,再设置子框flex: 1,最后设置子框与子框的间距margin-left。
如何获取一个class
// 直接获取---需要高版本浏览器支持
document.querySelectorAll("div.aa")
// 类似属性选择器的写法
document.querySelectorAll("div[class='aa']")
// 补充一下还可以not选择器
document.querySelectorAll(".aa:not(ul)")
document.getElementsByClassName('cls')
// jq
$('.className')
Event Loop
js 单线程:
用途决定,操作 DOM
任务队列
排队原因:计算量大的同步执行,IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据)异步。
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
只要主线程空了,就去检查异步的任务队列,如果异步事件触发,则将其加到主线程的执行栈
深入了解定时器
-
零延迟
setTimeout(func, 0)
零延迟并不是意味着回调函数立刻执行。它取决于主线程当前是否空闲与“任务队列”里其前面正在等待的任务。
调用setTimeout()之后,该方法会返回一直数值ID,表示超时调用。这个超时调用ID是计划执行代码的唯一标识符,可以通过它来取消超时调用
超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,严格模式下是undefined。
Event Loop
异步与event loop没有太直接的关系,准确的来讲event loop 只是实现异步的一种机制
主任务 ——> micro task ——> 渲染视图 ——> macro task
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
Javascript 中的事件循环是以任务为单位的,将很多个待执行的任务串联在一起就形成了队列 Task Queue,很多的队列先后按顺序执行任务就形成了 Event Loop
一个事件循环(EventLoop)中会有一个正在执行的任务(Task),而这个任务就是从 macrotask 队列中来的。
当这个 macrotask 执行结束后,所有可用的 microtask 将会在同一个事件循环中执行
当这些 microtask 执行结束后还能继续添加 microtask 一直到真个 microtask 队列执行结束。
-
一个事件循环(event loop)会有一个或多个任务队列(task queue)
task queue 就是 macrotask queue
- 每一个 event loop 都有一个 microtask queue
- task queue == macrotask queue != microtask queue
- 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
当一个 task 被放入队列 queue(macro或micro) 那这个 task 就可以被立即执行了
Micro Task
当我们想以同步的方式来处理异步任务时候就用 microtask(比如我们需要直接在某段代码后就去执行某个任务,就像Promise一样)
- process.nextTick
- promise
- Object.observe
- MutationObserver
Macro Task
- setTimeout
- setInterval
- setImmediate
- I/O
任务队列中,在每一次事件循环中,从 macrotask 队列开始执行,macrotask只会提取一个执行,而microtask会一直提取,直到microsoft队列为空为止。
也就是说如果某个microtask任务被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个macrotask,主线程执行完成该任务后又会检查microtasks 队列并完成里面的所有任务后再执行macrotask的任务。
执行过程如下:
- 主线程空闲时首先执行 micro
- 之后从 macro 中提取一个 task 到主任务,完成后再次执行 micro queue(执行一个cycle)
- 反复过程2, 每个周期为一个事件循环
为啥要用 microtask?
- micro 执行总在 macro 之前
- micro 全部执行完毕后会更新 UI 和执行下一个macro
根据HTML Standard,在每个 task 运行完以后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就可以得到最新的 UI 了
// 验证
(function () {
const $test = document.getElementById('test')
let counter = 0
function func1() {
$test.innerText = ++counter
alert('func1')
}
function func2() {
$test.innerText = ++counter
alert('func2')
}
function func3() {
$test.innerText = ++counter
alert('func3')
}
function func4() {
$test.innerText = ++counter
alert('func4')
}
(function () {
// main task
func1()
// macro task
setTimeout(() => {
func2()
// micro task
Promise.resolve().then(func4)
}, 0);
// macro task
setTimeout(func1, 0);
// micro task
Promise.resolve().then(func3)
// main task
func4()
})()
// alert func1
// alert func4
// alert func3
// UI update ---> counter = 3
// alert func2
// alert func4
// UI update ---> counter = 5
// alert func1
// UI update ---> counter = 6
})()
接收后台资源的方法(除了Ajax)
- Ajax
-
fetch
返回一个Promise对象, 根据 Promise Api 的特性, fetch可以方便地使用then方法将各个处理逻辑串起来mode
// fetch可以设置不同的模式使得请求有效 fetch(url, {mode: 'cors'});
- same-origin
- cors
- cors-with-forced-preflight
- no-cors
推荐阅读: Fetch 进阶指南
Jsonp
- websocket
服务器推送技术之一
全双工 SSE(server-sent-events)
单向通道(服务器 -> 浏览器)
异步编程常用方法
ES 6以前:
- 回调函数
- 事件监听(事件发布/订阅)
- Promise对象
ES 6:
- Generator函数(协程coroutine)
ES 7:
- async和await
回调函数
一般是需要在一个耗时操作之后执行某个操作时可以使用回调函数
- 定时器
- 读取文件
问题:
在回调函数之外无法捕获到回调函数中的异常
var fs = require('fs');
try{
fs.readFile('not_exist_file', 'utf8', function(err, data){
console.log(data);
});
}
catch(e){
console.log("error caught: " + e);
}
尝试读取一个不存在的文件,这当然会引发异常,但是最外层的try/catch语句却无法捕获这个异常。这是异步代码的执行机制导致的
为什么异步代码回调函数中的异常无法被最外层的try/catch语句捕获?
异步调用一般分为两个阶段,提交请求和处理结果,这两个阶段之间有事件循环的调用,它们属于两个不同的事件循环(tick),彼此没有关联。
异步调用一般以传入callback的方式来指定异步操作完成后要执行的动作。而异步调用本体和callback属于不同的事件循环。
try/catch语句只能捕获当次事件循环的异常,对callback无能为力。
事件监听(订阅-发布)
典型的逻辑分离方式,对代码解耦很有用处
把不变的部分封装在组件内部,供外部调用,需要自定义的部分暴露在外部处理。
从某种意义上说,事件的设计就是组件的接口设计。
//发布和订阅事件
var events = require('events');
var emitter = new events.EventEmitter();
emitter.on('event1', function(message){
console.log(message);
});
emitter.emit('event1', "message for you");
Promise 对象
用同步操作的流程写法来表达异步操作,避免了层层嵌套的异步回调
-
Promise.prototype.then()
//原生Primose顺序嵌套回调示例 var fs = require('fs') var read = function (filename){ var promise = new Promise(function(resolve, reject){ fs.readFile(filename, 'utf8', function(err, data){ if (err){ reject(err); } resolve(data); }) }); return promise; } read('./text1.txt') .then(function(data){ console.log(data); return read('./text2.txt'); // 返回了一个新的Promise实例 }) .then(function(data){ console.log(data); });
Promise构造函数的参数是一个函数,在这个函数中我们写异步操作的代码
在异步操作的回调中,根据err变量来选择是执行resolve方法还是reject方法- 一般来说调用resolve方法的参数是异步操作获取到的数据(如果有的话),但还可能是另一个Promise对象,表示异步操作的结果有可能是一个值
- 也有可能是另一个异步操作,调用reject方法的参数是异步回调用的err参数
调用read函数时,实际上返回的是一个Promise对象,通过在这个Promise对象上调用then方法并传入resolve方法和reject方法来指定异步操作成功和失败后的操作。
-
Promise.prototype.catch()
用于指定发生错误时的回调函数
read('./text1.txt') .then(function(data){ console.log(data); return read('not_exist_file'); }) .then(function(data){ console.log(data); }) .catch(function(err){ console.log("error caught: " + err); }) .then(function(data){ console.log("completed"); })
使用Promise对象的catch方法可以捕获异步调用链中callback的异常
Promise对象的catch方法返回的也是一个Promise对象,因此,在catch方法后还可以继续写异步调用方法 - Promise异步并发
-
Promise.all()
- 将多个Promise实例,包装成一个新的Promise实例
var p = Promise.all([p1,p2,p3]);
- 接受一个数组作为参数,p1、p2、p3都是Promise对象实例
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
-
只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
var promises = [1, 2].map(function(fileno){ return read('./text' + fileno + '.txt'); }); Promise.all(promises) .then(function(contents){ console.log(contents); }) .catch(function(err){ console.log("error caught: " + err); })
- 将多个Promise实例,包装成一个新的Promise实例
-
Promise.race()
- 将多个Promise实例,包装成一个新的Promise实例
var p = Promise.race([p1,p2,p3]);
- p1、p2、p3只要有一个实例率先改变状态,p的状态就会跟着改变,那个率先改变的Promise实例的返回值,就传递给p的返回值。
- 如果Promise.all方法和Promise.race方法的参数不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理
- 将多个Promise实例,包装成一个新的Promise实例
-
Promise.resolve()
- 将现有对象转换成Promise对象
var p = Promise.resolve('Hello'); p.then(function (s){ console.log(s) });
-
Promise.reject()
- 返回一个新的Promise实例,该实例的状态为rejected。
-
Promise.reject
方法的参数reason,会被传递给实例的回调函数。
var p = Promise.reject('出错了'); p.then(null, function (s){ console.log(s) });
-
- Generator函数
- 可以交出函数的执行权(暂停执行)
-
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); var r1 = g.next(); // { value: 3, done: false } console.log(r1); var r2 = g.next() // { value: undefined, done: true } console.log(r2);
**Generator函数的函数名前面有一个"*"**
- 调用Generator函数,会返回一个内部指针(即遍历器)g,这是Generator函数和一般函数不同的地方,调用它不会返回结果,而是一个指针对象。
- 调用指针g的next方法,会移动内部指针,指向第一个遇到的yield语句
- next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)
- value属性是yield语句后面表达式的值,表示当前阶段的值;
- done属性是一个布尔值,表示Generator函数是否执行完毕,即是否还有下一个阶段
- Thunk 函数
Thunk 函数的含义和用法 ES 7中的async和await
- fetch
- 替代浏览器原生的XMLHttpRequest异步请求
get & post 剖析
入门回答
区别:
- get
- 参数包含在 URL 中
- 大小有限制
- 使用Request.QueryString 获取变量的值
- 安全性差,直观显示
- post
- 通过 request body 传递参数
- 通过 Request.Form 获取变量的值
当我满心充满着自信和喜悦时,仿佛看到了面试官眉头一皱,So?
“标准答案”
aspect | GET | POST |
---|---|---|
浏览器回退 | 无影响 | 回退会再次提交请求 |
地址标记 | 产生的 URL 地址可以被 Bookmark | 提交地址不被标记 |
cache | 该请求会被浏览器主动 cache | 该请求不会被缓存 |
编码 | 只能进行url编码 | 支持多种编码方式 |
参数保留 | 请求参数会被完整保留在浏览器历史记录里 | POST中的参数不会被保留 |
长度限制 | 有(浏览器限制,IE-2083个字符) | 无(限制作用的是服务器的处理程序的处理能力) |
参数类型 | 只接受ASCII字符 | 没有限制 |
参数传递 | 通过URL传递 | 放在Request body中 |
裸奔剖析
前端编码
字符 | 表示 | 补充 |
---|---|---|
二进制 | 0/1 | 八个二进制位可以组合出256种状态,这被称为一个字节(byte) |
八进制 | 0~7 | |
十进制 | 0~9 | |
十六禁止 | 0~9 A~F |
- 8byte = 1bit
- 1024 字节 = 1k
- 1024k = 1M
- 1024M = 1G
- 1024G = 1T
编码 | 特征 | 补充 |
---|---|---|
二十一进制码(BCD码) | 保留了十进制数的权,而数字则用二进制数码0和1的组合来表示 | 在需要高精度的计算中BCD编码比较常用(了解) |
ASCII码 | 美国信息交换标准委员会制定的7位字符编码,用7位二进制码表示一个字符,第8 位用于确定附加的128 个特殊符号字符、外来语字母和图形符号 | |
GB2312 | 为了保存非英文,使用127号之后的空位保存新的字母(一个8位的字节可以组合256种状态,ASCII只编到127号),一直编到最后一位255,而且不同国家表示的符号也不一样,也可以说GB2312是对ASCII的中文扩展 | 不够用,后来只要求只要第一个字节是大于127就固定表示这是一个汉字的开始,称之为GBK编码 |
GB18030 / DBCS | 编码中又增加了几千个新的少数民族的字,GBK扩展成了GB18030统称它们叫做DBCS | |
Unicode | ISO(国际标准化组织)废弃了所有地区性编码方案,做了一套包括了地球上所有文化、符号以及字母的编码;ISO规定:必须用两个字节,16位来统一表示所有的字符,无论是半角的英文字母,还是全角的汉字,它们都是统一的一个字符!也就是两个字节 | |
UTF-8 | UTF-8 互联网上使用最广的一种 Unicode 的实现方式,每次以8个位为单位传输数据;UTF-16就是每次 16 个位 | UTF-8 最大的一个特点,就是它是一种变长的编码方式,Unicode一个中文字符占 2 个字节,而UTF-8一个中文字符占3个字节,UTF-8是Unicode的实现方式之一 |
进制转换
------十进制转其他-------
var a = 24;
a.toString(2);//11000
a.toString(8);//30
a.toString(16);//18
------其他转十进制-------
var b=11000,c=30,d=18;
console.log(parseInt(b, 2)); // 二进制转十进制
console.log(parseInt(c, 8)); // 八进制转十进制
console.log(parseInt(d, 16));// 十六进制转十进制
前端编码问题
在使用nodeJS编写前端工具时,对文本文件的操作比较多,这就涉及到了文件的编码问题,常用的文本编码有UTF8和GBK两种,并且UTF8文件还可能带有BOM(字节顺序标记),在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理
-
移除 BOM
BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符("\uFEFF"),位于文本文件头部以告诉其他编辑器以utf8来显示字符
但是在网页上并不需要添加BOM头识别,因为网页上可以使用 head头 指定charset=utf8告诉浏览器用utf8来解释.但是你用window自动的编辑器,编辑,然后有显示在网页上这样就会显示出0xEF 0xBB 0xBF这3个字符。这样网页上就需要去除0xEF 0xBB 0xBF
- 可以使用editplus 选择不带BOM的编码,这样就可以去除了
-
js 去除
// 可以通过文件头的几个字节来判断文件是否包含BOM以及使用哪种Unicode,如果读取文件的时候不去掉BOM // 假如我们将几个JS文件合并成一个,如果文件中含有BOM字符,就会导致语法错误 // 所以我们用 nodeJS读取文件是一般先去掉BOM var bin = fs.readFileSync(pathname);//通过node 中fs模块同步读文件内容 //判断文件头的字节 if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) { bin = bin.slice(3); }
-
GBK 转 UTF8
NodeJS支持在读取文本文件时,或者在Buffer转换为字符串时指定文本编码,但GBK编码不在NodeJS自身支持范围内,一般我们借助iconv-lite这个三方包来转换编码,首先使用npm下载这个第三方包,读取GBK文件函数如下:
var iconv = require('iconv-lite'); function readGBKText(pathname) { var myFs = fs.readFileSync(pathname); return iconv.decode(myFs, 'gbk'); }
数据结构
常用的线性结构有:线性表,栈,队列,循环队列,数组
线性表中包括顺序表、链表等,其中:
- 栈和队列只是属于逻辑上的概念,实际中不存在,仅仅是一种思想,一种理念;
- 线性表则是在内存中数据的一种组织、存储的方式。
常见的排序算法及其时间复杂度
其他
- git 与 GitHub 有什么区别
- git 的一些指令
- Linux
计算机网络
**如有不足,欢迎交流,祝各位看官 offer 拿到手软 O(∩_∩)O**
前端相关片段整理——持续更新
更好阅读 移步这里
1. ES6
- 箭头函数
- 字符串模板
- generators(生成器)
- async/await
- 解构赋值
- class
- 引入module模块的概念
1.1. 箭头函数:
- 函数内的this对象,是定义时所在的对象,不是使用时所在的对象
- 不可当构造函数
- 用rest代替argument
- this指向一般可变,但在箭头函数中固定
- 简单,单行,不会复用的函数建议使箭头函数
复杂,行多,使用传统
1.2. promise
解决异步回调多层嵌套的问题
- 是一个容器;
包含某个未来结束的事件 - 是一个对象:
从它可获取异步操作的消息
- pending 进行中
- resolved 已完成
- rejected 已失败
特点
- 状态不受外界影响,只有事件结果决定
- 状态改变不会再变
缺点:
- 无法取消promise,一旦建立立即执行,中途无法撤回
- 无回掉函数的话,错误不反应到外部
- pending时,状态无法得知
Promise.all
接收 Promise 数组为参数,将多个Promise实例,包装成一个新的Promise实例,所有 resolve ,返回所有值
在不同的接口请求数据然后拼合成自己所需的数据,通常这些接口之间没有关联(例如不需要前一个接口的数据作为后一个接口的参数)
var p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况:
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race
它同样接收一个数组,不同的是只要该数组中的 Promise 对象的状态发生变化(无论是 resolve 还是 reject)该方法都会返回
async/await
async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来
- 是写异步代码的新方式,以前的方法有回调函数和Promise。
- 是基于Promise实现的,它不能用于普通的回调函数。
- 与Promise一样,是非阻塞的。
- 使得异步代码看起来像同步代码,这正是它的魔力所在。
1.3. interator
是一种接口,为所有数据结构提供一种统一的访问机制,即for...of 循环
作用:
- 一是为各种数据结构,提供一个统一的、简便的访问接口;
- 二是使得数据结构的成员能够按某种次序排列;
- 三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
interator遍历过程:
- 创建一个只针对象,指向当前数据结构的起始位置(遍历器对象本质是指针对象)
- 调用指针对象的next方法
使用场合:
- 解构赋值
- 扩展运算符(...)
- yield*
for...in
- 为遍历对象设计,不适用数组
- key
- 以字符串作为键名
- 遍历数字键以及手动添加的其他键
- 可能会以任意顺序遍历键名
for...of
- 语法简洁,无以上缺点
- 循环value
- 不同用于foreach方法,可以与break,continue,return配合使用
- 提供了遍历所有数据结构的统一操作接口,循环普通对象结合
bject.keys()
搭配使用 - 可自动遍历generator函数生成的iterator对象
- 除了遍历数组元素以外,还会遍历自定义属性
1.4. generator 函数
一种异步解决方案(一种封装了多个内部状态的状态机)
- 返回的不是函数运行结果,而是指向内部状态的指针对象
- 调用next方法,从停止地方开始执行,移向下一个状态
1.5. yield 与 return
相似:都能返回紧跟在语句后面那个表达式的值
区别:记忆功能,执行次数,返回值数量
1.6. 回调函数
JavaScript对异步编程的实现
1.7. ES6 Object.assign
将源对象(source)的所有可枚举属性,复制到目标对象(target)
Object.assign(target, source1, source2);
后面属性覆盖前面同名属性
- 一个参数时,返回该参数
- 参数不是对象,转成对象(undefined,null会报错),若为源对象位置,则跳过
- 可用来操作数组,将数组视为对象
- 浅拷贝非深拷贝(若源对象的有对象属性值,则拷贝的是该引用)
用途:
- 为兑现添加属性/方法
- 克隆对象
- 合并对象
- 为属性指定默认值
2. 通信
2.1. JSONP
被包含在一个回调函数中的 json
核心是: 动态添加script标签调用服务器提供的js脚本
2.2. cors
使用自定义的http头部让浏览器与服务器进行沟通,确定该请求是否成功
核心:由服务器发送一个响应标头
2.3. web安全
1) 将重要的cookie标记为http only
2) 只允许用户输入期望值
3) encode
4) 过滤或移除特殊标签
5) 过滤JavaScript事件标签
3. 架构
3.1. 模块化
原理: 将复杂系统分解为代码结构更合理,可维护性更高,可管理的模块
目的: 只需完成自己的业务代码
发展过程:
- commonjs
模块为单独的文件,require,同步使用
nodejs主要用于服务器进程,加载内容在本地磁盘
异步情况:浏览器环境中需要从服务器加载模块,需要采用异步模式 - AMD(Asynchronous Module Definition)
- 允许输出模块兼容commonjs规范
- require([module], callback);
- 模块加载与调用不同步,浏览器不会发生假死
- requirejs curljs
- CMD
seajs推广中对模块定义的产出
CMD与AMD区别:
- amd推崇依赖前置(定义模块时申明其依赖的模块),cmd推崇依赖就近(用时再require)
- amd的api默认一当多,cmd推崇职责单一(amd中require分全局和局部)
requirejs 与 seajs 分析:
- 定位,requirejs想成为浏览器端的模块加载器,也想成为rhino/node等环境的模块加载器
seajs专注web浏览器端,通过node扩展方式方便跑在node端 - 标准,requirejs醉醺amd规范,seajs遵循cmd,api不同
- 理念,requirejs尝试让第三方类库修改自身来支持requirejs,seajs不强推,采用资助封装方式,已较成熟封装策略
- 质量,require<seajs
- 插件
更多了解:
3.2. react
用于构建用户界面的JavaScript库,主要用于构建ui,将普通的DOM以数据结构的形式展现出来
永远只需要关心数据整体,两次数据之间的UI如何变化,则完全交给框架去做,使用React大大降低了逻辑复杂性
Virtual DOM并没有完全实现DOM,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性
基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新
虚拟DOM是内存数据,性能是极高的,而对实际DOM进行操作的仅仅是Diff部分,因而能达到提高性能的目的。这样,不再需要关注某个数据的变化如何更新到一个或多个具体的DOM元素,而只需要关心在任意一个数据状态下,整个界面是如何Render的
设计特点:
- 变换:react核心认为ui只是把数据通过映射关系变换成另一种形式的数据——函数
- 组合:将两个或多个不同的抽象合并为一个
- 组件化:推荐以组件的方式思考ui构成,将小组件通过组合或嵌套构成大组件
组件特征:
- 可组合
- 可重用
- 可维护
jsx语法:
HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写
生命周期:
组件的生命周期分成三个状态:
- Mounting:已插入真实 DOM
- Updating:正在被重新渲染
- Unmounting:已移出真实 DOM
React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数:
- componentWillMount()
- componentDidMount()
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
- componentWillUnmount()
两种特殊状态的处理函数:
- componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
- shouldComponentUpdate(object nextProps, object nextState):判断是否重新渲染时调用
3.3. angular
特性:
- MVVM
- 模块化
- 自动化双向数据绑定
- 语义化标签
- 依赖注入
3.4. vue
- 父-子
props - 子-父
on/emit - 其他
使用空的vue实例作为*事件总线
3.5. angular与react之对比
- React 和 Angular 之间的巨大差异是 单向与双向绑定
- React 和 Vue 都使用了虚拟 DOM —— 不必在每个元素每次变化时重新渲染整个巨大的table
- 如果应用时常要处理大量的动态数据集,并以相对简便和高性能的方式对大型数据表进行显示和变更,由于双向数据绑定需要监听每一个可变元素, 数据量变大就会带来显著的性能问题,React是相当不错的选择。但是React不像AngularJS那样包含完整的功能,举例来说,React没有负责数据展现的控制器
3.6. 软件架构
模式之间不同 主要是 M与V 的数据传递的流程不同
3.6.1. mvc
- View 传送指令到 Controller
- Controller 完成业务逻辑后,要求 Model 改变状态
- Model 将新的数据发送到 View,用户得到反馈
MVC 可以分成两种方式:
- 通过 View 接受指令,传递给 Controller
- 直接通过controller接受指令
3.6.2. MVP
- 各部分之间的通信,都是双向的。
- View 与 Model 不发生联系,都通过 Presenter 传递。
- View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
3.6.3. MVVM
- 用数据“绑定”的形式让数据更新的事件不需要开发人员手动去编写特殊用例,而是自动地双向同步。
- 数据绑定可以认为是Observer模式或者是Publish/Subscribe模式,原理都是为了用一种统一的集中的方式实现频繁需要被实现的数据更新问题。
- MVVM不仅简化了业务与界面的依赖关系,还优化了数据频繁更新的解决方案
3.7. restful架构
Fielding将他对互联网软件的架构原则,定名为REST,即Representational State Transfer的缩写。我对这个词组的翻译是"资源的表现层状态转化"。
4. js
4.1. js垃圾回收与内存管理
各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数
4.1.1. 垃圾回收
自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存
垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存
- 标记清除
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记
- 然后,它会去掉环境中的变量以及被环境中的变量引用的标记
- 而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了
- 最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间
-
引用计数
跟踪记录每个值被引用的次数
当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1,释放那些引用次数为0的值所占的内存。function problem() { var objA = new Object(); var objB = new Object(); objA.someOtherObject = objB; objB.anotherObject = objA; }
这个方式存在一个比较大的问题就是循环引用
可以手动切断他们的循环引用myObj.element = null; element.someObject =null;
4.1.2. 减少JavaScript中的垃圾回收
- 在初始化的时候新建对象,然后在后续过程中尽量多的重用这些创建好的对象。
- 另外还有以下三种内存分配表达式(可能不像new关键字那么明显了):
- {} (创建一个新对象)
- [] (创建一个新数组)
- function() {…} (创建一个新的方法,注意:新建方法也会导致垃圾收集!!)
4.1.3. 优化
- 对象object优化
- 避免使用new/{}来新建对象
- cr.wipe(obj)—遍历此对象的所有属性,并逐个删除,最终将对象清理为一个空对象
-
数组array优化
arr = []; //将原数组变成一小片内存垃圾 arr.length = 0 //清空数组
4.2. 闭包
特点:
- 函数
- 能访问另外一个函数作用域中的变量
ES 6之前,Javascript只有函数作用域的概念,没有块级作用域。即外部是访问不到函数作用域中的变量。
总结
- 可以访问外部函数作用域中变量的函数
- 被内部函数访问的外部函数的变量可以保存在外部函数作用域内而不被回收---这是核心,后面我们遇到闭包都要想到,我们要重点关注被闭包引用的这个变量
4.3. 作用域链
为什么闭包就能访问外部函数的变量呢
Javascript中有一个执行环境(execution context)的概念,它定义了变量或函数有权访问的其它数据,决定了他们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中
当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不再存在父作用域了,这就是作用域链。
作用域链的顶端是全局对象。对于全局环境中的代码,作用域链只包含一个元素:全局对象
作用域链和原型继承:
有点类似,但又有点小区别:
- 如果去查找一个普通对象的属性时,在当前对象和其原型中都找不到时,会返回undefined
- 查找的属性在作用域链中不存在的话就会抛出ReferenceError
更多了解:
闭包的运用
- 匿名自执行函数
有的函数只需要执行一次,其内部变量无需维护,执行后释放变量 - 实现封装/模块化代码
变量作用域为函数内部,外部无法访问 - 实现面向对象中的对象
这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉
优点:
- 可以让一个变量常驻内存 (如果用的多了就成了缺点
- 避免全局变量的污染
- 私有化变量
缺点:
- 因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存
- 引起内存泄露
4.4. 事件委托和this
4.4.1. 事件委托
由其它元素而非事件目标元素来响应事件产生的行为的思想。如用ul元素来处理其子元素li的事件。
事件冒泡: stopPropagation、stopImmediatePropagation、preventDefault
订阅发布
优点:减少监听器数量,改善性能
缺点:父容器的侦听器可能需要检查事件来选择正确的操作
4.4.2. this
this 关键字在JavaScript中的一种常用方法是指代码当前上下文
- 默认指向全局对象,其通常是window
- this总是代表它的直接调用者(js的this是执行上下文), 例如 obj.func ,那么func中的this就是obj
- 在严格模式下,没有直接调用者的函数中的this是 undefined
- 使用call,apply,bind绑定的,this指的是 绑定的对象
在异步编程中,this可以很容易改变过程中一个功能操作。保持处理程序上下文的一个小技巧是将其设置到闭包内的一个变量,当在上下文改变的地方调用一个函数时,如setTimeout,你仍然可以通过该变量引用需要的对象。
箭头函数中的this
- 箭头函数没有自己的this, 它的this是继承而来
- 默认指向在定义它时所处的对象(宿主对象),而不是执行时的对象, 定义它的时候,可能环境是window
- 箭头函数可以方便地让我们在 setTimeout ,setInterval中方便的使用this
持续更新,欢迎交流~
前端基础精简总结
更好阅读 移步这里
1. JavaScript
1.1. 基础语法
包括:变量声明、数据类型、函数、控制语句、内置对象等
1.1.1. 变量声明
ES5:
var //普通变量
function //函数
ES6新增:
let //普通变量
const //常量 -- 声明的同时初始化,不可重新赋值,不能重复声明
import //模块
class //类
1.1.2. 数据类型
-- 类型介绍
ES5:String、Number、Boolean、Null、Undefined、Object
ES6增:Symbol
其中,object为引用,其他为基本类型
- 基本类型
占据空间固定,是简单的数据段,将其存储在栈(stack)中(按值访问) 便于提升变量查询速度
为了便于操作这类数据,ECMAScript提供了 3 个 基本包装类型 :Boolean、Number 和 String- 基本包装类型
一种特殊的引用类型,每当读取一个基本类型值的时候,JS内部就会创建一个对应的包装对象,从而可以调用一些方法来操作这些数据
- 基本包装类型
- 引用类型
- 由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度
- 将其存储在堆(heap)中,存储在变量处的值是一个指针,指向存储对象的内存处(按址访问)
- 可以为其添加、改变和删除属性和方法;但基本类型不可以添加属性和方法
-- 类型判断
- 原始类型
- typeof
- 引用类型
- isinstanceof -- 判断已知对象
- constructor -- 根据对象的constructor判断
constructor本来是原型对象上的属性,指向构造函数。但是根据实例对象寻找属性的顺序,若实例对象上没有实例属性或方法时,就去原型链上寻找,因此,实例对象也是能使用constructor属性的 - Object.prototype.toString.call
- $.type() -- 万能判断
var a = new Array(); console.log(a instanceof Array) // a是否Array的实例 true console.log(a.constructor == Array) // a实例所对应的构造函数是否为Array true // constructor属性是可以被修改的,会导致检测出的结果不正确 function Dog(){ } function Cat(){ } Cat.prototype = new Dog(); var m= new Cat(); console.log(m.constructor==Cat); // false console.log(John.constructor==Person); // true // instanceof 对于直接或间接引用都是true console.log(m instanceof Cat); // true console.log(John instanceof Person); // true //Object.prototype.toString.call function a() { }; var toString = Object.prototype.toString; console.log(toString.call(new Date) === '[object Date]'); //true console.log(toString.call(new String) ==='[object String]');//true console.log(toString.call(a) ==='[object Function]'); //true //$.type jQuery.type( undefined ) === "undefined" // true jQuery.type() === "undefined" // true jQuery.type( null ) === "null" // true jQuery.type( true ) === "boolean" // true
1.1.3. 函数
- 普通函数 -- 直接调用
- 构造函数 -- new 创建对象
- 对象方法 -- 对象调用
1.1.4. 内置对象
- window
- 全局对象,主要描述浏览器窗口相关的属性和状态
- Date
- Array
- JSON
- 主要用于对象的序列化和反序列化
- 实现对象的深拷贝
- RegExp
-- 浅复制与深拷贝
- 浅复制
对对象地址的复制,并没有开辟新的栈,复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,则另一个对象的属性也会改变 - 深拷贝
开辟新的栈,两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
方法
-
递归
var china = { nation : '中国', birthplaces:['北京','上海','广州'], skincolr :'yellow', friends:['sk','ls'] } //深复制,要想达到深复制就需要用递归 function deepCopy(o,c){ var c = c || {} for(var i in o){ if(typeof o[i] === 'object'){ //要考虑深复制问题了 if(o[i].constructor === Array){ //这是数组 c[i] =[] }else{ //这是对象 c[i] = {} } deepCopy(o[i],c[i]) }else{ c[i] = o[i] } } return c } var result = {name:'result'} result = deepCopy(china,result) console.dir(result)
-
JSON
var test ={ name:{ xing:{ first:'张', second:'李' }, ming:'老头' }, age :40, friend :['隔壁老王','宋经纪','同事'] } var result = JSON.parse(JSON.stringify(test)) result.age = 30 result.name.xing.first = '往' result.friend.push('fdagldf;ghad') console.dir(test) console.dir(result)
1.2. 函数原型链
JS是一种基于对象的语言,但在ES6 之前是不支持继承的,为了具备继承的能力,Javascript 在 函数对象上建立了原型对象prototype,并以函数对象为主线,从上至下,在JS内部构建了一条 原型链
Object 是所有对象的祖宗, 任何对象所建立的原型链最终都指向了Object
简单来说:
建立了变量查找机制,当访问一个对象的属性时,先查找对象本身是否存在,如果不存在就去该对象所在的原型连上去找,直到Object对象为止,如果都没有找到该属性才会返回undefined。因此,我们可以通过原型链来实现JS继承
1.3. 函数作用域
变量在声明它们的函数体以及这个函数体嵌套的任意函数体
JS中没有块级作用域,只有函数作用域
导致JS中出现了变量提升的问题
—— 将变量声明提升到它所在作用域的最开始的部分
为了解决变量提升带来的副作用,ES6新增了let 命令来声明变量,let 所声明的变量只在 let 命令所在的代码块内有效,所以不存在变量提升问题
1.4. this 指针
this 指针存在于函数中,用以标识函数运行时所处的上下文
- 普通函数
始终指向全局对象window - 构造函数
指向新创建的对象 - 方法
指向调用该方法的对象 - call、apply 和 bind
方法来改变函数的 this 指向,其中,call 和 apply 主动执行函数,bind一般在事件回调中使用, call 和 apply的区别只是参数的传递方式不同
1.5. new 操作符
函数的创建有三种方式,即 显式声明、匿名定义 和 new Function()
JS将新对象的原型链指向了构造函数的原型对象,于是就在新对象和函数对象之间建立了一条原型链,通过新对象可以访问到函数对象原型prototype中的方法和属性
1.6. 闭包
具有独立作用域的静态执行环境
和函数作用域不同的是:
- 闭包的作用域
静态的,可以永久保存局部资源 - 函数作用域
只存在于运行时,函数执行结束后立即销毁
因此,闭包可以形成一个独立的执行过程
1.7. 单线程和异步
JavaScript
- 单线程语言,在浏览器中,当JS代码被加载时,浏览器会为其分配一个主线程来执行任务(函数)
主线程会形成一个全局执行环境,执行环境在栈中采用后进先出(LIFO)的顺序来执行代码块,以保证所有的函数能按照正确的顺序被执行 - 执行环境中维护了一个异步队列(也叫工作线程),并将这些耗时任务放入队列中进行等待
- 如ajax请求、定时器、事件等
- 这些任务的执行时机并不确定,只有当主线程的任务执行完成以后,主线程才会去检查异步队列中的任务是否需要开始执行。
- JS中的 setTimeout 和 setInterval 就是典型的异步操作,它们会被放入异步队列中等待,即使 setTimeout(0)也不会被立即执行,需要等到当前同步任务结束后才会被执行。
1.8. 异步通信
浏览器专门用来和服务器进行交互的异步通讯技术
1.8.1. Ajax
- Ajax是浏览器专门用来和服务器进行交互的异步通讯技术
- 其核心对象是XMLHttpRequest,通过该对象可以创建一个Ajax请求
- Ajax请求是一个耗时的异步操作,当请求发出以后,Ajax提供了两个状态位来描述请求在不同阶段的状态,这两个状态位分别是
- readyState
- status
readyState 通过5个状态码来描述一个请求的5个阶段:
0 - 请求未发送,初始化阶段
1 - 请求发送中,服务器还未收到请求
2 - 请求发送成功,服务器已收到请求
3 - 服务器处理完成,开始响应请求,传输数据
4 - 客户端收到请求,并完成了数据下载,生成了响应对象
status- 1xx(临时响应)表示临时响应并需要请求者继续执行操作的状态码。
- 2xx(成功)表示成功处理了请求的状态码。
- 200(成功):服务器已成功处理了请求。通常,这表示服务器提供了请求的网页。
- 3xx(重定向)要完成请求,需要进一步操作。
- 301(永久移动):请求的网页已永久移动到新位置。
- 302(临时移动):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来响应以后的请求。
- 304(未修改):自从上次请求后,请求的网页未修改过。
- 4xx(请求错误)这些状态码表示请求可能出错,妨碍了服务器的处理。
- 400(错误请求):服务器不理解请求的语法。
- 404(未找到):服务器找不到请求的网页。
- 5xx(服务器错误)这些状态码表示服务器在处理请求时发生内部错误。
- 500(服务器内部错误):服务器遇到错误,无法完成请求。
- 503(服务不可用):服务器目前无法使用(由于超载或停机维护)
- 1xx(临时响应)表示临时响应并需要请求者继续执行操作的状态码。
常见问题:
- timeout 只会影响readyState,而不会影响status,因为超时只会中断数据传输,但不会影响服务器的处理结果。 如果 timeout 设置的不合理,就会导致响应码status 是200,但 response里却没有数据,这种情况就是服务器正确响应了请求,但数据的下载被超时中断了。
HTTP 相关请见:
-
1.8.2. JSONP && XSS
浏览器设置了安全限制——JavaScript或Cookie只能访问同域下的内容——同源策略
只允许请求和当前地址同域的服务器资源。但不限制脚本和标签发送跨域请求,比如script 和 img 标签,因此可以利用脚本跨域能力来实现跨域请求,即JSONP 的原理。
JSONP虽然可以解决跨域问题,但只能是get请求,并且没有有效的错误捕获机制
为了解决这个问题,XMLHttpRequest Level2 提出了CORS 模型,即 跨域资源共享, 它不是一个新的API,而是一个标准规范,当浏览器发现该请求需要跨域时,就会自动在头信息中添加一个 Origin字段,用以说明本次请求来自哪个源。服务器根据这个值,决定是否同意这次请求。
随着移动端的快速发展,Web技术的应用场景正在变得越来越复杂, 关注点分离 原则在系统设计层面就显得越来越重要,而XMLHttpRequest 是 Ajax 最古老的一个接口,因而不太符合现代化的系统设计理念。因此,浏览器提供了一个新的 Ajax 接口,即 Fetch API ,Fetch API 是基于Promise 思想设计的,更符合关注点分离原则。
更多请见:
1.9. 模块化
模块加载方案,最主要有 CMD 和 AMD 两种,分别以commonjs 和 requirejs为代表
ES6 在语言标准的层面上,实现了模块化编程,其设计思想是,尽量静态化,使得编译时就能确定模块的依赖关系,即编译时加载
CMD和AMD是在运行时确定依赖关系,即运行时加载
详情:
AMD && CMD
ES6 模块化
每一个ES6模块都是一个包含JS代码的文件,模块本质上就是一段脚本,而不是用module关键字定义一个模块,但是模块与脚本还是有两点区别:
- 在ES6模块中,无论你是否加入“use strict;”语句,默认情况下模块都是在严格模式下运行。
- 在模块中你可以使用import和export关键字。
默认情况下,你在模块中的所有声明相对于模块而言都是寄存在本地的。如果你希望公开在模块中声明的内容,并让其它模块加以使用,你一定要导出这些功能。想要导出模块的功能有很多方法,其中最简单的方式是添加export关键字,可以导出所有的最外层函数、类以及var、let或const声明的变量。
es6中 代码就是模块,不是一段脚本,所以所有的声明都被限定在模块的作用域中,对所有脚本和模块全局不可见。你需要做的是将组成模块公共API的声明全部导出。
webpack
在编译时计算所有依赖并将所有模块打包成一个文件,通过网络一次传输所有模块
减少加载模块时的网络往返时间
深入浅出ES6(模块):http://www.infoq.com/cn/articles/es6-in-depth-modules
1.10. Node.js
一个基于 Chrome V8 引擎的 JavaScript运行环境
Node.js在服务端的优势是,它采用单线程和异步I/O模型,实现了一个高并发、高性能的运行时环境。相比传统的多线程模型,Node.js实现简单,并且可以减少资源开销
1.11. ES6
目标是让JS能够方便的开发企业级大型应用程序
变化
- 新增 let、const 命令 来声明变量
和var 相比,let声明的变量不存在变量提升问题,但没有改变JS弱类型的特点,依然可以接受任意类型变量的声明;const
声明的变量不允许在后续逻辑中改变,提高了JS语法的严谨性。 - 新增解构赋值、rest语法、箭头函数
这些都是为了让代码看起来更简洁,而包装的语法糖。 - 新增模块化
这是JS走向规范比较重要的一步,让前端更方便的实现工程化。 - 新增类和继承的概念
配合模块化,JS也可以实现高复用、高扩展的系统架构。 - 新增模板字符串功能
高效简洁,结束拼接字符串的时代。 - 新增Promise对象
解决异步回调多层嵌套的问题
使得原本的多层级的嵌套代码,变成了链式调用 让代码更清晰,减少嵌套数
promise
- 容器:里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果
- 对象:从它可以获取异步操作的消息
特点
- 对象的状态不受外界影响
Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。 - 一旦状态改变,就不会再变,任何时候都可以得到这个结果
Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。
缺点
- 无法取消Promise
一旦新建它就会立即执行,无法中途取消 - 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
- 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
更多:
2. CSS
2.1. CSS 选择器
- 类选择器
- 标签选择器
- ID选择器
- 后代选择器
- 群组选择器
- 伪类选择器(before/after)
- 兄弟选择器(+~)
- 属性选择器等等
2.2. 盒子模型
- 块级盒子(block)
- 行内盒子(inline-block)
相关属性margin、border、padding、content
注意
- 只有普通文档流中块级盒子的垂直外边距才会发生合并,而行内盒子、浮动盒子或绝对定位之间的外边距不会合并
根据规范,一个盒子如果没有上补白(padding-top)和上边框(border-top),那么这个盒子的上边距会和其内部文档流中的第一个子元素的上边距重叠
为父元素增加一个border-top或者padding-top即可解决这个问题 - box-sizing 属性的设置会影响盒子width和height的计算
更多:
2.3. 浮动布局
设置元素的 float 属性,能使该元素脱离普通文档流
如果子元素全部设置为浮动,则父元素是塌陷的
- 清除浮动
clear:both, - BFC
浮动元素的父元素 + overflow:hidden 样式 - 行内盒子(inline-block)
- table也可以实现同样的效果。
2.4. 定位布局
脱离文档流:position 值为 relative/absolute/fixed
- relative
相对定位,它以自己原来的位置进行偏移,偏移后,原来的空间不会被其他元素占用 - absolute
绝对定位,它以离自己最近的定位父容器作为参照进行偏移
常用的方式就是设置父容器的poistion:relative - fixed
固定定位,以浏览器窗口为参照物
PC网页底部悬停的banner一般都可以通过fixed定位来实现,但fixed属性在移动端有兼容性问题,因此不推荐使用,可替代的方案是:绝对定位+内部滚动。
更多:
2.5. 弹性布局
即Flex布局,定义了flex的容器一个可伸缩容器
- 容器本身会根据容器中的元素动态设置自身大小
- 当Flex容器被应用一个大小时(width和height),将会自动调整容器中的元素适应新大小
- Flex容器也可以设置伸缩比例和固定宽度,还可以设置容器中元素的排列方向(横向和纵向)和是否支持元素的自动换行
容器的属性- flex-direction属性
- flex-wrap属性
- flex-flow
- justify-content属性
- align-items属性
- align-content属性
项目的属性 - order属性
- flex-grow属性
- flex-shrink属性
- flex-basis属性
- flex属性
- align-self属性
- 注意,设为Flex布局以后,子元素的float、clear和vertical-align属性将失效。
更多: Flex 布局
2.6. CSS3 动画
- transition
让元素的CSS属性值的变化在一段时间内平滑的过渡
CSS3引入了transfrom属性,它可以通过对元素进行 平移(translate)、旋转(rotate)、放大缩小(scale)、倾斜(skew)
等操作,来实现2D和3D变换效果
transiton 还有一个结束事件 transitionEnd,该事件是在CSS完成过渡后触发,如果过渡在完成之前被移除,则不会触发transitionEnd - animation
需要设置一个@keyframes,来定义元素以哪种形式进行变换
然后再通过动画函数让这种变换平滑的进行,从而达到动画效果- 动画可以被设置为永久循环演示
- animation-play-state:paused可以暂停动画
- animation-fill-mode:forwards 可以让动画完成后定格在最后一帧
- 可以通过JS监听animation的开始、结束和重复播放时的状态,分别对应三个事件,即
animationStart、animationEnd、animationIteration
注意,当播放次数设置为1时,不会触发 animationIteration
对比
- animation 设置动画效果更灵活更丰富
- transition 只能通过主动改变元素的css值才能触发动画效果,而animation一旦被应用,就开始执行动画
2.7. BFC
BFC---Block Formatting Context
是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素
比如:内部滚动就是一个BFC,当一个父容器的overflow-y设置为auto时,并且子容器的长度大于父容器时,就会出现内部滚动,无论内部的元素怎么滚动,都不会影响父容器以外的布局,这个父容器的渲染区域就叫BFC。
特点
- 盒子们自所在的containing block顶部一个接一个垂直排列
- 水平方向上撑满整个宽度(除非内部盒子自己建立了新的BFC)
- 两个相邻的BFC之间的距离由margin决定
- 在同一个BFC内部,两个垂直方向相邻的块级元素的margin会发生“塌陷”
触发BFC
根元素或其它包含它的元素
- float的值不为none
- overflow的值不为visible
- display的值为inline-block、table-cell、table-caption
- position的值为absolute或fixed
- flex boxes (元素的display: flex或inline-flex)
应用
- 清除内部浮动
对子元素设置浮动后,父元素会发生高度塌陷,也就是父元素的高度变为0。解决这个问题,只需要把把父元素变成一个BFC就行了。常用的办法是给父元素设置overflow:hidden - 垂直margin合并
属于同一个BFC的两个相邻元素的margin会发生重叠 —— 创建 BFC - 创建自适应两栏布局 —— 解决侵占浮动元素的问题
参考: CSS: 潜藏着的BFC
2.8. Sprite,Iconfont,@font-face
- Sprite图
为了减少http请求的次数,一般会将常用的小图标排到一个大图中,页面加载时只需请求一次网络,在css中通过设置background-position来控制显示所需要的小图标 - Iconfont
即字体图标,就是将常用的图标转化为字体资源存在文件中,通过在CSS中引用该字体文件,然后可以直接用控制字体的css属性来设置图标的样式
字体图标的好处是节省网络请求、其大小不受屏幕分辨率的影响,并且可以任意修改图标的颜色 - @font-face
是CSS3中的一个模块
通过@font-face可以定义一种全新的字体,通过css属性font-family来使用这个字体
3. HTML
3.1. BOM
Browser Object Model 浏览器对象模型
当一个浏览器页面初始化时,会在内存创建一个全局的对象,用以描述当前窗口的属性和状态,这个全局对象被称为浏览器对象模型,即BOM
BOM的核心对象就是window,window对象也是BOM的*对象,其中包含了浏览器的 6个核心模块:
- document
- 即文档对象,渲染引擎在解析HTML代码时,会为每一个元素生成对应的DOM对象,由于元素之间有层级关系,因此整个HTML代码解析完以后,会生成一个由不同节点组成的树形结构,俗称DOM树
- document用于描述DOM树的状态和属性,并提供了很多操作DOM的API。
- frames
- HTML 子框架,即在浏览器里嵌入另一个窗口
- 父框架和子框架拥有独立的作用域和上下文。
- history
- 以栈(FIFO)的形式保存着页面被访问的历史记录
- 页面前进即入栈,页面返回即出栈。
- location
- 提供了当前窗口中加载的文档相关信息以及一些导航功能
- navigator
- 用来描述浏览器本身,包括浏览器的名称、版本、语言、系统平台、用户特性字符串等信息
- screen
- 提供了浏览器显示屏幕的相关属性,比如显示屏幕的宽度和高度,可用宽度和高度。
3.2. DOM 系统
Document Object Model 文档对象模型,是所有浏览器公共遵守的标准
- DOM将HTML和XML文档映射成一个由不同节点组成的树型结构,俗称DOM树
- 其核心对象是document,用于描述DOM树的状态和属性,并提供对应的DOM操作API
3.3. 事件系统
事件是用户与页面交互的基础,到目前为止,DOM事件从PC端的 鼠标事件(mouse) 发展到了 移动端的 触摸事件(touch) 和
手势事件(guesture),touch事件描述了手指在屏幕操作的每一个细节,guesture 则是描述多手指操作时更为复杂的情况
总结如下:
- 第一根手指放下,触发 touchstart,除此之外什么都不会发生
- 手指滑动时,触发touchmove
- 第二根手指放下,触发 gesturestart
- 触发第二根手指的 touchstart
- 立即触发 gesturechange
- 任意手指移动,持续触发 gesturechange
- 第二根手指弹起时,触发 gestureend,以后将不会再触发 gesturechange
- 触发第二根手指的 touchend
- 触发touchstart (多根手指在屏幕上,提起一根,会刷新一次全局touch)
- 弹起第一根手指,触发 touchend
DOM2.0 模型将事件处理流程分为三个阶段,即 事件捕获阶段 、 事件处理阶段 、 事件冒泡阶段
- 事件捕获
当用户触发点击事件后,顶层对象document 就会发出一个事件流,从最外层的DOM节点向目标元素节点传递,最终到达目标元素。 - 事件处理
当到达目标元素之后,执行目标元素绑定的处理函数。如果没有绑定监听函数,则不做任何处理。 - 事件冒泡
事件流从目标元素开始,向最外层DOM节点传递,途中如果有节点绑定了事件处理函数,这些函数就会被执行。
利用事件冒泡原理可以实现 事件委托
所谓事件委托,就是在父元素上添加事件监听器,用以监听和处理子元素的事件,避免重复为子元素绑定相同的事件
- 方式
当目标元素的事件被触发以后,这个事件就从目标元素开始,向最外层元素传递,最终冒泡到父元素上,父元素再通过event.target获取到这个目标元素 - 好处
父元素只需绑定一个事件监听,就可以对所有子元素的事件进行处理了,从而减少了不必要的事件绑定,对页面性能有一定的提升。
更多: 事件委托和 this
3.4. HTML 解析过程
浏览器加载 html 文件以后,渲染引擎会从上往下,一步步来解析HTML标签
过程如下:
- 请求服务器返回HTML文件
用户输入网址,浏览器向服务器发出请求,服务器返回html文件; - 生成dom 树
渲染引擎开始解析 html 标签,并将标签转化为DOM节点,生成 DOM树; - css文件请求
如果head 标签中引用了外部css文件,则发出css文件请求,服务器返回该文件,该过程会阻塞后面的解析; - js 请求
如果引用了外部 js 文件,则发出 js 文件请求,服务器返回后立即执行该脚本,这个过程也会阻塞html的解析; - 生成渲染树
引擎开始解析 body 里面的内容,如果标签里引用了css 样式,就需要解析刚才下载好的css文件,然后用css来设置标签的样式属性,并生成渲染树; - 下载图片资源
如果 body 中的 img 标签引用了图片资源,则立即向服务器发出请求,此时引擎不会等待图片下载完毕,而是继续解析后面的标签; - 重新渲染
- 服务器返回图片文件,由于图片需要占用一定的空间,会影响到后面元素的排版,因此引擎需要重新渲染这部分内容;
- 如果此时 js 脚本中运行了 style.display="none",布局被改变,引擎也需要重新渲染这部分代码;
- 直到 html 结束标签为止,页面解析完毕。
3.5. 重绘 和 回流
- 回流
当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建
如上面的img文件加载完成后就会引起回流,每个页面至少需要一次回流,就是在页面第一次加载的时候 - 重绘
当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如 background-color
从上面可以看出,回流必将引起重绘,而重绘不一定会引起回流
会引起重绘和回流的操作如下:
- 添加、删除元素(回流+重绘)
- 隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)
- 移动元素,比如改变top,left的值,或者移动元素到另外一个父元素中。(重绘+回流)
- 对style的操作(对不同的属性操作,影响不一样)
- 还有一种是用户的操作,比如改变浏览器大小,改变浏览器的字体大小等(回流+重绘)
- 另外,transform操作不会引起重绘和回流,是一种高效率的渲染。这是因为transform属于合成属性,对合成属性进行 transition/animation 动画时将会创建一个合成层,这使得动画元素在一个独立的层中进行渲染,当元素的内容没有发生改变,就没必要进行重绘,浏览器会通过重新复合来创建动画帧。
3.6. 本地存储
避免取回数据前页面空白,减少请求服务器次数
- cookie
- 本地存储最原始的方式
cookie 是存放在本地浏览器的一段文本,数据以键值对的形式保存,可以设置过期时间。 - 不适合大量数据的存储
因为每请求一次页面,cookie 都会发送给服务器,这使得 cookie速度很慢而且效率也不高。因此cookie的大小被限制为4k左右(不同浏览器可能不同,分HOST)
- 本地存储最原始的方式
- html5 提供了两种在客户端存储数据的新方法:
- localStorage
永久存储 - sessionStorage
存储期限仅限于浏览器会话(session),即当浏览器窗口关闭后,sessionStorage中的数据被清除
都是以key/value的形式来存储数据
localStorage的存储空间大约5M左右(不同浏览器可能不同,分 HOST),这个相当于一个5M大小的前端数据库,相比于cookie,可以节约带宽,但localStorage在浏览器隐私模式下是不可读取的,当存储数据超过了 localStorage 的存储空间后会抛出异常。
此外,H5还提供了逆天的 websql 和 indexedDB,允许前端以关系型数据库的方式来存储本地数据 - localStorage
cookie作用是与服务器交互,作为HTTP规范的一部分,web storage仅仅为本地存储而生
3.7. 浏览器缓存机制
浏览器缓存机制是指通过 HTTP 协议头里的 Cache-Control (或 Expires) 和 Last-Modified (或 Etag) 等字段来控制文件缓存的机制。
- Cache-Control
- 用于控制文件在本地缓存有效时长
- 比如服务器回包:Cache-Control:max-age=600
表示文件在本地应该缓存,且有效时长是600秒 (从发出请求算起)。在接下来600秒内,如果有请求这个资源,浏览器不会发出 HTTP请求,而是直接使用本地缓存的文件。
- Last-Modified
- 标识文件在服务器上的最新更新时间
下次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间,发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改,服务器返回304告诉浏览器继续使用缓存;如果有修改,则返回200,同时返回最新的文件。
- 标识文件在服务器上的最新更新时间
- Cache-Control 通常与 Last-Modified 一起使用
一个用于控制缓存有效时间,一个在缓存失效后,向服务查询是否有更新。- Cache-Control 还有一个同功能的字段:Expires。Expires 的值为一个绝对的时间点
如:Expires: Thu, 10 Nov 2015 08:45:11 GMT,表示在这个时间点之前,缓存都是有效的。
- Cache-Control 还有一个同功能的字段:Expires。Expires 的值为一个绝对的时间点
- Etag 也是和 Last-Modified 一样,对文件进行标识的字段
不同的是,Etag 的取值是一个对文件进行标识的特征字串。在向服务器查询文件是否有更新时,浏览器通过 If-None-Match
字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新。没有更新回包304,有更新回包200。Etag 和 Last-Modified 可根据需求使用一个或两个同时使用。两个同时使用时,只要满足基中一个条件,就认为文件没有更新。
另外有两种特殊的情况:
- 手动刷新页面(F5)
浏览器会直接认为缓存已经过期(可能缓存还没有过期),在请求中加上字段:Cache-Control:max-age=0,发包向服务器查询是否有文件是否有更新。 - 强制刷新页面(Ctrl+F5)
浏览器会直接忽略本地的缓存(有缓存也会认为本地没有缓存),在请求中加上字段:Cache-Control:no-cache (或 Pragma:no-cache),发包向服务重新拉取文件
3.8. History
用户访问网页的历史记录通常会被保存在一个类似于栈的对象中,即history对象,点击返回就出栈,跳下一页就入栈
它提供了以下方法来操作页面的前进和后退:
- window.history.back( ) 返回到上一个页面
- window.history.forward( ) 进入到下一个页面
- window.history.go( [delta] ) 跳转到指定页面
HTML5 对History Api 进行了增强,新增了两个Api 和一个事件,分别是pushState、replaceState 和 onpopstate:
- pushState是往history对象里添加一个新的历史记录,即压栈。
- replaceState 是替换history对象中的当前历史记录。
- 当点击浏览器后退按钮或 js调用history.back 都会触发 onpopstate 事件。
与其类似的还有一个事件:onhashchange,onhashchange是老API,浏览器支持度高,本来是用来监听hash变化的,但可以被利用来做客户端前进和后退事件的监听,而onpopstate是专门用来监听浏览器前进后退的,不仅可以支持hash,非hash的同源 url 也支持。
3.9. HTML5离线缓存
HTML5离线缓存又叫Application Cache,是从浏览器的缓存中分出来的一块缓存区,如果要在这个缓存中保存数据,可以使用一个描述文件(manifest file),列出要下载和缓存的资源。
manifest 文件是简单的文本文件,它告知浏览器被缓存的内容(以及不缓存的内容)。manifest 文件可分为三个部分:
- CACHE MANIFEST - 在此标题下列出的文件将在首次下载后进行缓存
- NETWORK - 在此标题下列出的文件需要与服务器的连接,且不会被缓存
- FALLBACK - 在此标题下列出的文件规定当页面无法访问时的回退页面(比如 404 页面)
离线缓存为应用带来三个优势:
- 离线浏览 - 用户可在应用离线时使用它们
- 速度 - 已缓存资源加载得更快
- 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。
参考:
Web Storage
常用的web客户端存储
1. 引言
本地存储:避免取回数据前页面空白,减少请求服务器次数 chrome浏览器,查看resources
2. 常用存储方案
2.1. indexDB
类似SQL数据库的结构化数据存储机制,能够在客户端存储大量的结构化数据 缺点: 兼容性不好,浏览器支持度低
2.2. cookie
HTTP cookie 「浏览器」提供的一种机制,通过document.cookie访问
- 既可以服务器端设置,也可以客户端设置,会跟随任意HTTP请求发送
- cookie是存于用户硬盘的一个文件,这个文件通常对应于一个域名,也就是说,cookie可以跨越一个域名下的多个网页,但不能跨越多个域名使用
2.2.1. 用途
- 保存用户信息
- 购物车
- 跟踪用户行为
2.2.2. 查找过程
- 用户访问站点
- web应用读取cookie包含的信息
- 再次访问时,浏览器在本地硬盘上查找相关cookie
- 若存在该cookie,添加到request header cookie字段中,与该HTTP请求一起发送 存储在cookie的数据每次都会自动添加到请求中,滥用则降低性能
2.2.3. cookie相关属性
-
domain 和 path 共同决定了cookie的共享页面
例如cookie设置为"domain=.google.com.hk; path=/webhp",那么只有".google.com.hk/webhp"及"/webhp"下的任一子目录如"/webhp/aaa"或"/webhp/bbb"会发送cookie信息,而".google.com.hk"就不会发送,即使它们来自同一个域。
expries 和 max-age 是用来决定cookie的生命周期
secure cookie的安全标志 cookie中唯一一个非名值对儿的部分,默认为空,不论是 http 请求还是 https 请求,均会发送cookie。 指定后,cookie只有在使用SSL连接(如HTTPS请求)时才会发送到服务器
httponly 服务端设置 限制客户端脚本对cookie的访问,将 cookie 设置成 httponly 可以减轻xss攻击的危害
2.2.4. cookie设置
- 服务器端 通过发送一个名为 Set-Cookie 的HTTP头来创建一个cookie,作为 Response Headers 的一部分,每个Set-Cookie 表示一个 cookie(如果有多个cookie,需写多个Set-Cookie),每个属性也是以名/值对的形式(除了secure),属性间以分号加空格隔开。格式如下:
Set-Cookie: name=value[; expires=GMTDate][; domain=domain][; path=path][; secure]
只有name,value发送到服务器,其他是给浏览器的指示 2. 客户端 document.cookie = "name=value[; expires=GMTDate][; domain=domain][; path=path][; secure]"
var cookie = {
/**设置cookie
** name 标识
** value 值
** options {
** 'path': '访问路径',
** 'domain' : '域名',
** 'expire' : 过期时间
}
**/
setCookie : function(name,value,options){
var options = options ? options : {},
path = options.path ? options.path : '/',
domain = options.domain ? options.domain : document.domain,
time = options.expire ? (new Date().getTime() + options.expire * 1000) : '',
expire = new Date(time).toUTCString();
document.cookie = encodeURIComponent(name) + "="+ encodeURIComponent(value) + ";expires=" + expire +
";domain=" + domain + ";path=" + path;
},
//获取cookie
getCookie: function(name){
var arr,
reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
if(arr=document.cookie.match(reg)){
console.log(arr);
return unescape(arr[2]);
}
return null;
},
//移除cookie
removeCookie: function(name){
var val = this.getCookie(name);
if(val != null){
this.setCookie(name,val, {
expire : - 1
})
}
}
}
2.2.5. 优缺点
优点: 兼容性好 缺点:
- 增加了网络流量
- 数据容量有限(最多4kb,浏览器间有区别)
- 安全性
2.3. sessionStorage和localStorage
key value形式存储
- localStorage - 没有时间限制的数据存储
- sessionStorage - 针对一个 session 的数据存储
if(typeof(Storage)!=="undefined")
{
// 是的! 支持 localStorage sessionStorage 对象!
// 一些代码.....
} else {
// 抱歉! 不支持 web 存储。
}
2.3.1. 使用
localStorage.sitename = "超然haha";
localStorage.removeItem("lastname");
- 保存数据:
localStorage.setItem(key,value);
- 读取数据:
localStorage.getItem(key);
- 删除单个数据:
localStorage.removeItem(key);
- 删除所有数据:
localStorage.clear();
- 得到某个索引的key:localStorage.key(index);
2.3.2. 适用范围
- 不需要和服务器进行交互的一些数据 比如导航栏当前的状态,一些普通的数据进行缓存。甚至我们可以存储html片段,js或者css文件段
- 很多应用通过版本控制来存储一些不经常改动的js/css文件。减少用户请求带宽的同时优化整个页面的加载速度。
2.3.3. 注意
localstorage存储的值只能是字符串的形式 当我们存储数据为引用对象的时候,会默认调用对象的toString方法,转化为字符串存储 所以我们在存储数组时,存储的数据会将数据项以,隔开,解析的时候需要我们分解成为数组再操作。而对于对象,我们需要用JSON.stringify转化存储,获取数据后再用JSON.parse转化为对象
2.4. web storage 与 cookie
web storage优点:
- web storage 为了更大容量存储,一般限制为同一域名5M,,并且不同域名的数据不能相互访问
- localStorage是存储在用户本地的浏览器上,不像cookie一样携带在http请求头部的字段里面,这有效的节约了带宽
- cookie需要指定作用域,不可跨域调用
- 拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookie,getCookie
cookie优点:
- cookie作用是与服务器交互,作为HTTP规范的一部分,web storage仅仅为本地存储而生
You don't know js
“Give me a chance to know you. ”
更多内容: 移步这里
1. 作用域
1.1. 编译原理
尽管通常将 JavaScript 归类为“动态” 或“解释执行” 语言, 但事实上它是一门编译语言。
程序中的一段源代码在执行之前会经历三个步骤, 统称为“编译”
- 分词/词法分析(Tokenizing/Lexing)
- 这个过程会将由字符组成的字符串分解成(对编程语言来说) 有意义的代码块, 这些代
码块被称为词法单元(token)。 例如, 考虑程序 var a = 2;。 这段程序通常会被分解成
为下面这些词法单元: var、 a、 =、 2 、 ;。
- 这个过程会将由字符组成的字符串分解成(对编程语言来说) 有意义的代码块, 这些代
- 解析/语法分析(Parsing)
- 这个过程是将词法单元流(数组) 转换成一个由元素逐级嵌套所组成的代表了程序语法
结构的树。 这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)。
- 这个过程是将词法单元流(数组) 转换成一个由元素逐级嵌套所组成的代表了程序语法
- 代码生成
- 将 AST 转换为可执行代码的过程称被称为代码生成。
1.2. 作用域嵌套
当一个块或函数嵌套在另一个块或函数中时, 就发生了作用域的嵌套。 因此, 在当前作用
域中无法找到某个变量时, 引擎就会在外层嵌套的作用域中继续查找, 直到找到该变量,
或抵达最外层的作用域(也就是全局作用域) 为止。
将作用域处理的过程可视化,如下面的建筑:
作用域是一套规则, 用于确定在何处以及如何查找变量(标识符)。
2. 词法作用域
作用域共有两种主要的工作模型:
- 词法作用域(重点讨论)
- 动态作用域(如bash脚本,perl中的一些模式)
2.1. 词法阶段
词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义——名称来历
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的
如:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar( b * 3 );
}
foo( 2 ); // 2, 4, 12
可以将以上代码想象成几个逐级包含的气泡
① 包含着整个全局作用域, 其中只有一个标识符: foo。
② 包含着 foo 所创建的作用域, 其中有三个标识符: a、 bar 和 b。
③ 包含着 bar 所创建的作用域, 其中只有一个标识符: c。
作用域气泡由其对应的作用域块代码写在哪里决定, 它们是逐级包含的。
查找
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,作用域查找会在找到第一个匹配的标识符时停止
全局变量会自动成为全局对象(比如浏览器中的window对象)的属性,因此可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引用来对其进行访问。
window.a通过这种技术可以访问那些被同名变量所遮蔽的全局变量。 但非全局的变量
如果被遮蔽了, 无论如何都无法被访问到。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
词法作用域查找只会查找一级标识符,比如a、b和c。如果代码中引用了foo.bar.baz,词法作用域查找只会试图查找 foo 标识符,找到这个变量后, 对象属性访问规则会分别接管对 bar 和 baz 属性的访问
2.2. 欺骗词法
JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..)和with。前者可以对一段包
含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在
运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作
用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
3. 函数作用域和块作用域
究竟是什么生成了一个新的气泡?只有函数会生成新的气泡吗?JavaScript中的其他结构能生成作用域气泡吗?
3.1. 隐藏内部实现
3.1.1. 最小授权|最小暴露原则
指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。——可延伸到如何选择作用域来包含变量和函数
如:
function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2); // 15
/*在这个代码片段中, 变量 b 和函数 doSomethingElse(..) 应该是 doSomething(..) 内部具体实现的“私有” 内容。 给予外部作用域对 b 和 doSomethingElse(..) 的“访问权限” 不仅没有必要且危险*/
// 更合理
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
d
oSomething(2); // 15
//设计上将具体内容私有化
3.1.2. 规避冲突
-
全局命名空间
用变量作为库的命名空间
所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在*的词法作用域中如:
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ...26 } };
3.2. 函数作用域
区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
1. 匿名和具名
始终给函数表达式命名是一个最佳实践
setTimeout( function timeoutHandler() { // 快看, 我有名字了
console.log( "I waited 1 second!" );
}, 1000 );
2. 立即执行函数表达式
/*第一种*/
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
/* 第二种形式*/
(function foo(){ .. })()
/*进阶*/
/*将 window 对象的引用传递进去, 但将参数命名为 global*/
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
console.log( a ); // 2
/*IIFE 还有一种变化的用途是倒置代码的运行顺序, 将需要运行的函数放在第二位, 在 IIFE执行之后当作参数传递进去。*/
var a = 2;
(function IIFE( def ) {
def( window );
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
});
函数表达式 def 定义在片段的第二部分, 然后当作参数(这个参数也叫作 def) 被传递进
IIFE 函数定义的第一部分中。 最后, 参数 def(也就是传递进去的函数) 被调用, 并将
window 传入当作 global 参数的值。
函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { .. } 内部)。
4. 变量提升
先有蛋(声明) 后有鸡(赋值)。
JavaScript 引擎将 var a和 a = 2 当作两个单独的声明, 第一个是编译阶段的任务, 而第二个则是执行阶段的任务。无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数) 都会被“移动” 到各自作用域的最顶端, 这个过程被称为提升
只有声明本身会被提升, 而赋值或其他运行逻辑会留在原地。
4.1. 函数优先
函数声明和变量声明都会被提升。 但是一个值得注意的细节(这个细节可以出现在有多个
“重复” 声明的代码中) 是 函数会首先被提升, 然后才是变量。
5. 作用域闭包
闭包的创建和使用在你的代码中随处可见。你缺少的是根据你自己的意愿来识别、拥抱和影响闭包的思维环境
5.1 什么是闭包
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
function foo() {
var a = 2;
function bar() {
console.log(a); // 2
}
bar();
}
foo();
//基于词法作用域的查找规则, 函数bar() 可以访问外部作用域中的变量 a
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友, 这就是闭包的效果。
bar() 显然可以被正常执行。 但是在这个例子中, 它在自己定义的词法作用域以外的地方执行
foo() 执行后垃圾回收器用来释放不再使用的内存空间,闭包的“神奇”之处正是可以阻止这件事情的发生。 事实上内部作用域依然存在,bar() 依然持有对该作用域的引用, 而 这个引用就叫作闭包。
常见的闭包:
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
//timer 具有涵盖 wait(..) 作用域的闭包, 因此还保有对变量 message 的引用。
//wait(..) 执行 1000 毫秒后, 它的内部作用域并不会消失, timer 函数依然保有 wait(..)作用域的闭包。
只要使用了回调函数, 实际上就是在使用闭包!
5.2. 循环和闭包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i); //6
}, i * 1000);
}
/*所有的回调函数依然是在循环结束后才会被执行, 因此会每次输出一个 6 出来。*/
原因
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获” 一个 i 的副本。
但是根据作用域的工作原理, 实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
但是它们都被封闭在一个共享的全局作用域中, 因此实际上只有一个 i。
我们需要更多的闭包作用域, 特别是在循环的过程中每个迭代都需要一个闭包作用域
//它需要有自己的变量, 用来在每个迭代中储存 i 的值:
for (var i = 1; i <= 5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})();
}
//使用let
//本质上这是将一个块转换成一个可以被关闭的作用域。
for (var i = 1; i <= 5; i++) {
let j = i; // 是的, 闭包的块作用域!
setTimeout(function timer() {
console.log(j);
}, j * 1000);
}
//块作用域和闭包联手
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
5.3. 模块
5.3.1. 模块方式演进
模块有两个主要特征:
- 为创建内部作用域而调用了一个包装函数;
-
包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
```javascript
//exa1:
//两个私有数据变量 something和 another, 以及 doSomething() 和 doAnother()
//它们的词法作用域(而这就是闭包) 也就是 foo() 的内部作用域。
function foo() {
var something = "cool";
var another = [1, 2, 3];function doSomething() {
console.log(something);
}function doAnother() {
console.log(another.join(" ! "));
}
}
//模块
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
//1. CoolModule() 只是一个函数, 必须要通过调用它来创建一个模块实例。 如果不执行外部函数, 内部作用域和闭包都无法被创建。
//2. CoolModule() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。 这个返回的对象中含有对内部函数而不是内部数据变量的引用
//改进
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
//模块模式另一个简单但强大的变化用法是, 命名将要作为公共 API 返回的对象:
var foo = (function CoolModule(id) {
function change() {
// 修改公共 API
publicAPI.identify = identify2;
}
function identify1() {
console.log(id);
}
function identify2() {
console.log(id.toUpperCase());
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})("foo module");
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
```
当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创造了可以观察和实践闭包的条件。
因此 一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块
5.3.2. ES6的模块
ES6 的模块没有“行内” 格式, 必须被定义在独立的文件中(一个文件一个模块),可
以在导入模块时异步地加载模块文件。
//bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello
//foo.js
// 仅从 "bar" 模块导入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello(hungry).toUpperCase()
);
}
import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上(在我们的例子里是hello)。module会将整个模块的API导入并绑定到一个变量上(在我们的例子里是foo)。export会将当前模块的一个标识符(变量、函数)导出为公共API。这些操作可以在模块定义中根据需要使用任意多次。
5.3.3. 动态作用域
动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
function foo() {
console.log(a); //2 —— 如果是动态作用域3
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
JavaScript并不具有动态作用域。它只有词法作用域
主要区别:
词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用
6. this词法
6.1. _self
常见this绑定丢失解决方案: 'var _self = this'
6.2. 箭头函数
ES6 中的箭头函数引入了一个叫作 this 词法的行为
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(() => { // 箭头函数是什么鬼东西?
this.count++;
console.log("awesome?");
}, 100);
}
}
};
obj.cool(); // 很酷吧 ?
简单来说,箭头函数在涉及this绑定时的行为和普通函数的行为完全不一致。它放弃了所有普通this绑定的规则,取而代之的是用当前的词法作用域覆盖了this本来的值
这个代码片段中的箭头函数只是“继承”了cool()函数的this绑定(因此调用它并不会出错)。
6.3. bind
//正确使用和包含 this 机制
var obj = {
count: 0,
cool: function coolFn() {
if (this.count < 1) {
setTimeout(function timer() {
this.count++; // this 是安全的
// 因为 bind(..)
console.log("more awesome");
}.bind(this), 100); // look, bind()!
}
}
};
obj.cool(); // 更酷了。
var foo=function(){}
1 函数表达式?
2 也要有名字? var foo=function foooo(){}
为什么函数表达式有名字是最佳实践?
匿名函数写起来很方便,现在的很多库使用的也很多,但有几点风险:
1. 如果一个函数被调用了多次,出现问题后想要使用栈追踪想看看都在哪里调用了,如果是匿名函数的话不会显示有意义的函数名称,不方便调试
2. 调用自身只能用arguments.callee了
3. 解除事件绑定时,也需要函数名
4. 影响可读性