JS高级函数

高级函数

在 JavaScript 中使用函数的高级方法。

  • 数据类型的安全检测
  • 构造函数的安全作用域
  • 惰性载入函数
  • 函数绑定
  • 函数柯里化

数据类型的安全检测

检测数据类型,最简单的检测方法是typeof , 但是typeof在检测对象数据类型的时候,太笼统了,不精确;

instanceof 操作符在存在多个全局作用域(像一个页面包含多个 frame)的情况下,也是问题多多。

console.log(value instanceof Array);

以上代码要返回 true , value 必须是一个数组,而且还必须与 Array 构造函数在同个全局作用域中。(别忘了, Array 是 window 的属性。)如果 value 是在另个 frame 中定义的数组,那么以上代码就会返回 false 。

解决办法

在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串。每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名。

var testStr = 'cccccccc';
var testAry = [2,3,4,5];
var testObj = {
    name:"zhu",
    age:26,
    gender:"man"
};

console.log(Object.prototype.toString.call(testStr));//[object String]
console.log({}.toString.call(testAry));//[object Array]
console.log({}.toString.call(testObj));//[object Object]

其中 Object.prototype.toString,可以简写为{}.toString.;

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

这样就可以安全检测是不是数组了;

"[object Array]"的

  • 第一个object是因为用的是对象的原型上的;所以是object(小写的o);
  • 第二个Array是因为属于数组类,所以Array第一个字母是大写的A;

这一技巧也广泛应用于检测原生 JSON 对象。 Object 的 toString() 方法不能检测非原生构造函数的构造函数名。因此,开发人员定义的任何构造函数都将返回[object Object]。有些 JavaScript 库会包含与下面类似的代码。

var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) ==
"[object JSON]";

在 Web 开发中能够区分原生与非原生 JavaScript 对象非常重要。只有这样才能确切知道某个对象到底有哪些功能。这个技巧可以对任何对象给出正确的结论。

请注意, Object.prototpye.toString() 本身也可能会被修改。上面的技巧假设 Object.prototpye.toString() 是未被修改过的原生版本。

构造函数的安全作用域

当构造函数没有使用new生成实例,直接使用的时候;由于构造函数中 this 对象是在运行时绑定的 ; this关键字会映射到全局对象window上;导致错误对象属性的意外增加。

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

var person=Person("zhu",26,"WEB");
console.log(person);//undefined;
console.log(window.name);//"zhu;
console.log(window.age);//26;
console.log(window.job);//WEB";

这里,原本针对 Person 实例的三个属性被加到 window 对象上,因为构造函数是作为普通函数调用的,忽略了 new 操作符。这个问题是由 this 对象的晚绑定造成的,在这里 this 被解析成了 window对象。这个问题的解决方法就是创建一个作用域安全的构造函数

作用域安全的构造函数在进行任何更改前,首先确认 this 对象是正确类型的实例。如果不是,那么会创建新的实例并返回。

如下:

function Person(name,age,job){
    if(this instanceof Person){
        console.log("Person用法 - 正确");
        this.name=name;
        this.age=age;
        this.job=job;
    }else{
        console.log("Person用法 - 不正确");
        return new Person(name,age,job);
    }
}
var person=Person("hahahah",26,"WEB");
console.log(person);
console.log(person.name);//"hahahah";
console.log(window.name);//""
console.log(typeof window.name);//string
console.log(window.age);//26;
console.log(window.job);//WEB";

这段代码中的 Person 构造函数添加了一个检查并确保 this 对象是 Person 实例的 if 语句,它表示要么使用 new 操作符,要么在现有的 Person 实例环境中调用构造函数。任何一种情况下,对象初始化都能正常进行。如果 this 并非 Person 的实例,那么会再次使用 new 操作符调用构造函数并返回结果。最后的结果是,调用 Person 构造函数时无论是否使用 new 操作符,都会返回一个 Person 的新实例,这就避免了在全局对象上意外设置属性。

** 实现这个模式后,你就锁定了可以调用构造函数的环境。如果你使用构造函数窃取模式的继承且不使用原型链,那么这个继承很可能被破坏。**

function Parent(name,age,job){
    if(this instanceof Parent){
        console.log("Chilren用法 - 正确");
        this.name=name;
        this.age=age;
        this.job=job;
    }else{
        console.log("Chilren用法 - 不正确");
        return new Parent(name,age,job);
    }
}
function Chilren(parentName){
    Parent.call(this,"child","1","null");
    this.name=parentName;
}

var target=new Chilren("ooooo");
console.log(target);//{name: "ooooo"}
console.log(target.age);//undefined

在这段代码中, Parent 构造函数是作用域安全的,然而 Chilren 构造函数则不是。新创建一个 Chilren 实例之后,这个实例应该通过 Parent.call() 来继承 Parent 的 age 属性。但是,由于 Parent 构造函数是作用域安全的, this 对象并非 Parent 的实例,所以会创建并返回一个新的 Parent 对象。 Chilren 构造函数中的 this 对象并没有得到增长,同时 Parent.call() 返回的值也没有用到,所以 Chilren 实例中就不会有 age 属性。

function Parent(name,age,job){
    if(this instanceof Parent){
        console.log("Chilren用法 - 正确");
        this.name=name;
        this.age=age;
        this.job=job;
    }else{
        console.log("Chilren用法 - 不正确");
        return new Parent(name,age,job);
    }
}
function Chilren(parentName){
    Parent.call(this,"child","1","null");
    this.name=parentName;
}
Chilren.prototype = new Parent();//【加上这一行代码,让Chilren的实例可以指到Chilren即可】
var target=new Chilren("ooooo");
console.log(target);//Chilren {name: "ooooo", age: "1", job: "null"}
console.log(target.age);//1

上面这段重写的代码中,一个 Chilren 实例也同时是一个 Parent 实例,所以 Parent.call()会照原意执行,最终为 Chilren 实例添加了 age 属性。

多个程序员在同一个页面上写 JavaScript 代码的环境中,作用域安全构造函数就很有用了。届时,对全局对象意外的更改可能会导致一些常常难以追踪的错误。除非你单纯基于构造函数窃取来实现继承,推荐作用域安全的构造函数作为最佳实践。

惰性载入函数

惰性载入表示函数执行的分支仅会发生一次

function createXHR(){
    if(typeof XMLHttpRequest != 'undefined'){
        console.log("支持 XMLHttpRequest");
        return new XMLHttpRequest();
    }else if(typeof ActiveXObject != "undefined"){
        console.log("支持 ActiveXObject");
        if (typeof arguments.callee.activeXString != "string"){
            var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"];
            for (var i=0,len=versions.length; i < len; i++){
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch (ex){
                    //跳过
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
    }else{
        throw new Error("浏览器不支持XHR")
    }
}

每次调用 createXHR() 的时候,它都要对浏览器所支持的能力仔细检查。首先检查内置的 XHR,然后测试有没有基于 ActiveX 的 XHR,最后如果都没有发现的话就抛出一个错误。每次调用该函数都是这样,即使每次调用时分支的结果都不变:如果浏览器支持内置 XHR,那么它就一直支持了,那么这种测试就变得没必要了。即使只有一个 if 语句的代码,也肯定要比没有 if 语句的慢,所以如果 if 语句不必每次执行,那么代码可以运行地更快一些。解决方案就是称之为惰性载入的技巧

有两种实现惰性载入的方式
  • ** 第一种方法是函数的重写;**执行时对函数进行改写;

在函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用再经过执行的分支了。

function createXHR(){
    if(typeof XMLHttpRequest != 'undefined'){
        /*return new XMLHttpRequest();*/
        createXHR=function(){
            return new XMLHttpRequest();
        }
    }else if(typeof ActiveXObject != "undefined"){
        createXHR=function(){
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"];
                for (var i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch (ex){
                        //跳过
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        };
    }else{
        createXHR=function(){
            throw new Error("浏览器不支持XHR")
        };
    }
}
console.log(createXHR.toString());//createXHR原样打印出来;

//执行后,createXHR更改为最终的结果;
createXHR();
console.log(createXHR.toString());//createXHR已经是改写后的createXHR了

createXHR运行之前,打印的时候,会把这个函数原样打印出来;

createXHR运行之后,会被重新改写了,之后的createXHR是改写后的createXHR;

在chrome等标准浏览器会改写为下面这样的;

function (){
    return new XMLHttpRequest();
}

在这个惰性载入的 createXHR() 中, if 语句的每一个分支都会为 createXHR 变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下一次调用 createXHR() 的时候,就会直接调用被分配的函数,这样就不用再次执行 if 语句了。

第二种方法是:变量接收自执行函数

是在声明函数时就指定适当的函数。这样,第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。

//console.log(createXHR.toString());//这么写会报错
var createXHR=(function (){
    if(typeof XMLHttpRequest != 'undefined'){
        /*return new XMLHttpRequest();*/
        return function(){
            return new XMLHttpRequest();
        }
    }else if(typeof ActiveXObject != "undefined"){
        return function(){
            if (typeof arguments.callee.activeXString != "string"){
                var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"];
                for (var i=0,len=versions.length; i < len; i++){
                    try {
                        new ActiveXObject(versions[i]);
                        arguments.callee.activeXString = versions[i];
                        break;
                    } catch (ex){
                        //跳过
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        };
    }else{
        return function(){
            throw new Error("浏览器不支持XHR")
        };
    }
})();
console.log(createXHR.toString());//createXHR已经是改写后的createXHR了

//执行后,createXHR
createXHR();

console.log(createXHR.toString());//createXHR已经是改写后的createXHR了

使用的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。实际的逻辑都一样。不一样的地方就是第一行代码(使用 var 定义函数)、新增了自执行的匿名函数,另外每个分支都返回正确的函数定义,以便立即将其赋值给 createXHR() 。

惰性载入函数的优点是只在执行分支代码时牺牲一点儿性能。至于哪种方式更合适,就要看你的具体需求而定了。不过这两种方式都能避免执行不必要的代码

函数绑定

函数绑定要创建一个函数,可以在特定的 this 环境中以指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。

<script>
    //这里是事件工具库;
    var EventUtil = {
        addHandler: function (element, type, handler) {
            if (element.addEventListener) {
                element.addEventListener(type, handler, false);
            } else if (element.attachEvent) {
                element.attachEvent("on" + type, handler);
            } else {
                element["on" + type] = handler;
            }
        },
        removeHandler: function (element, type, handler) {
            if (element.removeEventListener) {
                element.removeEventListener(type, handler, false);
            } else if (element.detachEvent) {
                element.detachEvent("on" + type, handler);
            } else {
                element["on" + type] = null;
            }
        },

        getEvent: function (event) {
            return event ? event : window.event;
        },
        getTarget: function (event) {
            return event.target || event.srcElement;
        },
        preventDefault: function (event) {
            if (event.preventDefault) {
                event.preventDefault();
            } else {
                event.returnValue = false;
            }
        },
        stopPropagation: function (event) {
            if (event.stopPropagation) {
                event.stopPropagation();
            } else {
                event.cancelBubbles = true;
            }
        },
        getRelatedTarget: function (event) {
            if (event.relatedTarger) {
                return event.relatedTarget;
            } else if (event.toElement) {
                return event.toElement;
            } else if (event.fromElement) {
                return event.fromElement;
            } else { return null; }

        }

    };
    //这里是绑定函数的代码;
    var handler={
        message:"Event handler",
        handleClick:function(e){
            console.log(this.message);
        }
    };
    var oBtn=document.getElementById("btn");
    EventUtil.addHandler(oBtn,"click",handler.handleClick);

</script>

点击后,输出的是undefined;并不是"Event hanler";原因是没有保存handler.handClick()的运行环境,this对象最后指的是btn这个ID的按钮;而非hanler; 可以通过代码验证;

var oBtn=document.getElementById("btn");
oBtn.message="这是ID为btn的message属性";
EventUtil.addHandler(oBtn,"click",handler.handleClick);

解决办法:包一层函数

var oBtn=document.getElementById("btn");
oBtn.message="这是ID为btn的message属性";
EventUtil.addHandler(oBtn,"click",function(e){
    handler.handleClick(e)
});

这个解决方案在 onclick 事件处理程序内使用了一个闭包直接调用 handler.handleClick() 。当然,这是特定于这段代码的解决方案。创建多个闭包可能会令代码变得难于理解和调试。因此,很多JavaScript 库实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫 bind()

function bind(fn,context){
    return function(){
        return fn.apply(context,arguments);
    }
}
//这里是绑定函数的代码;
var handler={
    message:"Event handler",
    handleClick:function(e){
        console.log(this.message+":"+ e.type);
    }
};
var oBtn=document.getElementById("btn");
oBtn.message="这是ID为btn的message属性";
EventUtil.addHandler(oBtn,"click",bind(handler.handleClick,handler));

handler.handleClick() 方法和平时一样获得了 event 对象,因为所有的参数都通过被绑定的函数直接传给了它。

ECMAScript 5 为所有函数定义了一个原生的 bind() 方法,进一步简单了操作。换句话说,你不用再自己定义 bind() 函数了,而是可以直接在函数上调用这个方法。

通过输出到控制台查看下;

console.dir(bind);
//这里是绑定函数的代码;
var handler={
    message:"Event handler",
    handleClick:function(e){
        console.log(this.message+":"+ e.type);
    }
};
var oBtn=document.getElementById("btn");
oBtn.message="这是ID为btn的message属性";
//EventUtil.addHandler(oBtn,"click",bind(handler.handleClick,handler));
EventUtil.addHandler(oBtn,"click",handler.handleClick.bind(handler));//原生的bind方法;

原生的 bind() 方法与前面介绍的自定义 bind() 方法类似,都是要传入作为 this 值的对象。支持原生 bind() 方法的浏览器有 IE9+、Firefox 4+和 Chrome。

只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。它们主要用于**事件处理程序以及 setTimeout() 和 setInterval() **。然而,被绑定函数与普通函数相比有更多的开销,它们需要更多内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。

函数柯里化

与函数绑定紧密相关的主题是函数柯里化(function currying),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数

function add(arg1,arg2){
    return arg1+arg2;
}

function curriesAdd(num2){
    return add(5,num2);
}
console.log(add(2,3));//5
console.log(curriesAdd(5));//10

这段代码定义了两个函数: add() 和 curriedAdd() 。后者本质上是在任何情况下第一个参数为 5的 add() 版本。尽管从技术上来说 curriedAdd() 并非柯里化的函数,但它很好地展示了其概念

柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式。

function curry(fn){
    var args=Array.prototype.slice.call(arguments,1);
    return function (){
        var innerArgs=Array.prototype.slice.call(arguments);
        var finnalArgs=args.concat(innerArgs);
        console.log("args->"+args+"  innerArgs->"+innerArgs+"   finnalArgs->"+finnalArgs);
        return fn.apply(null,finnalArgs);
    }
}

function add(arg1,arg2){
    return arg1+arg2;
}

var curriedAdd = curry(add, 5);
console.log(curriedAdd(3)); //当调用 curriedAdd() 并传入3时,3会成为add()的第二个参数,同时第一个参数依然是5,最后结果便是和8。

也可以第一次直接把2个参数传进去;

var curriedAdd = curry(add, 5, 12);
console.log(curriedAdd()); //17     在这里,柯里化的 add() 函数两个参数都提供了,所以以后就无需再传递它们了。

函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的 bind() 函数。

//这里是绑定函数的代码;
var handler = {
    message: "Event handled",
    handleClick: function(name, event){
        console.log(this.message + ":"+ name + ":"+ event.type);
    }
};
function bind(fn,context){
    var args=Array.prototype.slice.call(arguments,2);
    return function (){
        var innerArgs=Array.prototype.slice.call(arguments);
        var finnalArgs=args.concat(innerArgs);
        console.log("args->"+args+"  innerArgs->"+innerArgs+"   finnalArgs->"+finnalArgs);
        return fn.apply(context,finnalArgs);
    }
}

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

JavaScript 中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用 bind() 还是 curry()要根据是否需要 object 对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容