• 函数与函数式编程


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

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

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

    函数声明

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

    变量对象的创建过程中,函数声明比变量声明具有更为优先的执行顺序,即我们常常提到的函数声明提前。因此我们在执行上下文中,无论在什么位置声明了函数,我们都可以在同一个执行上下文中直接使用该函数

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

    函数表达式 

    与函数声明不同,函数表达式使用了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;
        }
    }
    // 给原型添加方法
    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)        //30

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

    函数自执行与块级作用域

    在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');
    二、函数参数传递方式:按值传递

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

    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() {
            ... ...
        }
    }
  • 相关阅读:
    C# FTP上传文件时出现"应 PASV 命令的请求,服务器返回了一个与 FTP 连接地址不同的地址。"的错误
    ESP32 学习笔记
    ESP32 学习笔记
    C# 实现窗口无边框,可拖动效果
    C# 获取IP地址
    C# 实现程序开机自启动
    C# 设置程序最小化到任务栏右下角,鼠标左键单击还原,右键提示关闭程序
    C# 生成机器码
    C# 隐藏窗口标题栏、隐藏任务栏图标
    C# 测量程序运行时间
  • 原文地址:https://www.cnblogs.com/QianBoy/p/8562719.html
Copyright © 2020-2023  润新知