美文网首页JS相关
JS 函数式编程思维简述(五):闭包 02

JS 函数式编程思维简述(五):闭包 02

作者: 阿拉拉布 | 来源:发表于2018-12-21 18:59 被阅读45次
    1. 简述
    2. 无副作用(No Side Effects)
    3. 高阶函数(High-Order Function)
    4. 柯里化(Currying)
    5. 闭包(Closure)
      -- JavaScript 作用域
      -- 面向对象关系
      -- this调用规则
      -- 配置多样化的构造重载
      -- 更多对象关系维护——模块化
      -- 流行的模块化方案
    6. 不可变(Immutable)
    7. 惰性计算(Lazy Evaluation)
    8. Monad

    前言

           闭包是一个缓存多个函数内部成员供函数外部引用的设计过程,思考这一设计过程时,方案的衍生、注意事项就变得尤为重要。

    5.3 this调用规则

           无论是通过函数的方式,还是对象的方式进行数据封装及导出,我们都不可避免的遇到一个问题——在函数/对象内部,调用其他的内部成员。

    类方法中的 this

           支持面向对象的编程语言通常都会有一个用于描述对象模板的结构,比如 JavaC# 中规定,以 class 作为关键字,声明一个 作为对象模板。在 中可以声明一些方法 (我们将隶属于某对象的函数称之为方法),而在 方法 的设计过程中,有的时候我们需要调用除 本方法 外的其他 类成员,需要使用关键字 this

    // 以 Java 例举
    class User{
        
        private String username; // 登录账号
        private String password; // 登录密码
        
        /**
         * 模拟登录方法
         */
        public void login(String u, String p){
            // 通过 this 关键字,可以调用成员变量
            this.username = u;
            this.password = p;
            
            // 通过 this 关键字,也可以调用其他成员方法
            this.print();
        }
        
        /**
         * 用于展示登录信息
         */
        private void print(){
            // 将登录信息输出至控制台
            System.out.println("欢迎您["+ this.username +"],您的密码是:" + this.password);
        }
    }
    
    

    而在被外部调用时,可能是这样的方式:

    // 构建用户1:路飞
    User user1 = new User();
    user1.login("路飞", "lufei"); // 结果: 欢迎您[路飞],您的密码是:lufei
    
    // 构建用户2:特拉法尔加·罗
    User user2 = new User();
    user2.login("特拉法尔加·罗", "law"); // 结果: 欢迎您[特拉法尔加·罗],您的密码是:law
    

    在这个示例中,类模板 声明了方法 login() ,这个方法在类中也只算是一个 模板方法,用于描述 对象 在真正调用时的行为。方法中,通过关键字 this 可以引用类中定义的其他成员。这个 this 关键字所表示的意义就是:代指当前真正调用者(对象)。比如:

    • 对象 user1 调用方法时,方法内部的 this 就相当于是 user1
    • 对象 user2 调用方法时,方法内部的 this 就相当于是 user2

    在不同的语言环境中,对于描述 当前对象 的方式也有着不同的变体,比如:

    • PHP 中使用关键字 $this
    • Swift 的类中使用 self 关键字描述类的实例本身;
    • Python 的类方法的第一个形参即代表类实例,通常命名都是 self
    • Ruby 使用操作符 @ 来在类中引用成员;
    • 一般情况下,JavaScript 中使用 this 来表示调用当前方法的对象。
    • ...

    JS 函数中的 this

           在 JavaScript 中,函数也可以独立存在(不定义在类中)。同时,每一个函数中也可以使用 this 关键字,但单独的函数中使用的 this 究竟代表了什么,却是一件应用规则比较复杂的事情。JS 函数中的 this 会在不同场景下遵循如下规则:

    • 默认规则
    • 严格模式
    • 隐式绑定
    • 显示绑定
    • new 绑定
    • 箭头函数绑定

    让我们逐一领略一番...

    默认规则

           默认规则是指,在默认情况下函数中使用 this 关键字时, this 所绑定的对象规则。在默认情况下, this 关键字指向的是全局对象

    // 浏览器环境下
    let foo = function() { 
        console.log(this);
    }
    
    foo(); // Window
    
    // node.js 环境下
    let foo = function() { 
        console.log(this);
    }
    
    foo(); // global
    

    严格模式

           如果在严格模式(strict mode)下,这样的调用就不会绑定全局环境的对象,this 所指向的将是 undefined 值:

    // 严格模式下
    'use strict';
    let foo = function() { 
        console.log(this);
    }
    
    foo(); // undefined
    

    隐式绑定

           隐式绑定是我们在 JavaScript 中最常见的 this 绑定机制。他是指,当前函数的调用者。 实际上默认绑定规则也是隐式绑定的一种表现:因为在 JavaScript 环境中,如果未指定当前函数的调用者,其调用者就默认被当做是 全局对象

    // 声明一个函数
    function printExample() {
        console.log('调用者是:', this.name);
    }
    
    // 声明调用者 01
    const user01 = {
        name: '娜美',
        print: printExample
    };
    
    // 声明调用者 02
    const user02 = {
        name: '乌索普',
        print: printExample
    };
    
    // 调用对象方法
    user01.print(); // 调用者是:娜美
    user02.print(); // 调用者是:乌索普
    

    甚至于,当我们将 user02print() 方法赋值为 user01.print 时,只要最终调用的对象依然是 user02 那么结果也不会变化:

    // 声明调用者 02
    const user02 = {
        name: '乌索普',
        print: user01.print
    };
    
    // 调用对象方法
    user01.print(); // 调用者是:娜美
    user02.print(); // 调用者是:乌索普
    

    隐式绑定规则非常简单,只要注意观测函数的直接调用者是谁即可。让我们来对上述代码做一个变体:

    // 声明一个函数
    function printExample() {
        console.log('调用者是:', this.name);
    }
    
    // 声明调用者 01
    const user01 = {
        name: '娜美',
        print: printExample
    };
    
    // 声明调用者 02
    const user02 = {
        name: '乌索普',
        print: printExample,
        user01: user01 // 此处,为对象 user02 添加 user01 作为属性
    };
    
    // 调用对象方法
    user02.user01.print(); // 调用者是:娜美
    

    该例中,虽然 user01 作为 user02 的属性,但最终调用时,依然是 user01 在调用 print() 方法,因此 this.name 获取到的属性值依然是 user01 对象中定义的值 娜美

    显式绑定

           隐式绑定规则描述的情况是——虽然我们没刻意指定,但运行过程中隐式的帮我们做了 this 指定。那么相反的,显式绑定规则则是指:明确的指定了函数的调用者是谁
           在显式绑定规则中,我们通常使用函数对象的方法 bind()call()apply() 来进行描述:

    • <font color=red>Function.prototype.bind:</font> 函数用于创建一个新绑定函数(bound function,BF),在新函数中, this 关键字将始终以 bind(thisArg) 中的参数 thisArg 作为绑定对象:
    // 创建一个公共函数作为示例
    function print() {
        console.log(this.name + '正在调用...');
    }
    
    // 定义对象
    const user01 = {name : '索隆'};
    const user02 = {name : '山治'};
    
    // 使用 bind() 绑定调用对象,并将 print() 函数覆盖
    print = print.bind(user01);
    // 为 user02 对象创建 print() 方法
    user02.print = print;
    
    // 虽然调用者看起来是 user02
    // 但 print() 方法中已将 this 绑定为 user01
    // 因此 调用的结果是: 索隆正在调用...
    user02.print();
    
    • <font color=red>Function.prototype.call:</font> 函数用于调用另一个函数,并且在调用时指定 this 值,以及传递参数:
    // 创建一个公共函数作为示例
    function print(tricks) {
        console.log(this.name + '的绝招是:' + tricks);
    }
    
    // 定义对象
    const user01 = {name : '索隆'};
    const user02 = {
        name : '山治',
        print: print
    };
    
    // 调用 user02 对象的 print() 函数,但 实际调用对象已经指定了 user01
    user02.print.call(user01, '三十六烦恼凤'); // 索隆的绝招是:三十六烦恼凤
    
    // 将 print 函数的调用者绑定为 user02,并且调用
    print.call(user02, '恶魔风脚'); // 山治的绝招是:恶魔风脚
    
    // 普通调用时 this 指向了全局对象
    print('龟派气功'); // 的绝招是:龟派气功
    
    • <font color=red>Function.prototype.apply:</font> 函数用于调用另一个函数,并且在调用时指定 this 值,以及传递参数。与 call 方法不同的是,call() 方法的参数部分是逐一传递的,而apply()的参数是作为数组形式传递给方法的第二个参数
    // call 方法的参数传递方式
    // fun.call(thisArg, arg1, arg2, ...)
    
    // apply 方法的参数传递方式
    // fun.apply(thisArg, [argsArray])
    
    

    new 绑定

           new 关键字用于调用一个构造函数,并缔造一个实体对象。而当 JavaScript 中的类成员方法中使用 this 关键字时,使用 new 构造的对象会绑定到方法中 主作用域this

    // javascript 定义类的语法糖
    class User{
    
        constructor(name){
            // 构造方法
            this.name = name;
        }
    
        print(){
            // 定义的普通方法
            console.log('我的名字是: ' + this.name);
        }
    }
    
    const user01 = new User('伊丽莎白');
    user01.print(); // 我的名字是伊丽莎白
    

    常见绑定问题——函数嵌套

           在 JavaScript 中,我们经常会遇到函数的嵌套语法。而且函数嵌套时,多个函数中的 this 关键字所指向的对象往往显得复杂、混乱,this 究竟指向外层函数作用域,还是内层的函数作用域呢?我们往往使用 代词 来描述外层作用域,解决这个问题:

    // 声明一个对象
    const obj = {
        arr: [ 1, 3, 5, 7, 9],
        seed: 3,
        calc: function(){
            // 通过当前对象的 arr 属性作为数组模板
            return this.arr.map(function(e, ind){
                // 对偶数位(2,4,6,...)数据进行运算,生成新的集合
                return ind % 2 !== 0 ? e + this.seed : e ;
            });
        }
    };
    
    obj.calc(); // 结果: [1, NaN, 5, NaN, 9]
    

    为什么会出现这样的结果,偶数位数据运算后竟然都是 NaN ?原因很简单,在 calc() 函数的主作用域中的 this ,因为调用时调用者就是 obj ,所以 this.arr 引用到了属性 obj.arr 。而** this.arr.map 函数中,作为参数的函数也希望引用到 calc() 隐式绑定的 this 对象,但 map() 中的函数参数却由于 每个函数内部都拥有一个独立的 this,因为调用时的就近原则导致 this.seed 无法指向外层函数所绑定的 this 对象。**因此,内部的 this 指向了一个诡异的位置,而这个 this.seed 也并不是对象 objseed 属性。如何改造呢?

    // 声明一个对象
    const obj = {
        arr: [ 1, 3, 5, 7, 9],
        seed: 3,
        calc: function(){
    
            // 通过新的代词描述外部作用域的 this 对象
            const self = this;
    
            // 通过当前对象的 arr 属性作为数组模板
            return this.arr.map(function(e, ind){
                // 对偶数位(2,4,6,...)数据进行运算,生成新的集合
                // 注意:此处为了避免回调函数内部 this 冲突
                // 显式的调用了函数外层作用域中的 self 所代表的外层 this
                return ind % 2 !== 0 ? e + self.seed : e ;
            });
        }
    };
    
    obj.calc(); // 结果: [1, 6, 5, 10, 9]
    

    箭头函数绑定

           通过 Lambda表达式 定义的箭头函数,其应用自身独有的一套规则,即:**捕获函数定义位置作用域的 this,作为自己函数内部的 this **。与此同时,其他 this 绑定规则 将无法影响箭头函数中已捕获的 this

    // 全局作用域
    window.bar = 'window 对象';
    
    // 函数 foo 通过箭头函数定义
    const foo = () => console.log(this.bar);
    
    // 定义一个 baz 对象,添加函数 foo 作为方法
    const baz = {
        bar: 'baz 对象',
        foo: foo
    }
    
    foo();              // 结果:window 对象
    baz.foo();          // 结果:window 对象
    foo.call(baz);      // 结果:window 对象
    foo.bind(baz)();    // 结果:window 对象
    

    通过如上特性,如果我们遇到了嵌套函数,也可以使用箭头函数来描述子函数作用域引用外层的父作用域的 this

    // 声明一个对象
    const obj = {
        arr: [ 1, 3, 5, 7, 9],
        seed: 3,
        calc: function(){
    
            // 通过当前对象的 arr 属性作为数组模板
            // 子函数作用域定义箭头函数的位置处于父函数的位置
            // 因此直接绑定了外层父函数作用域中的 this 引用
            return this.arr.map((e, ind) => {
                // 对偶数位(2,4,6,...)数据进行运算,生成新的集合
                return ind % 2 !== 0 ? e + this.seed : e ;
            });
        }
    };
    
    obj.calc(); // 结果: [1, 6, 5, 10, 9]
    

    小结:闭包是一种依赖于函数的设计过程,而在 JavaScript 函数中,对于 this 的引用经常会让不清楚规则的同学觉得很怪异。因此,对于 this 应用的了解,能够帮助我们设计出更好的基于闭包环境的应用模块。

    相关文章

      网友评论

        本文标题:JS 函数式编程思维简述(五):闭包 02

        本文链接:https://www.haomeiwen.com/subject/zepvkqtx.html