- 简述
- 无副作用(No Side Effects)
- 高阶函数(High-Order Function)
- 柯里化(Currying)
- 闭包(Closure)
-- JavaScript 作用域
-- 面向对象关系
-- this调用规则
-- 配置多样化的构造重载
-- 更多对象关系维护——模块化
-- 流行的模块化方案- 不可变(Immutable)
- 惰性计算(Lazy Evaluation)
- Monad
前言
闭包是一个缓存多个函数内部成员供函数外部引用的设计过程,思考这一设计过程时,方案的衍生、注意事项就变得尤为重要。
5.3 this调用规则
无论是通过函数的方式,还是对象的方式进行数据封装及导出,我们都不可避免的遇到一个问题——在函数/对象内部,调用其他的内部成员。
类方法中的 this
支持面向对象的编程语言通常都会有一个用于描述对象模板的结构,比如 Java
和 C#
中规定,以 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(); // 调用者是:乌索普
甚至于,当我们将 user02
的 print()
方法赋值为 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
也并不是对象 obj
的 seed
属性。如何改造呢?
// 声明一个对象
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
应用的了解,能够帮助我们设计出更好的基于闭包环境的应用模块。