Study Notes
本博主会持续更新各种前端的技术,如果各位道友喜欢,可以关注、收藏、点赞下本博主的文章。
函数是一等公民(First-class Function)
特性
可将函数赋值给变量,即函数可存储在变量中
函数可作为参数
函数可作为返回值
说明
在 JavaScript 中函数就是一个普通的对象 (可以通过 new Function() ),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过 new Function('alert(1)') 来构造一个新的函数。
demo
// 可将函数赋值给变量,即函数可存储在变量中
const foo = function () {
console.log('foobar');
};
// 用变量来调用它
foo();
// 函数可作为参数
function sayHello() {
return 'Hello, ';
}
function greeting(helloMessage, name) {
console.log(helloMessage() + name);
}
// 传递 `sayHello` 作为 `greeting` 函数的参数
greeting(sayHello, 'JavaScript!'); // Hello, JavaScript!
// 函数可作为返回值
function sayHello() {
return function () {
console.log('Hello!');
};
}
高阶函数(Higher-order function)
特性
可以把函数作为参数传递给另一个函数
可以把函数作为另一个函数的返回结果
使用高阶函数的意义
抽象可以帮我们屏蔽细节,只需要关注我们的目标
高阶函数是用来抽象通用的问题
demo
可以把函数作为参数传递给另一个函数
/**
* forEach 遍历数组
* @param array {Array} 所需遍历的数组
* @param fn {Function} 返回值
*/
const forEach = (array, fn) => {
for (let val of array) {
fn(val);
}
};
// 测试
let array = [1, 343, 5, 7, 8345, 8];
forEach(array, (val) => console.log(val));
/* 输出
* 1
* 343
* 5
* 7
* 8345
* 8
* */
/**
* filter 数组过滤,并返回新的数组
* @param array {Array} 所需过滤的数组
* @param fn {Function} 过滤处理函数
* @returns {Array} 返回值
*/
const filter = (array, fn) => {
let result = [];
for (let val of array) {
if (fn(val)) {
result.push(val);
}
}
return result;
};
// 测试
let result = filter(array, (val) => val > 100);
console.log('filter:', result);
/* 输出
* filter: [ 343, 8345 ]
* */
可以把函数作为参数传递给另一个函数
/**
* makeFn 函数生成
* @returns {function(): void}
*/
const makeFn = () => {
const msg = 'Hello World';
return () => console.log(msg);
};
// 测试
//调用方式一
const fn = makeFn();
fn();
// 调用方式二
// makeFn()();
/* 输出
* Hello World
* */
/**
* once 只执行一次
* @param fn {Function} 执行函数
* @returns {Function} 返回值
*/
const once = (fn) => {
let done = false;
// 因为下面使用this,这里切不可使用箭头函数,箭头函数里this的指向是上下文里对象this指向,如果没有上下文对象,this则指向window
return function () {
if (!done) {
done = true;
return fn.apply(this, arguments);
}
};
};
//测试
const pay = once((money) => {
console.log(`支付:¥${money}`);
});
pay(100);
pay(100);
pay(100);
// 这里调用了三次函数,但是结果只输出一次,所以我们的once达到了预期效果
/* 输出
* 支付:¥100
* */
模拟常用高阶函数 map、every、some
/**
* map 遍历数组,对其进行处理并返回新的数组
* @param array {Array} 所需遍历数组
* @param fn {Function} 处理函数
* @returns {Array} 返回值
*/
const map = (array, fn) => {
let result = [];
for (let val of array) {
result.push(fn(val));
}
return result;
};
// 测试
let newArr = map(array, (val) => val * val);
console.log(newArr);
/* 输出
* [ 1, 117649, 25, 49, 69639025, 64 ]
* */
/**
* every 遍历数组,判断数组所有元素是否全部满足指定条件,并返回结果
* @param array {Array} 所需遍历的数组
* @param fn {Function} 指定条件函数
* @returns {boolean} 返回值
*/
const every = (array, fn) => {
let result = true;
for (let val of array) {
result = fn(val);
if (!result) {
break;
}
}
return result;
};
// 测试
let result1 = every(array, (val) => val > 0);
let result2 = every(array, (val) => val > 1);
console.log(result1);
console.log(result2);
/* 输出
* true
* false
* */
/**
* some 遍历数组,判断数组所有元素是否有满足指定条件的元素,并返回结果
* @param array {Array} 所需遍历的数组
* @param fn {Function} 指定条件函数
* @returns {boolean} 返回值
*/
const some = (array, fn) => {
let result = false;
for (let val of array) {
result = fn(val);
if (result) {
break;
}
}
return result;
};
// 测试
let result3 = some(array, (val) => val > 0);
let result4 = some(array, (val) => val > 10000);
console.log(result3);
console.log(result4);
/* 输出
* true
* false
* */
闭包(Closure)
描述
闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
浏览器调试工具使用
Call Stack 函数调用栈(在匿名函数中调用) 一个函数执行后,会从函数调用栈中移除
Scope 作用域
demo
/**
* makePower 生成幂数函数
* @param power {Number} n次方
* @returns {function(*=): number} 返回值
*/
const makePower = (power) => {
return (number) => {
return Math.pow(number, power);
};
};
// 测试
const power2 = makePower(2);
const power3 = makePower(3);
console.log(power2(2));
console.log(power2(3));
console.log(power3(2));
/**
* 输出
* 4
* 9
* 8
*/
/**
* makeSalary 工资生成器
* @param base {Number} 基本工资
* @returns {function(*): *}
*/
const makeSalary = (base) => {
return (performance) => {
return base + performance;
};
};
// 测试
const getSalaryLevel1 = makeSalary(10000);
const getSalaryLevel2 = makeSalary(12000);
console.log(getSalaryLevel1(2000));
console.log(getSalaryLevel1(3000));
console.log(getSalaryLevel2(2000));
/**
* 输出
* 12000
* 13000
* 14000
*/
纯函数(Pure Functions)
纯函数概念
纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
- 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库(lodash 的 fp 模块提供了对函数式编程友好的方法),提供了对数组、数字、对象、字符串、函数等操作的一些方法
数组的 slice 和 splice 分别是:纯函数和不纯的函数
- slice 返回数组中的指定部分,不会改变原数组
- splice 对数组进行操作返回该数组,会改变原数组
// 纯函数
let array = [1, 2, 3, 4, 5];
console.log('slice: ', array.slice(0, 3));
console.log('slice: ', array.slice(0, 3));
console.log('slice: ', array.slice(0, 3));
// 不纯的函数
console.log('splice: ', array.splice(0, 3));
console.log('splice: ', array.splice(0, 3));
console.log('splice: ', array.splice(0, 3));
// slice: [ 1, 2, 3 ]
// slice: [ 1, 2, 3 ]
// slice: [ 1, 2, 3 ]
// splice: [ 1, 2, 3 ]
// splice: [ 4, 5 ]
// splice: []
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理
纯函数的好处
可缓存
- 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
const _ = require('lodash');
const add = (a, b) => {
console.log(a, b);
return a + b;
};
const result = _.memoize(add);
console.log(result(1, 2));
console.log(result(1, 2));
console.log(result(1, 2));
const memoize = (fn) => {
let cache = {};
// 箭头函数没有arguments,因为下面使用了arguments,所以这里不能使用箭头函数
return function () {
let key = JSON.stringify(arguments);
cache[key] = cache[key] || fn.apply(fn, arguments);
return cache[key];
};
};
const result1 = memoize(add);
console.log(result1(1, 2));
console.log(result1(1, 2));
console.log(result1(1, 2));
// 输出结果 从结果可以看出来,参数只被打印一次,说明函数缓存成功
// 1 2
// 3
// 3
// 3
// 1 2
// 3
// 3
// 3
可测试
- 纯函数让测试更方便
并行处理
- 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)
副作用
// 不纯的
let mini = 18;
function checkAge(age) {
return age >= mini;
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge(age) {
let mini = 18;
return age >= mini;
}
副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用来源:
- 配置文件
- 数据库
- 获取用户的输入
- ……
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
柯里化 (Haskell Brooks Curry)
柯里化 (Currying):
- 当一个函数有多个参数的时候先传递一部分的参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数来接收剩余的参数,返回结果
lodash 中的柯里化函数
_.curry(func)
- 功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。
- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
// lodash 中的柯里化函数
const _ = require('lodash');
const getSum = (a, b, c) => {
return a + b + c;
};
const curried = _.curry(getSum);
console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));
/**
* 输出结果
* 6
* 6
* 6
*/
// 案例
const match = _.curry((res, str) => {
return str.match(res);
});
const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);
const filter = _.curry((fun, array) => {
return array.filter(fun);
});
const findSpace = filter(haveSpace);
const findNumber = filter(haveNumber);
console.log(haveSpace('ss ss'));
console.log(haveNumber('ss 12'));
console.log(findSpace(['ss12', 's ss']));
console.log(findNumber(['ss12', 'sss']));
/**
* 输出结果
* [ ' ' ]
* [ '12' ]
* [ 's ss' ]
* [ 'ss12' ]
*/
模拟 _.curry() 的实现
// 模拟 curry
const getSum = (a, b, c) => {
return a + b + c;
};
const curry = (func) => {
return (curryFn = (...args) => {
if (args.length < func.length) {
return function () {
return curryFn(...args.concat(Array.from(arguments)));
};
}
return func(...args);
});
};
const curried = curry(getSum);
console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));
/**
* 输出结果
* 6
* 6
* 6
*/
总结
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
这是一种对函数参数的'缓存'
让函数变的更灵活,让函数的粒度更小
可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
函数组合(compose)
纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
- 获取数组的最后一个元素再转换成大写字母, .toUpper(.first(_.reverse(array)))
函数组合
函数组合 (compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
函数组合默认是从右到左执行
lodash 中的组合函数
- lodash 中组合函数 flow() 或者 flowRight(),他们都可以组合多个函数
- flow() 是从左到右运行
- flowRight() 是从右到左运行,使用的更多一些
const _ = require('lodash');
// lodash 中组合函数 flowRight(),将数组的最后一个元素转换为大写
/**
* first 获取字符串第一个字符或数组第一个元素
* @param str {String || Array} 字符串或数组
* @returns {*} 返回值
*/
const first = (str) => {
return _.first(str);
};
/**
* reverse 颠倒字符串
* @param str {String} 字符串
* @param isReverse {Boolean} 是否颠倒
* @returns {*} 返回值
*/
const reverse = (str, isReverse) => {
let array = str.split('');
array = isReverse ? array.reverse() : array;
return array;
};
/**
* toUpperCase 将字母转换为大写
* @param str {String} 字符串
* @returns {string} 返回值
*/
const toUpperCase = (str) => {
return str.toUpperCase();
};
const fn = _.flowRight(toUpperCase, first, reverse);
console.log(fn('abc', true));
/**
* 输出结果
* C
*/
模拟实现 lodash 的 flowRight 方法
// 模拟 flowRight
/**
* compose 函数组合
* @param args 参数
* @returns {function(...[*]=): *}
*/
const compose = (...args) => {
let first = false;
return (...args1) => {
return args.reverse().reduce((val, fn) => {
if (!first) {
first = true;
return fn(...val);
}
return fn(val);
}, args1);
};
};
const fn = compose(toUpperCase, first, reverse);
console.log(fn('abc', false));
/**
* 输出结果
* A
*/
函数的组合要满足结合律 (associativity):
- 我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
// 满足结合律
const fn1 = compose(toUpperCase, first, reverse);
const fn2 = compose(compose(toUpperCase, first), reverse);
const fn3 = compose(toUpperCase, compose(first, reverse));
console.log(fn1('abc', true));
console.log(fn2('abc', true));
console.log(fn3('abc', true));
/**
* 输出结果
* C
* C
* C
*/
lodash 的 fp 模块提供了实用的对函数式编程友好的方法
提供了不可变 auto-curried iteratee-first data-last 的方法
Point Free
Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数
demo
/// 非 Point Free 模式
// Hello World => hello_world
function f(word) {
return word.toLowerCase().replace(/\s+/g, '_');
}
console.log(f('Hello World'));
// Point Free
const fp = require('lodash/fp');
const firstLetterToUpper = fp.flowRight(
fp.join('. '),
fp.map(fp.flowRight(fp.first, fp.toUpper)),
fp.split(' '),
);
console.log(firstLetterToUpper('world wild web'));
// => W. W. W
函子(Functor)
概念
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
总结
函数式编程的运算不直接操作值,而是由函子完成
函子就是一个实现了 map 契约的对象
我们可以把函子想象成一个盒子,这个盒子里封装了一个值
想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
最终 map 方法返回一个包含新值的盒子(函子)
demo
// Functor
class Container {
/**
* constructor 构造函数
* @param value 入参
*/
constructor(value) {
this._value = value;
}
/**
* of 实例化
* @param value 入参
* @returns {Container} 返回Container对象
*/
static of(value) {
return new Container(value);
}
/**
* map 方法
* @param fn {function} 处理函数
* @returns {Container} 返回Container对象
*/
map(fn) {
return Container.of(fn(this._value));
}
}
let r = Container.of(5)
.map((v) => v * v)
.map((v) => v + 1);
console.log(r);
/**
* 输出结果
* Container { _value: 26 }
*/
MayBe 函子
我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
demo
// MayBe 函子
class MayBe {
/**
* constructor 构造函数
* @param value 入参
*/
constructor(value) {
this._value = value;
}
/**
* of 实例化
* @param value 入参
* @returns {MayBe} 返回MayBe对象
*/
static of(value) {
return new MayBe(value);
}
/**
* map 方法
* @param fn {function} 处理函数
* @returns {MayBe} 返回MayBe对象
*/
map(fn) {
return this.isNothing() ? MayBe.of(this._value) : MayBe.of(fn(this._value));
}
/**
* isNothing 判断是否为null或undefined
* @returns {boolean}
*/
isNothing() {
return this._value === null || this._value === undefined;
}
}
let r = MayBe.of('abc')
.map((val) => val.toUpperCase())
.map((val) => val.split(''));
let r1 = MayBe.of(null)
.map((val) => val.toUpperCase())
.map((val) => val.split(''));
let r2 = MayBe.of(undefined)
.map((val) => val.toUpperCase())
.map((val) => val.split(''));
console.log(r);
console.log(r1);
console.log(r2);
/**
* 输出结果
* MayBe { _value: [ 'A', 'B', 'C' ] }
* MayBe { _value: null }
* MayBe { _value: undefined }
*/
Either 函子
Either 两者中的任何一个,类似于 if...else...的处理
异常会让函数变的不纯,Either 函子可以用来做异常处理
demo
// Either 函子
class Either {
/**
* constructor 构造函数
* @param value 入参
*/
constructor(value) {
this._value = value;
}
/**
* of 实例化
* @param value 入参
* @returns {Either} 返回Either对象
*/
static of(value) {
return new Either(value);
}
/**
* map 方法
* @param fn {function} 处理函数
* @returns {Either} 返回Either对象
*/
map(fn) {
return Either.of(fn(this._value));
}
}
class Error extends Either {
/**
*
* @param fn
* @returns {Error} 返回Error对象的this
*/
map(fn) {
return this;
}
}
/**
* parseJson 解析字符串JSON
* @param str {string} 入参
* @returns {Either} 返回Either对象
*/
const parseJson = (str) => {
try {
return Either.of(JSON.parse(str));
} catch (e) {
return Error.of({ error: e.message });
}
};
const r = parseJson('{"name": "zs"}');
const r1 = parseJson('{name: "zs"}');
console.log(r.map((val) => val.name.toUpperCase())); // 正常
console.log(r1); // 异常
/**
* 输出结果
* Either { _value: 'ZS' }
* Either { _value: { error: 'Unexpected token n in JSON at position 1' }}
*/
IO 函子
IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
把不纯的操作交给调用者来处理
demo
// IO 函子
const _ = require('lodash');
class IO {
/**
* 构造函数
* @param fn {function} 函数
*/
constructor(fn) {
this._value = fn;
}
/**
* 实例化
* @param value
* @returns {IO} IO对象
*/
static of(value) {
return new IO(function () {
return value;
});
}
/**
* map 方法
* @param fn {function} 处理函数
* @returns {IO} 返回IO对象
*/
map(fn) {
return new IO(_.flowRight(fn, this._value));
}
}
const r = IO.of(process).map((p) => p.execPath);
console.log(r._value());
/**
* 输出结果
* D:\Development\nodeJS\node.exe
*/
IO 函子的问题
- 嵌套函子时,需要多次获取 value
/**
* 读取文件内容
* @param filename {string} 文件名
* @returns {IO} 返回IO对象
*/
const readFile = (filename) => {
return new IO(() => {
return fs.readFileSync(filename, 'utf-8');
});
};
/**
* 打印
* @param value {string} 入参
* @returns {IO} 返回IO对象
*/
const print = (value) => {
return new IO(() => {
return value;
});
};
/**
* 读取文件并打印
*/
const cat = _.flowRight(print, readFile);
// 因为这里是嵌套函子,所以这边打印出来的是IO(IO())
console.log(cat('package.json'));
// 所以需要连续两次获取value才能取到值
console.log(cat('package.json')._value()._value());
Task 异步执行
folktale 一个标准的函数式编程库
// Task 异步执行
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
/**
* 读取文件内容
* @param filename {String} 文件名
* @returns {*} 返回task对象
*/
const readFile = (filename) => {
return task((resolve) => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolve.reject(err);
resolve.resolve(data);
});
});
};
readFile('package.json')
.map((v) => JSON.parse(v))
.run()
.listen({
onRejected: (err) => {
console.log('异常', err);
},
onResolved: (data) => {
console.log(data.version);
},
});
/**
* 输出结果
* 1.0.0
*/
Monad 函子
Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
// Monad 函子
const _ = require('lodash');
const fs = require('fs');
class IO {
/**
* 构造函数
* @param fn {function} 函数
*/
constructor(fn) {
this._value = fn;
}
/**
* 实例化
* @param value
* @returns {IO} IO对象
*/
static of(value) {
return new IO(function () {
return value;
});
}
/**
* map 方法
* @param fn {function} 处理函数
* @returns {IO} 返回IO对象
*/
map(fn) {
return new IO(_.flowRight(fn, this._value));
}
join() {
return this._value();
}
flatMap(fn) {
return this.map(fn).join();
}
}
/**
* 读取文件内容
* @param filename {string} 文件名
* @returns {IO} 返回IO对象
*/
const readFile = (filename) => {
return new IO(() => {
return fs.readFileSync(filename, 'utf-8');
});
};
/**
* 打印
* @param value {string} 入参
* @returns {IO} 返回IO对象
*/
const print = (value) => {
return new IO(() => {
return value;
});
};
/**
* 读取文件并打印
*/
const cat = readFile('package.json')
.map((v) => JSON.parse(v))
.map((v) => v.version)
.flatMap(print)
.join();
console.log(cat);
/**
* 输出结果
* 1.0.0
*/
异步模式
异步任务
异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程,当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务
/**
* 任何函数的声明和任何变量的声明都不会压入调用栈(Call Stack)
* 将console.log(1)压入调用栈执行,执行后移除
* 将setTimeout压入调用栈执行,执行setTimeout,因为setTimeout是异步,所以将a()压入Web APIs后,从调用栈中移除,倒计时等待
* 将console.log(5)压入调用栈执行,执行后移除
* 将setTimeout压入调用栈执行,执行setTimeout,因为setTimeout是异步,所以将c()压入Web APIs后,从调用栈中移除,倒计时等待
* 将console.log(6)压入调用栈执行,执行后移除
* 1秒倒计时结束后,将a()压入消息队列,Event Loop(事件循环)监听到后,将a()压入调用栈
* 将console.log(2)压入调用栈执行,执行后移除
* 将a()里的setTimeout压入调用栈执行,执行setTimeout,因为setTimeout是异步,所以将b()压入Web APIs后,从调用栈中移除,倒计时等待
* 0.5秒倒计时结束后,将b()压入消息队列,Event Loop(事件循环)监听到后,将b()压入调用栈
* 将console.log(3)压入调用栈执行,执行后移除
* 2秒倒计时结束后,将c()压入消息队列,Event Loop(事件循环)监听到后,将c()压入调用栈
* 将console.log(4)压入调用栈执行,执行后移除
*/
// 异步代码
console.log(1);
setTimeout(
(a = () => {
console.log(2);
setTimeout(
(b = () => {
console.log(3);
}),
500,
);
}),
1000,
);
console.log(5);
setTimeout(
(c = () => {
console.log(4);
}),
2000,
);
console.log(6);
/**
* 输出结果打印
* 1
* 5
* 6
* 2
* 3
* 4
*/
同步模式
单线程
概念
JavaScript 是一门单线程的语言,因此,JavaScript 在同一个时间只能做一件事,单线程意味着,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务
使用单线程原因
JavaScript 的单线程,与它的用途是有很大关系,我们都知道,JavaScript 作为浏览器的脚本语言,主要用来实现与用户的交互,利用 JavaScript,我们可以实现对 DOM 的各种各样的操作,如果 JavaScript 是多线程的话,一个线程在一个 DOM 节点中增加内容,另一个线程要删除这个 DOM 节点,那么这个 DOM 节点究竟是要增加内容还是删除呢?这会带来很复杂的同步问题,因此,JavaScript 是单线程的
同步任务
同步任务是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务,当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务
// 同步代码
/**
* 任何函数的声明和任何变量的声明都不会压入调用栈(Call Stack)
* 将console.log(1)压入调用栈执行,执行后移除
* 将console.log(3)压入调用栈执行,执行后移除
* 将a()压入调用栈执行,执行console.log(2),将console.log(2)压入调用栈执行,执行后依次移除
*/
console.log(1);
const a = () => console.log(2);
console.log(3);
a();
/**
* 输出结果打印
* 1
* 3
* 2
*/
副作用
因为 JavaScript 是单线程,因此同个时间只能处理同个任务,所有任务都需要排队,前一个任务执行完,才能继续执行下一个任务,但是,如果前一个任务的执行时间很长,比如文件的读取操作或 ajax 操作,后一个任务就不得不等着,拿 ajax 来说,当用户向后台获取大量的数据时,不得不等到所有数据都获取完毕才能进行下一步操作,用户只能在那里干等着,严重影响用户体验
Promise
Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.
参数
executor
executor 是带有 resolve 和 reject 两个参数的函数 。Promise 构造函数执行时立即调用 executor 函数, resolve 和 reject 两个函数作为参数传递给 executor(executor 函数在 Promise 构造函数返回所建 promise 实例对象前被调用)。resolve 和 reject 函数被调用时,分别将 promise 的状态改为 fulfilled(完成)或 rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用 resolve 函数来将 promise 状态改成 fulfilled,要么调用 reject 函数将 promise 的状态改为 rejected。如果在 executor 函数中抛出一个错误,那么该 promise 状态为 rejected。executor 函数的返回值被忽略。
描述
Promise 对象是一个代理对象(代理一个值),被代理的值在 Promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象
一个 Promise 有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
pending 状态的 Promise 对象可能会变为 fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then 方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当 Promise 状态为 fulfilled 时,调用 then 的 onfulfilled 方法,当 Promise 状态为 rejected 时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。
因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回 promise 对象, 所以它们可以被链式调用。
demo
// Promise基本使用
let promise = new Promise((resolve, reject) => {
// resolve('成功');
reject('失败');
});
promise.then(
(res) => {
console.log(res);
},
(e) => {
console.log(e);
},
);
const Ajax = (url) => {
// promise方式Ajax使用
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
xhr.send();
});
};
Ajax('fed-e-task-01-01/notes/promise/api/user.json').then(
(value) => {
console.log(value);
},
(e) => {
console.log(e);
},
);
// promise链式调用
Ajax('fed-e-task-01-01/notes/promise/api/user.json')
.then((value) => {
console.log(value);
return Ajax('fed-e-task-01-01/notes/promise/api/class.json');
})
.then((value) => {
console.log(value);
return 'abc';
})
.then((value) => console.log(value));
promise 静态方法使用
- all 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
/**
* 输出结果
* [3, 42, "foo"]
*/
- race 返回一个 promise,一旦迭代器中的某个 promise 解析或拒绝,返回的 promise 就会解析或拒绝。
const promise4 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise5 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise4, promise5]).then((value) => {
console.log(value);
// 都解析了,但是promise2更快
});
/**
* 输出结果
* two
*/
- reject 方法返回一个带有拒绝原因的 Promise 对象
Promise.reject(new Error('fail')).catch((e) => console.error(e.message()));
/**
* 输出结果
* fail
*/
- resolve 方法返回一个以给定值解析后的 Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是 thenable(即带有"then" 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终状态;否则返回的 promise 将以此值完成。此函数将类 promise 对象的多层嵌套展平
Promise.resolve('success').then((res) => console.log(res));
/**
* 输出结果
* success
*/
Event Loop(事件循环)
Event Loop 它最主要是分三部分:主线程、宏任务(macrotask)、微任务(microtask)
宏任务
宏任务,macrotask,也叫 tasks。 一些异步任务的回调会依次进入 macro task queue,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
微任务
微任务,microtask,也叫 jobs。 另一些异步任务的回调会依次进入 micro task queue,等待后续被调用,这些异步任务包括:
- Promise
- Object.observe
- MutationObserver
- process.nextTick
执行顺序
主线程 > 微任务 > 宏任务
Generator(生成器)
生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。
function* gen() {
yield 1;
yield 2;
yield 3;
}
let g = gen();
// "Generator { }"
方法
Generator.prototype.next()
- 返回一个由 yield 表达式生成的值。
const arr = [1, 2, 3];
function* idMaker() {
for (let val of arr) {
yield val;
}
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
/**
* 输出结果
* 1
* 2
* 3
*/
Generator.prototype.return()
- 返回给定的值并结束生成器。
const arr = [1, 2, 3];
function* idMaker() {
for (let val of arr) {
yield val;
}
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value);
console.log(gen.return('结束').value);
console.log(gen.next().value);
console.log(gen.next().value);
/**
* 输出结果
* 1
* Uncaught Error: error
* undefined
* undefined
*/
Generator.prototype.throw()
- 向生成器抛出一个错误。
const arr = [1, 2, 3];
function* idMaker() {
for (let val of arr) {
yield val;
}
}
let gen = idMaker(); // "Generator { }"
console.log(gen.next().value);
// 阻止生成器函数往下运行,并抛出异常
gen.throw(new Error('error'));
console.log(gen.next().value);
console.log(gen.next().value);
/**
* 输出结果
* 1
* Uncaught Error: error
*/
demo
// generator配合promise使用
const Ajax = (url) => {
// promise方式Ajax使用
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
xhr.send();
});
};
function* main() {
try {
const result = yield Ajax('fed-e-task-01-01/notes/promise/api/user.json');
console.log(result);
const result1 = yield Ajax('fed-e-task-01-01/notes/promise/api/class.json');
console.log(result1);
} catch (e) {
console.error(e);
}
}
const co = (generator) => {
const g = generator();
function handelResult(result) {
if (result.done) return;
result.value.then(
(res) => {
handelResult(g.next(res));
},
(e) => {
handelResult(g.throw(e));
},
);
}
handelResult(g.next());
};
co(main);
Async 函数
async function 用来定义一个返回 AsyncFunction 对象的异步函数。异步函数是指通过事件循环异步执行的函数,它会通过一个隐式的 Promise 返回其结果。如果你在代码中使用了异步函数,就会发现它的语法和结构会更像是标准的同步函数。
语法
async function name([param[, param[, ... param]]]) { statements }
描述
一个 async 异步函数可以包含 await 指令,该指令会暂停异步函数的执行,并等待 Promise 执行,然后继续执行异步函数,并返回结果。
记住,await 关键字只在异步函数内有效。如果你在异步函数外使用它,会抛出语法错误。
注意,当异步函数暂停时,它调用的函数会继续执行(收到异步函数返回的隐式 Promise)
const Ajax = (url) => {
// promise方式Ajax使用
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
xhr.send();
});
};
async function main() {
try {
const result = await Ajax('fed-e-task-01-01/notes/promise/api/user.json');
console.log(result);
const result1 = await Ajax('fed-e-task-01-01/notes/promise/api/class.json');
console.log(result1);
} catch (e) {
console.error(e);
}
}
main();
Promise
描述
Promise 是一个类,参数是一个执行器函数, 执行器函数自执行。
Promise 有 3 个状态 Pending 默认等待态 、Fulfilled 成功态 、Rejected 失败态 状态一改变就不能再次修改 Pending -> Fulfilled || Pending -> Rejected
执行器函数参数有 resolve 方法和 reject 方法 resolve 方法将 Pending 3.> Fulfilled reject 方法将 Rejected -> Rejected resolve 方法的参数将作为 then 方法成功回调的值, reject 方法的参数将作为 then 方法失败回调的原因。
then 方法有两个参数 一个是成功回调的函数 successCallback,一个是失败回调的函数 failCallback。 promise 的成功态会将成功的值传给成功的回调并且执行,失败态会将失败的原因传递给失败的回调并执行。
执行器中 resolve 和 reject 在异步中执行的时候,当前状态还是等待态 需要把 then 方法的成功回调和失败回调存起来,等异步调用 resolve 的时候调用成功回调,reject 的时候调用失败回调
-
then 方法
a. then 方法多次调用添加多个处理函数;
b. 实现 then 的链式调用: then 方法链式调用识别 promise 自返回 then 链式调用 返回的是一个新的 promise 对象
c. 判断 then 方法成功回调和失败回调的返回值 x x 返回的是一个 pormise,判断 x 和当前 then 返回是不是同一个 promise,如果是同一个 promise 就报错。x 和 then 返回的不是同一个 promise,将 x 的 then 方法执行返回给下一个 then。 x 是常量直接当作下一个 then 的成功回调的参数。后面 then 方法的回调函数拿到值的是上一个 then 方法的回调函数的返回值。
d.捕获错误及 then 链式调用其他状态代码补充
e. then 方法的参数可选
f. then 方法的值穿透
executor 执行器函数 / then 方法可能有错误,有错误直接调用 reject 方法
-
all 和 race 方法的实现
all 和 race 方法不管 promise 成功还是失败都不会影响其他 promise 执行 all 方法返回一个新的 promise all 参数里面每一项执行完成,才把所有结果依次按原顺序 resolve 出去 all 方法只要一个 promise 失败就 reject 失败的 promise 结果 Promise.race 方法,谁执行的快就返回谁
resolve 和 reject 方法的实现
resolve 方法: 相当于实例化 Promise 对象,并且调用了 resolve 方法 reject 方法:相当于实例化 Promise 对象,并且调用了 reject 方法
finally 方法的实现
finally 的实现,不管是成功状态还是失败态都会进这个 finally 方法 (等待态不会进)。finally 方法会返回一个新的 promise,它拿不到上次 then 执行的结果(所以没有参数),内部会手动执行一次 promise 的 then 方法。finally 方法有错误会把错误作为下次 then 方法的失败回调的参数。
-
catch 方法的实现
catch 方法相当于执行 then 方法的失败回调
demo
自定义 promise
class Promise {
/**
* 构造函数
* @param executor {Function} 执行器
*/
constructor(executor) {
/**
* 初始状态,既不是成功,也不是失败状态
* @type {string}
*/
this.PENDING = 'pending';
/**
* 意味着操作成功完成
* @type {string}
*/
this.FULFILLED = 'fulfilled';
/**
* 意味着操作失败
* @type {string}
*/
this.REJECTED = 'rejected';
/**
* Promise 状态
* @type {string} 默认值PENDING
*/
this.status = this.PENDING;
/**
* 成功回调函数入参的值
* @type {string} 默认值undefined
*/
this.value = undefined;
/**
* 失败回调函数入参的值
* @type {string} 默认值undefined
*/
this.reason = undefined;
/**
* successCallback 成功回调函数
* @type {Array} 默认值[]
*/
this.successCallback = [];
/**
* failCallback 失败回调函数
* @type {Array} 默认值[]
*/
this.failCallback = [];
/**
* privateResolve 解析
* @param value 成功回调的入参
*/
this.privateResolve = (value) => {
// 在promise的状态为PENDING时,允许改变其状态
if (this.status === this.PENDING) {
// 触发resolve,将promise的状态改为fulfilled(完成)
this.status = this.FULFILLED;
// 将resolve传递的参数,保存在Promise对象的value里
this.value = value;
// 如果successCallback成功回调函数存在,则执行
this.successCallback.forEach((callback) => {
callback();
});
}
};
/**
* privateReject 驳回
* @param reason 失败回调入参
*/
this.privateReject = (reason) => {
// 在promise的状态为PENDING时,允许改变其状态
if (this.status === this.PENDING) {
// 触发reject,将promise的状态改为rejected(失败)
this.status = this.REJECTED;
// 将reject传递的参数,保存在MyPromise对象的reason里
this.reason = reason;
// 如果failCallback失败回调函数存在,则执行
this.failCallback.forEach((callback) => {
callback();
});
}
};
// 捕获执行器异常,并从reject抛出
try {
executor(this.privateResolve, this.privateReject);
} catch (e) {
this.privateReject(e);
}
}
then(successCallback, failCallback) {
// 判断successCallback和failCallback是否存在,不存在将值向下传递
successCallback = successCallback ? successCallback : (value) => value;
// failCallback = failCallback ? failCallback : (reason) => reason;
failCallback = failCallback
? failCallback
: (reason) => {
throw reason;
};
// 链式调用实现,返回Promise函数对象
let promise = new Promise((resolve, reject) => {
// 判断promise的状态,当其状态为FULFILLED时,触发成功回调函数;当其状态为REJECTED时,触发成功回调函数
// 后面的 then 方法的回调函数的入参取自上一个 then 方法的回调函数的返回值
// promise对象未生成,所以将resolvePromise放到异步里调用,等待promise对象未生成后执行
if (this.status === this.FULFILLED) {
// 捕获异常,并从reject抛出
setTimeout(() => {
try {
resolvePromise(
promise,
successCallback(this.value),
resolve,
reject,
);
} catch (e) {
reject(e);
}
}, 0);
} else if (this.status === this.REJECTED) {
// 捕获异常,并从reject抛出
setTimeout(() => {
try {
resolvePromise(promise, failCallback(this.reason), resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
} else {
// 当判断promise的状态,当其状态为PENDING时,存储回调函数
this.successCallback.push(() => {
// 捕获异常,并从reject抛出
setTimeout(() => {
try {
resolvePromise(
promise,
successCallback(this.value),
resolve,
reject,
);
} catch (e) {
reject(e);
}
}, 0);
});
this.failCallback.push(() => {
// 捕获异常,并从reject抛出
setTimeout(() => {
try {
resolvePromise(
promise,
failCallback(this.reason),
resolve,
reject,
);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise;
}
/**
* finally 等待Promise对象执行完所有任务,并执行
* @param callback 回调函数
* @returns {Promise} 返回Promise对象
*/
finally(callback) {
return this.then(
(value) => {
return Promise.resolve(callback()).then(() => value);
},
(reason) => {
return Promise.resolve(callback()).then(() => {
throw reason;
});
},
);
}
/**
* catch 返回失败回调函数
* @param callback 回调函数
* @returns {Promise} 返回Promise对象
*/
catch(callback) {
return this.then(undefined, callback);
}
/**
* 等待所有Promise对象执行完毕,并返回结果
* @param array {Array} 数组对象
* @returns {Promise} 返回Promise对象
*/
static all(array) {
// 返回数组
let result = [];
// 计数器
let index = 0;
return new Promise((resolve, reject) => {
/**
* addResult 向result数组添加返回值
* @param key {Number} key值
* @param value {Object} value值
*/
const addResult = (key, value) => {
result[key] = value;
index++;
// 判断所有异步操作完成后,执行resolve
if (array.length === index) {
resolve(result);
}
};
// 遍历
array.forEach((val, i) => {
// 判断当前值,是否属于Promise对象
if (val instanceof Promise) {
// Promise对象
// 执行成功回调函数,获取返回值,并添加到result数组里
// 执行失败回调函数,直接通过reject抛出失败原因
val.then(
(value) => addResult(i, value),
(reason) => reject(reason),
);
} else {
// 普通对象,将当前值添加到result数组里
addResult(i, val);
}
});
});
}
/**
* resolve 解析
* @param value
* @returns {Promise}
*/
static resolve(value) {
// 判断value是否是Promise对象,是则直接返回,否则创建Promise对象并返回
if (value instanceof Promise) return value;
return new Promise((resolve) => resolve(value));
}
/**
* reject 驳回
* @param value
* @returns {Promise}
*/
static reject(value) {
// 判断value是否是Promise对象,是则直接返回,否则创建Promise对象并返回
if (value instanceof Promise) return value;
return new Promise((undefined, reject) => reject(value));
}
/**
* 返回一个 promise,一旦迭代器中的某个 promise 解析或拒绝,返回的 promise 就会解析或拒绝。
* @param array {Array} 数组对象
* @returns {Promise} 返回Promise对象
*/
static race(array) {
return new Promise((resolve, reject) => {
// 遍历
array.forEach((val, i) => {
// 判断当前值,是否属于Promise对象
if (val instanceof Promise) {
// Promise对象
// 执行成功回调函数,获取返回值,并通过resolve弹出
// 执行失败回调函数,直接通过reject抛出失败原因
val.then(
(value) => resolve(value),
(reason) => reject(reason),
);
} else {
// 普通对象,将当前值通过resolve弹出
resolve(val);
}
});
});
}
}
/**
* resolvePromise 解析Promise
* @param promise promise对象
* @param result 上一个then回调函数返回值
* @param resolve 解析函数
* @param reject 驳回函数
*/
const resolvePromise = (promise, result, resolve, reject) => {
// 判断promise与result是否相等,如果相等,则是promise循环调用,这里应该抛出异常,并阻止往下执行
if (promise === result) {
return reject(new TypeError('Chaining cycle detected for my-promise'));
}
// 判断result是普通对象还是属于Promise对象
if (result instanceof Promise) {
// 查看Promise对象返回结果,调用对应的resolve或reject
result.then(resolve, reject);
} else {
resolve(result);
}
};
module.exports = Promise;
调用
const Promise = require('./promise');
const Ajax = (url) => {
// promise方式Ajax使用
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open('get', url);
xhr.responseType = 'json';
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
xhr.send();
});
};
// Promise基本使用
let promise = new Promise((resolve, reject) => {
// resolve('成功');
reject('失败');
});
promise
.then((res) => {
console.log(res);
})
.catch((e) => console.error(e));
// promise方式Ajax使用
Ajax('fed-e-task-01-01/notes/promise/api/user.json').then(
(value) => {
console.log(value);
},
(e) => {
console.error(e);
},
);
// promise链式调用
Ajax('fed-e-task-01-01/notes/promise/api/user.json')
.then((value) => {
console.log(value);
return Ajax('fed-e-task-01-01/notes/promise/api/class.json');
})
.then((value) => {
console.log(value);
return 'abc';
})
.then((value) => console.log(value));
// promise静态方法使用
// all 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
/**
* 输出结果
* [3, 42, "foo"]
*/
// race返回一个 promise,一旦迭代器中的某个promise解析或拒绝,返回的 promise就会解析或拒绝。
const promise4 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise5 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise4, promise5]).then((value) => {
console.log(value);
// Both resolve, but promise5 is faster
});
/**
* 输出结果
* two
*/
// reject方法返回一个带有拒绝原因的Promise对象
Promise.reject(new Error('fail')).catch((e) => console.error(e.message));
/**
* 输出结果
* fail
*/
// resolve方法返回一个以给定值解析后的Promise 对象。如果这个值是一个 promise ,那么将返回这个 promise ;如果这个值是thenable(即带有"then" 方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平
Promise.resolve('success').then((res) => console.log(res));
/**
* 输出结果
* success
*/