你不知道的JavaScript

变量提升

如果在函数声明后有同名的变量被定义,但是没有赋值,则不会被覆盖,如果同名变量被赋值这函数声明被覆盖

1
2
3
4
5
6
7
function a() {
alert(10)
}
var a;
console.log(a);//function a(){}
a = 1;
console.log(a)//1
1
2
3
4
5
6
7
8
9
10
alert(a)
a();
var a = 3;

function a() {
alert(10)
}
alert(a)
a = 6;
a();

等价于

1
2
3
4
5
6
7
8
9
10
var a
function a() {
alert(10)
}
alert(a)//function a(){}
a();
a = 3;
alert(a)//3
a = 6;
a();//TypeError

alert会把函数转为字符串function a(){...},对象会调用toString方法转为[object Object]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = 1,
y = 0,
z = 0;

function add(x) {
return (x = x + 1);
}
y = add(x);
console.log(y)

function add(x) {
return (x = x + 3);
}
z = add(x);
console.log(z)

等价于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function add(x) {
return (x = x + 1);
}
// 上面函数被覆盖
function add(x) {
return (x = x + 3);
}
var x = 1,
y = 0,
z = 0;
y = add(x);
console.log(y)
z = add(x);
console.log(z)
this指向

在构造函数中,如果在this指定属性前访问,会返回undefined

1
2
3
4
5
function go() {
console.log(this.a);
this.a = 30;
}
new go()//undefined

如果在原型连上定义,则会去原型链上查找,找不到会返回undefined,但是不会去查找全局作用域,因为通过 new 操作符,this 指向生成的对象实例

1
2
3
4
5
6
function go() {
console.log(this.a);
this.a = 30;
}
go.prototype.a = 40
new go()//40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
this.a = 20;

function go() {
console.log(this.a);
this.a = 30;
}
go.prototype.a = 40;
var test = {
a: 50,
init: function (fn) {
fn();
console.log(this.a);
return fn;
}
};

//在执行new构造函数时,this.a还没有被赋值,所以去原型链上查找返回40
//在读取对象实例的a属性时,this.a 已经被赋值,所以返回30
console.log((new go()).a); //40 30

//在init方法中fn没有被具体对象调用,所以fn执行时,go方法中this指向全局,返回20
//在下一步中对this.a赋值,this指向window对象所以把全局的a修改为30
//init中的this指向调用init方法的test对象返回50
test.init(go);//20 50

//再次执行fn即go方法,这时this.a已经被修改为30
//init中的this还是指向test对象返回test.a = 50
var p = test.init(go);//30 50

//用一个p变量接受最后返回的go方法,在调用时相当于window调用,最终返回被修改后的a属性为30
p();//30

一些常见的变量

可以通过self判断时否时windows环境

1
self.self===self

由于self变量经常被修改所以又创建了一个新变量表示全局对象 globalThis,也是为了和 Node 环境靠拢

1
2
3
self === globalThis
top === globalThis
parent===globalThis
严格模式

函数中的严格模式只对函数作用域生效,如果在函数中调用其他函数,其他函数不受严格模式影响 ES6不建议使用局部严格模式

参数传递

js中基本类型时按值传递的,引用类型是按地址传递的,形参和实参没有关系

1
2
3
4
5
6
7
8
9
10
function test(m) {
m = {
v: 5
}
}
var m = {
k: 30
};
test(m);
alert(m.v);//undefined
块级作用域
1
2
3
4
5
{
function init(){}
init = 4
}
console.log(init);// function init(){}
1
2
3
4
5
6
7
8
9
10
11
// 在块级作用域中,如果在函数声明前变量已经定义,则函数声明和后面的赋值都不会执行
{
init = 6
function init() {}
init = 4
// 如果重新定义会报错Identifier 'init' has already been declared
// const init = 7
init = 7
console.log(init); //7
}
console.log(init);//6

类似于函数的声明不能覆盖变量的定义

1
2
3
var init = 1;
function init() {}
console.log(init)
条件语句中的函数声明
1
2
3
4
5
6
7
8
9
function init(){
console.log(1);
}
if(false){
function init(){
console.log(2);
}
}
init()

变量被提升,函数不会被提升,等价于:

1
2
3
4
5
6
7
8
9
10
function init(){
console.log(1);
}
var init;
if(false){
function init(){
console.log(2);
}
}
init()

如果在函数体内部,会被提升至作用域顶端

1
2
3
4
5
6
7
8
9
10
11
function init(){
console.log(1);
}
(function(){
if(false){
function init(){
console.log(2);
}
}
init()
})

等价于

1
2
3
4
5
6
7
8
9
10
11
12
function init(){
console.log(1);
}
(function(){
var init;
if(false){
function init(){
console.log(2);
}
}
init() //TypeError
})
继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function extend(sup, sub) {
var F = function () {}
F.prototype = sup.prototype;
F.prototype.constructor = sub;
sub.prototype = new F();
const stck = Object.keys(Super);
for (var i = 0; i < stck.length; i++) {
sub[stck[i]] = sup[stck[i]]
}
}

function Super() {
this.color = 'red'
}
Super.prototype.init = function () {}
Super.time = Date.now();

function Sub() {
Super.call(this);
}
extend(Super, Sub);
console.log(new Sub())
正则的拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/** Used to match `RegExp` flags from their coerced string values. */
var reFlags = /\w*$/;

/**
* @private
* @param {Object} regexp The regexp to clone.
* @returns {Object} Returns the cloned regexp.
*/
function cloneRegExp(regexp) {
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
result.lastIndex = regexp.lastIndex;
return result;
}
const reg = /foo/g
let reg2 = cloneRegExp(reg) // /foo/g
console.log(reg2);
柯理化

实现 before after

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function test(a, b, c) {
console.log('test', a, b, c);
return 'test';
}

Function.prototype.after = function (cb) {
const _this = this;
return function _after() {
if (_this.name !== '_before') {
cb();
const res = _this.apply(_this, arguments);
return res;
}
const res = _this.apply(_this, arguments);
cb(res);
}
}

Function.prototype.before = function (cb) {
const _this = this;
function _before() {
if (_this.name === '_after') {
const res = _this.apply(_this, arguments);
cb(res);
return
}
const res = cb();
_this.apply(_this, arguments);
return res;
}
return _before
}

test
.before(function () {
console.log('before');
return 10;
})
.after(function (res) {
console.log('after');
})
(1, 2, 3)
反柯理化

从字面讲,意义和用法跟函数柯里化相比正好相反,扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。或者说让一个对象去借用一个原本不属于他的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//简单实现
var uncurrying = (fn) => (context,...rest)=>fn.apply(context,rest);

//使用call.apply省略context参数
//调用fn的call方法,和Funcion的call方法是同理的,省略了一步原型链查找的过程
//call方法需要执行call执行时候的上下文即fn函数,并把其他参数分别传入
//加上apply方法,重指定call执行时候的上下文,并且call方法的参数可以用数组的形式传入 也就是rest包含[context,...rest]
var uncurrying = fn => (...rest)=> fn.call.apply(fn,rest)

var uncurrying = fn => (...rest)=> Function.prototype.call.apply(fn,rest)

//也可以直接挂载在Function上面
Function.prototype.uncurrying = function (){
return this.call.bind(this)
}

使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//借用自己
var un = Function.prototype.uncurrying.uncurrying();
var a = un(Array.prototype.map)([1,2],function(i){console.log(i)});

//改变函数的执行上下文
function sayHi () {
return "Hello " + this.value +" "+[].slice.call(arguments);
}
var sayHiuncurrying=sayHi.uncurrying();
console.log(sayHiuncurrying({value:'world'},"hahaha"));

//借用方法
var obj = {
push:function(v){
return Array.prototype.push.uncurrying()(this,v)
}
}
obj.push('first');

基于流式布局的轮播图实现思路

布局

这是一个流式布局下得轮播图案例,问题还比较多,可以提供一种不错的思路

通过让父元素禁止换行,每个图片子元素设置inline-block,流式排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
* {
padding: 0;
margin: 0;
}

#root {
width: 500px;
height: 280px;
margin:auto;
white-space: nowrap;
overflow: hidden;
}

#root>div {
width: 500px;
height: 280px;
display: inline-block;
transition: .5s linear;
}
1
2
3
4
5
6
<div id='root'>
<div style='background: url(./assets/1b809d9a2bdf3ecc481322d7c9223c21)'></div>
<div style='background: url(./assets/1b809d9a2bdf3ecc481322d7c9223c21)'></div>
<div style='background: url(./assets/1b809d9a2bdf3ecc481322d7c9223c21)'></div>
<div style='background: url(./assets/1b809d9a2bdf3ecc481322d7c9223c21)'></div>
</div>

开始拖动

通过CSS的transform属性来改变图片的位置,这里我们不让如父元素一起滚动,而是控制每一个元素的滚动,来看一下实现的思路

在鼠标按下的时候加一个标识,表示正在拖动,并且记录下鼠标点击的位置

1
2
3
4
root.addEventListener('mousedown', event => {
mark = true;
startX= event.clientX;
})

在鼠标移动的时候,判断是否,已经开始移动

1
2
3
document.addEventListener('mousemove', function (event) {
if (!mark) return false
});

由于是使用流式布局,每一个元素都在自己的位置上,而且偏移量为0

那么当前正在窗口中的图片的索引标记为pos,初始化为0

再滑动的时候需要知道当前窗口内的图片索引,当滑动距离的绝对值超过500,则pos切换为上一张图片,或下一张图片,

1
2
3
4
5
//错误,不能直接向下取整,因为向左滑动的时候不足500不能算作前一张,向上也同理
let current = position - Math.floor(x / 500);

// 需要去掉偏移量之后,看是否超过了500
let current = position - Math.ceil((x-x % 500) / 500);

下面,我们只关心pos左侧和右侧的图片是哪一张

如果是右边的一张图片,那索引就会 +1, 按照[1,2,3,0,1,2,3,0,...]的顺序循环,需要让他在到达最后索引的时候归零,这里可以通过取余实现

1
const right = (pos+1)%length

但是左边的一张图片,是按倒叙排列的 [3,2,1,0,3,2,1,0,...]pos===0左边一张图片的索引为3,也就是让位置[-1]和 图片索引[3]对应,这里我们还是通过取余操作,但是要先加上子元素的长度length,把它转为整数,对于右边的图片来说,对于多加的长度没有影响,因为加了一倍的长度,最终取余的时候还是可以约调

1
const index = (pos+ offset + lenth) % length

下面是如何把中间位置,和左右两张图片放到对应的位置

首先减去偏移量,把图片移动到窗口位置,类似绝对定位的效果,500为图片的宽度

1
let dx = - index * 500

在加上左右位置的偏移量,和鼠标滑动的偏移量

注意:为了学习理解,滑动的事件实在document上面的,所以滑动距离可能会超过500px,这里需要取余计算剩余偏移量

1
let dx  = -index * 500 + offset * 500 + x % 500 

最终滑动事件为

1
2
3
4
5
6
7
8
9
10
11
document.addEventListener('mousemove', function (event) {
if (!mark) return false
let x = event.clientX - startX;
let current = position - Math.ceil((x - x % 500) / 500);
for (let offset of [-1, 0, 1]) {
let pos = current + offset;
pos = (pos + children.length) % children.length
children[pos].style.transition = 'none';
children[pos].style.transform = `translateX(${- pos * 500 + offset * 500 + x % 500}px)`
}
});

结束拖动

鼠标松开的时候,需要知道偏移量是否超过组件宽度的一半,如果偏移量大于250就是下一张,如果小于-250就是前一张,有四种情况可以画数轴感受一下

1
2
3
4
if (x % 500 >= -250 && x % 500 <= 0) c = 1;
if (x % 500 <= 250 && x % 500 >= 0) c = -1
if (x % 500 < -250) c = -1
if (x % 500 > 250) c = 1

最终滚动时候开启动画,并且忽略鼠标拖动的偏移量,通过css动画让图片恢复到对应的位置上

1
2
3
4
5
6
7
8
9
10
11
document.addEventListener('mouseup', function (event) {
mark = false;
let x = event.clientX - startX;
position = position - Math.round(x / 500);
for (let offset of [0, c]) {
let pos = position + offset;
pos = (pos + children.length) % children.length
children[pos].style.transition = '';
children[pos].style.transform = `translateX(${- pos * 500 + offset * 500}px)`
}
});

上面判断前后哪一张的条件语句可以用数学的算法优化

1
2
3
4
5
6
7
8
9
10
11
document.addEventListener('mouseup', function (event) {
mark = false;
let x = event.clientX - startX;
position = position - Math.round(x / 500);
for (let offset of [0, Math.sign(x % 500 - Math.sign(x) * 250)]) {
let pos = position + offset;
pos = (pos + children.length) % children.length
children[pos].style.transition = '';
children[pos].style.transform = `translateX(${- pos * 500 + offset * 500}px)`
}
});

5.针对复杂应用的设计模式

命令式处理异常方式的问题
使用容器,以防访问无效数据
用Functor的实现来做数据转换
利于组合的Monad数据类型
使用Monadic类型来巩固错误处理策略
Monadic类型的组合与交错

命令式错误处理的不足

函数式编程其实可以把错误处理得比任何其他开发风格更为优雅,软件中的许多问题都是由于数据不经意地变成了nullundefined、出现了异常、失去网络连接等情况造成的。所以需要大量得错误捕获代码,在每一个使用引用得地方判断nullundefined,是的代码得逻辑越来月复杂

try catch

我们尝试使用try,catch来捕获错误

1
2
3
4
5
try {
var student = findStudent('444-44-4444');
} catch (e) {
console.log('ERROR' + e.message);
}

但这样会与函数式的设计有兼容性问题。

  • 难以与其他函数组合或链接。
  • 违反了引用透明性,因为抛出异常会导致函数调用出现另一出口,所以不能确保单一的可预测的返回值。
  • 会引起副作用,因为异常会在函数调用之外对堆栈引发不可预料的影响。
  • 违反非局域性的原则,因为用于恢复异常的代码与原始的函数调用渐行渐远。当发生错误时,函数离开局部栈与环境。即 try 中的逻辑会在遇到错误时停止执行,转移到catch处理错误逻辑
  • 不能只关注函数的返回值,调用者需要负责声明catch块中的异常匹配类型来管理特定的异常。
  • 当有多个异常条件时会出现嵌套的异常处理块

其中一个常见的场景是JavaScript中因在null对象上调用函数所产生的TypeError。

空值(null)检查问题

本来可以简单地创建一个lens来获取该属性,若是null即返回undefined,但它并不会打印任何错误信息。

这使代码需要大量的判空检查代码。不管是使用try-catch还是null检查,都是被动的解决方式。

Functor 一种更好得解决方案

思想说起来也非常简单,创建一个安全的容器,来存放危险代码

包裹不安全的值

map可以是一个更广义的map的概念,而不仅仅是数组。在函数式JavaScript中,map只不过是一个函数,由于引用透明性,只要输入相同,map永远会返回相同的结果。当然,还可以认为map是可以使用lambda表达式变换容器内的值的途径。比如,对于数组,就可以通过map转换值,返回包含新值的新数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Wrapper {
constructor(value) {//存储任意类型值的简单类型
this._value = value;
}
// map :: (A -> B) -> A -> B
map(f) { //用一个函数来 map 该类型(就像数组一样)
return f(this.val);
};
toString() {
return 'Wrapper (' + this.value + ')';
}
}
const wrap = (val) => new Wrapper(val); //能够根据值快速创建Wrapper 的帮助函数

Wrapper类型使用map安全地访问和操作值。在这种情况下,通过映射 identity 函数就能在容器中提取值

取值得方法

1
2
const wrappedValue = wrap('Get Functional');
wrappedValue.map(R.identity); //-> 'Get Functional' <--- 值的提取

映射任何函数到该容器,比如记录日志或是变换该值:

1
2
wrappedValue.map(log);
wrappedValue.map(R.toUpper); //-> 'GET FUNCTIONAL' <--- 对内部值应用函数

现在所有得对值操作都与要通过map方法先伸入到容器中取值,可以说得到了一定得保护,但现在还没有对null,和undefined得处理

当初始化一个空值得时候

1
2
const wrappedNull = wrap(null);
wrappedNull.map(doWork); // doWork 被赋予了空值检查的责任

不应该通过dowork来检查,完全可以交给Wrapper类型来做错误处理。换句话说,可以在调用函数之前,检查null、空字符串或者负数,等等。因此,Wrapper.map的语义就由具体的Wrapper类型来确定。

map的变种——fmap

1
2
3
4
// fmap :: (A -> B) -> Wrapper[A] -> Wrapper[B]
Wrapper.prototype.fmap = function (f) {
return wrap(f(this.val)); // 先将返回值包裹到容器中,再返回给调用者
};

fmap知道如何在上下文中应用函数值。它会先打开该容器,应用函数到值,最后把返回的值包裹到一个新的同类型容器中。拥有这种函数的类型称为Functor。

Functor定义

Functor 只是一个可以将函数应用到它包裹的值上,并将结果再包裹起来的数据结构。下面是fmap的一般定义:

1
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B) // Wrapper 可以是任何容器类型

fmap函数接受一个从A->B的函数,以及一个Wrapper(A) Functor,然后返回包裹着结果的新FunctorWrapper(B)。下图显示了用increment函数作为A->B的映射函数,只是这里的A和B为同一类型。

一个简单得例子

通过柯里化初始化一个求和函数

1
2
const plus = R.curry((a, b) => a + b);
const plus3 = plus(3);

把数字2放到wrap中

1
const two = wrap(2);

再调用fmap把plus3映射到容器上

1
2
const five = two.fmap(plus3); //-> Wrapper(5) <--- 返回一个具有上下文包裹的值
five.map(R.identity); //-> 5

fmap返回同样类型的结果,可以通过映射R.identity来提取它的值。不过需要注意的是,值会一直在容器中,因此可以 fmap 任意次函数来转换值。

1
two.fmap(plus3).fmap(plus10); //-> Wrapper(15)

Functor有如下一些重要的属性约束。

  • 必须是无副作用的。若映射R.identity函数可以获得上下文中相同的值,即可证明Functor是无副作用的:
1
wrap('Get Functional').fmap(R.identity); //-> Wrapper('Get Functional')
  • 必须是可组合的。这个属性的意思是fmap 函数的组合,与分别fmap函数是一样的
1
two.fmap(R.compose(plus3, R.tap(infoLogger))).map(R.identity); //-> 5

Functor的这些属性并不奇怪。遵守这些规则,可以免于抛出异常、篡改元素或者改变函数的行为。其实际目的只是创建一个上下文或一个抽象,以便可以安全地应用操作到值,而又不改变原始值。这也是map可以将一个数组转换到另一个数组,而不改变原数组的原因。而Functor就是这个概念的推广。

使用Monad函数式地处理错误

Functor本身并不需要知道如何处理null。例如Ramda中的R.compose,在收到为null的函数引用时就会抛出异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const findStudent = R.curry(function(db, ssn) {
return wrap(find(db, ssn)); //包裹对象获取逻辑,以避免找不到对象所造成的问题
});
const getAddress = function(student) {
return wrap(student.fmap(R.prop('address'))); //用 Ramda 的 R.prop()函数来map 对象以获取其地址, 再将结果包裹起来
}

const studentAddress = R.compose(
getAddress,
findStudent(DB('student'))
);

//返回的值是被包裹了两层的address对象

studentAddress('444-44-4444').map(R.identity).map(R.identity)
Monad:从控制流到数据流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const R = ramda;
const Wrap = function (value) {
this._value = value
}
Wrap.of = (value) => new Wrap(value)
Wrap.prototype.fmap = function (f) {
return Wrap.of(f(this._value))
}
Wrap.prototype.map = function (f) {
return f(this._value)
}
// console.log(wrap)
// console.log(wrap.map(R.identity))

const Empty = function () {}
Empty.of = () => new Empty();
Empty.prototype.fmap = function () {
return this
}
Empty.prototype.map = function () {
return this
}
const empty = Empty.of()

const add = R.curry((a, b) => a + b);
const plus10 = add(10);
const isEven = (num) => Number.isFinite(num) && num % 2 === 0;
const half = (num) => isEven(num) ? Wrap.of(num / 2) : empty;

console.log(half(10).fmap(plus10).map(R.identity)) //15
console.log(half(9).fmap(plus10).map(R.identity)) // Empty

Monad——为Monadic操作提供抽象接口。
Monadic类型——该接口的具体实现。

Monadic类型类似于本章介绍的Wrapper对象。不过每个Monad都有不同的用途,可以定义不同的语义便于确定其行为(例如map或fmap)。使用这些类型可以进行链式或嵌套操作,但都应遵循下列接口定义。

  • 类型构造函数——创建Monadic类型(类似于Wrapper的构造函数)。
  • unit函数——可将特定类型的值放入Monadic结构中(类似于wrap和前面看到的empty函数)。对于Monad的实现来说,该函数也被称为of函数。
  • bind函数——可以链式操作(这就是Functor的fmap,也被称为flatmap)
  • join函数——将两层Monadic结构合并成一层。这会对嵌套返回Monad的函数特别有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Wrapper {
//类型构造器
constructor(value) {
this._value = value;
}
static of (a) {
//unit 函数
return new Wrapper(a);
}
map(f) {
//bind 函数( Functor)
return Wrapper.of(f(this.value));
}
join() {
//压平嵌套的Wrapper
if (!(this.value instanceof Wrapper)) {
return this;
}
return this.value.join();
}
toString() {
//返回一个当前结构的文本描述
return `Wrapper (${this.value})`;
}
}

join函数用于逐层扁平化嵌套结构,就像剥洋葱一样。这可以用来消除之前用functor时发现的问题

1
2
3
4
5
6
7
8
9
10
// findObject :: DB -> String -> Wrapper
const findObject = R.curry(function(db, id) {
return Wrapper.of(find(db, id));
});
// getAddress :: Student -> Wrapper
const getAddress = function(student) {
return Wrapper.of(student.map(R.prop('address')));
}
const studentAddress = R.compose(getAddress, findObject(DB('student')));
studentAddress('444-44-4444').join().get(); // Address

Monad通常有更多的操作,这里提及的最小接口只是其整个API的子集。一个Monad本身只是抽象,没有任何实际意义。只有实际的实现类型才有丰富的功能。幸运的是,大多数函数式编程的代码只用一些常用的类型就可以消除大量的样板代码,同时还能完成同样的工作。下面来看丰富的Monad实例:Maybe、Either和IO。

使用Maybe Monad和Either Monad来处理异常

除了用来包装有效值,Monadic的结构也可用于建模null或undefined。函数式编程通常使用Maybe和Either来做下列事情。

  • 隔离不纯。
  • 合并判空逻辑。
  • 避免异常。
  • 支持函数组合。
  • 中心化逻辑,用于提供默认值。
用Maybe合并判空

Maybe Monad侧重于有效整合null -判断逻辑。Maybe是一个包含两个具体字类型的空类型(标记类型)。

  • Just(value)——表示值的容器。
  • Nothing()——表示要么没有值或者没有失败的附加信息。当然,还可以应用函数到Nothing上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const R = ramda;
class Maybe { //容器类型(父类)
static just(a) {
return new Just(a);
}
static nothing() {
return new Nothing();
}
static fromNullable(a) {
return a != null ? Maybe.just(a) :
Maybe.nothing(); //由一个可为空的类型创建 Maybe(即构造函数)。如果值为空,则 建一个 Nothing; 否则, 将值存储在 Just 子类型中来表示其存在性
}
static of (a) {
return Maybe.just(a);
}
get isNothing() {
return false;
}
get isJust() {
return false;
}
}
class Just extends Maybe { //Just 子类型用于处理存在的值
constructor(value) {
super();
this._value = value;
}
get value() {
return this._value;
}
map(f) {
return Just.of(f(this.value)); //将映射函数应用于 Just,变换其中的值,并存储回容器中
}
getOrElse() {
return this.value; //Monad 提供默认的一元操
}
filter(f) {
Maybe.fromNullable(f(this.value) ? this.value : null);
}
get isJust() {
return true;
}
toString() { //返回该结构的文本描述
return `Maybe.Just(${this.value})`;
}
}
class Nothing extends Maybe { //Nothing子类型用于为无值的情况提供保护
map(f) {
return this;
}
get value() {
throw new TypeError(
'Can"t extract the valueof a Nothing.'); //任何试图从 Nothing 类型中取值的操作会引发表征错误使用 Monad的异常(后文会予以介绍)
}
getOrElse(other) {
return other; //忽略值,返回 other
}
filter() {
return this.value; //如果存在的值满足所给的断言,则返回包含值的 Just,否则,返回 Nothing
}
get isNothing() {
return true;
}
toString() {
return 'Maybe.Nothing'; //返回结构的文本描述
}
}

Maybe显式地抽象对“可空”值(null和undefined)的操作,可让开发者关注更重要的事情。如上述代码所示,Maybe是Just和Nothing的抽象,Just和Nothing各自包含自己的Monadic的实现。正如前面提到的,对于Monadic操作的实现最终取决于具体类型给予的语义。例如,map的行为具体取决于该类型是 Nothing 还是Just

1
2
3
4
5
6
7
8
9
10
11
12
13
const find = (db, id) => (id ? {
name: '小红',
address: '北京'
} : undefined)
const findStudent = R.curry((db, id) => Maybe.fromNullable(find(db, id)))
const findStudentByDB = findStudent('DB');
// 如果有意义得值传入会生成just
console.log(findStudentByDB('id').map(R.prop('address')).value)

// 如果没有意义得值会生成nothing,如果使用value取值会报错
console.log(findStudentByDB(undefined).map(R.prop('address')).getOrElse(123))
//赋值时需要注意
document.querySelector('#student-firstname').value = username.getOrElse('Enter first name');

提升函数

很明显,Maybe擅长于集中管理的无效数据的检查,但它没有(双关Nothing)提供关于什么地方出了错的信息。我们需要一个更积极的,可以知道失败原因的解决方案。解决这个问题,要最好的工具是Either monad。

使用Either

Either跟Maybe略有不同。Either代表的是两个逻辑分离的值a和b,它们永远不会同时出现。这种类型包括以下两种情况。

Left(a)——包含一个可能的错误消息或抛出的异常对象。
Right(b)——包含一个成功的值。

Either通常操作右值,这意味着在容器上映射函数总是在Right(b)子类型上执行。它类似于Maybe的Just分支。

Either的常见用法是为失败的结果提供更多的信息。在不可恢复的情况下,左侧可以包含一个合适的异常对象

与MayBe 类似,合法值也是主右的

IO Monad

3. 无重复字符的最长子串

LeetCode

注意

暴力解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var lengthOfLongestSubstring = function (s) {
if (s.length == 0) return 0;
var len = 1;
for (var i = 0; i < s.length; i++) {
var str = s[i];
for (var j = i + 1; j < s.length; j++) {
var nstr = s[j];
var mark = false;
for (var k = 0; k < str.length; k++) {
if (str[k] === nstr) {
mark = true;
break
}
}
if (mark === false) {
str += nstr;
if (str.length > len) len = str.length;
} else {
break;
}

}
}
return len;
};

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

暴力解法优化

通过map缓存已经查找过的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 */
var lengthOfLongestSubstring = function (s) {
if (s.length == 0) return 0;
var len = 1;
for (var i = 0; i < s.length; i++) {
var map = {
length: 1
};
map[s[i]] = true;
for (var j = i + 1; j < s.length; j++) {
var nstr = s[j];
if (!map[nstr]) {
map[nstr] = true;
map.length++;
if (map.length > len) len = map.length
} else {
break;
}
}
}
return len;
};

复杂度分析

  • 时间复杂度:,

  • 空间复杂度:,时间换空间

窗口移动

  • 如果下一个字符和之前的字符重复,则重复字符之前的字符都被舍弃

  • 每次读取新字符,判断一次当前位置到舍弃位置的长度是否比之前的总长度大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var lengthOfLongestSubstring = function (s) {
if (s == '') return 0;
if (s == ' ') return 1;
var map = {
start: 0,
end: 0,
len: 0
}
for (var i = 0; i < s.length; i++) {
if (map[s[i]] !== undefined && map[s[i]] > map.start) {
map.start = map[s[i]];
}
map[s[i]] = i + 1;
map.end = i + 1;
map.len = Math.max(map.end - map.start, map.len)
}
return map.len;
};

复杂度分析

  • 时间复杂度:,

  • 空间复杂度:,时间换空间

优化窗口移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var lengthOfLongestSubstring = function (s) {
let map = new Map(),
//i为上面方法的start指针
i = 0,
j = 0,
max = 0;
for (j = 0; j < s.length; j++) {
if (map.has(s[j])) {
//如果存在,当前这个值对应的索引不能比start指针小
i = Math.max(map.get(s[j]) + 1, i)
}
map.set(s[j], j);
max = Math.max(max, j - i + 1)
console.log(map, i, max);

}
return max;
};

4.模块化且可重用的代码

函数链与函数管道的比较
Ramda.js 函数库
柯里化、部分应用(partial application)和函数绑定
通过函数式组合构建模块化程序
利用函数组合子增强程序的控制流

先将问题分解成较小的部分,再重建这些部分以形成整体的解决方案。

方法链与函数管道的比较

Haskell中定义函数的符号。该符号先给出了函数的名称,随后用一个操作符来设置函数的输入和输出类型

方法链接(紧耦合,有限的表现力)

它与方法所属的对象紧紧地耦合在一起,限制链中可以使用的方法数量,也就限制了代码的表现力。这样就只能够使用由Lodash提供的操作,而无法轻松地将不同函数库的(或自定义的)函数连接在一起。

1
2
3
4
5
6
7
_.chain(names)
.filter(isValid) //每一个“点”后只能调用 Lodash 提供的方法
.map(s => s.replace(/_/, ' '))
.uniq()
.map(_.startCase)
.sort()
.value();

函数的管道化(松耦合,灵活)

方法链接通过对象的方法紧密连接;而管道以函数作为组件,将函数的输入和输出松散地连接在一起。但是,为了实现管道,被连接的函数必须在元数(arity)和类型上相互兼容。

函数式编程将管道视为构建程序的唯一方法.

对于不同的任务,问题的定义与解决方案间总是存在很大的差异。因此,特定的计算必须在特定的阶段进行。这些阶段由不同的函数表征,而所选函数的输入和输出需要满足以下两个兼容条件。

类型=>函数的返回类型必须与接收函数的参数类型相匹配。

元数=>接收函数必须声明至少一个参数才能处理上一个函数的返回值。

元组

对传入的参数进行参数类型,和个数的校验,可以用TS替代

  • 不可变的——一旦创建,就无法改变一个元组的内部内容。
  • 避免创建临时类型——元组可以将可能毫不相关的数据相关联。而定义和实例化一些仅用于数据分组的新类型使得模型复杂并令人费解。
  • 避免创建异构数组——包含不同类型元素的数组使用起来颇为困难,因为会导致代码中充满大量的防御性类型检查。传统上,数组意在存储相同类型的对象。

柯里化

要求所有参数都被明确地定义,因此当使用部分参数调用时,它会返回一个新的函数,在真正运行之前等待外部提供其余的参数。

R._curry1

1
2
3
4
5
6
7
8
9
10
11
function curry1(fn) {
return function f1(a) {
// 判断a是否为占位符
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1
} else {
// 多余的参数也可以传入
return fn.apply(this, arguments);
}
}
}

部分应用和函数绑定

  • 柯里化在每次分步调用时都会生成嵌套的一元函数。在底层,函数的最终结果是由这些一元函数的逐步组合产生的。同时,curry的变体允许同时传递一部分参数。因此,可以完全控制函数求值的时间与方式。

  • 部分应用将函数的参数与一些预设值绑定(赋值),从而产生一个拥有更少参数的新函数。该函数的闭包中包含了这些已赋值的参数,在之后的调用中被完全求值。

延迟函数绑定

当期望目标函数使用某个所属对象来执行时,使用函数绑定来设置上下文对象就变得尤为重要。例如,浏览器中的setTimeout和setInterval等函数,如果不将this的引用设为全局上下文,即window对象,是不能正常工作的。传递undefined在运行时正确设置它们的上下文。

1
2
3
4
5
6
7
8
9
10
11
const Scheduler = (function () {
const delayedFn = _.bind(setTimeout, undefined, _, _);
return {
delay5: _.partial(delayedFn, _, 5000),
delay10: _.partial(delayedFn, _, 10000),
delay: _.partial(delayedFn, _, _)
};
})();
Scheduler.delay5(function () {
consoleLog('Executing After 5 seconds!')
});

函数组合

1
2
Function.prototype.compose = R.compose;
const cleanInput = checkLengthSsn.compose(normalize).compose(trim);

对副作用的处理是,拆分副作用的函数,并且固定副作用函数的参数

point-free编程

使用compose(或者pipe)就意味着永远不必再声明参数了(称为函数的points),这无疑会使代码更加声明式、更加简洁,或更加point-free。

1
R.compose(first, getName, reverse, sortByGrade, combine);

point-free编程使JavaScript的函数式代码更接近于Haskell和UNIX的理念。它可以用来提高抽象度,促使开发者关注高级组件的组合,而不是低级的函数求值的细节。柯里化在这里也起着很重要的作用,因为它能够灵活地部分定义一个只差最后一个参数的内联函数。这种编码风格也被称为Tacit编程。

在将组合改为这种编码风格时,要记住,过度的使用会使得程序晦涩且令人费解。

组合子

组合器是一些可以组合其他函数(或其他组合子),并作为控制逻辑运行的高阶函数。组合子通常不声明任何变量,也不包含任何业务逻辑,它们旨在管理函数式程序的流程。除了compose和pipe,还有无数的组合子,一些最常见的组合子如下。

  • identity(I-combinator)

为以函数为参数的更高阶函数提供数据,如之前清单4.12中的point-free代码。
在单元测试的函数组合器控制流中作为简单的函数结果来进行断言。例如,可以使用identity函数来编写compose的单元测试。
函数式地从封装类型中提取数据

1
//identity :: (a) -> a
  • tap(K-组合子)

该函数接收一个输入对象a和一个对a执行指定操作的函数。它使用提供的对象调用给定的函数,然后再返回该对象

1
2
3
4
function tap(fn, x) {
fn(x);
return x;
}
  • alt (OR-组合子) alternation

alt组合子能够在提供函数响应的默认行为时执行简单的条件逻辑。该组合器以两个函数为参数,如
果第一个函数返回值已定义(即,不是false、null或undefined)

用于处理if else 逻辑

1
2
3
4
5
const alt = function (func1, func2) {
return function (val) {
return func1(val) || func2(val);
}
};
  • seq(S-组合子) sequence

seq组合子用于遍历函数序列。它以两个或更多的函数作为参数并返回一个新的函数,会用相同的值顺序调用所有这些函数

seq组合子不会返回任何值,只会一个一个地执行一系列操作。如果要将其嵌入函数组合之间,可以使用R.tap将它与其余部分进行桥接。

1
2
3
4
5
6
7
8
const seq = function(/*funcs*/) {
const funcs = Array.prototype.slice.call(arguments);
return function (val) {
funcs.forEach(function (fn) {
fn(val);
});
};
};
  • fork(join)组合子

1
2
3
4
5
const fork = function(join, func1, func2){
return function(val) {
return join(func1(val), func2(val));
};
};
1
2
3
const eqMedianAverage = fork(R.equals, R.median, R.mean);
eqMedianAverage([80, 90, 100])); //-> True
eqMedianAverage([81, 90, 100])); //-> False

总结

  • 用于连接可重用的、模块化的、组件化程序的函数链与管道。
  • Ramda.js是一个功能强大的函数库,适用于函数的柯里化与组合。
  • 可以通过部分求值和柯里化来减少函数元数,利用对参数子集的部分求值将函数转化为一元函数。
  • 可以将任务分解为多个简单的函数,再通过组合来获得整个解决方案。
  • 以point-free的风格编写,并用函数组合子来组织的程序控制流,可解决现实问题。

Generator 异步

es6 之前实现异步的方法

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象

异步

所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

错误处理

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

使用指针对象的throw方法抛出的错误,可以被函数体内的try…catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离

1
2
3
4
5
6
7
8
9
10
11
12
13
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

异步任务的封装

虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fetch = require('node-fetch');

function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});

Thunk 函数的含义

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

JavaScript 语言的 Thunk 函数

“传值调用”(call by value),即在进入函数体之前,就计算x + 5的值(等于 6),再将这个值传入函数f。C 语言就采用这种策略。

“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。

1
2
3
4
5
6
7
8
9
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

Thunkify

增加了重复执行的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function thunkify(fn) {
return function() {
var args = new Array(arguments.length);
var ctx = this;

for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}

return function (done) {
var called;

args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});

try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};

基于Thunk自动流程管理

  • 实现自动执行,一定会自动调用Generator函数执行后的next()方法

通过while的判断是否需要执行下一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function run(fn) {
const g = fn();
let res = g.next();
while (res.value) {
res = g.next();
}
};

function* g() {
yield 1;
yield 2;
return 3
}
run(g);

结合函数式编程的思想,这里不应该通过循环实现,应该递归调用,考虑创建一个子函数 next用于递归调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function run(fn) {
const g = fn();
function next() {
const res = g.next();
if (!res.done) next()
}
next()
};

function* g() {
yield 1;
yield 2;
return 3
}
run(g);

有返回值的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function run(fn) {
const g = fn();

function next(val) {
const res = g.next(val);
if (!res.done) next(res.value)
}
next()
};

function* g() {
const a = yield 2;
const b = yield a * 2;
const c = yield b * 2;
return c * 2
}
run(g);

内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const fs = require('fs');
const Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};

const readFileThunk1 = Thunk(fs.readFile)('./1.txt');
const readFileThunk2 = Thunk(fs.readFile)('./2.txt');
const readFileThunk3 = Thunk(fs.readFile)('./3.txt');

function run(fn) {
const g = fn();

function next(err, data) {
if (err) throw new Error();
const res = g.next(data)
if (!res.done) res.value(next)
}
next()
};

function* g() {
const a = yield readFileThunk1;
const b = yield readFileThunk2;
const c = yield readFileThunk3;
}
console.log(run(g));

基于Promise的自动执行器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const fs = require('fs');

const readFile = function (path) {
return new Promise((resolve, reject) => {
fs.readFile(path, function (err, data) {
if (err) return reject(err);
resolve(data);
})
})
}


function run(fn) {
const g = fn();

function next(data) {
const res = g.next(data);
if (!res.done) res.value.then(data => next(data));
return res.value;
}
next()
};

function* g() {
const a = yield readFile('./1.txt');
const b = yield readFile('./2.txt');
const c = yield readFile('./3.txt');
return 'end'
}
run(g)

co的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
// 统一返回Promise对象
return new Promise(function (resolve, reject) {
//co 先检查参数gen是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为resolved。
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
//co 将 Generator 函数的内部指针对象的next方法,包装成onFulfilled函数。这主要是为了能够捕捉抛出的错误。
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}

function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}

function next(ret) {
if (ret.done) return resolve(ret.value);
//确保每一步的返回值,是 Promise 对象。
var value = toPromise.call(ctx, ret.value);
//使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数。
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
//在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为rejected,从而终止执行。
return onRejected(new TypeError(
'You may only yield a function, promise, generator, array, or object, ' +
'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}

O53.0~n-1中缺失的数字

LeetCode

注意

  • 清楚题目的意思,有一个数字不在数组中,是返回这个数字的本身,而不是索引。[0]长度为1,取值范围是[0,1]其中1不在数组中,所以返回1

暴力解法

1
2
3
4
5
6
7
8
9
10
var missingNumber = function(nums) {
var i=0;
var len = nums.length;
for(var i=0;i<len;i++){
if(nums[i]!==i){
return i;
}
}
return i;
};

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

二分法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var missingNumber = function (nums) {
var left = 0;
var right = nums.length-1;
var middle = Math.floor((left + right) / 2)
while (left <= right) {
if (nums[middle] === middle) {
left = middle + 1;
} else {
right = middle-1;
}
middle = Math.floor((left + right) / 2)
}
return left
};

复杂度分析

  • 时间复杂度:

  • 空间复杂度:

3.轻数据结构,重操作

理解程序的控制流
更易理解的代码与数据
命令抽象函数map、reduce 以及filter
Lodash.js 及函数链
递归的思考

理解程序的控制流

程序为实现业务目标所要行进的路径被称为控制流。命令式程序需要通过暴露所有的必要步骤才能极其详细地描述其控制流。

链接方法

1
2
3
4
'Functional Programming'.substring(0, 10).toLowerCase() + ' is fun';

//函数式
concat(toLowerCase(substring('Functional Programming', 1, 10))),' is fun');

函数链

  • lambda表达式

lambda表达式适用于函数式的函数定义,因为它总是需要返回一个值。

  • R.map()
1
2
3
4
5
6
7
8
9
10
11
export default function _map(fn, functor) {
var idx = 0;
var len = functor.length;
var result = Array(len);
while (idx < len) {
result[idx] = fn(functor[idx]);
idx += 1;
}
return result;
}

Generator

概念

Generator 函数是 ES6 提供的一种异步编程解决方案。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

yeild

yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

1
2
3
4
5
6
7
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}

yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

1
2
3
4
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

1
2
3
4
5
6
7
8
function* gen(){
// some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

next 方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

for…of 循环

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

一旦next方法的返回对象的done属性为truefor...of循环就会中止,return语句返回的不包括在for...of循环之中。

Generator.prototype.throw()

Generator.prototype.return()

共同点

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

yield*

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。

ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。

Generator 函数也不能跟new命令一起用,会报错。

首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}

function F() {
return gen.call(gen.prototype);
}

var f = new F();

f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

Generator 与状态机

Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。

1
2
3
4
5
6
7
8
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};

应用

  • 异步操作的同步化表达
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}

function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}

var it = main();
it.next();
  • 控制流管理

  • 部署 Iterator 接口

  • 作为数据结构

2.进阶

为什么说JavaScript是适合函数式的编程语言
JavaScript语言的多范型开发
不可变性和变化的对策
理解高阶函数和一等函数
闭包和作用域的概念探讨
闭包的实际使用

函数式与面向对象

面向对象的关键是创建继承层次结构(如继承Person的Student对象)并将方法与数据紧密的绑定在一起。函数式编程则更倾向于通过广义的多态函数交叉应用于不同的数据类型,同时避免使用this

属性保护

试用递归来冻结数属性

1
2
3
4
5
6
7
8
9
10
var isObject = (val) => val && typeof val === 'object';
function deepFreeze(obj) {
if(isObject(obj) //遍历所有属性并递归调用Object.freeze()(使用第3章介绍的map)
&& !Object.isFrozen(obj)) { //跳过已经冻结过的对象,冻结没有被冻结过的对象
Object.keys(obj). //跳过所有的函数,即使从技术上说,函数也可以被修改,但是我们更希望注意在数据的属性上
forEach(name => deepFreeze(obj[name])); //递归地自调用(第3章会介绍递归)
Object.freeze(obj); //冻结根对象
}
return obj;
}

R.lensProp 保证对象属性的不可变性

函数

需要区分表达式(如返回一个值的函数)和语句(如不返回值的函数)。命令式编程和过程式程序大多是由一系列有序的语句组成的,而函数式编程完全依赖于表达式,因此无值函数在该范式下并没有意义。

  • 一等函数

  • 高阶函数

闭包作用域

  • Copyrights © 2015-2026 SunZhiqi

此时无声胜有声!

支付宝
微信