共计 7904 个字符,预计需要花费 20 分钟才能阅读完成。
简答题
1. 描述引用计数的工作原理和优缺点。
引用计数的工作原理:设置对象的引用数,有一个引用计数器来维护这些引用数,引用关系改变时修改引用数。判断当前对象引用数是否为 0,引用数为 0 时立即回收。
引用计数算法优点:
- 发现垃圾时立即回收
- 最大限度减少程序暂停
引用计数算法缺点:
- 无法回收循环引用的对象
- 时间开销大,资源消耗较大
2. 描述标记整理算法的工作流程。
标记整理算法工作流程:
- 标记整理可以看做是标记清除的增强,也是分标记和清除两个阶段来完成
- 标记阶段:遍历所有对象找标记活动对象
- 清除阶段:先执行整理,移动对象的位置,然后遍历所有对象清除没有标记的对象
- 最后回收相应的空间
标记整理算法优缺点:
- 减少碎片化空间
- 不会立即回收垃圾对象
3. 描述 V8 中新生代存储区垃圾回收的流程。
新生代存储区垃圾回收流程:
- 回收过程采用复制算法 + 标记整理
- 新生代内存区分为两个等大小空间 From 和 To
- 使用空间为 From,空闲空间为 To
- 活动对象存储在 From 空间,标记整理后将活动对象拷贝至 To
- 拷贝过程中可能出现晋升(晋升就是将新生代对象移动至老生代)。
- 一轮 GC 还存活的新生代需要晋升;To 空间使用率超过 25%,也要将活动对象移动至老生代。
- 最后将 From 与 To 交换空间完成内存释放
4. 描述增量标记算法在何时使用,以及工作原理。
增量标记算法:将一整段的垃圾回收操作,拆分成多个小步,组合完成整个垃圾回收操作。我们知道,当垃圾回收工作的时候,会阻塞 JS 程序执行,当我们需要优化垃圾回收的效率时,就可以使用增量标记算法。
优点:让垃圾回收与程序执行可以交替完成,让时间消耗更合理,达到效率优化的好处。
工作原理:
- JS 程序执行的过程中,会伴随着垃圾回收的工作
- 当垃圾回收工作时,需要遍历对象进行标记,此时不需要将所有对象进行标记,可以先将直接可达的对象进行标记,此时停下标记操作
- 然后让 JS 程序执行一会,之后,再让 GC 机制去做二步的标记操作,去标记那些间接可达的对象
- 重复以上两步,让程序执行和垃圾回收的标记操作交替执行,来达到优化效率和提升用户体验的目的
- 直到标记操作完成之后,最后执行垃圾回收
代码题 1
基于以下代码完成下面的练习
const fp = require('lodash/fp');
// 数据
// horsepower 马力,dollar_value 价格,in_stock 库存
const cars = [{ name: "Ferrari FF", horsepower: 660, dollar_value: 700000, in_stock: true},
{name: "Spyker Cl2 Zagato", horsepower: 650, dollar_value: 648000, in_stock: false},
{name: "Jaguar XKR-S", horsepower: 550, dollar_value: 132000, in_stock: false} ,
{name: "Audi R8", horsepower: 525, dollar_value: 114200, in_stock: false},
{name: "Aston Martin One-77", horsepower: 750, dollar_value: 1850000, in_stock: true},
{name: "Pagani Huayra" , horsepower: 700, dollar_value: 1300000, in_stock: false}
];
练习 1
使用函数组合 fp.flowRight() 重新实现下面这个函数
let isLastInStock = function(cars) {
// 获取最后一条数据
let last_car = fp.last(cars);
// 获取最后一条数据的 in_stock 属性值
return fp.prop('in_stock', last_car);
}
答:
let isLastInStock = fp.flowRight(fp.prop('in_stock'), fp.last)
console.log(isLastInStock(cars)); // false
练习 2
使用 fp.flowRight(), fp.prop() 和 fp.first() 获取第一个 car 的 name
答:
let getFirstCarName = fp.flowRight(fp.prop('name'), fp.first);
console.log(getFirstCarName(cars)); // Ferrari FF
练习 3
使用帮助函数 _average 重构 averageDollarValue,使用函数组合的方式实现
// _average 无需改动
let _average = function(xs) {return fp.reduce(fp.add, 0, xs) / xs.length;
}
let averageDollarValue = function(cars) {let dollar_values = fp.map(function(car) {return car.dollar_value}, cars);
return _average(dollar_values);
}
答:
let averageDollarValue = fp.flowRight(_average, fp.map(car => car.dollar_value));
console.log(averageDollarValue(cars)); // 790700
练习 4
使用 flowRight 写一个 sanitizeNames() 函数,返回一个下划线连接的小写字符串,把数组中的 name 转换为这种形式:例如:sanitizeNames([“Hello World”]) => [“hello_world”]
let _underscore = fp.replace(/\W+/g, '_'); // 无需改动,并在 sanitizeNames 中使用它
答:
let sanitizeNames = fp.flowRight(fp.map(fp.flowRight(_underscore, fp.toLower)));
console.log(sanitizeNames(["Hello World"]));
// ['hello_world']
console.log(sanitizeNames(["Hello World", "I am Lxcan"]));
// ['hello_world', 'i_am_lxcan']
代码题 2
基于下面提供的代码,完成后续的练习
// support.js
class Container {static of (value) {return new Container(value);
}
constructor (value) {this._value = value;}
map (fn) {return Container.of(fn(this._value));
}
}
class Maybe {static of (x) {return new Maybe(x);
}
isNothing () {return this._value === null || this._value === undefined;}
constructor (x) {this._value = x;}
map (fn) {return this.isNothing() ? this : Maybe.of(fn(this._value));
}
}
module.exports = {
Maybe,
Container
}
练习 1
使用 fp.add(x, y) 和 fp.map(f, x) 创建一个能让 functor 里的值增加的函数 ex1
const fp = require('lodash/fp');
const {Maybe, Container} = require('./support.js');
let maybe = Maybe.of([5, 6, 1]);
// let ex1 = // ... 你需要实现的位置
答:
// let ex1 = fp.flowRight(fp.map(v => fp.add(v, 1)));
// console.log(maybe.map(ex1));
// Maybe {_value: [ 6, 7, 2] }
let ex1 = n => maybe.map(arr => fp.map(v => fp.add(v, n), arr));
console.log(ex1(1)); // 数组每一项加 1
// Maybe {_value: [ 6, 7, 2] }
练习 2
实现一个函数 ex2,能够使用 fp.first 获取列表的第一个元素
const fp = require('lodash/fp');
const {Maybe, Container} = require('./support.js');
let xs = Container.of(['do', 'ray', 'me', 'fa', 'so', 'la', 'ti', 'do']);
// let ex2 = // ... 你需要实现的位置
答:
let ex2 = fn => xs.map(fn)._value;
console.log(ex2(fp.first)); // "do"
练习 3
实现一个函数 ex3,使用 safeProp 和 fp.first 找到 user 的名字的首字母
const fp = require('lodash/fp');
const {Maybe, Container} = require('./support.js');
let safeProp = fp.curry(function (x, o) {return Maybe.of(o[x]);
});
let user = {id: 2, name: 'Albert'};
// let ex3 = // ... 你需要实现的位置
答:
let ex3 = () => safeProp('name', user).map(fp.first)._value;
console.log(ex3()); // A
练习 4
使用 Maybe 重写 ex4,不要有 if 语句
const fp = require('lodash/fp');
const {Maybe, Container} = require('./support.js');
let ex4 = function (n) {if (n) {return parseInt(n)
}
}
答:
let ex4 = n => Maybe.of(n).map(parseInt)._value;
console.log(ex4('100')); // 100
学习笔记
函数式编程
函数式编程就是对运算过程的抽象,其中的函数是指数学中的函数即映射关系,相同的输入始终要得到相同的输出(纯函数)。
函数是一等公民
- 函数可以存储在变量中
- 函数作为参数
- 函数作为返回值
函数是一等公民是我们后面要学习的高阶函数、柯里化等的基础
使用高阶函数的意义:
- 抽象可以帮我们屏蔽细节,只需要关注于我们的目标
- 高阶函数是用来抽象通用的问题
闭包
可以在另一个作用域中调用一个函数的内部函数并访问到该函数作用域中的成员
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
纯函数
相同的输入永远会得到相同的输出,而且没有任务可观察的副作用。
纯函数的好处:
- 可缓存,纯函数对相同的输入始终有相同的结果,所以可以吧结果缓存起来
- 可测试,纯函数让测试更方便
- 并行处理,纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数
副作用
副作用让一个函数变得不纯,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用来源:
- 全局变量
- 配置文件
- 数据库
- 获取用户的输入
- …
柯里化(Currying)
- 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接收剩余的参数,返回结果
总结:
- 柯里化可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的“缓存”
- 让函数变得更加灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
函数组合
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 通过函数组合可以把多个一元函数组合成一个功能更强大的函数
- 函数组合默认是从右到左执行
- 函数组合需要满足结合律
函数组合要满足 结合律(associativity)
我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h));
// true
lodash/fp
- lodash 的 fp 模块提供了实用的对 函数式编程友好 的方法
- 提供了不可变 auto-curried , iteratee-first, data-last 的方法
PointFree
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
Functor(函子)
什么是函子?
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
MayBe 函子
- 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
- Maybe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
Either 函子
- Either 两者中的任何一个,类似于 if…else… 的处理
- 异常会让函数变的不纯,Either 函子可以用来做异常处理
IO 函子
- IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
- IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行)
- 包装当前的操作,把不纯的操作交给调用者来处理
folktale
- 一个标准的函数式编程库
- 和 lodash、ramda 不同的是,他没有提供很多功能函数
- 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task,Either,MayBe 等
- task 异步执行
Pointed 函子
- Pointed 函子是实现了 of 静态方法的函子
- of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context 中(把值放到容器中,使用 map 来处理值)
Monad 函子
- Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
- 一个函子如果具有 join 和 of 两个方法,并遵守一些定律就是一个 Monad
JavaScript 性能优化
内存管理
介绍
- 内存:由可读写单元组成,表示一片可操作空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理:开发者主动申请空间、使用空间、释放空间
- 管理流程:申请 - 使用 - 释放
垃圾回收与常见 GC 算法
JavaScript 中的垃圾
- JS 中的内存管理是自动的
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
JavaScript 中的可达对象
- 可以访问到的对象就是可达对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JS 中的根就可以理解为是全局变量对象
引用计数算法
- 核心思想:设置引用数,判断当前对象引用数是否为 0
- 引用计数器
- 引用关系改变时修改引用数字
- 引用数字为 0 时立即回收
引用计数算法优点:
- 发现垃圾时立即回收
- 最大限度减少程序暂停
引用计数算法缺点:
- 无法回收循环引用的对象
- 时间开销大,资源消耗较大
标记清除算法
- 核心思想:分标记和清除两个阶段完成
- 遍历所有对象找标记活动对象
- 遍历所有对象清除没有标记的对象
- 回收相应的空间
标记清除算法优点:
- 相对于引用计数算法,它可以回收循环引用的对象
标记清除算法缺点:
- 它释放的空间是分散的,地址不连续
- 会产生空间碎片化的问题,浪费空间
- 不会立即回收垃圾对象
标记整理算法
- 标记整理可以看做是标记清除的增强
- 标记阶段的操作和标记清除一致
- 清除阶段会先执行整理,移动对象的位置
标记整理算法优缺点:
- 减少碎片化空间
- 不会立即回收垃圾对象
V8 引擎的垃圾回收
认识 V8
- V8 是一款主流的 JavaScript 执行引擎
- V8 采用即时编译,速度很快
- V8 内存设限,64 位操作系统最大 1.5G,32 位操作系统最大 800M
V8 垃圾回收策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8 中常用的 GC(Garbage Collection) 算法
- 分代回收、空间复制、标记清除、标记整理、标记增量
V8 内存分配
- V8 内存空间一分为二
- 小空间用于存储新生代对象(64 位操作系统:32M,32 位操作系统:16M)
- 新生代指的是存活时间较短的对象
新生代对象回收实现
- 回收过程采用复制算法 + 标记整理
- 新生代内存区分为两个等大小空间 From 和 To
- 使用空间为 From,空闲空间为 To
- 活动对象存储在 From 空间,标记整理后将活动对象拷贝至 To
- From 与 To 交换空间之后完成内存释放
回收细节说明
- 拷贝过程中可能出现晋升
- 晋升就是将新生代对象移动至老生代
- 一轮 GC 还存活的新生代需要晋升
- To 空间的使用率超过 25%,也要将活动对象移动至老生代
老生代对象说明
- 老生代对象存放在右侧的老生代区域
- 64 位操作系统 1.4G,32 位操作系统 700M
- 老生代对象就是指存活时间较长的对象
老生代对象回收实现
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化
- 采用增量标记进行效率优化
细节对比
- 新生代区域垃圾回收使用空间换时间,会浪费一部分空间
- 老生代区域垃圾回收不适合复制算法
V8 总结
- V8 是一款主流的 JavaScript 执行引擎
- V8 内存设置上限(从 web 应用、用户体验考虑)
- V8 采用基于分代回收思想实现垃圾回收
- V8 内存分为新生代和老生代
- V8 垃圾回收常见的 GC 算法
Performance 工具
为什么使用 Performance
- GC 的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- Performance 提供多种监控方式
Performance 使用步骤
- 打开浏览器输入目标网址
- 进入开发人员工具面板,选择性能
- 开启录制功能,访问具体界面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
内存问题的外在表现
- 页面出现延迟加载或经常性暂停
- 页面持续性出现糟糕的性能
- 页面的性能随时间延长越来越差
界定内存问题的标准
- 内存泄漏:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题
- 频繁垃圾回收:通过内存变化图进行分析
监控内存的几种方式
- 浏览器任务管理器
- Timeline 时序图记录
- 堆快照查找分离 DOM
- 判断是否存在频繁的垃圾回收
如何确定存在频繁的垃圾回收?
- Timeline 中频繁的上升下降
- 任务管理器中数据频繁的增加减小
代码优化实例
如何精准测试 JavaScript 性能
- 本质上就是采集大量的执行样本进行数学统计和分析
- 使用基于 Benchmark.js 的 https://jsperf.com/ 完成
jsperf 使用流程
- 使用 GitHub 账号登录
- 填写个人信息(非必须)
- 填写详细的测试用例信息 (title,slug)
- 填写准备代码(DOM 操作时经常使用)
- 填写必要的 setup 与 teardown 代码
- 填写测试代码片段
慎用全局变量
- 全局变量定义在全局执行上下文,是所有作用域链的顶端
- 全局执行上下文一直存在于上下文执行栈,直到程序退出
- 如果某个局部作用域出现了同名变量则会覆盖或污染全局