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 保证对象属性的不可变性

函数

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

  • 一等函数

  • 高阶函数

闭包作用域

Iterator

概念

ES6在原有Array,Object增加了Map,Set,共有4种用于表示集合的数据解构

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作

Iterator 的作用有三个:

  • 一是为各种数据结构,提供一个统一的、简便的访问接

  • 二是使得数据结构的成员能够按某种次序排列

  • 三是 ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

Iterator 的遍历过程是这样的。

  • 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  • 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

  • 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

  • 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

默认 Iterator 接口

当使用for…of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性.

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

调用场合

  • 解构赋值

  • 扩展运算符

  • yield*

数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口

  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()

结合Generator函数

1
2
3
4
5
6
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};

遍历器对象的 return(),throw()

return()方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()方法。

return()方法必须返回一个对象,这是 Generator 语法决定的。

throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

与其他遍历语法

forEach无法中途跳出

for...in循环有几个缺点。数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。

for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。某些情况下,for...in循环会以任意顺序遍历键名。

for...of有着同for…in一样的简洁语法,但是没有for…in那些缺点。不同于forEach方法,它可以与break、continue和return配合使用。提供了遍历所有数据结构的统一操作接口。

实现自己的 call(),apply(),bind(),new()

call()

第一步,改变 this 的指向, 当函数作为对象属性调用时,this 会指向对象,因此需要将参数包装成一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.myCall = function (thisArg) {
let that = thisArg;
if (that === undefined || that === null) that = window;
if (typeof that === "number") that = new Number(that);
if (typeof that === "string") that = new String(that);
if (typeof that === "boolean") that = new Boolean(that);
if (typeof that === "bigint") that = new BigInt(that);
that.fn = this;
that.fn();
};

function a() {
console.log(this);
}
a.myCall(1);
a.myCall("2");

第二步,实现参数的传递,如果使用 ES6 可以使用剩余参数和扩展运算符

1
2
3
4
5
6
7
8
9
Function.prototype.myCall = function (thisArg, ...args) {
thisArg.fn = this;
thisArg.fn(...args);
};

function a(a, b, c) {
console.log(this, a, b, c);
}
a.myCall({ name: "name" }, 1, 2, 3);

在 ES5 中只用通过 eval() 实现,它可以执行一段字符串的脚本, 可以使用一个数组收集参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Function.prototype.myCall = function (thisArg) {
var that = thisArg;
if (that === undefined || that === null) that = global;
if (typeof that === "number") that = new Number(that);
if (typeof that === "string") that = new String(that);
if (typeof that === "boolean") that = new Boolean(that);
if (typeof that === "bigint") that = new BigInt(that);
if (typeof that === "symbol") that = new Symbol(that);
var args = [];

for (var i = 1; i < arguments.length; i++) {
args.push("arguments[" + i + "]");
}
that.fn = this;

// 这里args会进行隐式类型转换,调用toString方法
return eval("that.fn(" + args + ")");
};

function a(a, b, c) {
console.log(this, a, b, c);
return a + b + c;
}
console.log(a.myCall(null, 1, 2, 3));

第三步,消除副作用, 由于在执行前在 thisArg 上添加了,fn 方法所以需要清除,也可以使用 Symbol 或自定义的随机数,从创建唯一的标识

1
2
3
4
5
{
var result = eval("that.fn(" + args + ")");
delete that.fn;
return result;
}

apply()

apply 与 call 的区别只是参数的形式是数组,因此可以直接遍历参数,而不用使用 arguments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.myApply = function (thisArg, args) {
var argsArr = [];
var result = void 0;
thisArg.fn = this;

if (!args) {
result = eval("that.fn()");
} else {
for (var i = 1; i < args.length; i++) {
argsArr.push("arguments[" + i + "]");
}
result = eval("that.fn(" + args + ")");
}

// 这里args会进行隐式类型转换,调用toString方法
delete that.fn;
return result;
};

bind()

第一步, 验证 this 必须是一个函数, 这是为了避免 bind 方法 this 被修改 fn.bind.call(null), 把 this 包装成新的函数并返回

1
2
3
4
5
6
7
8
9
Function.prototype.myBind = function (thisArg) {
if (typeof this !== "function")
throw new TypeError("Bind must be called on a function");
var boundTargetFunction = this;
var args =
return function boundFunction() {
return boundTargetFunction.apply(newThis);
};
};

第二步,处理参数,新函数调用时的参数需要和 bind 执行时的参数合并,共同传递给原函数

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myBind = function (thisArg) {
if (typeof this !== "function")
throw new TypeError("Bind must be called on a function");
var targetFunction = this;
var args = Array.prototype.slice.call(arguments, 1);
return function boundFunction() {
return targetFunction.apply(
args.concat(Array.prototype.slice.call(arguments, 1))
);
};
};

第三步,新函数需要保留原函数的原型方法,并区分调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.myBind = function (thisArg) {
if (typeof this !== "function")
throw new TypeError("Bind must be called on a function");
var targetFunction = this;
var args = Array.prototype.slice.call(arguments, 1);
function boundFunction() {
return targetFunction.apply(
// 如果bind之后的函数用 new 调用,那么原函数中的 this 指向的应该是对象实例
this instanceof boundFunction
? this
: // 如果以普通函数调用,原函数this指向bind绑定时传入的值
thisArg,
args.concat(Array.prototype.slice.call(arguments, 1))
);
}
// 拷贝源函数的原型链
boundFunction.prototype = this.prototype;

return boundFunction;
};

new

new 操作接受一个函数

  • 创建一个新对象这个对象就是 this
  • 设置对象的原型为构造函数的原型
  • 将函数的执行上下文添加进 this
  • 执行函数,如果返回值是一个对象就直接返回
  • 如果不是对象就返回创建的 this
1
2
3
4
5
6
7
8
9
function myNew(Constructor, ...args) {
const newObj = {};

Object.setPrototypeOf(newObj, Constructor.prototype);

let result = Constructor.apply(newObj, args);

return result instanceof Object ? result : newObj;
}

1.函数式

面向对象编程(OO)通过封装变化使得代码更易理解。

函数式编程(FP)通过最小化变化使得代码更易理解。

函数式思想

JavaScript是一种拥有很多共享状态的动态语言,用不了多久,代码就会积累足够的复杂性。面向对象的编程思想可以解决一部分问。

我们需要的是一个可以对数据处理,并能处理交互,IO的编程范式,函数式编程在处理数据流转很有帮助,下一步我们希望深入函数式编程的理念,能让其处理异步交互,并且解决代码的复杂性

在应用设计时应该考虑一下几点:

  • 可扩展性——我是否需要不断地重构代码来支持额外的功能?
  • 易模块化——如果我更改了一个文件,另一个文件会不会受到影响?
  • 可重用性——是否有很多重复的代码?
  • 可测性——给这些函数添加单元测试是否让我纠结?
  • 易推理性——我写的代码是否非结构化严重并难以推理?

使用Javascript语言的其中一个问题是,缺乏一些能够妥当管理状态的原生解构,需要开发人员自身把控。随着复杂度的增高,变得难以控制。

声明式编程

命令式编程是很具体的告诉计算机如何执行某个任务,而声明式编程是将程序的描述和求值分离。

副作用和纯函数

纯函数

  • 仅取决于提供的输入,而不依赖于任何在函数求值期间或调用间隔时可能变化的隐藏状态和外部状态。

  • 不会造成超出其作用域的变化,例如修改全局对象或引用传递的参数。

副作用的发生情况

  • 改变一个全局的变量、属性或数据结构。this的使用容易发生问题。

  • 改变一个函数参数的原始值。

  • 处理用户输入。

  • 抛出一个异常,除非它又被当前函数捕获了。

  • 屏幕打印或记录日志。

  • 查询 HTML 文档、浏览器的 cookie或访问数据库。

引用透明和可置换性

引用透明使得开发者可以用这种系统的甚至是数理的方法来推导程序

1
2
3
increment();
increment();
print(counter);//引用不透明,调用期间如果被修改,会影响结果
1
2
const plus = run(increment,increment);
print(run(0))//总是为初始值加2

存储不可变数据

JavaScript 开发人员面临的问题都是由大量使用严重依赖外部共享变量的、存在太多分支的以及没有清晰的结构大函数所造成的。

即便是一些由很多文件组成并执行得很成功的应用,也会形成一种共享的可变全局数据网,难以跟踪和调试。

函数式编程的优点

  • 促使将任务分解成简单的函数。

  • 使用流式的调用链来处理数据。

  • 通过响应式范式降低事件驱动代码的复杂性。

复杂异步应用中的响应

总结

  • 使用纯函数的代码绝不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性。

  • 函数式编程采用声明式的风格,易于推理。这提高了应用程序的整体可读性,通过使用组合和lambda表达式使代码更加精简。

  • 集合中的数据元素处理可以通过链接如map和reduce这样的函数来实现。

  • 函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和可重用性。

  • 可以利用响应式编程组合各个函数来降低事件驱动程序的复杂性。

函数

与解构赋值默认值结合使用

参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。

1
2
3
4
5
6
7
8
9
10
function throwIfMissing() {
throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}

foo()
// Error: Missing parameter

函数的 length 属性

没有默认值的时候,函数的length属性是形式参数的个数

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length属性。

1
(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

1
2
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域

1
2
3
4
5
6
7
8
9
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}

foo() // 3
x // 1

上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

如果将var x = 3的var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。

1
2
3
4
5
6
7
8
9
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}

foo() // 2
x // 1

rest

rest 参数之后不能再有其他参数

函数的length属性,不包括 rest 参数

严格模式

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

name

函数的name属性,返回该函数的函数名。

Function构造函数返回的函数实例,name属性的值为anonymous。

1
(new Function).name // "anonymous"
1
2
3
4
function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

箭头函数

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

1
2
let foo = () => { a: 1 };
foo() // undefined

上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

1
let fn = () => void doesNotReturn();

箭头函数注意点

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

不适用的场景

  • 定义对象的方法,且该方法内部包括this

  • 需要动态this的时候,也不应使用箭头函数

1
2
3
4
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});

尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数。

即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。

尾递归

“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。

1
2
3
4
5
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};

return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

尾递归的两种形式:

柯里化(currying)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);
};
}

function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120

ES6 的函数默认值

1
2
3
4
5
6
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial(5) // 120

严格模式

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。

  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

1
2
3
4
5
6
function restricted() {
'use strict';
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();

尾递归的实现

tco函数是尾递归优化的实现,它的奥妙就在于状态变量active
默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

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
function tco(f) {
var value; //记录返回值
var args = []; //记录参数
var active = false; //记录执行状态
// 通过返回新函数,形成闭包共享状态
return function accumulator() {
//每次目标函数执行前保存参数
args.push(arguments);
//上一次执行的状态
if (!active) {
active = true;
// 因为每次执行前缓存了参数,所以下一次执行时一定会有参数
// 通过while实现递归的效果
while (args.length) {
value = f.apply(this, args.shift());
}
active = false;
}
}
}

var sum = tco(function (x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
} else {
console.log(x);
return x
}
});

函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。函数参数与数组和对象的尾逗号规则,保持一致了。

  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信