JavaScript | 思维导图 | 这些高阶函数你都会了吗?

3,288 阅读12分钟

0 / 闭包作用域练习题

闭包的作用:① 保护 ② 保存

let x = 5;
function fn(x){
    return function (y) {
        console.log(y + (++x));
    }
}
let f = fn(6);
f(7);
fn(8)(9);
f(10);
console.log(x);

△ 结果是?

(1)++ii++ 的区别
let i = 1;
i++;
console.log(i);

i=1;
i+=1; // i=i+1;
console.log(i);

△ 结果是?

let i = '1';
i++;
console.log(i);

i='1';
i+=1; // i=i+1;
console.log(i);

△ 结果是?

i++/++i 一定是数学运算,+N 也是把N变为数字类型的值

++ii++ 的区别:

[相同点]:都是自身基础上累加1

[不同点]:计算和累加的顺序

++i 自身先累加1,再根据累加后的结果进行运算

i++ 先根据原始值进行运算,运算完之后再自身累加1

let i = 1;
console.log(5 + i++);
//=> 或者 5+(i++)
// ① 先算 5+i
// ② 再 i++: i=2
console.log(i); //=> 2

i = 1;
console.log(5 + (++i));
// ① 先算 ++i : i=2
// ② 5+i = 7

△ 区别

let i = 2;
console.log(2 + (++i) - (i++) + 3 - (i--) + (i--));
console.log(i);

△ 自己算吧

(2)图解

公众号:朝霞的光影笔记 ID:zhaoxiajingjing △ 图1.1_注意:函数创建和函数执行

fn(8) ——>0x000001(8) 执行完后,返回值0x100002紧接着会被执行,那么EC(FN2)这个私有的执行上下文临时不被释放,当它用完以后才会被释放掉

公众号:朝霞的光影笔记 ID:zhaoxiajingjing

△ 图1.2_注意:函数创建和函数执行

GC :浏览器的垃圾回收机制,内存管理

在这里插入图片描述

①【谷歌】:查找引用

浏览器的渲染引擎会在空闲的时候(定期一个时间),依次遍历所有的内存:栈、堆

[堆内存]:当前堆内存如果被占用了(指针关联地址了)则不能释放掉;如果没有任何的事物占用这个堆,则浏览器会自动把这个堆内存释放掉

[栈内存]:当前执行上下文中是否有内容(一般是堆内存)被此上下文以外的事物所占用,如果被占用则无法释放(闭包),如果没有被占用则释放掉。其中,EC(G) 是在加载页面时候创建,只有在关闭页面时候才会释放掉

②【IE】:引用计数

每一个内存中都有一个数字N,记录被占用的次数

如果当前内存被占用一次,则内存中的N会累加一次,反之取消占用,N会累减:直到N变为0,则释放内存

此方案,经常会导致内存泄漏

【思考题】总结内存泄漏的情况

③ 手动优化

null 是不占用空间的,把占用的事物手动赋值为null,可以实现内存的手动优化

fn = null 释放0x000001

f = null 释放0x100001 后,EC(FN1) 也会被释放掉

(3)重构函数
let a=0,
    b=0;
function A(a){
    A=function(b){
        alert(a+b++);
    };
    alert(a++);
}
A(1);
A(2);

△ 函数重构

let a = 1,
    b = 2;
// 等价于
let a = 1;
let b = 2;
//==============
let a = b =1;
// 等价于
b = 1;
let a = 1;

△ 赋值

公众号:朝霞的光影笔记 ID:zhaoxiajingjing

△ 图1.3_函数重构图解

1 / this的5种基础情况

在这里插入图片描述

this是函数的执行主体,不等价于执行上下文/作用域。

this就是谁把函数执行了

分清楚函数执行主体THIS,按照5点规律来说:

1、事件绑定

2、普通函数执行

3、构造函数执行

4、箭头函数执行

5、基于call/apply/bind强制改变this

此处先说前两点,后面三点与面向对象相关的

在浏览器中运行JS代码,非函数中的THIS一般都是window

"use strict;"
console.log(this); //=> window

△ 非函数中的this

我们研究this一般都是函数中的this

还有个特殊情况:ES6+中“块级上下文”中的this,是其所在执行上下文中的this【理解为:块级上下文中没有自己的this

(1)事件绑定
document.body.onclick = function (){
    console.log(this);
};
document.body.addEventListener('click', function (){
    console.log(this);
});

△ 事件绑定

1、不论是DOM0还是DOM2级事件绑定,给元素E的某个事件行为绑定方法,当事件触发方法执行时,方法中的this是当前元素E本身

2、特殊情况:

(1)IE6~8中,基于attachEvent 实现DOM2事件绑定,事件触发执行,方法中的this不是元素本身,大部分情况都是window

(2)基于call/apply/bind强制改变了函数中的this,我们也是以强制改变为主

(2)普通函数执行
① 普通函数

函数执行,看函数前面是否有“点”:

1、有“点”,“点”前面是谁this就是谁

2、没有“点”,this是window。JS严格模式下是:undefined

function fn(){
    console.log(this);
}
var obj = {
    name:'朝霞的光影笔记',
    id:'zhaoxiajingjing',
    fn:fn
};

fn(); //=> 没有点:this=>window
obj.fn(); //=> 有点:this=>obj

△ 普通函数的this

xxx.__proto__.fn():方法执行,前面有“点”,this是:xxx.__proto__

② 自执行函数

自执行函数执行,里面的this一般是window非严格模式/undefined严格模式

(function (){
    console.log(this);
})();

△ 自执行函数

var obj = {
    num:(function (){
        //=> 把自执行函数执行的返回值,赋值给obj.num
        //=> 这里面的this一般是window/undefined
        return 10;
    })()
};

△ 自执行函数

③ 回调函数

回调函数:把一个函数作为值,传递给另一个函数,在另一个函数中把传过来的函数调用

回调函数执行中的this一般也是window/undefined,除非做过特殊处理

setTimeout(function (){
    console.log(this); //=> 一般是window/undefined
}, 1000);

[10].forEach(function (){
    console.log(this); //=> 一般是window/undefined
});

let obj = {id:'zhaoxiajingjing'};
[10].forEach(function (){
    console.log(this); //=> obj
}, obj); //=> 指定回调函数中的this

△ 回调函数

④ 括号表达式中的this很变态
function fn(){
    console.log(this);
}
var obj = {
    name:'朝霞的光影笔记',
    id:'zhaoxiajingjing',
    fn:fn
};

fn(); //=> 没有点:this=>window
obj.fn(); //=> 有点:this=>obj
(obj.fn)(); //=> this:obj 小括号中只有一项,不算是括号表达式
(fn, 10, obj.fn); //=> 括号表达式:有多项只取最后一项
(fn, 10, obj.fn)(); //=>this:window

△ 括号表达式

括号表达式中:小括号中有多项,只取最后一项,如果把其执行,不论之前this是谁,现在基本都会变为window

(3)题目
var x = 3,
    obj = {x: 5};
obj.fn = (function () {
    this.x *= ++x;
    return function (y) {
        this.x *= (++x)+y;
        console.log(x);
    }
})();
var fn = obj.fn;
obj.fn(6);
fn(4);
console.log(obj.x, x);

△ 找THIS

a+=b-c => a = a+(b-c)

a*=b-c => a = a* (b-c)

公众号:朝霞的光影笔记 ID:zhaoxiajingjing

△ 图1.4_图解

公众号:朝霞的光影笔记 ID:zhaoxiajingjing

△ 图1.5_计算过程

2 / JS高阶编程技巧

在这里插入图片描述

JS高阶编程技巧:利用闭包的机制,实现出来的一些高阶编程的方式

1、模块化思想

2、惰性函数

3、柯理化函数

4、compose组合函数

5、高阶组件 React中的

6、……

(1)模块化思想

单例 -> AMD(require.js)->CMD(sea.js)-> CommonJS(Node) ->ES6Module

// 实现天气板块
var time = '2020-11-01';
function queryData(){
    // ...CODE
}
function changeCity(){
    // ...CODE
}

// 实现咨询板块
var time = '2020-11-1';
function changeCity(){
    // ...CODE
}

△ 很久以前的没有模块化思想之前

在没有模块化思想之前,团队协作开发或者代码量较多的情况下,会导致全局变量污染【变量冲突

团队之前开发,合并到一起的代码,变量命名冲突了,让谁改都不合适,那怎么办呢?

① 闭包

闭包:保护

暂时基于闭包的“保护作用”防止全局变量污染

但是,每个版块的代码都是私有的,无法相互调用

// 实现天气板块
(function fn() {
    var time = '2020-11-01';
    function queryData() { }
    function changeCity() {  }
})();

(function fn() {
    // 实现咨询板块
    var time = '2020-11-1';
    function changeCity() { }
})();

△ 基于闭包的保护作用

② 某一种方案

把需要供别人调用的API方法,挂在到全局上

但是也不能写太多,还是会引起全局变量污染

// 实现天气板块
(function fn() {
    var time = '2020-11-01';
    function queryData() { }
    function changeCity() {  }
    window.queryData=queryData;
    window.changeCity=changeCity;
})();

(function fn() {
    // 实现咨询板块
    var time = '2020-11-1';
    function changeCity() { }
    window.changeCity=changeCity;
})();

△ 挂在window上

③ 再一种方案

对象的特点:每一个对象都是一个单独的堆内存空间每一个对象也是单独的一个实例:Object的实例,这样即使多个对象中的成员名字相同,也互不影响

仿照其他后台语言,obj1/obj2不仅仅是对象名,更被称为【命名空间】给堆内存空间起个名字

let obj1 = {
    name:'朝霞的光影笔记',
    id:'zhaoxiajingjing',
    show:function(){}
};
let obj2 = {
    name:'zxjj',
    show:function (){}
};

△ 对象

每个对象都是一个单独的实例,用来管理自己的私有信息,即使名字相同,也互不影响:【JS中的单例设计模式】

④ 进阶一下

实现私有性和相互调用

// 实现天气板块
var weatherModule = (function fn() {
    var time = '2020-11-01';
    function queryData() { }
    function changeCity() { }

    return {
        queryData:queryData,
        changeCity:changeCity
    };
})();

var infoModule = (function fn() {
    // 实现咨询板块
    var time = '2020-11-1';
    function changeCity() { }
    // 可以调用其他模块的方法
    weatherModule.queryData();
    return {
        changeCity:changeCity
    }
})();

△ 单例模式

(2)惰性函数

惰性函数,一定要抓住精髓:惰性 =>

window.getComputedStyle(document.body)获取当前元素经过浏览器计算的样式,返回样式对象

在IE6~8中,不兼容这个写法,需要使用 元素.currentStyle 来获取

① 一开始这样写
function getCss(element, attr){
    if('getComputedStyle' in window){
        return window.getComputedStyle(element)[attr];
    }
    return element.currentStyle[attr];
}
var body = document.body;
console.log(getCss(body, 'height'));
console.log(getCss(body, 'margin'));
console.log(getCss(body, 'backgroundColor'));

△ 获取样式

当浏览器打开后,在第一次调用getCss方法时,就检测了兼容性了,那么,在第二次、第三次调用时是不是就没必要再去检测了

【优化思想】:第一次执行getCss我们就知道是否兼容了,第二次及以后再次调用getCss时,则不想再处理兼容的容错处理了,这就是“惰性思想”【就是“懒”,干一次可以搞定的,绝对不去做第二次了

② 优化一下

也能实现,但不是严谨意义上的惰性思想

var flag = 'getComputedStyle' in window;

function getCss(element, attr){
    if(flag){
        return window.getComputedStyle(element)[attr];
    }
    return element.currentStyle[attr];
}
var body = document.body;
console.log(getCss(body, 'height'));
console.log(getCss(body, 'margin'));
console.log(getCss(body, 'backgroundColor'));

△ 优化一下

③ 惰性思想

惰性是啥?就是

懒是啥?能坐着不站着,能躺着不坐着,能少干活就少干活

function getCss(element, attr){
    //=> 第一次执行,根据是否兼容,实现函数的重构
    if('getComputedStyle' in window){
        getCss = function (element, attr){
             return window.getComputedStyle(element)[attr];
        };
    } else {
        getCss = function (element, attr){
              return element.currentStyle[attr];
        };
    }
    // 为了保证第一次 也可以获取信息,需要把重构后的函数执行一次
    return getCss(element, attr);
}
var body = document.body;
console.log(getCss(body, 'height'));
console.log(getCss(body, 'margin'));
console.log(getCss(body, 'backgroundColor'));

△ 惰性函数+重构函数

(3)柯理化函数

函数柯理化:预先处理思想

形成一个不被释放的闭包,把一些信息存储起来,以后基于作用域链访问到事先存储的信息,然后进行相关处理。所有符合这种模式的(闭包应用)都称为 柯理化函数

//=> x 是预先存储的值
function curring(x){}
var sum = curring(10);
console.log(sum(20)); //=> 10+20
console.log(sum(20,30)); //=> 10+20+30

△ 请实现柯理化函数

① 把类数组转换为数组:
let arg = {0:10, 1:20, length:2};
let arr = [...arg];
arr = Array.from(arg);
arr = [].slice.call(arg);

△ 类数组转为数组

② 数组求和

数组求和

1、for循环/forEach

2、eval([10,20,30].join('+'))

3、[10,20,30].reduce()

命令式编程:[关注怎么做] 自己编写代码,管控运行的步骤和逻辑【自己灵活掌控执行步骤

函数式编程:[关注结果] 具体实现的步骤已经被封装称为方法,我们只需要调用方法即可,无需关注怎么实现的【好处:使用方便,代码量减少。弊端:自己无法灵活掌控执行步骤

③ 数组的reduce方法

API:developer.mozilla.org/zh-CN/docs/…

arr.reduce(callback()[, initialValue])

callback(accumulator, currentValue[, index[, array]])

let arr = [10,20,30,40];
let res = arr.reduce(function (result, item, index){
    console.log(result, item, index);
});

△ reduce

公众号:朝霞的光影笔记 ID:zhaoxiajingjing

△ 图1.6_reduce执行

1、initialValue 初始值不传递,result 默认初始值是数组的第一项,reduce是从数字第二项开始遍历的

2、每遍历数组中的一项,回调函数被触发执行一次

① result 存储的是上一次回调函数返回的结果。除了第一次是初始值或者数字第一项

② item 当前遍历这一项

③ index 当前遍历这一项的索引

let arr = [10,20,30,40];
let res = arr.reduce(function (result, item, index){
    console.log(result, item, index);
    return item + result;
});
console.log(res); //=> 100

△ arr.reduce

数组的reduce方法:在遍历数组过程中,可以累积上一次处理的结果,基于上次处理的结果继续遍历处理

let arr = [10,20,30,40];
let res = arr.reduce(function (result, item, index){
    console.log(result, item, index);
}, 0 );

△ reduce 传递初始值了

arr.reduce 传递了initialValue了,则result 的第一次结果就是初始值,item从数组第一项开始遍历

公众号:朝霞的光影笔记 ID:zhaoxiajingjing

△ 图1.7_reduce执行

自己实现reduce

Array.prototype.reduce = function reduce(callback, initialValue){
    let self = this,
        i = 0;    //=> THIS:arr

    if(typeof callback !== 'function') throw new TypeError('callback must be a function');
    
    if(typeof initialValue === "undefined"){
        initialValue = self[0];
        i = 1;
    }
    // 迭代数组每一项
    for(; i < self.length; i++){
        var item = self[i],
            index = i;
        initialValue = callback(initialValue, item, index, self);
    }
    return initialValue;
};

△ 自己手写reduce

④ 柯理化函数
function curring(x){
    return (...args)=>{
        //=> 把预先存储的x,放到数组的开头
        args.unshift(x);
        return args.reduce((res,item)=>(res+item), 0);
    };
}
var sum = curring(10);
console.log(sum(20)); //=> 10+20
console.log(sum(20,30)); //=> 10+20+30

△ 柯理化函数

(4)compose 组合函数
① 题目描述

在函数式编程当中有一个很重要的概念就是函数组合, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。 例如:

const add1 = x => x + 1;
const mul3 = x => x * 3;
const div2 = x => x / 2;
div2(mul3(add1(add1(0)))); //=>3

△ 函数组合

而这样的写法可读性明显太差了,我们可以构建一个compose函数,它接受任意多个函数作为参数(这些函数都只接受一个参数),然后compose返回的也是一个函数,达到以下的效果:

const operate = compose(div2, mul3, add1, add1)
operate(0) //=>相当于div2(mul3(add1(add1(0)))) 
operate(2) //=>相当于div2(mul3(add1(add1(2))))

△ 可读性较好

​ 简而言之:compose可以把类似于f(g(h(x)))这种写法简化成compose(f, g, h)(x),请你完成 compose函数的编写

② 答题
function compose(...funcs){
    return function(x){
        let result,
            len=funcs.length;
       
        if(len===0){
             //一个函数传递,那就把参数直接返回
            result=x;
        }else if(len===1){
            // 只传递了一个函数
            result=funcs[0](x);
        }else{
            // funcs参数执行顺序从右到左
            result=funcs.reduceRight((result, item) => {
                if(typeof item !== 'function') return result;
                return  item(result);
            },x);
        }
        return result;
    }
}

△ 实现compose组合函数

3 / 思考题

1、内存泄漏

2、严格模式和非严格模式区别API:developer.mozilla.org/zh-CN/docs/…

3、分析redux 中的compose 方法的实现逻辑

function compose(...funcs) {
    if (funcs.length === 0) {
        return arg => arg
    }

    if (funcs.length === 1) {
        return funcs[0]
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

△ redux中的compose实现

- end -