>웹 프론트엔드 >JS 튜토리얼 >고급 프런트 엔드 기본 사항(7): 함수 및 함수형 프로그래밍

고급 프런트 엔드 기본 사항(7): 함수 및 함수형 프로그래밍

阿神
阿神원래의
2017-02-23 14:24:271200검색

자바스크립트에서 꼭 익혀야 할 핵심 지식을 모두 살펴보면, 함수는 우리가 처음 배울 때 가장 쉽게 간과하는 지식 포인트입니다. 학습하는 과정에서 객체지향이 중요하고 프로토타입이 중요하다고 말하는 사람과 기사가 많을 수 있지만, 객체지향의 거의 모든 핵심과 어려운 점이 기능과 밀접하게 연관되어 있다고 말하는 사람은 거의 없습니다.

이전 글에서 소개한 실행 컨텍스트, 변수 객체, 클로저, this 등을 포함하면 모두 함수 중심으로 돌아갑니다.

저는 많은 사람들이 객체지향 학습을 시작하고, 모듈을 배우고, 인기 있는 프레임워크를 배우고, 빠르게 마스터가 되기를 열망하고 학습하고 있다는 것을 알고 있습니다. 하지만 이러한 기능에 대한 기본 사항을 어느 정도 이해하지 못하면 학습 진행이 확실히 어려울 것이라는 점을 책임감 있게 말씀드릴 수 있습니다.

그래서 다들 기능에 주목하셔야 해요!


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

입니다. 따라서 선언 방법이 다르기 때문에 함수 선언과 함수 표현식의 사용에 약간의 차이가 있습니다. , 그 외에는 이 두 가지 형태의 함수 간에 사용법에 차이가 없습니다.

위의 예와 관련하여 함수 표현식의 할당 연산은 다른 곳에서도 자주 사용됩니다.

在构造函数中添加方法
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에는 블록 수준 범위가 아니기 때문에 우리는 종종 독립적인 실행 컨텍스트를 제공하는 블록 수준 범위를 모방하기 위해 함수 자체 실행을 사용하여 모듈화의 기초를 제공합니다.

(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())

我之所以说这是一个高级应用,是因为在单页应用中,我们很可能会用到这样的思路。根据我们提到过的知识,理解这个例子其实很简单,其中的难点估计就在于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。

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.