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

  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() 来进行描述:

  • Function.prototype.bind: 函数用于创建一个新绑定函数(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();
  • Function.prototype.call: 函数用于调用另一个函数,并且在调用时指定 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('龟派气功'); // 的绝招是:龟派气功
  • Function.prototype.apply: 函数用于调用另一个函数,并且在调用时指定 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)