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

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~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值