《JavaScript高級程序設計》筆記:高級技巧


高級函數

安全的類型檢測

在任何值上調用Object原生的toString()方法,都會返回一個[object NativeConstructorName]格式的字符串。每個類在內部都有一個[[Class]]屬性,這個屬性就指定了上述字符串中的構造函數名。

var arr = [];
function fn(){
    
}
var reg = /^\d/;
var json = {
    "name":"Jack",
    "age":20
,}
console.log(Object.prototype.toString.call(arr) == "[object Array]");  //true
console.log(Object.prototype.toString.call(fn) == "[object Function]"); //true
console.log(Object.prototype.toString.call(reg) == "[object RegExp]");  //true
console.log(window.JSON && Object.prototype.toString.call(json) == "[object Object]"); //true

作用域安全的構造函數

作用域安全的構造函數在進行任何更改前,首先確認this對象是正確類型的實例。如果不是,會創建新的實例並返回,如下例子:

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("jack",29,"IT");
console.log(window.name); //""
console.log(person1.name);//jack

var person2 = Person("Tom",20,"teacher");
console.log(person2.name);//Tom

如下例子,Rectangle實例中沒有添加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;
    }
}

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

修改后,Rectangle實例中添加了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();
var rect = new Rectangle(5,10);
console.log(rect.sides); //2

惰性載入函數

function createXHR(){
    if(typeof XMLHttpRequest != "undefined"){
        return new XMLHttpRequest();
    }else if(typeof ActiveXObject != "undefined"){
        if(typeof arguments.callee.activeXString != 'string'){
            var verisions = ["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 = version[i];
                    break;
                }catch(ex){
                    //跳過
                }
            }
        }
        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 verisions = ["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 = version[i];
                        break;
                    }catch(ex){
                        //跳過
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        }    
    }else{
        createXHR = function(){
            throw  new Error("NO XHR object available.");
        }
    }
    return createXHR();
}

第二種實現惰性載入的方式是在聲明函數時就指定適當的函數,如下代碼:

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 verisions = ["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 = version[i];
                        break;
                    }catch(ex){
                        //跳過
                    }
                }
            }
            return new ActiveXObject(arguments.callee.activeXString);
        }    
    }else{
        return function(){
            throw  new Error("NO XHR object available.");
        }
    }
})();

 函數綁定

var handler = {
    message:"Event handler",
    handleClick:function(event){
        console.log(this.meesage); //undefined
    }
}
var btn = document.getElementsByClassName("my-btn")[0];
btn.addEventListener("click",handler.handleClick,false);

上面的結果貌似會顯示“Event handler”的結果,但是結果是undefined,是因為沒有保存handler.handleClick()的環境。所以this對象最后是指向了DOM按鈕,而非handler對象(在IE8中,this指向window)。可以使用一個閉包來修正這個問題,如下代碼:

var handler = {
    message:"Event handler",
    handleClick:function(event){
        console.log(this.message); //Event handler
    }
}
var btn = document.getElementsByClassName("my-btn")[0];
btn.addEventListener("click",function(event){
    handler.handleClick(event);
},false);

很多javascript庫實現了一個可以將函數綁定到指定環境的函數。這個函數一般都叫做bind()。

一個簡單的bind()函數接收一個函數和一個環境。並返回一個在給定函數中調用給定函數的函數,並且將所有參數原封不動的傳遞過去。語法如下:

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

那么我們就可以用上面的bind()方法來實現綁定,如下代碼:

var handler = {
    message:"Event handler",
    handleClick:function(event){
        console.log(this.message); //Event handler
    }
}
var btn = document.getElementsByClassName("my-btn")[0];
btn.addEventListener("click",bind(handler.handleClick,handler),false);

ECMAScript5為所有函數定義了一個原生的bind()方法,那么上面代碼可以如下:

var handler = {
    message:"Event handler",
    handleClick:function(event){
        console.log(this.message); //Event handler
    }
}
var btn = document.getElementsByClassName("my-btn")[0];
btn.addEventListener("click",handler.handleClick.bind(handler),false);

原生的bind()方法和上面自定義的bind()方法很相似,都要傳入作為this值的對象。支持原生bind()方法的瀏覽器有IE9+、Firefox4+和Chrome。

 函數柯里化

與函數綁定緊密相關的是主題是函數柯里化(function curring),它用於創建已經設置好了一個或多個參數的函數。函數柯里化的基本方法和函數綁定是一樣的:使用一個閉包返回一個函數。兩者的區別在於,當函數被調用時,返回的函數還需要設置一些傳入的參數,如下例子:

function add(num1,num2){
    return num1 + num2;
}
function curriedAdd(num2){
    return add(5,num2);
}
console.log(add(2,3)); //5
console.log(curriedAdd(3)); //8

 盡管從技術上來說curriedAdd()並非柯里化的函數,但它很好的展示了其概念。

柯里化函數通常由以下步驟動態創建:調用另一個函數並為它傳入要柯里化的函數和必要參數。下面是創建柯里化函數的通用方式:

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);
    }
}

調用方式:

function add(num1,num2){
    return num1 + num2;
}
var curriedAdd = curry(add,5);
curriedAdd(3); //8

也可像下面方式調用:

function add(num1,num2){
    return num1 + num2;
}
var curriedAdd = curry(add,5,3);
curriedAdd(); //8

函數柯里化還常常作為函數綁定的一部分包含在其中,構造出更為復雜的bind()函數,如下:

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);
    }
}

實例:

var handler = {
    message:"Event handled",
    handleclick:function(name,event){
        console.log(this.message + ":" + name + ":" + event.type);
    }
}
var btn = document.getElementById('my-btn');
btn.addEventListener('click',bind(handler.handleclick,handler,"my-btn"),false);  //Event handled:my-btn:click

ECMAScript5的bind()方法也實現了函數的柯里化,如下代碼:

var handler = {
    message:"Event handled",
    handleclick:function(name,event){
        console.log(this.message + ":" + name + ":" + event.type);
    }
}
var btn = document.getElementById('my-btn');
btn.addEventListener('click',handler.handleclick.bind(handler,"my_btn"),false);  //Event handled:my-btn:click

 防篡改對象

注意:一旦把對象定義為防篡改,就無法撤銷。

不可擴展對象Object.preventExtensions()

使用Object.preventExtensions()就不能給原對象添加新的屬性和方法了,如下例子:

var person = {
    name:"jack"
};
Object.preventExtensions(person);
person.age = 20;
console.log(person.age); //undefined  嚴格模式下拋出異常

使用Object.isExtensible()確定對象是否可以擴展,如下:

var person = {
    name:"jack"
};
console.log(Object.isExtensible(person)); //true

Object.preventExtensions(person);
console.log(Object.isExtensible(person)); //false

密封的對象Object.seal()

密封對象不可擴展,而且已有成員的[[configurable]]特性將被設置為false。這就意味着不能刪除屬性和方法,因為不能使用Object.defineProperty()把數據屬性修改為訪問器屬性,或者相反。屬性值是可以修改的。

var person = {
    name:"jack"
};
Object.seal(person);
person.age = 20;
console.log(person.age); //非嚴格模式下:undefined  嚴格模式下:拋出異常

delete person.name;
console.log(person.name); //非嚴格模式下:jack   嚴格模式下:拋出異常

使用Object.isSealed()方法可以確定對象是否被密封了。因為被密封的對象不可擴展,所以調用Object.isExtensible()檢測密封的對象也會返回false。

var person = {
    name:"jack"
};
console.log(Object.isExtensible(person)); //true
console.log(Object.isSealed(person)); //false

Object.seal(person);
console.log(Object.isExtensible(person));//false
console.log(Object.isSealed(person));//true

凍結的對象Object.freeze()

最嚴格的防篡改級別是凍結對象。凍結的對象既不可擴展,又是密封的,而且對象數據屬性[[writable]]特性會被設置為false。如果定義[[Set]]函數,訪問器屬性仍然是可寫的。

ECMAScript5定義的Object.freeze()方法可以用來凍結對象,如下代碼:

var person = {
    name:"jack"
};
Object.freeze(person);

person.age = 20;
console.log(person.age); //非嚴格模式:undefined  嚴格模式下:拋出異常

delete person.name;
console.log(person.name);//非嚴格模式:jack  嚴格模式下:拋出異常

person.name = "Tom";
console.log(person.name);//非嚴格模式:jack  嚴格模式下:拋出異常

使用Object.isFrozen()方法檢測凍結對象。因為凍結對象既是密封的又是不可擴展的,所以調用Object.isExtensible()和Object.isSealed()方法分別返回false和true。

var person = {
    name:"jack"
};

console.log(Object.isExtensible(person)); //true
console.log(Object.isSealed(person)); //false
console.log(Object.isFrozen(person)) //false

Object.freeze(person);
console.log(Object.isExtensible(person));//false
console.log(Object.isSealed(person));//true
console.log(Object.isFrozen(person)) //true

 高級定時器

函數節流

DOM操作比起非DOM交互需要更多的內存和CPU。連續嘗試進行過多的DOM相關操作可能會導致瀏覽器掛起,有時候甚至會崩潰。比如在IE瀏覽器中使用onresize事件處理程序的時候容易發生,當調整瀏覽器窗口的時候,該事件會連續發生。在onresize事件處理程序內部如果嘗試進行DOM操作,其高頻率的更改可能會讓瀏覽器崩潰。為了解決這個問題,可以使用定時器對該函數進行節流。

基本形式代碼結構如下:

var processor = {
    timeoutId:null,
    
    //實際進行處理的方法
    performProcessing:function(){
         //實際執行的代碼
    },
    
    //初始處理調用的方法
    process:function(){
        clearTimeout(this.timeoutId);
        var that = this;
        this.timeoutId = setTimeout(function(){
            that.performProcessing();
        },100);
    }
}

//嘗試開始 執行
processor.process();

時間間隔設置為了100ms,這表示最后一次調用process()之后,至少100ms后才會調用performProcessing()。所以如果100ms之內調用了20次process(),也只會調用performProcessing()一次。

這個模式可以使用throttle函數來簡化,這個函數可以自動進行定時器的設置和清除,如下代碼:

function throttle(method,context){
    clearTimeout(method.tId);
    method.tId = setTimeout(function(){
        method.call(context);
    },100);
}

throttle()函數接受兩個參數:要執行的函數以及在哪個作用域中執行。

來看個例子,假如有一個div元素需要保持它的高度始終等於寬度。那么實現這個JS代碼如下:

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

上面代碼有兩個問題可能造成瀏覽器運行緩慢,一個是計算offsetWidth屬性,如果該元素或者頁面上的其它元素有非常復雜的css樣式,那么這個過程將會很復雜。 另一個設置某個元素的高度需要對頁面進行回流來令改動生效。如果頁面有很多元素同時應用了相當數量的CSS的話,這有需要很多計算。這就可以用到throttle()函數,如下代碼:

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

window.onresize = function(){
    throttle(resizeDiv);
}

只要代碼是周期性執行的,都應該使用節流。

自定義事件

事件是一種叫做觀察者的設計模式,這是一種創建松散耦合代碼的技術。對象可以發布事件,用來表示在該對象生命周期中某個有趣的時刻到了。然后其它對象可以觀察該對象,等等這些有趣的時刻到來並通過運行代碼來響應。

觀察者模式由兩類對象組成:主體和觀察者。主體負責發布事件,同時觀察者通過訂閱這些事件來觀察該主體。該模式的一個關鍵概念是主體並不知道觀察者的任何事情,也就是說它可以獨自存在並正常運作即便觀察者不存在。從另一個方面來說,觀察者知道主體並能注冊事件的回調函數(事件處理程序)。涉及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);
        }
    }
};

EventTarget類型有一個單獨的屬性handlers,用於存儲事件處理程序。
定義的三個方法如下:

  • 1.addHandler:用於注冊給定類型事件的事件處理程序;該方法接受兩個參數,事件類型和用於處理該事件的函數。
  • 2.fire:觸發一個事件;該方法接受一個單獨的參數,是一個至少包含type屬性的對象。fire()方法先給event對象設置一個target屬性,如果它尚未被指定的話。然后它就查找對應該事件類型的一組處理程序,調用各個函數,並給出event對象。因為這些都是自定義事件,所以event對象上還需要的額外信息由你自己決定。
  • 3.removeHandler:注銷某個事件類型的事件處理程序;它接受的參數跟addHandler是一樣的。

使用EventTarget類型的自定義事件可以如下使用:

function  handlerMessage(event){
    console.log("Message received:" + event.message);
}

//創建一個新對象
var target = new EventTarget();

//添加一個事件處理程序
target.addHandler("message",handlerMessage);

//觸發事件
target.fire({type:"message",message:"Hello world!"});

//移除事件處理程序
target.removeHandler("message",handlerMessage);

//再次,應沒有處理程序
target.fire({type:"message",message:"Hello world!"});

 如下實例:

function object(o){
    function F(){};
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

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

inheritPrototype(Person,EventTarget);

Person.prototype.say = function(message){
    this.fire({type:"message",message:message});
}

Person類型使用了寄生組合繼承方法來繼承EventTarget。怎樣使用:

function  handlerMessage(event){
    console.log(event.target.name + " says:" + event.message);
}

//創建新Person
var person = new Person("Jack",29);

//添加一個事件處理程序
person.addHandler("message",handlerMessage);

//在該對象上調用一個方法,它觸發消息事件
person.say('Hi there');

 拖放

拖放功能

下例代碼自己添加了一部分控制在屏幕區域的代碼,如下:

var DragDrop = function(){
    var dragging = null,
            differX = 0,
            differY = 0,
            targetWidth = 0,
            targetHeight = 0,
            windowWidth = 0,
            windowHeight = 0,
            _isMove = false; //是否移動
    
    function handleEvent(event){
        //獲取事件和目標
        event = event || window.event;
        var target = event.target || event.srcElement;
        
        //確認事件類型
        switch(event.type){
            case "mousedown":
                if(target.className.indexOf("drggable") != -1){
                    dragging = target;
                    _isMove = true;
                    differX = event.clientX - target.offsetLeft;
                    differY = event.clientY - target.offsetTop;
                    targetWidth = target.offsetWidth;
                    targetHeight = target.offsetHeight;
                    windowWidth = document.documentElement.clientWidth;
                    windowHeight = document.documentElement.clientHeight;
                }
                break;
            case "mousemove":
                if(dragging !== null && _isMove){
                    var left = event.clientX - differX,
                            top = event.clientY - differY;
                    if(left < 0){
                        left = 0;
                    }
                    else if(left > windowWidth - targetWidth){
                        left = windowWidth - targetWidth;
                    }
                    
                    if(top < 0){
                        top = 0;
                    }
                    else if(top > windowHeight - targetHeight){
                        top = windowHeight - targetHeight;
                    }
                    
                    dragging.style.left = left  + "px";
                    dragging.style.top = top + "px";
                }
                break;
            case "mouseup":
                dragging = null;
                _isMove =false;
                break;
        }
    }
    
    //公共接口
    return {
        enable:function(){
            document.addEventListener("mousedown",handleEvent,false);
            document.addEventListener("mousemove",handleEvent,false);
            document.addEventListener("mouseup",handleEvent,false);
        },
        disable:function(){
            document.removeEventListener("mousedown",handleEvent,false);
            document.removeEventListener("mousemove",handleEvent,false);
            document.removeEventListener("mouseup",handleEvent,false);
        }
    }
    
}();
DragDrop.enable();

添加自定義事件

 上面寫的拖放功能還不能真正應用起來,除非能知道什么時候拖動開始了。從這點來看,前面的代碼沒有提供任何方法表示拖動開始、正在拖動或者拖動結束。這時,可以使用自定義事件來指示這幾個事件的發生,讓應用的其它部分和拖動功能進行交互。如下代碼:

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);
        }
    }
};


var DragDrop = function(){
    var dragdrop = new EventTarget(),
            dragging = null,
            differX = 0,
            differY = 0;
    
    function handleEvent(event){
        //獲取事件和目標
        event = event || window.event;
        var target = event.target || event.srcElement;
        
        //確認事件類型
        switch(event.type){
            case "mousedown":
                if(target.className.indexOf("drggable") != -1){
                    dragging = target;
                    _isMove = true;
                    differX = event.clientX - target.offsetLeft;
                    differY = event.clientY - target.offsetTop;
                    dragdrop.fire({type:"dragstart",target:dragging,x:event.clientX,y:event.clientY})
                }
                break;
            case "mousemove":
                if(dragging !== null){
                    //指定位置
                    dragging.style.left = (event.clientX - differX)  + "px";
                    dragging.style.top = (event.clientY - differY) + "px";
                    
                    //觸發自定義事件
                    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;
        }
    }
    
    //公共接口
        dragdrop.enable = function(){
            document.addEventListener("mousedown",handleEvent,false);
            document.addEventListener("mousemove",handleEvent,false);
            document.addEventListener("mouseup",handleEvent,false);
        };
        dragdrop.disable = function(){
            document.removeEventListener("mousedown",handleEvent,false);
            document.removeEventListener("mousemove",handleEvent,false);
            document.removeEventListener("mouseup",handleEvent,false);
        };
        return dragdrop;    
}();

這段代碼定義了三個自定義事件:dragstart、drag、dragend,它們都將被拖動的元素設置為了target,並給出了x和y屬性來表示當前的位置。調用如下:

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 += "<br/> Dragged" + event.target.id + " to(" + event.x + "," + event.y + ")";
});
DragDrop.addHandler("dragend",function(event){
    var status = document.getElementById('status');
    status.innerHTML += "<br/> Dropped" + event.target.id + " at(" + event.x + "," + event.y + ")";
});

貼出HTML代碼:

<div id="status"></div>
<div id="myDiv" class="drggable"></div>

CSS代碼:

*{margin:0;padding:0;}
#myDiv{width:200px;height:200px;background: blue;position: absolute;top:50px;left:400px;}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM