函数的多层嵌套(执行+参数+功能相分离)
函数加工厂, 条件的抽离
函数的二次回调形式
函数执行形态 和 函数定义形态的转换? 快照?
小思考2
说实话, 作为一个js小白,有很多东西是很难想明白的.
或者说,关键在于提出一个正确,或者一系列正确的问题,
并且这些问题, 所引出的知识能够被重新梳理成一个系统.
或者通过外部的一个系统,重新被整理.
这样我才能感觉比较清楚.
我在学习js的过程当中,
确实总觉得"一无所知", 觉得什么都不太会,
这种不太会的感觉,
我想可能来自以下一个原因
第一种是,确实是刚开始学,很多基础的api, 最基本的知识点没能记得很牢.
往往出现学了后面的, 似乎前面的都忘得差不多.第二种就是, 我感觉到了什么, 或者应该说洞察到了什么,
或者说, 时常遇到要提出问题, 或者总结问题的时候.
但这种情况下, 我既无法提出正确的问题, 又缺乏足够的整理能力.
当然, 每次试着回答提出的问题, 以及整理没能整理的一些东西,
起码都让我加深了某些知识点的理解.
但那种, 还是什么都不太懂的感觉, 时常困扰我
比如说,关于函数.
一个function的标配 和 各种基本形态
其实之前最觉得神奇的地方就是, 函数的多层嵌套产生的神奇效果.
刚开始接触的是 数组的几个模拟方法
比如 forEach, map, reduce, some, every 等等.
js小白模拟系列:模拟数组 forEach,filter,map,every,some,reduce
记得当时是看陈老师的直播课, 第一次接触这个, 感觉有点震撼.
有点神奇.
然后我就试着想要去理解这个东西.
因为在我看来,
forEach 是把某种很基本的行为给抽象了出来,
而这种把行为抽象出来的手段, 就是通过多个函数的嵌套.
然后对函数的一个基本理解就是,
函数就是一段代码的封装,
能够执行某些动作,
并且能够返回值.
然后就觉得, 实际上我们写的每一句都可以看成是
一个函数, 都有执行, 都有返回.
然后每一句的执行代码前, 实际上都存在一个状态, 或者条件的判断.
当时思考的重点是,
我怎么进行分割? 一连串的代码总是可以分割成几个代码的调用.
但是以什么方式进行分割?
实际上这个问题自我脑力出现以来, 从没有从我的脑子里出去过,
当然到今天我也没能有个大概,清晰的一个答案,或者思路.
(你看到了吧, 就是因为这种问题, 我时常觉得自己什么都不太会)
后来除了forEach, 又碰到一两次, 比较神奇的代码模拟封装.
js节流封装
js防抖封装
js小白模拟系列:模拟bind
特别是这个 模拟bind , 也给了我比较震撼的感觉.
(发现这几个都是陈老师的,cto果然是cto)
(记得还有一个缓存的模拟封装)
然后我自己总结的时候, 就总结成 函数的多层嵌套的用途.
函数多层嵌套的时候,
核心就是
函数的多层嵌套能够把功能,参数,执行相分离
函数的多层嵌套能够绑定参数,绑定this, 或者保护参数,保护this?
最一般的,正常的情况是, 函数的功能, 参数, 和执行是同时完成的
function show (name) {
console.log(name);
}
show("mike");/此时,执行,功能,参数是在一起的
绑定参数的情况(也可以认为保护了参数?)
function show (name) {
console.log(name);
}
function showMike () {
show("mike");
}
showMike();/ 我传什么, 功能 show 和 参数mike 都已经绑定在一起.
绑定this的情况(也可以认为保护了this)
let name = "peter";
function show (name) {
console.log(this.name);
}
let obj = {name : "mike"}
function showMike () {
obj.show = show;
obj.show();
}
showMike();
关于this的理解,
第一种理解是, this指向的永远都是谁调用他.
第二种理解是, this其实就是个传参入口.
所以一个函数正常是有两个传参入口的, 一个是 this, 一个是普通传参入口().
这两个入口之间其实是可以转换的, 比如一个参数如果能够通过(普通)进来,
就能更改成通过 this传进来.
let name = "peter";
function show (name) {
console.log(name);
}
function showName () {
show(this)
}
showName.call(name);
我们举得例子都不是很恰当,,,
这个时候, 我们试着转一下思路,
我们知道, 一个函数, 可以把另一个函数当做参数,
也可以返回一个函数,
从整体上来看就是, 进入一个函数, 出来一个函数.
然后就会出现很多神奇的东西.
我们先称这种处理函数的函数为 函数加工厂?
function show (name) {
console.log(name);
}
function typeLimit (fn) {
return function (name) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
}
let showString = typeLimit(show);
showString("mike");
showString(12);
功能是 show, 函数加工厂是 typeLimit
跟最上面的 绑定参数相比,
这里做得事情是, 对参数进行了校验?
其实你完全可以不用这么写
而是这么写
function showString (name) {
if (typeof name == "string") {
console.log(name);
}else{
console.log(`${name} type is not a string`);
}
}
但这个时候, 回头好好想想, 你就会发现一个很神奇的事情,
你发觉到了嘛? 反正我觉得很神奇,
那就是, 我把 一个if else 条件给抽离了出去!
一般如果我们对上面这个函数进行分割,
可能会这么做, 其实一直到现在,一直到此刻, 我都是这么做的.
function show (name) {
console.log(name);
}
function showString (name) {
if (typeof name == "string") {
show(name);
}else{
console.log(`${name} type is not a string`);
}
}
或者我会自认为更聪明一点, 把函数当做一个参数传进去,
以显得依赖关系更加明确?
function show (name) {
console.log(name);
}
function showString (cb,name) {
if (typeof name == "string") {
cb(name);
}else{
console.log(`${name} type is not a string`);
}
}
showString(show,"mike");
showString(show,12);
可是我们回头从新看一遍这个
function show (name) {
console.log(name);
}
function typeLimit (fn) {
return function (name) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
}
let showString = typeLimit(show);
showString("mike");
showString(12);
你不觉得很神奇吗?
为什么我会觉得很神奇?
第一, 我不是单纯把功能(show)分割出去,
实际上, 我是把功能(show) 和 条件(typeLimit) 都分割了出去.
第二, 这里的三个函数的语义都非常明确.更加清晰.
而这是通过返回一个新函数来实现的.
我觉得这个例子能够启发很多东西,
其中一个就是, 我们其实能够把一些条件 分割出去.
实际上我们好好回想一下就会发现
js节流封装
js防抖封装
这两个的封装,其实跟上面的是一模一样的套路.
回过头来说, 把这些条件分割出去,形成的函数, 就是刚才我们所说的
函数加工厂.
现在我们指定一个功能, 然后我们试着给他添加各种条件,
也就是增加各种函数加工厂.
比如我们增加一个用户权限
function show (name) {// 核心功能
console.log(name);
}
function typeLimit (fn) {// 类型限制为 必须是字符串
return function (name) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
}
let user = "admin";
function isUser (fn) {// 只有当是管理员时才能 执行
return function (data) {
if (user === "admin") {
fn(data);
}else {
console.log("you have no enough power");
}
}
}
let showString = isUser(typeLimit(show));/通过两个函数工厂 返回一个函数
showString("mike");/mike
showString(12);/12 type is not a string
user = "peter";
showString("mike");/you have no enough power
神奇不神奇?
我觉得那是相当的神奇啊.
我们正常写是这样的.
function showString (data) {
if (user === "admin") {
if (typeof name == "string") {
console.log(name);
}else{
console.log(`${name} type is not a string`);
}
}else {
console.log("you have no enough power");
}
}
看起来代码量是少, 但是结构比较深啊.
而且关键还不在这里.
关键在于..
如果我们现在还要增加一个条件
增加一个什么条件好呢?
判断一个this吧
let user = "admin";
function Person () {// 构造函数
}
function showString (name) {
if (!(this instanceof Person)) {
console.log("who are you");
return
}
if (user === "admin") {
if (typeof name == "string") {
console.log(name);
}else{
console.log(`${name} type is not a string`);
}
}else {
console.log("you have no enough power");
}
}
let person = new Person();
showString.call(person,"mike");
showString.call(person,12);
showString(12);
user = "peter";
showString.call(person,"mike");
改成函数加工厂
function show (name) { /核心功能
console.log(name);
}
function typeLimit (fn) {/ 函数加工厂, 类型限制
return function (name) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
}
let user = "admin";
function isUser (fn) { / 函数加工厂, 用户权限限制
return function (data) {
if (user === "admin") {
fn(data);
}else {
console.log("you have no enough power");
}
}
}
function isPerson (fn) {/ 函数加工厂, 原型限制
return function (data) {
if (this instanceof Person) {
fn(data)
} else {
console.log("who are you");
}
}
}
let showString = isPerson(isUser(typeLimit(show)));/ 加工返回函数
function Person () {/ 构造函数
}
let person = new Person();
showString.call(person,"mike");
showString.call(person,12);
showString(12);
user = "peter";
showString.call(person,"mike");
从直观上来看, 确实代码量似乎更多,
但实际上, 我个人觉得语义化更高,
而且关键在于,
- 条件功能的复用 : 不只是可以用在这一个核心功能上, 也可以用在别的功能函数上
- 添加一个新条件的时候, 我们拥有了另一种思路! 那就是别急着在功能函数里
增加if else, 我们可以通过增加 函数加工厂的方式
当然了,
如果你细心一点, 可能会有好几个疑问,
我自己现在就有两个疑问,
第一,必须要返回函数嘛? 不返回函数,难道就不能把条件抽离嘛?
第二, 这种方式抽离条件, 有没有无法抽离的情况? 或者说,所有的条件都能用这种方式抽离嘛? 或者说, 这种抽离条件函数加工厂的方式, 有没有限制?
先回答第一个问题
必须要返回函数嘛? 不返回函数, 难道就不能把条件抽离嘛?
似乎是不行
function show (name) {
console.log(name);
}
function typeLimit (fn) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
let user = "admin";
function isUser (fn) {
if (user === "admin") {
fn(data);
}else {
console.log("you have no enough power");
}
}
function isPerson (fn) {
if (this instanceof Person) {
fn(data)
} else {
console.log("who are you");
}
}
function Person () {// 构造函数
}
isPerson(isUser(typeLimit(show)));
问题在于,我们怎么从isPerson开始传递给功能函数show 参数?
第一种思路是, 我们直接传参?
function show (name) {
console.log(name);
}
function typeLimit (fn,name) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
let user = "admin";
function isUser (fn,data) {
if (user === "admin") {
fn(data);
}else {
console.log("you have no enough power");
}
}
function isPerson (fn,data) {
if (this instanceof Person) {
fn(data)
} else {
console.log("who are you");
}
}
function Person () {// 构造函数
}
isPerson(isUser(typeLimit(show,data),data),"mike");/ 这里会报错
/ data is not define
发现会报错, 其实很明显.
但我们总是对很明显的东西, 归纳能力不够, 或者解释能力不够,
这里的mike 不会传给 data, 这很明显
看一下这句, 这一句的执行顺序是什么?
isPerson(isUser(typeLimit(show,data),data),"mike")
应该是先执行 typeLimit(show,data) 然后把返回值传递给 isUser()
所以我们刚才的思路就是个笑话.
变量data在传进去之前会在当前作用域里去找. 那必然是undefined啊.
或者我们换一个问题, 为什么之前返回函数的时候, 可以依次传递进去?
然后就会发现, 实际上这跟函数的定义形态非常相关.
我们重新观察一下
function show (name) {
console.log(name);
}
function typeLimit (fn) {
return function (name) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
}
我们在上面说
函数的多层嵌套能够把功能,参数,执行相分离
函数的多层嵌套能够绑定参数,绑定this, 或者保护参数,保护this?
到这里,我们是不是可再加一条函数本身的一个特性?
函数的定义形态, 能够产生自己的作用域, 定义自己的变量, 以及能够定义形参, 的同时, 能够把父级的作用域链全都储存进来.
所以实际上每次 return function 的时候, functon 已经把父级作用域链上的定义的变量, 全都拷贝了一份.
我们这里是依次传递所以没太大感觉,
实际上这么看一下
function show (name) {
console.log(name);
}
function typeLimit (fn,age,sex) {
return function (函数1) (name, game, round) {
if (typeof name == "string") {
fn(name);
}else{
console.log(`${name} type is not a string`);
}
}
}
function isUser (fn,num,string,good) {
return function (函数2) (data,a,b,c) {
if (user === "admin") {
fn(data);
}else {
console.log("you have no enough power");
}
}
}
isUser(typeLimit(show))
在函数1 里, 能够访问的变量有,fn,age,sex,name, game, round
在函数2 里, 能够访问的变量有, fn,num,string,good,data,a,b,c
这里要怎么描述呢?好难啊.
不管上面的, 我们试着这样总结,
其实上面是有两种传递方式,
- 第一种是, 通过return
比如 isUser(typeLimit(show)), 这里的tpeLimit 和 isUser中的fn 参数,
实际上是通过 return的 函数来传递的. - 第二种是, 通过函数作用域?
比如 每个return 的function 都拥有自己的 参数入口,然后在内部调用fn(data)
的方式,进行传递
这么一总结, 就越发感觉整个结构很巧妙.
(返回一个函数的时候, 实际上有很多神奇的东西)
其实我的理解没那么深, 之所以现在比第一次接触这种形式的时候,
没那么烧脑, 没那么震撼的原因是, ... 我习惯了,
尽管没能完全深刻的理解, 但用着用着就有点习惯了.
把跑了题, 拉回来,
还是那个问题, 我们能不能不通过返回函数的方式来完成抽离?
刚才问题变成了, 怎么传递参数的问题,
结论似乎是不能?
未必,
我们刚才说, 存在两种传参方式, 第二种是依赖于返回函数的时候,
但第一种不是啊, return的方式传参就可以啊.
function show (name) {
console.log(name);
}
function typeLimit (data) {
if (typeof data == "string") {
return data
}else{
console.log(`${data} type is not a string`);
}
}
let user = "admin";
function isUser (data) {
if (user === "admin") {
return data
}else {
console.log("you have no enough power");
}
}
function isPerson (data) {
if (this instanceof Person) {
return data
} else {
console.log("who are you");
}
}
function Person () {// 构造函数
}
let person = new Person();
/不符合条件时
show(typeLimit(isUser(isPerson("mike"))))
/who are you
/undefined type is not a string
/undefined
/符合条件时
show(typeLimit(isUser(isPerson.call(person,"mike"))))/ mike
基本上是可以的,
但我们发现不符合条件时, 执行的有点过多, 我们希望的是,
当遇到第一个不满足条件的情况时, 就打断,
我们发现无法打断.
而且发现我们把调用顺序也给调换了.
不过上面这段代码, 很容易让人想起昨天学的 函数的组合.
function compose () {
let args = [].slice.call(arguments);
let len = args.length - 1;
return function (data) {
let result = args[len](data);
while (len--){
result = args[len](result);
}
return result
}
}
let fn = compose(show,typeLimit,isUser,isPerson.bind(person));
fn('mike');
吃了个饭, 思路全打断,(⊙o⊙)…
上面提出的另一个问题,
通过函数抽离条件的用法, 有没有限制?
应该是有的,, 不清楚,,
不过我觉得最重要的是, 返回一个函数,
说到底,其实就是返回了一个变量,
但一个变量最基本的能力就是, 复用性.
能够穿越时空,
当我们通过函数工厂返回一个函数的时候,
返回这个变量的时候,
实际上我们返回一个能够穿越时空,
可复用的功能 + 参数?
之前我们说 函数加工厂, 返回函数的方式,
能够把执行+ 功能 + 参数, 相分离, 或者相绑定.
回顾前几天学的promise封装,
就感觉这个东西很重要.
因为promise当中, 对抗异步的方法, 说到底其实就是
函数的 执行+功能+参数 相分离的结果.
之前我觉得对抗异步的方式, 跟以下几个要素相关.
- 异步
- 监听
- 状态
- 回调函数
// 状态
let state = true;
// 监听
let timer = null;
timer = setInterval(() => {
if (state === false) {
fn();
clearInterval(timer);
}
},100);
// 异步任务
let timer2 = null;
timer2 = setTimeout(() => {
state = false;
},1000)
// 回调
function fn () {
console.log("i am here");
}
然后有时候, 似乎不需要监听,
只需要有触发回调就可以?
// 状态
let state = true;
// 异步任务
let timer2 = null;
timer2 = setTimeout(() => {
state = false;
fn();/ 触发回调
},1000)
// 回调
function fn () {
console.log("i am here");
}
基本公式就是, 异步任务, 用回调来对抗.
但有几个问题,
- 我们希望根据不同状态, 执行不同功能.
// 状态
let state = true;
// 数据
let i = 10;
// 异步任务
let timer2 = null;
function go (suc,err) {
clearTimeout(timer2);
timer2 = setTimeout(() => {
state = Math.random() - 0.5 > 0;
if (state) {
suc(i);
} else {
err(i);
}
},1000)
}
// 回调
function fn1 () {
console.log(++i);
}
function fn2 () {
console.log(--i);
}
go(fn1,fn2);
如果和promise 相比较,
我们会发现, promise是先请求了一个异步任务,
然后,再注册一个回调函数!
而且不是一个,可以是多个.
后续可以继续添加.
是不是感觉非常牛逼?
我们上面的go(fn1,fn2),
则是在请求一个异步任务的同时注册回调函数.
后续无法再追加.
而且promise 能够把返回的数据递给所有注册的回调函数.
怎么实现?
思路是什么?
根据今天的分析,最基本应该有的思路是
状态, 数据, 函数(功能), 这三者都必须能够跨越时空,
而想要跨越时空, 就必须使其变量化.
也就是说, 状态发生变化时, 并不需要马上执行回调函数,
也不只是只能执行一次回调函数.
也就是说状态和数据必须存下来.
另一个问题是, 必须把发送请求异步任务和 注册回调函数相分离.
否则每次注册回调函数, 岂不是都要发送异步任务请求?
// 状态
let state = true;
// 数据
let i = 10;
// 异步任务
let timer2 = null;
function go () {
clearTimeout(timer2);
timer2 = setTimeout(() => {
state = Math.random() - 0.5 > 0;// 存状态
i = Math.random() * 100; //存数据
},1000)
}
// 注册回调
function dodo (suc,err) {
if (state) {
suc(i);
} else {
err(i);
}
}
// 回调
function fn1 () {
console.log(++i);
}
function fn2 () {
console.log(--i);
}
go();
dodo(fn1,fn2);/ 11
原因是, dodo 执行时, state 必然是初始值 true
越写越觉得promise的设计者很牛逼
所以状态起码要分成三种,
默认, 成功, 失败
刚才我们 发送异步请求任务 和 注册回调函数 相分离了.
然后还要把函数进行储存.
但什么时候执行 回调函数?
也就是说, 把执行回调函数也分离了.
// 状态
let state = 0;
// 数据
let i = 10;
// 储存回调
let fn1cb = null;
let fn2cb = null;
// 异步任务
let timer2 = null;
function go () {
clearTimeout(timer2);
timer2 = setTimeout(() => {
state = Math.random() - 0.5 > 0;// 存状态
i = Math.random() * 100; //存数据
// 执行注册的回调函数
if (state) {
fn1cb();
} else {
fn2cb();
}
},1000)
}
// 注册回调的接口
function dodo (suc,err) {
if (state == 1) {
suc(i);// 如果满足 则执行
} else if (state == 2) {
err(i);/ 如果满足 则执行
} else {/ 不满足时, 存下来.留给返回状态时执行.
fn1cb = function () {
suc(i)
}
fn2cb = function () {
err(i)
}
}
}
// 回调
function fn1 () {
console.log(++i);
}
function fn2 () {
console.log(--i);
}
go();// 发送异步任务请求
dodo(fn1,fn2);// 注册回调函数
我们继续往promise 的思路靠.
实际上, 我们上面的代码只发送了一个异步任务请求
如果我们再发送一次异步请求怎么办?
那我们还要重写一遍?
假设我们要重写一遍, 为了防止冲突,
我们必然要整理一下, 这种整理一下数据的行为,
就很自然的想到, 对象的方式
// 状态
let promise = {};
promise.state = 0;
// 数据
promise.i = 10;
// 储存回调
promise.fn1cb = null;
promise.fn2cb = null;
// 异步任务
promise.go = function() {
setTimeout(() => {
this.state = Math.random() - 0.5 > 0;// 存状态
this.i = Math.random() * 100; //存数据
// 执行
if (this.state) {
this.fn1cb();
} else {
this.fn2cb();
}
},1000)
}
// 注册回调
promise.dodo = function (suc,err) {
if (this.state == 1) {
suc(this.i);
} else if (this.state == 2) {
err(this.i);
} else {
this.fn1cb = function () {
suc(this.i)
}
this.fn2cb = function () {
err(this.i)
}
}
}
// 回调
function fn1 (i) {
console.log(++i);
}
function fn2 (i) {
console.log(--i);
}
promise.go();// 发送异步任务请求
promise.dodo(fn1,fn2);// 注册回调函数
这个时候,有一个问题,
假设我要发出另一个异步请求任务,
那我要重写一遍go嘛?
所以有个需求就是, go里要传一个回调函数,
用来接收 异步请求任务的函数
最简单的是这样
promise.go = function(fn) {
fn();
}
function changestate () {
setTimeout(() => {
this.state = Math.random() - 0.5 > 0;// 存状态
this.i = Math.random() * 100; //存数据
// 执行
if (this.state) {
this.fn1cb();
} else {
this.fn2cb();
}
},1000)
}
promise.go(changestate);/ 报错
但必然报错.
这是因为, this指向早已不是 promise
但我们需要在异步任务执行的时候,更改 promise 的 状态,
以及更改promise的 数据,
还要执行一遍有存储的函数.
想要修改该对象的状态,
就要利用这个对象提供的接口.
下面这个设计, 真的是打死我也想不出来啊,真真佩服promise 设计者
(也许是我功力不到家的缘故)
promise.go = function(fn) {
let resolve = (i) => {
if (this.state == 0) {
this.state = 1;
this.i = i;
this.fn1cb();
}
}
let reject = (i) => {
if (this.state == 0) {
this.state = 2;
this.i = i;
this.fn2cb();
}
}
fn(resolve, reject);
}
promise.go(function (函数1) (res,rej) {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if (swich) {
res(Math.random() * 100);
} else {
rej(Math.random() * 100);
}
},1000)
});
哇,, 这个真是,怎么都想不到啊.
提供两个接口, 传入的参数, 可以更改相应的数据,并储存.
而关键是,res,rej 从肉眼上看, 压根看不到和promise 有一毛钱关系,
这个设计到底要怎么理解呢?
这里总共存在几个函数?
promise.go 是一个函数
函数1 是一个函数
resolve,reject 各是一个函数
函数1 这个接口, 可以让调用者自行设计执行代码.
这种思路第一见到的地方是 forEach
Array.prototype.myForEach = function (fn) {
let len = this.length;
for(let i = 0; i < len ; i++) {
fn(this[i],i,this)
}
}
但这里不存在提供二次回调函数啊!
我们根据这种二次回调函数,试着用一下
let arr = [1,2,3];
Array.prototype.awsome = function (fn) {
let self = this;
let len = self.length;
let i = 0;
function next () {
return self[i++]
}
function prev () {
return self[i--]
}
fn(next,prev)
}
arr.awsome(function (next,prev) {
console.log(next());
})
我们先继续promise 的思路,
现在确实能够做到, 给一个异步任务添加回调.
当然也可以稍微改一下之后,能够添加多个回调.
// 状态
let promise = {};
promise.state = 0;
// 数据
promise.i = 10;
// 储存回调
promise.fn1cb = [];
promise.fn2cb = [];
// 异步任务
promise.go = function(fn) {
let resolve = (i) => {
if (this.state == 0) {
this.state = 1;
this.i = i;
this.fn1cb.forEach((item) => {
item();
});
}
}
let reject = (i) => {
if (this.state == 0) {
this.state = 2;
this.i = i;
this.fn2cb.forEach((item) => {
item();
});
}
}
fn(resolve, reject);
}
promise.go(function (res,rej) {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if (swich) {
res(Math.random() * 100);
} else {
rej(Math.random() * 100);
}
},1000)
});
// 注册回调
promise.dodo = function (suc,err) {
if (this.state == 1) {
suc(this.i);
} else if (this.state == 2) {
err(this.i);
} else {
this.fn1cb.push( () => {
suc(this.i)
})
this.fn2cb.push(() => {
err(this.i)
})
}
}
如果我们想要发出多个异步任务请求,
就需要多个promise,
很自然的就会想到用构造函数返回实例的方式,
去生成每个异步任务对应的, promise 对象
class myPromise {
constructor() {
this.state = 0;
this.i = undefined;
this.fn1cb = [];
this.fn2cb = [];
}
go(fn) {
let resolve = (i) => {
if(this.state == 0) {
this.state = 1;
this.i = i;
this.fn1cb.forEach((item) => {
item();
});
}
}
let reject = (i) => {
if(this.state == 0) {
this.state = 2;
this.i = i;
this.fn2cb.forEach((item) => {
item();
});
}
}
fn(resolve, reject);
}
dodo(suc, err) {
if(this.state == 1) {
suc(this.i);
} else if(this.state == 2) {
err(this.i);
} else {
this.fn1cb.push(() => {
suc(this.i)
})
this.fn2cb.push(() => {
err(this.i)
})
}
}
}
let promise = new myPromise();
promise.go((res, rej) => {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if(swich) {
res(Math.random() * 100);
} else {
rej(Math.random() * 100);
}
}, 1000)
});
// 回调
function fn1(i) {
console.log(++i);
}
function fn2(i) {
console.log(--i);
}
promise.dodo(fn1, fn2); // 注册回调函数
promise.dodo((data) => {
console.log("suc");
}, () => {
console.log("err");
})
如果多个异步任务之间,没有执行顺序上的互相影响,
那么,上面的promise 就足够可以应付,
但假设如果一个异步任务的请求, 必须是另一个异步任务得到结果之后才能请求,
要怎么办?
这个时候, 问题变得很复杂,
假设在我遇到promise之前自己思考到这里,
我99%会放弃的,
因为,问题在于, 另一个异步任务到底什么之后发出请求?
因为我们现在把发送任务请求, 注册回调函数, 和执行回调函数全都分割了开来.
我想了想,
第二个异步任务请求, 应该是需要在 执行回调函数之后, 发送?
// 第一个异步任务
let promise = new myPromise();
promise.go((res, rej) => {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if(swich) {
res('suc');
} else {
rej('err');
}
}, 1000)
});
promise.dodo((data) => {
console.log(data);
// 第二个异步任务
let promise1 = new myPromise();
promise1.go((res, rej) => {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if(swich) {
res('win');
} else {
rej('lose');
}
}, 1000);
});
promise1.dodo((data) => {console.log(data);},
(data) => {console.log(data);});
}, (data) => {
console.log(data);
})
这就很low了, 观察一下,就会发现,
promise1的生成,以及异步任务的发出, 还有 回调函数的注册,都是在
promise的dodo里完成的.
low在什么地方?
low在如果还有异步任务需要后序发出,那这个跟回调地狱没什么两样.
会层层嵌套.
如果我们想扁平操作, 异步任务的发出可以在dodo的回调里进行,
但第二个回调函数的注册, 应该在外面进行.
也就是说, 我们需要把在 里面生成的 promise1这个对象传到外面去?
这样才能在外面用这个对象进行调用promise1.dodo();
想要真正return 出去, 还需要在 dodo 上进行修改.
dodo(suc, err) {
if(this.state == 1) {
let rus = suc(this.i);
if (rus instanceof myPromise) {
return rus
}
} else if(this.state == 2) {
let rus = err(this.i);
if (rus instanceof myPromise) {
return rus
}
} else {
this.fn1cb.push(() => {
suc(this.i)
})
this.fn2cb.push(() => {
err(this.i)
})
}
}
你发现 state == 1, state == 2 的时候,都好办,
state == 0;的时候就很麻烦.
(如果是我自己思考这个问题, 我肯定已经放弃了,现在我们是揣摩promise)
麻烦在哪里?
麻烦在于, state == 0 时, 按照我们的思路, 我们是要对 执行的结果进行判断,
如果是promise 对象, 就返回该对象, 可state == 0 也就是异步任务的时候,
dodo执行完, 并不代表注册的回调函数都执行完.
因为dodo执行完的时候, 异步任务还没完成, 状态还没有变化.
当状态发生变化时,再想通过dodo 返回 一个 promise对象, 那根本不可能.
解决思路是, state == 0 时, 先返回一个新的promise对象.
在外部在这个新的promise对象上注册回调函数.
dodo(suc, err) {
if(this.state == 1) {
let rus = suc(this.i);
if (rus instanceof myPromise) {
return rus
}
} else if(this.state == 2) {
let rus = err(this.i);
if (rus instanceof myPromise) {
return rus
}
} else {
this.fn1cb.push(() => {
suc(this.i)
})
this.fn2cb.push(() => {
err(this.i)
})
let promise = new myPromise();
return promise
}
}
// 第一个异步任务
let promise = new myPromise();
promise.go((res, rej) => {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if(swich) {
res('suc');
} else {
rej('err');
}
}, 1000)
});
let promise2 = promise.dodo((data) => {
console.log(data);
// 第二个异步任务
let promise1 = new myPromise();
promise1.go((res, rej) => {
setTimeout(() => {
let swich = Math.random() - 0.5 > 0;
if(swich) {
res('win');
} else {
rej('lose');
}
}, 1000);
});
return promise1;
}, (data) => {
console.log(data);
})
// 第二个异步任务的回调函数注册
promise2.dodo((data) => {console.log(data);},
(data) => {console.log(data);});
这里稍微有点绕, 很容易混淆,
首先,上面的代码肯定是不行的,
上面总共有三个 promise 对象.
第一个是 promise
第二个是 promise1
第三个是 promise2
请问promise1 和 promise2 是同一个对象嘛?
如果promise发布的是异步任务, 那么promise1 和 promise2
必然不可能是同一个 对象.
现在的情况是,
promise1 能够决定,状态和数据.(state 和 i)
promise2 注册了,本来应该在promise1上注册的回调函数.
现在我们要做的是, 让回调函数和状态,数据结合在一起,
换言之,要么让 promise1 把状态和数据传给 promise2
要么让 promise2 把注册的回调函数传给 promise1
丁老师带我们写的模拟封装, 用的是 promise1 把状态传给了 promise2
用得是非常巧妙的方法.首先, 想要更改promise2 的状态,
就必须调用 promise2.go(res,rej), 通过调用res,rej 能够传递状态和数据.
其次, 我们想要在promise1的状态发生变化之后, 去执行上面这个行为,
就需要在promise1.dodo()里注册该行为.
也就是 promise1.dodo() 能够监听状态 并能注册函数,
promise2.go() 能够改变状态.
就这个绝对是点睛之笔, 巧妙得很.佩服佩服.
dodo(suc, err) {
if(this.state == 1) {
let rus = suc(this.i);
if (rus instanceof myPromise) {
return rus
}
} else if(this.state == 2) {
let rus = err(this.i);
if (rus instanceof myPromise) {
return rus
}
} else {
let promise = new myPromise();
promise.go((res,rej) => {
/ promise 的点睛之笔
this.fn1cb.push(() => {
let rus = suc(this.i);
if (rus instanceof myPromise) {
/ promise 的点睛之笔
rus.dodo(res,rej);
}
})
this.fn2cb.push(() => {
let rus =err(this.i);
if (rus instanceof myPromise) {
rus.dodo(res,rej);
}
})
})
return promise
}
}
可能你会有疑问, 似乎promise2 和 promise1之间,只传递了状态,
没有传递数据啊?!
其实是传了, 我也是沿着执行顺序走了一遍, 才明白这个值是怎么传递的.
你会有第二个疑问, 现在的promise 和 之前丁老师封装的promise相比,
还差一步.
差在哪里呢?
差在, dodo 的回调函数中, 返回的若不是 promise对象时,
则该返回值会传递给下一个返回的promise,
差在,当state == 1 或 state == 2 时, 依旧会返回一个promise对象,
这么做的原因是, 能够持续进行链式调用.
上面这一段 开始是如何对抗异步,
然后顺便回顾一下promise的封装,
虽然有很多牵强的部分,
但我个人还是有点收获的.
当然promise的设计思路, 可能不是像我这么推导来的,
promise的精彩的地方, 肯定也不止这些.
我们回头看一下promise,
从函数应用的角度来讲,
有两个地方我印象比较深刻,
一个是go(res,rej), 二次回调的用法,令人印象深刻.
一个是当我们把注册函数用push方式存进去的时候,就好像存了一个档
我们是把功能和参数已经绑定了, 然后在后面的某个时刻,重新执行.
关键是, 就仿佛回到了该函数注册的现场, 所有的变量都是定义时刻的变量.
我觉得函数种种神奇的表现, 都是源于此.
上面这句话,换句话来讲就是,
函数的定义很重要, 函数在哪里定义很重要,
函数的执行,远远没有函数定义的时候的信息丰富?
当我们把一个功能(也就是执行形态) 添加丰富的信息,
最简单的方法就是 给他套上一个函数定义,
因为当函数定义的时候, 我们就好像是拍了个快照,
这个快照里 拥有当时的所有变量.
而这个快照是可以穿越时空的.
而实际上, 这种快照可以很轻易就可以"拍"
哎, 看看我说的都是些什么, 这抽象总结能力, 真的是让人吐血.
我们观察一下forEach 的用法.
而forEach 从形式上看,
要怎么理解呢,
Array.prototype.test = function (fn) {
let a = "a"
fn(a);
}
let arr = [1,2,3];
function show (data) {
console.log(data);
}
arr.test(show);
其实仔细观察你才会发现一些很神奇的结论,
上面的变量a 是test函数的私有变量.
从结果上来看, 外部函数 show 访问了 变量a!
之前我的印象当中, 想要在外部访问内部的变量,
必须要返回一个里层函数(作为接口), 形成闭包,才能访问变量.
而返回这个函数的方式有这么几种
一种是内部return, 外部用变量接收
一种是内部直接把函数定义在外部变量上,
或者直接在内部定义一个外部变量.
比如
第一种
function game () {
let a = "a"
function love () {
console.log(a);
}
return love
}
let show = game();
第二种
let show;
function game () {
let a = "a"
function love () {
console.log(a);
}
show = love;
}
game();
show();
第三种
function game () {
let a = "a"
function love () {
console.log(a);
}
show = love;/ 相当于 window.show = love
}
game();
show();
然后我们惊奇的发现,还有一个口可以形成类似"闭包"?
就是参数入口, 传入函数的方式.也就是回调函数的方式.
之前理解回调函数的时候,
我只觉得就是用来解决一个执行顺序问题,执行时机问题.
或者是用来代码分割.
显然理解有待提高.要怎么理解呢?
假设我们认为, 一个代码的执行 需要功能 + 参数,
我们一般的定义函数的思路是,
先定义功能, 参数待定, 然后执行的时候, 参数确定, 使两者结合.
function show (a) {/ 参数待定
console.log(a);/确定了功能
}
show('a');/ 执行时, 参数确定, 功能 + 参数
看下面这个
function show (a) {/ 参数待定, 执行待定
console.log(a);/功能确定
}
function addA (fn) {
return function () {/ 执行待定 功能待定
fn('a');/ 参数确定(或者限定)
}
}
let test = addA(show);/ 参数 + 功能 都确定 执行待定.
test(); / 执行确定.
待定的状态非常有利,
1. 作为一个变量, 能够穿越时空, 能够复用
2. 适用范围变广.
我觉得整篇乱七八糟的加起来, 我就可能只是为了得出上面这个东西.
也就是说, 把函数当做形参设定的时候,
语义上的意思是, 功能待定, 但参数以及其他可能相对限定?
所以当我们有数据, 但功能不确定的时候, 有两种思路
一种是返回一个api, 调用这个api 就能对数据进行操作,
一种是提供一个回调接口, 通过这个接口访问数据.
第一种返回接口
function data () {
let num = 1;
return {
get () {return num},
add () {return ++num},
minus () {return --num}
}
}
let obj = data();
obj.get();
obj.add();
obj.minus();
第二种 通过 回调函数的方式 提供接口
function data (fn) {
let num = 1;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
fn(get,add,minus);
}
data((get,add,minus) => {
get();
add();
minus();
})
现在我们比较一下这两者,
- 第一种, 接口可以在data 函数之外调用
而第二种, 接口只能在data函数里面调用.
可以认为第一种的复用性更强?
而第二种模块性更好? 因为不会污染全局?
2.按照我之前的思维方式, 第一种似乎更好理解, 也更好用.
因为第二种的调用方式, 必须要记忆提供的参数有哪些,
不过第二种应该是有特别的优点吧?
- 第二种回调函数的方式,虽然能够定义功能, (也能访问特定的接口),
但执行却似乎不受自己控制,
实参无法自己控制,
而且return 也无法控制,
都必须看设计时怎么控制的.
function data (fn) {
let num = 1;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
/ return 是在这里控制的. 如果这里不设置return
/那么回调函数中的return 是无法把值通过return的方式传出去的.
return fn(get,add,minus);/ 实参和 执行是在这里控制的
}
let b = data(function (get,add,minus) {
get();
add();
minus();
return "some"
})
console.log(b);/'some'
函数多层嵌套能够控制 执行时机, 功能 ,参数入口, this入口, return 出口
使得这些能够相互分离.
还能存快照.保存作用域链上的所有变量.
现在我们观察一下,
在这种情况之下, 能不能
把值传进来
把值传出去
控制执行时机
把值传进来, 实参入口,和this入口都被限制, 只能通过访问作用域链上的变量的方式
function data (fn) {
let num = 1;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
return fn(get,add,minus);
}
function data2 (fn) {
let num = 10;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
return fn(get,add,minus);
}
let b = data(function (get,add,minus) {
/ data2通过作用域链传了进来.
return data2(function (get2,add2,minus2) {
return get() + get2();
});
})
console.log(b);/11
/ 写得我自己都感觉很绕, 关键在于, 我虽然写了出来, 但我无法用一种系统来归纳这种用法.
- 把值传出去, 假设return 接口被限制, 则传出去的通道只剩下, 作用域链上的变量
function data (fn) {
let num = 1;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
fn(get,add,minus);
}
let obj = {};
data(function (get,add,minus) {
obj = {
get,
add,
minus
}
})
你会发现这跟上面返回对象接口的结果很相似.
- 能否控制执行时机?
实际上, 所谓的控制执行时机, 就是把功能形态,用定义形态弄成变量,
其实上面就已经做到了这个目标
function data (fn) {
let num = 1;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
fn(get,add,minus);
}
let arr = [];
data(function (get,add,minus) {
arr.push((num) => {
return get() + add() + add () + num;
})
})
arr.forEach((item) => {
item(8);/14
})
就是, 感觉很神奇...到底哪里神奇呢?
通过在 回调中定义函数, 在定义函数时, 能够访问该回调当中的变量,
或者说, 能够把该回调当中的变量用快照存下来?
我们改一下, 写得更赤裸一点
function data (fn) {
let num = 1;
let add = () => {return ++num};
let minus = () => {return --num};
let get = () => {return num};
fn(get,add,minus);
}
let getApi;
data(function (get,add,minus) {
getApi = () => {
return {
get,
add,
minus
}
}
})
let obj = getApi();
定义函数时能够访问变量, 也能间接储存变量.
所以函数定义的位置很重要,
所以通过函数定义的方式, 我们能够进行"快照"?
我觉得我不断逼近一个结论, 但始终无法清晰的表达.
我突然想到一个一点实用性没有,
但可能挺有意思的形态.
function data (fn) {
let num = 1;
let get = () => {return num};
let cb = fn(get);
if(typeof cb !== "function") {return}
cb(function (add) {
let num = add();
console.log(num);/2
})
}
data(function (get) {
let num = get();
let add = () => {return ++num};
return function (fn) {
fn(add);
}
})
这里干的事情是, 互相提供了回调函数接口?
能否接着写?
function data (fn) {
let num = 1;
let get = () => {return num};
let cb = fn(get);
if(typeof cb !== "function") {return}
cb(function (add) {
num = add();
console.log(num);/2
let minus = () => {return num - 10}
return function (fn) {
fn(minus);
}
})
}
data(function (get) {
let num = get();
let add = () => {return ++num};
return function (fn) {
let cb = fn(add);
if(typeof cb !== "function") {return}
cb(function (minus) {
num = minus();
console.log(num);/-8
})
}
})
确实能够接着写.
不过确实很绕.
当然我们也能通过这个例子,再次感受一下,
函数的出口和入口
上面两个函数的"交互"可以一直写下去.
我还不知道,这有什么实际用途,
我们试着分析一下, 上面的代码到底干了什么
一个函数的定义, 入口, 出口 控制, 完成了交互.
function(fn){} 这个fn是个入口
fn(get) 这个 get 是个出口
return function () {} 这个return 是个出口
cb = fn(get) 这个 cb = 是个入口
特别是 cb = fn(get) 既传了参数, 又接收了返回值. 出口入口同时兼备.
我怕是还总结不出这个东西,
function name () {
/ 定义本身 会返回一个 函数变量 name
/ 函数的定义到底代表什么?
/ 在函数内部, 我们可以定义,功能, 和参数,
/ 但函数在定义的时候是不执行的
/ 定义之后, 用返回的变量执行.
/ 所以天然的, 函数的定义与执行的分开, 保证了复用性 和 穿越时空性.
/ 在定义函数的时候, 能够访问外部的所有变量, 也能自定义参数.
/ 也就是说, 我们能够确定一些功能, 确定一些参数
/ 但同时, 我们也能待定一些参数, 待定一些功能.
/ 因为 功能和参数都可以在执行的时候 确定.
/ 也就是说, 天然的, 我们能够把 功能和参数 相分离, 和执行相分离.
/ 也就是说, 定义函数的行为, 本身就能确定一些功能, 确定一些参数,
也能待定一些功能, 待定一些参数,并且 和执行相分离.
/ 也就是说, 如果我们想要让 执行,功能,参数,相分离, 我们就可以用定义函数的方式.
}
刚开始是这样,参数待定, 执行分离
function show (data) {/ 待定数据 待定执行
console.log(data);/ 功能
}
show("go");/ 确定数据/ 执行
然后可以 参数 , 功能都待定
function show (fn,data) {/ 待定功能, 待定参数, 待定执行
console.log(data);/ 确定功能
fn(data);/ 待定功能, 待定参数
}
function addOne (data) {/ 待定参数
console.log(data + "-one");/确定功能
}
show(addOne,"go");/ 确定功能, 确定参数
如果想待定, 就可以定义一个函数
function show (fn,data) {
console.log(data);
if (typeof data !== "undefined") {
fn(data);
} else{
return function (data) {/ 定义一个函数/ 当我们定义一个函数的时候,
/ 就能把执行代码变成 待定执行代码.
fn(data)/ 返回这个函数时, 功能fn 是确定的, 参数data 是不确定的.
}
}
}
function addOne (data) {
console.log(data + "-one");
}
let a = show(addOne);
有时候定义一个函数, 并且想要完成复用,
并不一定需要返回一个变量, 用数组接收也可以
let arr = [];
function go (fn,name,arr1 = arr) {
arr1.push(function () {/ 返回的匿名函数会存在 arr[0] 中
fn(name);
})
}
function show (name) {
console.log(name);
}
go(show,"mike");
arr[0]();/ 也能进行复用