前端基础进阶(七):函数与函数式编程

函数:菜鸟收割者

纵观JavaScript中全部必须须要掌握的重点知识中,函数是咱们在初学的时候最容易忽视的一个知识点。在学习的过程当中,可能会有不少人、不少文章告诉你面向对象很重要,原型很重要,但是却不多有人告诉你,面向对象中全部的重点难点,几乎都与函数息息相关。javascript

包括我以前几篇文章介绍的执行上下文,变量对象,闭包,this等,都是围绕函数的细节来展开。css

我知道不少人在学习中,很急切的但愿本身快一点开始学习面向对象,学习模块,学习流行框架,而后迅速成为高手。可是我能够很负责的告诉你,关于函数的这些基础东西没理解到必定程度,那么你的学习进展必定是举步维艰的。前端

因此,你们必定要重视函数!java

固然,关于函数的重点,难点在前面几篇文章都已经说得差很少了,这篇文章主要总结一下函数的基础知识,并初步学习函数式编程的思惟。react

1、函数声明、函数表达式、匿名函数与自执行函数

关于函数在实际开发中的应用,大致能够总结为函数声明、函数表达式、匿名函数、自执行函数。express

函数声明编程

咱们知道,JavaScript中,有两种声明方式,一个是使用var的变量声明,另外一个是使用function的函数声明。redux

前端基础进阶(三):变量对象详解中我有提到过,变量对象的建立过程当中,函数声明比变量声明具备更为优先的执行顺序,即咱们经常提到的函数声明提早。所以咱们在执行上下文中,不管在什么位置声明了函数,咱们均可以在同一个执行上下文中直接使用该函数。小程序

fn();  // function

function fn() {
    console.log('function');
}

函数表达式 segmentfault

与函数声明不一样,函数表达式使用了var进行声明,那么咱们在确认他是否能够正确使用的时候就必须依照var的规则进行判断,即变量声明。咱们知道使用var进行变量声明,实际上是进行了两步操做。

// 变量声明
var a = 20;

// 实际执行顺序
var a = undefined;  // 变量声明,初始值undefined,变量提高,提高顺序次于function声明
a = 20;  // 变量赋值,该操做不会提高

一样的道理,当咱们使用变量声明的方式来声明函数时,就是咱们经常说的函数表达式。函数表达的提高方式与变量声明一致。

fn(); // 报错
var fn = function() {
    console.log('function');
}

上例子的执行顺序为:

var fn = undefined;   // 变量声明提高
fn();    // 执行报错
fn = function() {   // 赋值操做,此时将后边函数的引用赋值给fn
    console.log('function');
}
所以,因为声明方式的不一样,致使了函数声明与函数表达式在使用上的一些差别须要咱们注意,除此以外,这两种形式的函数在使用上并没有不一样。

关于上面例子中,函数表达式中的赋值操做,在其余一些地方也会被常用,咱们清楚其中的关系便可。

在构造函数中添加方法
function Person(name) {
    this.name = name;
    this.age = age;
    // 在构造函数内部中添加方法
    this.getAge = function() {
        return this.age;
    }
    this.
}
// 给原型添加方法
Person.prototype.getName = function() {
    return this.name;
}

// 在对象中添加方法
var a = {
    m: 20,
    getM: function() {
        return this.m;
    }
}

匿名函数

在上面咱们大概讲述了函数表达式中的赋值操做。而匿名函数,顾名思义,就是指的没有被显示进行赋值操做的函数。它的使用场景,多做为一个参数传入另外一个函数中。

var a = 10;
var fn = function(bar, num) {
    return bar() + num;
}

fn(function() {
    return a;
}, 20)

在上面的例子中,fn的第一个参数传入了一个匿名函数。虽然该匿名函数没有显示的进行赋值操做,咱们没有办法在外部执行上下文中引用到它,可是在fn函数内部,咱们将该匿名函数赋值给了变量bar,保存在了fn变量对象的arguments对象中。

// 变量对象在fn上下文执行过程当中的建立阶段
VO(fn) = {
    arguments: {
        bar: undefined,
        num: undefined,
        length: 2
    }
}

// 变量对象在fn上下文执行过程当中的执行阶段
// 变量对象变为活动对象,并完成赋值操做与执行可执行代码
VO -> AO

AO(fn) = {
    arguments: {
        bar: function() { return a },
        num: 20,
        length: 2
    }
}

因为匿名函数传入另外一个函数以后,最终会在另外一个函数中执行,所以咱们也经常称这个匿名函数为回调函数。关于匿名函数更多的内容,我会在下一篇深刻探讨柯里化的文章中进行更加详细讲解。

匿名函数的这个应用场景几乎承担了函数的全部难以理解的知识点,所以咱们必定要对它的这些细节了解的足够清楚,若是对于变量对象的演变过程你还看不太明白,必定要回过头去看这篇文章:前端基础进阶(三):变量对象详解

函数自执行与块级做用域

在ES5中,没有块级做用域,所以咱们经常使用函数自执行的方式来模仿块级做用域,这样就提供了一个独立的执行上下文,结合闭包,就为模块化提供了基础。而函数自执行,实际上是匿名函数的一种应用。

(function() {
   // ...
})();

一个模块每每能够包括:私有变量、私有方法、公有变量、公有方法。

根据做用域链的单向访问,外面可能很容易知道在这个独立的模块中,外部执行环境是没法访问内部的任何变量与方法的,所以咱们能够很容易的建立属于这个模块的私有变量与私有方法。

(function() {
    // 私有变量
    var age = 20;
    var name = 'Tom';

    // 私有方法
    function getName() {
        return `your name is ` + name;
    }
})();

可是共有方法和变量应该怎么办?你们还记得咱们前面讲到过的闭包的特性吗?没错,利用闭包,咱们能够访问到执行上下文内部的变量和方法,所以,咱们只须要根据闭包的定义,建立一个闭包,将你认为须要公开的变量和方法开放出来便可。

若是你对闭包了解不够, 前端基础进阶(四):详细图解做用域链与闭包应该能够帮到你。
(function() {
    // 私有变量
    var age = 20;
    var name = 'Tom';

    // 私有方法
    function getName() {
        return `your name is ` + name;
    }

    // 共有方法
    function getAge() {
        return age;
    }

    // 将引用保存在外部执行环境的变量中,造成闭包,防止该执行环境被垃圾回收
    window.getAge = getAge;
})();

固然,闭包在模块中的重要做用,咱们也在讲解闭包的时候已经强调过,可是这个知识点真的过重要,须要咱们反复理解而且完全掌握,所以为了帮助你们进一步理解闭包,咱们来看看jQuery中,是如何利用咱们模块与闭包的。

// 使用函数自执行的方式建立模块
(function(window, undefined) {

    // 声明jQuery构造函数
     var jQuery = function(name) {

        // 主动在构造函数中,返回一个jQuery实例
         return new jQuery.fn.init(name);
     }

    // 添加原型方法
     jQuery.prototype = jQuery.fn = {
         constructor: jQuery,
         init:function() { ... },
         css: function() { ... }
     }
     jQuery.fn.init.prototype = jQuery.fn;

    // 将jQuery更名为$,并将引用保存在window上,造成闭包,对外开发jQuery构造函数,这样咱们就能够访问全部挂载在jQuery原型上的方法了
     window.jQuery = window.$ = jQuery;
 })(window);

// 在使用时,咱们直接执行了构造函数,由于在jQuery的构造函数中经过一些手段,返回的是jQuery的实例,因此咱们就不用再每次用的时候在本身new了
$('#div1');

在这里,咱们只须要看懂闭包与模块的部分就好了,至于内部的原型链是如何绕的,为何会这样写,我在讲面向对象的时候会为你们慢慢分析。举这个例子的目的所在,就是但愿你们可以重视函数,由于在实际开发中,它无处不在。

接下来我要分享一个高级的,很是有用的模块的应用。当咱们的项目愈来愈大,那么须要保存的数据与状态就愈来愈多,所以,咱们须要一个专门的模块来维护这些数据,这个时候,有一个叫作状态管理器的东西就应运而生。对于状态管理器,最出名的,我想非redux莫属了。虽然对于还在学习中的你们来讲,redux是一个有点高深莫测的东西,可是在咱们学习以前,能够先经过简单的方式,让你们大体了解状态管理器的实现原理,为咱们将来的学习奠基坚实的基础。

先来直接看代码。

// 自执行建立模块
(function() {
    // states 结构预览
    // states = {
    //     a: 1,
    //     b: 2,
    //     m: 30,  
    //     o: {}
    // }
    var states = {};  // 私有变量,用来存储状态与数据

    // 判断数据类型
    function type(elem) {
        if(elem == null) {
            return elem + '';
        }
        return toString.call(elem).replace(/[\[\]]/g, '').split(' ')[1].toLowerCase();
    }


    /**
     * @Param name 属性名
     * @Description 经过属性名获取保存在states中的值
    */
    function get(name) {
        return states[name] ? states[name] : '';
    }

    function getStates() {
        return states;
    }

    /*
    * @param options {object} 键值对
    * @param target {object} 属性值为对象的属性,只在函数实现时递归中传入
    * @desc 经过传入键值对的方式修改state树,使用方式与小程序的data或者react中的setStates相似
    */
    function set(options, target) {
        var keys = Object.keys(options);
        var o = target ? target : states;

        keys.map(function(item) {
            if(typeof o[item] == 'undefined') {
                o[item] = options[item];
            }
            else {
                type(o[item]) == 'object' ? set(options[item], o[item]) : o[item] = options[item];
            }
            return item;
        })
    }

    // 对外提供接口
    window.get = get;
    window.set = set;
    window.getStates = getStates;
})()

// 具体使用以下

set({ a: 20 });     // 保存 属性a
set({ b: 100 });    // 保存属性b
set({ c: 10 });     // 保存属性c

// 保存属性o, 它的值为一个对象
set({
    o: {
        m: 10,
        n: 20
    }
})

// 修改对象o 的m值
set({
    o: {
        m: 1000
    }
})

// 给对象o中增长一个c属性
set({
    o: {
        c: 100
    }
})
console.log(getStates())

demo实例在线地址

我之因此说这是一个高级应用,是由于在单页应用中,咱们极可能会用到这样的思路。根据咱们提到过的知识,理解这个例子其实很简单,其中的难点估计就在于set方法的处理上,由于为了具备更多的适用性,所以作了不少适配,用到了递归等知识。若是你暂时看不懂,没有关系,知道如何使用就好了,上面的代码能够直接运用于实际开发。记住,当你须要保存的状态太多的时候,你就想到这一段代码就好了。

函数自执行的方式另外还有其余几种写法,诸如 !function(){}()+function(){}()
2、函数参数传递方式:按值传递

还记得基本数据类型与引用数据类型在复制上的差别吗?基本数据类型复制,是直接值发生了复制,所以改变后,各自相互不影响。可是引用数据类型的复制,是保存在变量对象中的引用发生了复制,所以复制以后的这两个引用实际访问的实际是同一个堆内存中的值。当改变其中一个时,另一个天然也被改变。以下例。

var a = 20;
var b = a;
b = 10;
console.log(a);  // 20

var m = { a: 1, b: 2 }
var n = m;
n.a = 5;
console.log(m.a) // 5

当值做为函数的参数传递进入函数内部时,也有一样的差别。咱们知道,函数的参数在进入函数后,实际是被保存在了函数的变量对象中,所以,这个时候至关于发生了一次复制。以下例。

var a = 20;

function fn(a) {
    a = a + 10;
    return a;
}
fn(a);
console.log(a); // 20
var a = { m: 10, n: 20 }
function fn(a) {
    a.m = 20;
    return a;
}

fn(a);
console.log(a);   // { m: 20, n: 20 }

正是因为这样的不一样,致使了许多人在理解函数参数的传递方式时,就有许多困惑。究竟是按值传递仍是按引用传递?实际上结论仍然是按值传递,只不过当咱们指望传递一个引用类型时,真正传递的,只是这个引用类型保存在变量对象中的引用而已。为了说明这个问题,咱们看看下面这个例子。

var person = {
    name: 'Nicholas',
    age: 20
}

function setName(obj) {  // 传入一个引用
    obj = {};   // 将传入的引用指向另外的值
    obj.name = 'Greg';  // 修改引用的name属性
}

setName(person);
console.log(person.name);  // Nicholas 未被改变

在上面的例子中,若是person是按引用传递,那么person就会自动被修改成指向其name属性值为Gerg的新对象。可是咱们从结果中看到,person对象并未发生任何改变,所以只是在函数内部引用被修改而已。

4、函数式编程

虽然JavaScript并非一门纯函数式编程的语言,可是它使用了许多函数式编程的特性。所以了解这些特性可让咱们更加了解本身写的代码。

当咱们想要使用一个函数时,一般状况下其实就是想要将一些功能,逻辑等封装起来。相信你们对于封装这个概念并不陌生。

咱们一般经过函数封装来完成一件事情。例如,我想要计算任意三个数的和,咱们就能够将这三个数做为参数,封装一个简单的函数。

function add(a, b, c) {
  return a + b + c;
}

当咱们想要计算三个数的和时,直接调用该方法便可。

add(1, 2, 3); // 6

固然,当咱们想要作的事情比较简单的时候,可能还看不出来封装成为函数以后带来的便利。若是咱们想要作的事情稍微复杂一点呢。例如我想要计算一个数组中的全部子项目的和。

function mergeArr(arr) {
    var result = 0;
    for(var i = 0; i < arr.length; i++) { result  += arr[i] }
    return result;
}

若是咱们不经过函数封装的方式,那么再每次想要实现这个功能时,就不得不从新使用一次for循环,这样的后果就是咱们的代码中充斥着愈来愈多的重复代码。而封装以后,当咱们想要再次作这件事情的时候,只须要一句话就能够了。

mergeArr([1, 2, 3, 4, 5]);

固然,我相信你们对于函数封装的意义都应该有很是明确的认知,可是咱们要面临的问题是,当咱们想要去封装一个函数时,如何作才是最佳实践呢?

函数式编程能给咱们答案。

咱们在初学时,每每会情不自禁的使用命令式编程的风格来完成咱们想要干的事情。由于命令式编程更加的简单,直白。例如咱们如今有一个数组,array = [1, 3, 'h', 5, 'm', '4'],如今想要找出这个数组中的全部类型为number的子项。当咱们使用命令式编程思惟时,可能就会直接这样作。

var array = [1, 3, 'h', 5, 'm', '4'];
var res = [];
for(var i = 0; i < array.length; i ++) {
    if (typeof array[i] === 'number') {
        res.push(array[i]);
    }
}

在这种实现方式中,咱们平铺直叙的实现了咱们的目的。这样作的问题在于,当咱们在另外的时刻,想要找出另一个数组中全部的子项时,咱们不得不把一样的逻辑再写一次。当出现次数变多时,咱们的代码也变得更加糟糕且难以维护。

而函数式编程的思惟则建议咱们将这种会屡次出现的功能封装起来以备调用。

function getNumbers(array) {
    var res = [];
    array.forEach(function(item) {
        if (typeof item === 'number') {
            res.push(item);
        }
    })
    return res;
}



// 以上是咱们的封装,如下是功能实现
var array = [1, 3, 'h', 5, 'm', '4'];
var res = getNumbers(array);

所以当咱们将功能封装以后,咱们实现一样的功能时,只须要写一行代码。而若是将来需求变更,或者稍做修改,咱们只须要对getNumbers方法进行调整就能够了。并且咱们在使用时,只须要关心这个方法能作什么,而不用关心他具体是怎么实现的。这也是函数式编程思惟与命令式不一样的地方之一。

函数式编程思惟还具备如下几个特征。

函数是第一等公民

所谓"第一等公民"(first class),指的是函数与其余数据类型同样,处于平等地位,能够赋值给其余变量,也能够做为参数,传入另外一个函数,或者做为别的函数的返回值。这些场景,咱们应该见过不少。

var a = function foo() {}  // 赋值
function fn(function() {}, num) {}   // 函数做为参数

// 函数做为返回值
function var() {
    return function() {
        ... ...
    }
}

固然,这都是JavaScript的基本概念。可是我想不少人,甚至包括正在阅读的你本身均可能会无视这些概念。能够用一个简单的例子来验证一下。

咱们先自定义这样一个函数。

function delay() {
    console.log('5000ms以后执行该方法.');
}

如今要作的是,若是要求你结合setTimeout方法,让delay方法延迟5000ms执行,应该怎么作?

其实很简单,对不对,直接这样就能够了。

var timer = setTimeout(function() {
    delay();
}, 5000);

那么如今问题来了,若是你对函数是一等公民有一个深入的认知,我想你会发现上面这种写法实际上是有一些问题的。因此思考一下,问题出在哪里?

函数既然可以做为一个参数传入另一个函数,那么咱们是否是能够直接将delay做为setTimeout的第一个参数,而不用额外的多加一层匿名函数呢?

所以,其实最正确的解法应该这样写。

var timer = setTimeout(delay, 5000);

固然,若是你已经提早想到这样作了,那么恭喜你,说明你在JavaScript上比普通人更有天赋。其实第一种糟糕的方式不少人都在用,包括有多年工做经验的人也没有彻底避免。而他们甚至还不知道本身问题出在什么地方。

在将来的实践中,你还会遇到更多相似的场景。为了验证读者朋友们的理解,咱们不妨来思考一下如何优化下面的代码。

function getUser(path, callback) {
    return $.get(path, function(info) {
        return callback(info);
    })
}

getUser('/api/user', function(resp) {
    // resp为成功请求以后返回的数据
    console.log(resp);
})

优化的原理和setTimeout的例子如出一辙,我这里卖个关子,不打算告诉你们结论,仅提示一句,getUser优化以后,仅有一句代码。考验你们学习成果的时候到了 ^ ^。

只用"表达式",不用"语句"

"表达式"(expression)是一个单纯的运算过程,老是有返回值;"语句"(statement)是执行某种操做,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,并且都有返回值。

假如咱们的项目中,多处须要改变某个元素的背景色。所以咱们能够这样封装一下。

var ele = document.querySelector('.test');
function setBackgroundColor(color) {
    ele.style.backgroundColor = color;
}

// 多处使用
setBackgroundColor('red');
setBackgroundColor('#ccc');

咱们能够很明显的感觉到,setBackgroundColor封装的仅仅只是一条语句。这并非理想的效果。函数式编程指望一个函数有输入,也有输出。所以良好的习惯应该以下作。

function setBackgroundColor(ele, color) {
    ele.style.backgroundColor = color;
    return color;
}

// 多处使用
var ele = document.querySelector('.test');
setBackgroundColor(ele, 'red');
setBackgroundColor(ele, '#ccc');

了解这一点,可让咱们本身在封装函数的时候养成良好的习惯。

纯函数

相同的输入总会获得相同的输出,而且不会产生反作用的函数,就是纯函数。

所谓"反作用"(side effect),指的是函数内部与外部互动(最典型的状况,就是修改全局变量的值),产生运算之外的其余结果。

函数式编程强调没有"反作用",意味着函数要保持独立,全部功能就是返回一个新的值,没有其余行为,尤为是不得修改外部变量的值。

即所谓的只要是一样的参数传入,返回的结果必定是相等的。

例如咱们指望封装一个函数,可以获得传入数组的最后一项。那么能够经过下面两种方式来实现。

function getLast(arr) {
    return arr[arr.length];
}

function getLast_(arr) {
    return arr.pop();
}

var source = [1, 2, 3, 4];

var last = getLast(source); // 返回结果4 原数组不变
var last_ = getLast_(source); // 返回结果4 原数据最后一项被删除

getLast与getLast_虽然一样可以得到数组的最后一项值,可是getLast_改变了原数组。而当原始数组被改变,那么当咱们再次调用该方法时,获得的结果就会变得不同。这样不可预测的封装方式,在咱们看来是很是糟糕的。它会把咱们的数据搞得很是混乱。在JavaScript原生支持的数据方法中,也有许多不纯的方法,咱们在使用时须要很是警戒,咱们要清晰的知道原始数据的改变是否会留下隐患。

var source = [1, 2, 3, 4, 5];

source.slice(1, 3); // 纯函数 返回[2, 3] source不变
source.splice(1, 3); // 不纯的 返回[2, 3, 4] source被改变

source.pop(); // 不纯的
source.push(6); // 不纯的
source.shift(); // 不纯的
source.unshift(1); // 不纯的
source.reverse(); // 不纯的

// 我也不能短期知道如今source被改变成了什么样子,干脆从新约定一下
source = [1, 2, 3, 4, 5];

source.concat([6, 7]); // 纯函数 返回[1, 2, 3, 4, 5, 6, 7] source不变
source.join('-'); // 纯函数 返回1-2-3-4-5 source不变

闭包

闭包是函数式编程语言的重要特性,我也在前面几篇文章中说了不少关于闭包的内容。这里再也不赘述。

柯里化

下一章。

前端基础进阶系列目录

clipboard.png

相关文章
相关标签/搜索