Javascript高级程序设计第22章(高级技巧)

1.高级函数

①var isArray = value instanceof Array;

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

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

alert(Object.prototype.toString.call(value));     //"[object Array]"

由于原生数组的构造函数名与全局作用域无关,所以使用toString()都能保证返回一致的值。因此验证是否是一个数组:

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

②作用域安全的构造函数

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 person1 = Person("Nicholas", 29, "Software Engineer");
        alert(window.name);   //""
        alert(person1.name);  //"Nicholas"
        
        var person2 = new Person("Shelby", 34, "Ergonomist");
        alert(person2.name);  //"Shelby"

想一想,如果没验证this是否为Person,person1中的this就会解析成window对象。由于window.name属性是用于识别链接目标和frame的,所以这里对该属性的偶然覆盖可能会导致该页面上出现错误。window.name的作用是什么?详情戳: window.name实现的跨域

③惰性载入函数

function createXHR(){
            if (typeof XMLHttpRequest != "undefined"){
                return new XMLHttpRequest();
            } else if (typeof ActiveXObject != "undefined"){
                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){
                            //skip
                        }
                    }
                }
            
                return new ActiveXObject(arguments.callee.activeXString);
            } else {
                throw new Error("No XHR object available.");
            }
        }

像上面那样,每次调用createXHR(),它都要对浏览器所支持的能力仔细检查,所以如果if语言不必每次都执行,那么代码可以运行地更快一些,解决方案就是称之为惰性载入的技巧

惰性函数表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式。

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

function createXHR(){
            if (typeof XMLHttpRequest != "undefined"){
                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"],
                            i, len;
                
                        for (i=0,len=versions.length; i < len; i++){
                            try {
                                new ActiveXObject(versions[i]);
                                arguments.callee.activeXString = versions[i];
                            } catch (ex){
                                //skip
                            }
                        }
                    }
                
                    return new ActiveXObject(arguments.callee.activeXString);
                };
            } else {
                createXHR = function(){
                    throw new Error("No XHR object available.");
                };
            }
            
            return createXHR();
        }
        
        var xhr1 = createXHR();
        var xhr2 = createXHR();
这个堕入载入的createXHR()钟,if语句的每一个分支都会为creatXHR()变量赋值,有效覆盖了原有的函数。最后一步便是调用新赋的函数。下次调用createXHR()的时候,就会直接调用被分配的函数,这样就不用再次执行if语句了。

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

var createXHR = (function(){
            if (typeof XMLHttpRequest != "undefined"){
                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"],
                            i, len;
                
                        for (i=0,len=versions.length; i < len; i++){
                            try {
                                new ActiveXObject(versions[i]);
                                arguments.callee.activeXString = versions[i];
                                break;
                            } catch (ex){
                                //skip
                            }
                        }
                    }
                
                    return new ActiveXObject(arguments.callee.activeXString);
                };
            } else {
                return function(){
                    throw new Error("No XHR object available.");
                };
            }
        })();
        
        var xhr1 = createXHR();
        var xhr2 = createXHR();
总结:惰性载入函数的优点是只在执行分支代码时牺牲一点而性能。至于两种方式哪种更适合,就要看你的具体需求而定。不过这两种方式都能避免执行不必要的代码。

4.函数绑定

该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境

函数绑定的bind方法是:fun.bind(binedObject),binedObject是为fun函数绑定的对象,但这原生的bind()方法的兼容性为:IE9+、Firefox、Chrome,得自己写一个bind()函数

      var handler = {
            message: "Event handled",
        
            handleClick: function(event){
                alert(this.message + ":" + event.type);
            }
        };
        
        var btn = document.getElementById("my-btn");
        EventUtil.addHandler(btn, "click", handler.handleClick));


这个this对象指向了DOM按钮而非handler(在IE中,this指向window),所以可以使用闭包来修改这个问题。

function bind(fn, context){
            return function(){
                return fn.apply(context, arguments);
            };
        }
    
        var handler = {
            message: "Event handled",
        
            handleClick: function(event){
                alert(this.message + ":" + event.type);
            }
        };
        
        var btn = document.getElementById("my-btn");
        EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler));
不过不懂的是为什么要在bind()函数的闭包函数也return,明明不写return也能正常执行的,而且返回的还是undefined

这个函数看似简单,但其功能是非常强大的。注意这里使用的arguments对象是内部函数的,而非bind()的。

函数绑定主要用于事件处理程序以及setTimeout()和setInterval()。然而,被绑定函数与普通函数相比有更多的开销,它们需要更多内存(闭包),同时也因为多重函数调用稍微慢一些,所以最好只在必要时使用。


⑤函数柯里化(function currying)(但说实话,我还是没搞懂要怎么用,好扯淡。。)

它用于创建已经设置好了一个或多个参数的函数。基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的函数

柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。其简单概念如下:

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

function curriedAdd(num2){
  return add(5, num2);
]

alert(add(2, 3));   //5
alert(curriedAdd(3));   //8

下面是创建柯里化函数的通用方式:

function curry(fn){
            var args = Array.prototype.slice.call(arguments, 1);   //args存放外部函数参数,除了fn这个参数
            return function(){
                var innerArgs = Array.prototype.slice.call(arguments),   //存放内部函数参数
                    finalArgs = args.concat(innerArgs);
                return fn.apply(null, finalArgs);
            };
        }
    
        function add(num1, num2){
            return num1 + num2;
        }
        
        var curriedAdd = curry(add, 5);
        alert(curriedAdd(3));   //8

        var curriedAdd2 = curry(add, 5, 12);
        alert(curriedAdd2());   //17

函数柯里化还常常作为函数绑定的一部分包含在其中
function bind(fn, context){
            var args = Array.prototype.slice.call(arguments, 2);   //注意这里要传除了函数参数fn和对象参数context
            return function(){
                var innerArgs = Array.prototype.slice.call(arguments),
                    finalArgs = args.concat(innerArgs);
                return fn.apply(context, finalArgs);             //注意这里要传入context参数
            };
        }
    
        var handler = {
            message: "Event handled",
        
            handleClick: function(name, event){
                alert(this.message + ":" + name + ":" + event.type);
            }
        };
        
        var btn = document.getElementById("my-btn");
        EventUtil.addHandler(btn, "click", bind(handler.handleClick, handler, "my-btn"));
JavaScript中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外开销。



2.防篡改对象

注意的是:一旦把对象定义为防篡改,就无法撤销了。ECMAScript增加了几个方法,通过它们可以指定对象的行为

①不可扩展对象:第一保护级别

使用Object.preventExtensions(object)使你不能再给对象添加属性和方法,object为你要改变的对象。在非严格模式下,尝试,给对象添加新成员会导致静默失败,因此object.property将是undefined。而在严格模式下,尝试给不可扩展的对象添加新成员会导致错误。但不会对已有的对象成员造成应该。

另外可使用Object.isExtensible()方法确定对象是否可扩展

②密封的对象:第二保护级别

密封对象不可扩展,而且已有成员的[[Configurable]]特性将被设置为false,这将意味着不能删除属性和方法。但已有属性值是可修改的。在严格模式和非严格模式下的影响和不可扩展对象一样,导致错误或被忽略。

另外可使用Object.isSealed()可确定对象是否被密封了。

③冻结的对象:第三保护级别

最严格的防篡改级别。冻结的对象既不可扩展,又是密封的,而且对象数据属性的[ [ Writable ]]特性会被设置为false。如果设置[ [ Set] ]函数,访问器属性仍然是可写的。在严格模式和非严格模式下的影响和不可扩展对象一样,导致错误或被忽略。

可使用Object.isFrozen()方法确定是否检测冻结对象

对JavaScript库的作者而言,冻结对象是很很有用的。因为JavaScript库最怕有人意外(或有意)地修改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题的发生。


3.高级定时器

虽然人们对JavaScript的定时器存在普遍的误解,认为它们是线程,其实JavaScript是运行在单线程的环境中的,而定时器仅仅只是计划在未来的某个时间执行。执行实际是不能保证的,因为在页面的声明周期内,不同时间可能有其他代码在控制进程。(如设定一个150ms的定时器不代表到了150ms代码就立刻执行,它表示会在150ms后会被加入到执行队列中)在页面下载完后的代码运行、时间处理程序、Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。

①重复的定时器:setInterval()

重复的定时器问题在于,定时器代码可能在代码再次被添加到队列之前还没完全执行,结果导致定时器代码连续执行好几次,而之间没有停顿。幸好,JavaScript引擎够聪明,能避免这个问题。当使用setInterval()时,仅当没有该定时器代码实例时,才将定时器代码代码添加到队列中。这确保定时器代码加入到队列中的最小时间间隔为指定间隔。

这个规则有两个问题:(1)某些间隔会被跳过;(2)多个定时器的代码执行之间的间隔可能会被预期的小。

为了避免setInterval()的重复定时器的这两个缺点,你可以用如下模式使用链式setTimeout()调用。

setTimeout(function(){
  //处理中
  
  setTimeout(argumrnts.callee, interval);

}, interval);
这么做的好处是,在前一个定时器代码执行完之前,不会向队列中插入新的定时器代码,确保不会有任何缺失的间隔。而且它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。应用如下:

setTimeout(function(){
        
           var div = document.getElementById("myDiv"),
               left = parseInt(div.style.left) + 5;
           div.style.left = left + "px";
        
           if (left < 200){
               setTimeout(arguments.callee, 50);
           }
        
        }, 50);

注意:每个浏览器窗口、标签页、或者frame都有各自的代码执行队列。这意味着,进行跨frame或者跨串口的定时调用,当代码同时执行的时候可能会导致竞争条件。无论何时需要使用这种通信类型,最好是在接受frame或者窗口中创建一个定时器来执行代码。



3.Yielding Processes(让出进程)

背景:运行在浏览器中的JavaScript都被分配了一个确定数量的资源。不同于桌面应该往往能够随意控制他们要的内存大小和处理器时间,JavaScript被严格限制了,以防止恶意的Web程序员把用户的计算机搞挂了。其中一个限制是长时间运行脚本的限制,如果代码运行超过特定的时间或特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的时间执行,询问是否其继续执行还是停止它。所有JavaScript开发人员的目标就是,确保用户永远不会在浏览器中看到这个令人费解的对话框。定时器是绕开此限制的方法之一。

脚本长时间运行的问题通常是由两个原因之一造成的:①过长的、过深嵌套的函数调用②进行大量处理的循环

  长时间运行的循环通常遵循以下模式:

for(var i =0, len=data.length; i
如果process()要处理很久,那意味着这段时间内用户无法与页面交互的时间会很久

在展开该循环之前,你需要回答以下两个问题

【】该处理是否必须同步完成?

【】数据是否必须按顺序完成?

因此,当你发现某个循环占用了大量时间,同时对于上述两个问题,你的回答都是否,那么你就可以使用定时器分隔这个循环。这是一种叫做数据分组(array chunking)的技术,小块小块地处理数组,通常每次处理一块。

var data = [12,123,1234,453,436,23,23,5,4123,45,346,5634,2234,345,342];
        
        function chunk(array, process, context){
            setTimeout(function(){
                var item = array.shift();
                process.call(context, item);
                
                if (array.length > 0){
                    setTimeout(arguments.callee, 100);         
                }
            }, 100);        
        }
    
        function printValue(item){
            var div = document.getElementById("myDiv");
            div.innerHTML += item + "
"; } chunk(data, printValue); //如果想保持原数组,chunck(data.concat(), printValue);


函数节流

背景:浏览器中某些计算和处理比其他的昂贵很多,例如DOM操作,消耗更多的内存和CPU时间

基本思想:某些代码不可以在没有间断的情况连续重复执行

实现:定时器

应用:只要代码是周期性执行,都应该使用节流,但是你不能控制请求执行的频率。

function throttle(method, scope) {
            clearTimeout(method.tId);
            method.tId= setTimeout(function(){
                method.call(scope);  //没有传入scope时,那么就在全局作用域内执行该方法
            }, 100);
        }

节流在resize事件中是最常见的。

function resizeDiv(){
            var div = document.getElementById("myDiv");
            div.style.height = div.offsetWidth + "px";
        }
        
        window.onresize = function(){
            throttle(resizeDiv);
        };

但要注意这函数节流在onscroll事件并不合适,因为onscroll事件要求滚动条每变化1像素就要求有所反应,要是有些滚动条变化没被响应,效果就没那么好了。



自定义事件

背景:事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应

观察者模式:由两类对象组成,主体和观察者。主题负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。

事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中-----通过实现自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:

function EventTarget(){
    this.handlers = {};    
}

EventTarget.prototype = {
    constructor: EventTarget,

    addHandler: function(type, handler){
        if (typeof this.handlers[type] == "undefined"){
            this.handlers[type] = [];
        }

        this.handlers[type].push(handler);
    },
    
    fire: function(event){
        if (!event.target){
            event.target = this;
        }
        if (this.handlers[event.type] instanceof Array){
            var handlers = this.handlers[event.type];
            for (var i=0, len=handlers.length; i < len; i++){
                handlers[i](event);
            }
        }            
    },

    removeHandler: function(type, handler){
        if (this.handlers[type] instanceof Array){
            var handlers = this.handlers[type];
            for (var i=0, len=handlers.length; i < len; i++){
                if (handlers[i] === handler){
                    break;
                }
            }
            
            handlers.splice(i, 1);
        }            
    }
};

简单的使用如下:
function handleMessage(event){
            alert("Message received: " + event.message);
        }

        var target = new EventTarget();
        
        target.addHandler("message", handleMessage);

        target.fire({ type: "message", message: "Hello world!"});
        
        target.removeHandler("message", handleMessage);

        target.fire({ type: "message", message: "Hello world!"});

应用:当代码中存在多个部分在特定时刻相互交互的情况,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象引用。那么整个代码就会紧密耦合,同时维护也变得很困难,因此对某个对象的修改也会影响其他对象。使用自定义事件有助于解耦相关对象,保护功能的隔绝。在很多情况中,触发事件的代码和监听事件代码是完全分离的。


拖放

因为没什么好说的,就直接贴代码了,下面是一个单例对象,并使用了模块模式来隐藏某些实现细节

var DragDrop = function(){
        
            var dragging = null,
                diffX = 0,
                diffY = 0;
            
            function handleEvent(event){
            
                //get event and target
                event = EventUtil.getEvent(event);
                var target = EventUtil.getTarget(event);            
            
                //determine the type of event
                switch(event.type){
                    case "mousedown":
                        if (target.className.indexOf("draggable") > -1){
                            dragging = target;
                            diffX = event.clientX - target.offsetLeft;   //注意这里的event和target
                            diffY = event.clientY - target.offsetTop;
                        }                     
                        break;
                        
                    case "mousemove":
                        if (dragging !== null){
                            
                            //assign location
                            dragging.style.left = (event.clientX - diffX) + "px";
                            dragging.style.top = (event.clientY - diffY) + "px";                    
                        }                    
                        break;
                        
                    case "mouseup":
                        dragging = null;
                        break;
                }
            };
            
            //public interface
            return {            
                enable: function(){
                    EventUtil.addHandler(document, "mousedown", handleEvent);
                    EventUtil.addHandler(document, "mousemove", handleEvent);
                    EventUtil.addHandler(document, "mouseup", handleEvent);
                },
                
                disable: function(){
                    EventUtil.removeHandler(document, "mousedown", handleEvent);
                    EventUtil.removeHandler(document, "mousemove", handleEvent);
                    EventUtil.removeHandler(document, "mouseup", handleEvent);
                }
            }
        }();
        
        DragDrop.enable();

添加自定义事件----拖放

拖放功能还不能真正应用起来,除非能知道什么时候拖动开始了。从这点看,前面的代码没有提供任何方法表示拖动开始、正在拖动或者已经结束。这是,可以使用自定义事件来指示这几个事件的发生,让应用的其他部分与拖动功能进行交互。

var DragDrop = function(){
        
            var dragdrop = new EventTarget(),
                dragging = null,
                diffX = 0,
                diffY = 0;
            
            function handleEvent(event){
            
                //get event and target
                event = EventUtil.getEvent(event);
                var target = EventUtil.getTarget(event);            
            
                //determine the type of event
                switch(event.type){
                    case "mousedown":
                        if (target.className.indexOf("draggable") > -1){
                            dragging = target;
                            diffX = event.clientX - target.offsetLeft;
                            diffY = event.clientY - target.offsetTop;
                            dragdrop.fire({type:"dragstart", target: dragging, x: event.clientX, y: event.clientY});
                        }                     
                        break;
                        
                    case "mousemove":
                        if (dragging !== null){
                            
                            //assign location
                            dragging.style.left = (event.clientX - diffX) + "px";
                            dragging.style.top = (event.clientY - diffY) + "px";   

                            //fire custom event
                            dragdrop.fire({type:"drag", target: dragging, x: event.clientX, y: event.clientY});
                        }                    
                        break;
                        
                    case "mouseup":
                        dragdrop.fire({type:"dragend", target: dragging, x: event.clientX, y: event.clientY});
                        dragging = null;
                        break;
                }
            };
            
            //public interface
            dragdrop.enable = function(){
                    EventUtil.addHandler(document, "mousedown", handleEvent);
                    EventUtil.addHandler(document, "mousemove", handleEvent);
                    EventUtil.addHandler(document, "mouseup", handleEvent);
            };
                
            dragdrop.disable = function(){
                    EventUtil.removeHandler(document, "mousedown", handleEvent);
                    EventUtil.removeHandler(document, "mousemove", handleEvent);
                    EventUtil.removeHandler(document, "mouseup", handleEvent);
            };
            
            return dragdrop;
        }();
        
        DragDrop.enable();
                        
        DragDrop.addHandler("dragstart", function(event){
            var status = document.getElementById("status");
            status.innerHTML = "Started dragging " + event.target.id;
        });
        
        DragDrop.addHandler("drag", function(event){
            var status = document.getElementById("status");
            status.innerHTML += "
Dragged " + event.target.id + " to (" + event.x + "," + event.y + ")"; }); DragDrop.addHandler("dragend", function(event){ var status = document.getElementById("status"); status.innerHTML += "
Dropped " + event.target.id + " at (" + event.x + "," + event.y + ")"; });

这段代码定义了三个事件:dragstart、drag和dragend。和上面不支持事件的代码的区别还是很大的。为DragDrop添加自定义事件可以使这个对象更加健壮,它将可以在网络应用中处理复杂的拖放功能。


总结:①可以创建作用域安全的构造函数,确保在缺少new操作符时调用构造函数不会改变错误的环境对象

②可以使用惰性载入函数,将任何代码分支推迟到第一次调用函数的时候

③函数绑定可以让你创建始终在制定环境中运行的函数,同时函数函数柯里化可以让你创建已经填了某些参数的函数

④将绑定和函数柯里化结合起来,就能够给你一种在任意环境中以任意参数执行任意函数的方法

⑤定时器原理可以让你实现数组分块和函数节流的技术

⑥使用自定义事件有助于将不同部分的代码相互之间解耦,让维护更加容易,并减少引入错误的机会。

⑦将拖放行为和自定义事件结合起来可以创建一个可重复使用的框架,它能应用于各种不同的情况下。

这一章真的能学到很多,而且并不是空洞的知识,本人亲测,这些知识都能应用在实际中,棒棒哒。PS:看不懂就先放下,这本书第一次看不懂很正常,等有了一些项目实践后再看,会更加清晰,而且必须看很多遍,等书上的例子都应用到实际中用烂了,这本书你才算真正的看懂了。

你可能感兴趣的:(js/jquery)