JavaScript
作用域
作用域分静态作用域和动态作用域两种,其中静态作用域又被叫做词法作用域。在 JavaScript 中没有动态作用域,采用的是词法作用域,也就是说函数的作用域在函数定义的时候就决定了。
按照变量的有效范围可以分为全局作用域、函数作用域和块级作用域。其中,块级作用域需要配合 let
或 const
关键字。
继承
原型链:
- 实例对象通过
__proto__
属性或Object.getPrototypeOf()
访问原型对象; -
null
没有原型对象,并且是原型链的最后一个环节; - 访问对象的属性时会从自身开始沿原型链查找,直到找到或到尾。
一个字面量的原型链如下:
字面量 ---> 内置对象.prototype ---> Object.prototype ---> null
当继承的函数被调用时,this
指向的是当前继承的实例对象而非继承的函数所在的原型对象。
可以通过以下几种方式来创建对象和生成原型链:
遍历
for...in
以任意顺序遍历一个对象的可枚举属性。
for...of
作用于可迭代对象,如 Array
、String
、Map
、Set
和 TypedArray。
事件
事件流
传递方向 |
---|
绑定方式 | 阶段 |
---|---|
DOM 事件模型 | IE 专有事件模型 |
---|---|
const el = event.target || event.srcElement;
...
}
事件代理与事件绑定
闭包
闭包又称词法闭包或函数闭包,是引用了自由变量的函数,是由函数以及创建该函数的词法环境组合而成,这个环境包含了闭包创建时所能访问的所有局部变量。
return function() {
alert(name);
}
}
const whatIsMyName = createClosure();
whatIsMyName();
用闭包可以模拟基于类的 OOP 中的私有属性和方法,从而隐藏和封装数据。从性能角度考虑,实例对象的公共方法应该通过继承原型对象实现而不用闭包。
对象拷贝
数组
数组的复制可以通过 [].slice()
或 [].concat()
来实现。
对象
对象的复制分为浅拷贝和深拷贝两种。
浅拷贝的特点:
深拷贝的特点:
最简单省力的深拷贝可以通过调用 JSON.parse()
和 JSON.stringify()
来实现。但这种方式有个缺陷,就是源对象必须符合 JSON 规范。完全的深拷贝只能通过手写递归来实现。
异步编程
目前在 JavaScript 中进行异步编程可以采用的形式有回调函数、事件监听、观察者模式、Promise、Generator 和 async/await。
Promise
- 是一个拥有
then()
的对象或函数; - 有
pending
、resolved
和rejected
三种状态,只能由pending
变成resolved
或rejected
,resolved
和rejected
互相不能转换; - 执行成功时调用
then()
的第一个回调函数,失败时调用第二个回调函数; - 实例对象的
then()
必须返回一个新的 Promise 实例; - 解决了多重嵌套的回调函数带来的「回调地狱」;
- 没有提供原生的中止 Promise 链的方法。
Generator
async/await
- 可以理解为是 Generator 的语法糖,
async
相当于*
,await
相当于yield
; - 内置执行器;
-
async
函数的返回值是 Promise 实例; -
await
后是 Promise 实例或会被转化为 Promise 实例的普通值; -
await
只能在async
函数中使用。
防抖与节流
防抖(debounce)和节流(throttle)的作用都是防止函数多次调用。区别在于——假设一个用户一直触发这个函数,且每次触发函数的间隔小于 wait
,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(参数 wait
)调用函数。
防抖的使用场景:
- 文本输入相关逻辑,如搜索、校验。
节流的使用场景:
- DOM 元素的拖拽(
mousemove
); - 射击游戏的
mousedown
、keydown
事件(单位时间只能发射一颗子弹); - 计算鼠标移动的距离(
mousemove
); - Canvas 模拟画板功能(
mousemove
); - 监听
scroll
事件滚动到页面底部加载更多内容。
程序设计
数据结构
数据结构是计算机中存储、组织数据的方式。数据结构意味着接口或封装:一个数据结构可被视为两个函数之间的接口,或者是由数据类型联合组成的存储内容的访问方法封装。
数组
数组(array)是一个存储元素的线性集合,元素可以通过索引来任意存取,索引通常是数字,用来计算元素之间存储位置的偏移量。
列表
列表(list)或序列(sequence),是一种抽象数据类型,一种有限的有序值的集合,其中每个值可以出现多次。列表的一个实例是在计算机中用来表现出数学上有限序列的概念;列表的无限类似是流。列表是容器的一个基本例子,因为它们包含其他值。在列表中的每个值(value),称为项目(item)、条目(entry)或元素(element);如果相同的值出现多次,每一次出现都认为是分立的一个项目。列表和数组区别在列表只允许顺序访问,而数组允许随机访问。
栈
栈(stack)是一种按照后进先出(LIFO,Last In First Out)的原理运作的有序集合,新添加的或待删除的元素都保存在栈的末尾,称作「栈顶」,另一端为「栈底」。在栈里,新元素都靠近栈顶,旧元素都接近栈底。
栈数据结构使用两种基本操作:推入(压栈,push)和弹出(弹栈,pop)。推入是将数据放入栈的顶端;弹出是将顶端数据数据输出(回传)。
栈的基本特点:
- 先入后出,后入先出;
- 除头尾节点之外,每个元素有一个前驱和一个后继。
队列
队列(queue)是先进先出(FIFO,First-In-First-Out)的线性表,只允许在后端(rear)进行插入操作,在前端(front)进行删除操作。
链表
链表(linked list)是存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成。
单向链表
链表中最简单的一种是单向链表,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
双向链表
每个节点有两个连接:一个指向前一个节点,而另一个指向下一个节点。当连接为第一个或最后一个时,指向空值或者空列表。
循环链表
块状链表
字典
字典(dictionary)又称关联数组(associative array)、映射(map),是以 [键,值]
有序对为数据形态的数据结构,其中键名用来查询特定元素。
散列
根据关键码值(Key value)直接进行访问的数据结构;它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度;这个映射函数叫做散列函数,存放记录的数组叫做散列表。
集合
由一组无序且唯一(即不能重复)的项组成;这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。
树
树(tree)是由 n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做「树」是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的,基本呈一对多关系,树也可以看做是图的特殊形式。
图
图(graph)是网络结构的抽象模型;图是一组由边连接的节点(顶点);任何二元关系都可以用图来表示,常见的比如:道路图、关系图,呈多对多关系。
算法
冒泡排序
相邻的两个元素依次比较,小的放在左边。
选择排序
从未排序序列中找到最大(小)值存放到已排序序列末尾。
插入排序
从已排序序列中找到小于或等于当前数的位置并插到其后。
希尔排序
归并排序
归并排序(merge sort)是创建在归并操作上的一种有效的排序算法。归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
使用递归实现,不改变原数组:
const ll = left.length;
const rl = right.length;
let li = 0;
let ri = 0;
while ( li < ll && ri < rl ) {
if ( left[li] <= right[ri] ) {
arr.push(left[li++]);
}
else {
arr.push(right[ri++]);
}
}
while ( li < ll ) {
arr.push(left[li++]);
}
while ( ri < rl ) {
arr.push(right[ri++]);
}
return arr;
}
function mergeSort( arr ) {
const len = arr.length;
if ( len < 2 ) {
return arr;
}
const mid = Math.floor(len / 2);
return merge(mergeSort(arr.slice(0,mid)),mergeSort(arr.slice(mid)));
}
快速排序
找到一个数,把小于它的放在左边,大于它的放在右边,等于它的任选一边。
二分搜索
在计算机科学中,二分搜索(binary search),也称折半搜索(half-interval search)、对数搜索(logarithmic search),是一种在有序数组中查找某一特定元素的搜索算法。
搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
使用递归实现:
const copy = arr.slice(start,end);
const len = copy.length;
if ( len === 0 ) {
return -1;
}
if ( len === 1 ) {
return copy[0] === val ? start : -1;
}
const idx = Math.floor(len / 2) - 1;
const mid = start + idx;
const base = copy[idx];
if ( val === base ) {
return mid;
}
if ( val < base ) {
end = mid;
}
else {
start = mid + 1;
}
return binarySearch(arr,end);
}
使用循环实现:
while ( start <= end ) {
const mid = Math.floor((start + end) / 2);
const base = arr[mid];
if ( base < val ) {
start = mid + 1;
}
else if ( base > val ) {
end = mid - 1;
}
else {
return mid;
}
}
return -1;
}
设计模式
设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。
设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。面向对象设计模式通常以类别或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
构造函数模式
在像 JavaScript 这种没有类的概念的语言中,如果想创建特定的实例对象,可以通过在函数之前加上 new
关键字来实现。在调用构造函数时可以传入参数以初始化实例对象的属性值。用这种方式创建的实例对象的属性和方法是独立的,不与其他实例对象共享。
在 JavaScript 中,构造函数一般长这样:
this.introduce = function() {
return 'My name is ' + this.name + '. I\'m ' + this.age + ' years old.';
}
}
其中,name
和 age
是成员属性,introduce()
是成员方法。
让我们来用「家庭成员」构造函数 FamilyMember()
来创建几个实例对象看看吧!
console.log(host.introduce()); // "My name is Ourai. I'm 18 years old."
console.log(hostess.introduce()); // "My name is Julia. I'm 17 years old."
console.log(host === hostess); // false
console.log(host.introduce === hostess.introduce); // false
由上例中的 ===
比较可以看出,两个实例对象 host
和 hostess
的 introduce()
方法是不同的,即构造函数 FamilyMember()
的成员方法不是共享的。
原型模式
原型模式是创建型模式的一种,其特点在于通过「复制」一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的「原型」,这个原型是可定制的。
原型模式多用于创建复杂的或者耗时的实例,因为这种情况下,复制一个已经存在的实例使程序运行更高效;或者创建值相等,只是命名不一样的同类数据。
将构造函数模式中的示例稍作改造,用 prototype
实现成员方法:
FamilyMember.prototype.introduce = function() {
return 'My name is ' + this.name + '. I\'m ' + this.age + ' years old.';
}
创建两个 FamilyMember()
的实例并用 ===
比较 introduce()
方法:
console.log(host.introduce()); // "My name is Ourai. I'm 18 years old."
console.log(hostess.introduce()); // "My name is Julia. I'm 17 years old."
console.log(host === hostess); // false
console.log(host.introduce === hostess.introduce); // true
这回两个实例的 introduce()
方法是同一个函数,即构造函数 FamilyMember()
的成员方法是共享的。
单例模式
单例模式(singleton pattern)是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
在 JavaScript 里,单例作为一个命名空间提供者,从全局命名空间里提供一个唯一的访问点来访问该对象。
建造者模式
工厂模式
装饰者模式
外观模式
代理模式
观察者模式
观察者模式(observer pattern)是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。
策略模式
命令模式
迭代器模式
中介者模式
享元模式
职责链模式
适配器模式
适配器模式(adapter pattern)有时候也称包装样式或者包装(wrapper)。将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中。
组合模式
模板方法模式
状态模式
桥接模式
桥接模式(bridge pattern)是软件设计模式中最复杂的模式之一,它把事物对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。事物对象仅是一个抽象的概念。如「圆形」、「三角形」归于抽象的「形状」之下,而「画圆」、「画三角」归于实现行为的「画图」类之下,然后由「形状」调用「画图」。
抽象工厂模式
抽象工厂模式(abstract factory pattern)是一种软件开发设计模式。抽象工厂模式提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来。在正常使用中,客户端程序需要创建抽象工厂的具体实现,然后使用抽象工厂作为接口来创建这一主题的具体对象。客户端程序不需要知道(或关心)它从这些内部的工厂方法中获得对象的具体类型,因为客户端程序仅使用这些对象的通用接口。抽象工厂模式将一组对象的实现细节与他们的一般使用分离开来。
工厂方法模式
工厂方法模式(factory method pattern)是一种实现了「工厂」概念的面向对象设计模式。就像其他创建型模式一样,它也是处理在不指定对象具体类型的情况下创建对象的问题。工厂方法模式的实质是「定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。」
浏览器
垃圾回收
垃圾回收在计算机科学中是一种自动的内存管理机制。当一个计算机上的动态内存不再需要时,就应该予以释放以让出内存,这种内存资源管理称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会。
垃圾回收基于两个原理:
- 考虑某个对象在未来的程序运行中将不会被访问;
- 向这些对象要求归还内存。
然而,最主要的也是最艰难的部分就是找到「所分配的内存确实已经不再需要了」。
在 JavaScript 中,主要有引用计数和标记-清除这两种方式来找到不再使用的内存。
引用计数
在内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将「对象是否不再需要」简化成「对象有没有其他对象引用到它」,如果没有对象引用这个对象,那么这个对象将会被回收。
但是引用计数有个最大的问题:循环引用。
标记-清除
这个算法把「对象是否不再需要」简化定义为「对象是否可以获得」。
这个算法假定设置一个叫做根(root)的对象(在 JavaScript 里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
从 2012 年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对「对象是否不再需要」的简化定义。
跨域
我们所遇到的跨域问题是因为浏览器的同源策略所导致的。同源策略分为 DOM 和 XHR 两种,其中 DOM 同源策略对 有所限制。
在进行跨域访问时,根据具体情况一般会采用以下几种方式:
- CORS,客户端发请求时设置
withCredentials
为true
,服务端需要在响应头中设置Access-Control-Allow-Origin
和Access-Control-Allow-Credentials
; - JSONP;
- 服务器代理;
-
window.name
配合;
-
window.postMessage()
; - 修改
document.domain
跨子域; - WebSocket。
页面性能优化
桌面端性能优化
网络加载
- 减少 HTTP 请求次数;
- 减小 HTTP 请求大小;
- 将 CSS 或 JavaScript 放到外部文件中,避免使用标签直接引入;
- 避免页面中空的
href
和src
属性; - 为 HTML 指定
Cache-Control
或Expires
; - 合理设置
Etag
和Last-Modified
; - 减少页面重定向;
- 使用静态资源分域存放来增加下载并行数;
- 使用 CDN 存放文件;
- 使用 CDN Combo 下载传输内容;
- 使用可缓存的 AJAX;
- 使用
GET
请求方法来完成获取数据的 AJAX 请求; - 减小 cookie 大小并对其进行隔离;
- 缩小
favicon.ico
并缓存; - 推荐使用异步 JavaScript 资源;
- 消除阻塞渲染的 CSS 及 JavaScript;
- 避免使用 CSS import 引用加载 CSS。
页面渲染
- 把 CSS 资源引用放在 HTML 文件顶部;
- 把 JavaScript 资源引用放在 HTML 文件底部;
- 不要在 HTML 中直接缩放图片;
- 减少 DOM 的数量和深度;
- 尽量避免使用
、
等慢元素;
- 避免运行耗时的 JavaScript;
- 避免使用 CSS 表达式或滤镜。
移动端性能优化
用于桌面端页面的性能优化建议绝大部分也同样可以用到移动端页面中,这里主要是额外的一些针对移动端页面的优化建议。
网络加载
- 首屏数据请求提前,避免 JavaScript 文件加载后才请求数据;
- 首屏加载和按需加载,非首屏内容按需加载,保证首屏内容最小化;
- 模块化资源并行下载;
- 内联首屏必备的 CSS 和 JavaScript;
- 通过 Meta dns prefetch 设置 DNS 预解析;
- 资源预加载;
- 合理利用 MTU 策略。
缓存
- 合理利用浏览器缓存;
- 静态资源离线方案;
- 尝试使用 AMP HTML;
- 尝试使用 PWA 模式。
图片
- 图片压缩处理;
- 使用较小的图片,合理使用 Base64 内嵌图片;
- 使用更高压缩比格式的图片;
- 图片懒加载;
- 使用媒体查询或
srcset
根据不同屏幕加载不同大小的图片; - 使用图标字体代替图片图标;
- 定义图片大小设置;
- 强缓存策略。
JavaScript 脚本
- 尽量使用
id
选择器; - 合理缓存 DOM 对象;
- 页面元素尽量使用事件代理,避免直接事件绑定;
- 使用
touchstart
代替click
; - 避免
touchmove
、scroll
连续事件处理; - 避免使用
eval
、with
; - 使用
[].join()
代替字符串连接符+
,推荐使用 ES6 的字符串模板; - 尽量使用 ES6+ 的特性来编程。
页面渲染
- 使用 viewport 固定屏幕渲染,可以加速页面渲染内容;
- 避免各种形式重排重绘;
- 使用 CSS3 动画,开启 GPU 加速;
- 合理使用 Canvas 和
requestAnimationFrame()
; - SVG 代替图片;
- 不滥用
float
; - 不滥用 web 字体或过多的
font-size
声明; - 做好脚本容错。
架构协议
- 尝试使用 SPDY 和 HTTP 2;
- 使用后端数据渲染;
- 使用 NativeView 代替 DOM 的性能劣势。
前端工程
Web 安全
通信协议
HTTP
- 传输超媒体文档的应用层协议;
- 遵循经典的客户端-服务端模型;
- 无状态;
- 通常基于 TCP/IP 实现,但可以在任何可靠的传输层使用,如 UDP;
- 默认使用 80 端口;
- 文本解析。
HTTPS
- 需要 CA 证书;
- 默认使用 443 端口;
- 运行在 SSL/TLS 之上,传输的内容加密过,防止网络运营商劫持;
- 开启 HSTS 可以让浏览器只能访问 HTTPS,HTTP 请求会被浏览器 307 内部重定向,能够预防中间人攻击。
SPDY
HTTP 2
- 多路复用,即多个请求可以共享一个连接;
- 二进制解析,实现方便且健壮;
- 首部压缩,使用 HPACK 算法对 header 信息进行压缩;
- 服务端推送,可以对客户端的一个请求发送多个响应,并能让同源的不同页面间共享缓存资源;
- 理论上 SSL/TLS 不是必需,但在 Chrome 等浏览器中访问时是必要的;
- 主流浏览器支持性好。