文章目录
写在前面
不知道看到这里的朋友有没有真正消化 上篇 中介绍的知识,这些内容我在全网介绍 JS 函数式编程的资料里还没见过类似的版本。希望大家继续保持空杯心态,持续深耕,学通悟透 JS 函数式编程就是水到渠成的事了。一起来看 FP 筑基部分的下篇——
3.2.4. 填充脚本 Polyfills
就像为普通变量赋值那样,能够对函数动态赋值的做法,还能在定义 polyfill
时更高效。
1. 检测 Ajax(Detecting Ajax)
回到 Ajax
刚开始出现的年代,鉴于不同的浏览器以不同的方式实现了 Ajax
调用,编写代码时必须考虑这些实现差异。 以下代码演示了 Ajax
调用的特性检测逻辑:
function getAjax() {
let ajax = null;
if (window.XMLHttpRequest) {
// modern browser? use XMLHttpRequest
ajax = new XMLHttpRequest();
} else if (window.ActiveXObject) {
// otherwise, use ActiveX for IE5 and IE6
ajax = new ActiveXObject("Microsoft.XMLHTTP");
} else {
throw new Error("No Ajax support!");
}
return ajax;
}
这段代码是行得通的,但也意味着每次调用都要进行 Ajax
检测,即便测试结果永远不变。其实利用函数作一等对象,可以得到一种更有效的实现方式:定义两个不同的函数,其中一个完成特性检测且只执行一次,然后将正确的实现逻辑赋给另一个函数,以供后续调用。代码如下:
(function initializeGetAjax() {
let myAjax = null;
if (window.XMLHttpRequest) {
// modern browsers? use XMLHttpRequest
myAjax = function() {
return new XMLHttpRequest();
};
} else if (window.ActiveXObject) {
// it's ActiveX for IE5 and IE6
myAjax = function() {
new ActiveXObject("Microsoft.XMLHTTP");
};
} else {
myAjax = function() {
throw new Error("No Ajax support!");
};
}
window.getAjax = myAjax;
})();
这段代码演示了两个重要概念:
- 可以对函数动态赋值:运行代码,
window.getAjax
(即全局getAjax
变量)会根据当前浏览器获取到三个候选项之一。稍后调用getAjax()
时,将执行正确的函数,无需再进行任何浏览器特性检测; - 定义了
initializeGetAjax
函数并立即执行——该模式称为 立即调用函数表达式(IIFE)。函数按预期运行并在完成逻辑后自行清理内存,因为它的所有变量都是局部变量,甚至在函数运行结束后就不存在了。
2. 替代函数 Adding missing functions
这种在运行时定义函数的想法也可用于编写替代脚本 polyfills
来提供其他缺失的函数。例如,所需代码逻辑如下:
if (currentName.indexOf("Mr.") !== -1) {
// it's a man
...
}
可能您更喜欢下面这样的新版实现:
if (currentName.includes("Mr.")) {
// it's a man
...
}
假设浏览器不支持 .includes()
怎么办呢?同样,我们可以按需定义想要的函数:若 .includes()
可用,则什么都不动;否则定义一个 polyfill
来实现同样的逻辑。代码如下:
小贴士
Mozilla
开发者网站为新版JavaScript
特性提供了大量填充脚本。例如,可以直接从 官方文档 获取到includes
的实现。
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
"use strict";
if (typeof start !== "number") {
start = 0;
}
if (start + search.length > this.length) {
return false;
} else {
return this.indexOf(search, start) !== -1;
}
};
}
这段代码在运行时它会检查 String
原型是否已实现 include
方法。若没有,则会为其定义一个执行相同逻辑的函数;此后便都能使用 .includes()
了。顺便提一下,还有其他定义 polyfill
的方法,详见 思考题 3.5。
提示
直接修改标准类型的原型对象通常是不被接受的,因为本质上相当于使用了全局变量,容易出错;然而,为一个成熟且已知的函数编写一个
polyfill
这种情况是不大可能引发什么代码冲突的。
最后,如果您碰巧认为前面演示的 Ajax
示例已经过时,那么请考虑一下这个场景:若要使用更现代的 fetch()
调用服务,您还会发现并非所有现代浏览器都支持该 API(查看 http://caniuse.com/#search=fetch 验证),因此您必须使用 polyfill
,例如 https://github.com/github/fetch 上的那版。研究上面的源码,会发现它基本上也用到了与前面相同的方法:检测是否需要 polyfill
,需要则创建一个。
3.2.5. 插入处理 Stubbing
本节来看一个与 polyfill
类似的应用场景:让函数根据环境完成不同的逻辑。这里的要领在于执行“打桩”——这个源于测试的概念要求将一个函数替换为另一个更简单的函数逻辑,而不走原逻辑。
“打桩”通常与日志一起使用。例如,需要应用程序在开发阶段执行详细的日志记录,而非在生产环境执行。一个常见的解决方案如下:
let myLog = someText => {
if (DEVELOPMENT) {
console.log(someText); // or some other way of logging
} else {
// do nothing
}
}
这段代码没问题,只是与前面的 Ajax
调用示例一样,代码完成的逻辑比预想的多,因为每次都会检查程序是否处于开发状态。 如果将日志函数存根,使其实际上不记入任何内容,则可简化代码并获得少量的性能提升:
let myLog;
if (DEVELOPMENT) {
myLog = someText => console.log(someText);
} else {
myLog = someText => {};
}
进一步使用三目运算符优化:
const myLog = DEVELOPMENT
? someText => console.log(someText)
: someText => {};
尽管代码有点不好理解,但我更倾向于这种写法,因为用到了常量,它的值不可变更。
提示
鉴于
JavaScript
允许使用比参数声明更多的参数来调用函数,考虑到未处于开发状态时,myLog
函数不执行任何操作,因此也可以写为() => {}
,这样也行得通;然而笔者更倾向于函数签名的统一,所以保留了someText
参数,即便不参与运行。具体视个人喜好而定。
后续您会反复看到函数作一等对象使用的身影。查看所有的示例代码就知道了。
3.2.6. 即时调用 Immediate invocation
还有一种常见的函数用法,常常出现在流行的 JavaScript
库和框架中,可将其他语言的一些模块化特性引入到甚至是旧版 JavaScript
的项目内,代码如下:
(function() {
// do something...
})();
专家提示
另一种等效写法为
(function(){ ... }())
,注意两者的细微差别。两种风格都有各自的拥护者。选择自己喜欢的风格,并注意保持写法上的一致即可。
也可以将一些参数传入函数作为其初始值:
(function(a, b) {
// do something, using the
// received arguments for a and b...
})(some, values);
最后,也可以令函数返回某些值:
let x = (function(a, b) {
// ...return an object or function
})(some, values);
前面提到,该模式称为 IIFE
(读作 /iffy/
),名字也不难理解:定义一个函数并立即调用该函数。至于为什么要这么写,而不是简单地编写内联代码,就要考虑作用域的问题了。
拓展
留意函数周围的括号。这能帮助解析器理解此时正在编写一个表达式,如若省略,
JavaScript
就会认定这是一个函数声明而非函数调用。小括号也能用作视觉注释(visual note
),以便读到这段代码的人立即认出IIFE
写法。
由于 JavaScript
定义的作用域是函数级作用域,在 IIFE
中定义的变量或函数都将是内部变量,外面任何位置的代码都无法访问。假如要实现一些复杂的初始化逻辑,例如:
function ready() { ... }
function set() { ... }
function go() { ... }
// initialize things calling ready(),
// set() and go() appropriately
可能出现的问题,在于如果出现同名的函数,则变量提升效应作用的结果,将导致实际执行的是最后声明的那个函数:
function ready() {
console.log("ready");
}
function set() {
console.log("set");
}
function go() {
console.log("go");
}
ready();
set();
go();
function set() {
console.log("UNEXPECTED...");
}
// "ready"
// "UNEXPECTED"
// "go"
出错了吧!如果改用 IIFE
,问题就可以避免。这么一来,其余代码甚至都看不到那三个内部函数,从而有效避免了全局命名空间的污染,典型代码实现如下:
(function() {
function ready() {
console.log("ready");
}
function set() {
console.log("set");
}
function go() {
console.log("go");
}
ready();
set();
go();
})();
function set() {
console.log("UNEXPECTED...");
}
// "ready"
// "set"
// "go"
如果 IIFE
中包含 return
语句,则可以回顾第一章中的相关示例,然后用如下代码实现一个计数器:
const myCounter = (function() {
let count = 0;
return function() {
count++;
return count;
};
})();
这之后,每调用一次 myCounter()
,都会返回一个递增的计数值,但是代码的任何其他部分都不会覆盖到 IIFE
内部计数变量 count
,因为它只能在返回的函数中被访问到。
3.3. 本章小结 Summary
本章讨论了 JavaScript
定义函数的几种方法,重点考察了箭头函数。它较标准函数具有更简洁的优点。同时还学习了 科里化(currying) 的相关概念,并将函数视为一等对象来使用。最后考察了一些在概念上满足纯粹的函数式编程的编码技术。这些知识将作为后续章节更高级技术的基石。让我们拭目以待!