ホームページ  >  記事  >  ウェブフロントエンド  >  フロントエンド応用編(7): 関数と関数プログラミング

フロントエンド応用編(7): 関数と関数プログラミング

PHPz
PHPzオリジナル
2017-04-04 17:52:251051ブラウズ

フロントエンド応用編(7): 関数と関数プログラミング

関数: 新人リーパー

JavaScript で習得する必要がある主要な知識をすべて見ると、関数は最初に学習するときに最も見落とされやすい知識ポイントです。学習の過程で、オブジェクト指向 が非常に重要であり、プロトタイプが非常に重要であることを伝える人や記事はたくさんあるかもしれませんが、オブジェクト指向のほとんどすべての重要な点や難しい点が密接に関連していることを伝える人はほとんどいません。機能に。以前の記事で紹介した

実行コンテキスト、

変数オブジェクト、クロージャ、thisなどを含め、それらはすべて関数を中心に展開します。

多くの人が学習中で、オブジェクト指向の学習を開始し、モジュールを学習し、人気の

フレームワークを学習して、すぐにマスターになりたいと非常に熱心に考えていることを私は知っています。しかし、責任を持って言えますが、こうした関数の基本的なことをある程度理解していないと、学習の進捗は確実に困難になります。

だから、皆さんも機能に注目してください!

1. 関数宣言、
関数式匿名関数、自己実行関数 実際の開発における関数の適用については、大きく分けると関数宣言、関数式、匿名関数、自己実行関数となります。関数を実行中。

関数宣言

JavaScript では 2 つの宣言方法があり、1 つは

関数宣言を使用する方法です。

var的变量声明,另一个是使用functionフロントエンドの基礎 応用編(3): 変数オブジェクトの詳細説明で、変数オブジェクトの作成処理において、実行順序では関数宣言の方が変数宣言よりも優先されると述べました。私たちがよく言及するのは事前です。したがって、実行コンテキストのどこで関数を宣言しても、同じ実行コンテキストで関数を直接使用できます。

fn();  // function

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

関数式

関数式は関数宣言と違いvarを使って宣言するため、正しく使えるかどうかを確認する際はvar、つまり変数宣言の規則に従って判断する必要があります。 var を使用して変数を宣言するのは、実際には 2 段階の操作であることがわかっています。

// 变量声明
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');
}

したがって、宣言方法の違いにより、関数宣言と関数式の使用には注意が必要ないくつかの違いがあります。関数の 2 つの形式は、使用方法に違いはありません。

上記の例に関して、関数式内の代入演算は他の場所でも頻繁に使用されますが、その関係を明確にする必要があります。
在构造函数中添加方法
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 変数の引数オブ​​ジェクトに保存します。物体。 。

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

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

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

匿名関数は別の関数に渡された後、最終的には別の関数で実行されるため、この匿名関数を

コールバック関数と呼ぶことがよくあります。匿名関数については、カリー化について詳しく説明する次の記事で詳しく説明します。

この無名関数の応用シナリオは、関数

の理解しにくい知識点をほぼすべて取り上げているため、変数オブジェクトの進化をまだ理解していない場合は、これらの詳細を十分に理解する必要があります。戻ってこの記事を読んでください: 高度なフロントエンドの基礎 (3): 変数オブジェクトの詳細な説明

関数の自己実行とブロックレベルのスコープ

ES5 にはブロックレベルのスコープがないため、

use 関数

はブロックレベルのスコープを模倣するために自己実行され、クロージャと組み合わせることで、 モジュール化 の基礎を提供します。

(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了
$('#p1');

在这里,我们只需要看懂闭包与模块的部分就行了,至于内部的原型链是如何绕的,为什么会这样写,我在讲面向对象的时候会为大家慢慢分析。举这个例子的目的所在,就是希望大家能够重视函数,因为在实际开发中,它无处不在。

接下来我要分享一个高级的,非常有用的模块的应用。当我们的项目越来越大,那么需要保存的数据与状态就越来越多,因此,我们需要一个专门的模块来维护这些数据,这个时候,有一个叫做状态管理器的东西就应运而生。对于状态管理器,最出名的,我想非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(){}()

二、函数参数传递方式:按值传递

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

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;
}

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对象并未发生任何改变,因此只是在函数内部引用被修改而已。

四、函数式编程

虽然JavaScript并不是一门纯函数式编程的语言,但是它使用了许多函数式编程的特性。因此了解这些特性可以让我们更加了解自己写的代码。

函数是第一等公民

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。这些场景,我们应该见过很多。

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

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

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

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

了解这一点,可以让我们自己在封装函数的时候养成良好的习惯。借助这个特性,我们在学习其他API的时候,了解函数的返回值也是一个十分重要的习惯。

没有"副作用"

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

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

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

闭包

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

柯里化

理解柯里化稍微有点难,我在下一篇文章里专门单独来深入分析。

五、函数封装

在我们自己封装函数时,最好尽量根据函数式编程的特点来编写。当然在许多情况下并不能完全做到,比如函数中我们常常会利用模块中的私有变量等。

普通封装

function add(num1, num2) {
    return num1 + num2;
}

add(20, 10); // 30

挂载在对象上

if(typeof Array.prototype.add !== 'function') {
  Array.prototype.add = function() {
    var i = 0,
        len = this.length,
        result = 0;

    for( ; i < len; i++) {
        result += this[i]
    }
    return result;
  }
}

[1, 2, 3, 4].add() // 10

修改数组对象的例子,常在面试中被问到类似的,但是并不建议在实际开发中扩展原生对象。与普通封装不一样的是,因为挂载在对象的原型上我们可以通过this来访问对象的属性和方法,所以这种封装在实际使用时会有许多的难点,因此我们一定要掌握好this。


以上がフロントエンド応用編(7): 関数と関数プログラミングの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。