说说 JavaScript 中那些有趣而且强大的高级函数

时间:2021-11-23 13:54:31

1 安全的类型检测

JavaScript 内置的类型检测机制并非完全可靠。比如 typeof 操作符,它会导致检测数据类型时得到不靠谱的结果(Safari 4 以及之前的版本,正则表达式会返回 function!)。

instanceof 操作符在存在多个全局作用域(比如一个页面包含多个框架)的情况下,很难使用。比如下面这行代码:

var isArray = value instanceof Array;

这里只有 value 与 Array 构造函数在同一个全局作用域中,才会返回 true。

解决办法是基于这样的一个事实:在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串,而每个类在内部都有一个[[Class]] 顺序,而这个属性就指定了上述字符串的构造函数名称。

<script type="text/javascript">
/**
* 是否是数组
* @param value
* @returns {boolean}
*/

function isArray(value) {
return Object.prototype.toString.call(value) == "[object Array]";
}

/**
* 是否是函数
* @param value
* @returns {boolean}
*/

function isFunction(value){
return Object.prototype.toString.call(value)=="[object Function]";
}

/**
* 是否是正则表达式
* @param value
* @returns {boolean}
*/

function isRegExp(value) {
return Object.prototype.toString.call(value) == "[object RegExp]";
}

//是否是浏览器原生的 JSON 对象
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";
</script>

注意: 旧版 IE 中的某些函数是以 COM 对象的形式实现的,所以上面代码中的 isFunction() 函数都是返回 false!还有 Object.prototype.toString 也有可能被修改,所以这里是假设 Object.prototype.toString 是未被修改过的原始版本!


2 作用域安全的构造函数

之前说过,构造函数是使用 new 操作符调用的函数,它内部的 this 会指向新创建的对象实例:

function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
}

var person = new Person("deniro", "15", "Software Engineer");

上面的代码会创建一个新的 Person 对象,同时会分配这些属性。但是如果没有使用 new 操作符来调用构造函数,就会导致 this 被映射到全局对象 window 上(因为 this 对象是运行时绑定):

var person2 = Person("deniro", "15", "Software Engineer");
console.log(window.name);//deniro
console.log(window.age);//15
console.log(window.job);//Software Engineer

window.name 用于识别链接目标和框架的,所以如果被意外覆盖,就会导致页面上出现其他的错误。所以我们要构造一个作用域安全的构造函数。

我们要确定构造函数内部的 this 对象是正确类型的实例:

<script type="text/javascript">
function Person(name, age, job) {
if (this instanceof Person) {
this.name = name;
this.age = age;
this.job = job;
} else {
return new Person(name, age, job);
}

}

var person = Person("deniro", "15", "Software Engineer");
console.log(window.name);//""
console.log(person.name);//deniro

var person2 = new Person("lily", "25", "artist");
console.log(person2.name);//lily

</script>

实现这个模式后,就可以锁定调用构造函数的环境!但如果使用构造函数借用模式来实现继承,那么这个继承会被破坏:

function Polygon(sides) {
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function () {
return 0;
};
} else {
return new Polygon(sides);
}
}

function Rectangle(width, height) {
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function () {
return this.width * this.height;
};
}

var rect = new Rectangle(5, 10);
console.log(rect.sides);//undefined

Polygon 的构造函数是作用域安全的,但 Rectangle 不是!Rectangle 里面的 this 对象不是 Polygon 的实例,所以它会创建并返回一个新的 Polygon 对象,这会导致 Rectangle 实例没有继承 Polygon 中的 sides!

构造函数继承模式结合原型链或者寄生组合,可以解决这个问题:

function Polygon(sides) {
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function () {
return 0;
};
} else {
return new Polygon(sides);
}
}

function Rectangle(width, height) {
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function () {
return this.width * this.height;
};
}

Rectangle.prototype = new Polygon();//一个 Rectangle 实例也是一个 Polygon 的实例

var rect = new Rectangle(5, 10);
console.log(rect.sides);//2

在多个程序员在同一个页面上写代码时,推荐使用作用域安全的构造函数O(∩_∩)O~

3 惰性载入函数

function createXHR() {
if (typeof XMLHttpRequest != "undefined") {
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined") {//IE7 之前的版本
if (typeof arguments.callee.activeXString != "string") {
var
versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len;

for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex) {
//跳过
}
}
} else {
throw new Error("No XHR object available.");
}
}
}

上面的这个函数是检测 XHR 的,一般情况下,如果浏览器支持某种内置的 XHR,那么它就会一直是支持的,所以不必每次都执行 if 语句。

惰性载入指的是函数执行的分支仅会发生一次,它有两种执行方式。第一种是在函数被调用时再处理的函数,即第一次调用时,这个函数会被覆盖为另外一个按照合适的方式执行的函数,我们使用这种方式改写了 createXHR():

<script type="text/javascript">
function createXHR() {
if (typeof XMLHttpRequest != "undefined") {
createXHR = function () {
return new XMLHttpRequest();
};
} else if (typeof ActiveXObject != "undefined") {//IE7 之前的版本
createXHR = function () {
if (typeof arguments.callee.activeXString != "string") {
var
versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len;

for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex) {
//跳过
}
}
}
};
}
else {
createXHR = function () {
throw new Error("No XHR object available.");
};
}
return createXHR();
}
</script>

第一次调用时,会为 createXHR 变量赋值为一个新函数,那么下一次调用就会直接指向这个新函数咯O(∩_∩)O~

第二种实现方式是,在声明函数时就指定适当的函数。这样只会在代码首次加载时损失一些性能,但在第一次调用时不会有性能损失:

<script type="text/javascript">
var createXHR = (function () {
if (typeof XMLHttpRequest != "undefined") {
return function () {
return new XMLHttpRequest();
};
} else if (typeof ActiveXObject != "undefined") {//IE7 之前的版本
return function () {
if (typeof arguments.callee.activeXString != "string") {
var
versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"], i, len;

for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex) {
//跳过
}
}
}
};
}
else {
return function () {
throw new Error("No XHR object available.");
};
}
return createXHR();
})();
</script>

我们创建了一个匿名、自执行的函数,用来确定最终的函数实现。

哪一种实现更合适,要依具体的需求而定。这两种实现都能避免执行不必要的代码。

4 函数绑定

函数绑定指的是创建了一个函数,它可以在特定的 this 环境中,以指定的参数来调用另一个函数。函数绑定经常用于回调函数与事件处理程序的场景中,它可以把函数作为变量传递的同时,保留代码的执行环境。

<script type="text/javascript">
var handler = {
message: "Event handled",
handleClick: function (event) {
console.log(this.message);//undefined
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick);
</script>

因为没有保存 handler.handleClick() 的环境,所以这里的 this 对象指向的是 DOM 按钮,我们可以使用闭包来修正这个问题。很多的 JavaScript 都会有一个可以把函数绑定到指定环境的函数,这个函数一般叫 bind()。

/**
* 绑定函数
* @param fn 函数
* @param context 环境
* @returns {Function}
*/

function bind(fn, context) {
return function () {
return fn.apply(context, arguments);
};
}

bind() 中创建了一个闭包,它使用 apply() 调用传入的函数。注意这里的 arguments 是内部函数的所有参数,使用方法如下:

var handler = {
message: "Event handled",
handleClick: function (event) {
console.log(this.message + ":" + event.type);//Event handled:click
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));

ECMAScript 5 为所有函数定义了一个原生的 bind() ,现在绑定函数更简单啦:

<script type="text/javascript">

var handler = {
message: "Event handled",
handleClick: function (event) {
console.log(this.message + ":" + event.type);
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler));
</script>

支持原生的 bind() 的浏览器有 IE9+、Firefox 4+、Chrome。

注意:绑定函数与普通函数相比,有更多的开销(比如内存啦),所以只在必要时使用它 O(∩_∩)O

5 函数柯里化

函数柯里化指的是创建已经设置好了的一个或多个参数的函数。它也是用闭包实现的,当函数被调用时,返回的函数还需要设置一些参数。

函数柯里化是动态创建的:当调用另一个函数的同时,为它传入要柯里化的函数和必要参数:

<script type="text/javascript">
/**
* 柯里化函数
* @param fn 要柯里化的函数
* @returns {Function}
*/

function curry(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return function () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(null, finalArgs);
}
}

</script>

curry 函数会对被返回函数的参数进行排序。它的第一个参数是要进行柯里化的函数,其他参数是要传入的值;args 数组包含了来自外部函数的参数,而 innerArgs 数组包含了来自内部函数的参数;然后把内、外部函数的参数进行合并,存入 finalArgs;最后使用 apply。注意这里没有考虑执行环境,所以 apply 的第一个参数是 null。使用方法如下:

function add(num1, num2) {
return num1 + num2;
}


var curriedAdd = curry(add, 5);

console.log(curriedAdd(3));//8

//一次性提供了两个参数
var curriedAdd2 = curry(add, 5, 12);
console.log(curriedAdd2());//17

函数柯里化经常是作为函数绑定的一部分:

<script type="text/javascript">

/*带函数柯里化的绑定函数*/
function bind(fn, context) {
var args = Array.prototype.slice.call(arguments, 2);//从第三个开始
return function () {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.concat(innerArgs);
return fn.apply(context, finalArgs);
};
}

</script>

上面的这个 bind() 函数接收一个被绑定的函数和一个 object 对象,所以被绑定的函数的参数是从第 3 个开始算起的哦O(∩_∩)O~

在给事件处理程序中传递额外的参数的场景中,非常有用:

var handler = {
message: "Event handled",
handleClick: function (name, event) {
console.log(this.message + ":" + name + ":" + event.type);
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));

ECMAScript 5 中的 bind() 方法也实现了函数柯里化:

<script type="text/javascript">


var handler = {
message: "Event handled",
handleClick: function (name, event) {
console.log(this.message + ":" + name + ":" + event.type);
}
};

var btn = document.getElementById("my-btn");
EventUtil.addHandler(btn, "click", handler.handleClick.bind(handler, "my-btn"));
</script>

柯里化函数和绑定函数可以创建出复杂的算和功能,但不能滥用,因为它们都会带来额外的开销哦O(∩_∩)O~