1. 程式人生 > >js之高階技巧

js之高階技巧

安全的型別檢測

js內建的型別檢測方法並不安全。

①typeOf 不能用於檢測array只能返回object,不能檢測null只能返回object,但是出乎意料的是可以檢測symbol、function和undefined。型別返回值都是字串、而且都是小寫打頭。

②instanceOf是用來判斷是不是某個東西的例項物件的,比如一道經典的題:

var isArray = value instanceof Array; 

要求value與Array要在同一個作用域,因為Array是window的屬性,所以肯定是全域性作用域,如果這個網頁用到了iframe,value是在頁面中另一個iframe中定義的陣列的話,以上程式碼會返回false。

所以我們會使用Object.prototype.toString.call(value) 會返回[object value的型別。即使是RexExp也可以使用。

作用域安全的建構函式

當我們不寫new時,是把建構函式當作普通函式呼叫,這個時候建構函式內的this指向widow。

為了避免這種情況的發生,我們要在建構函式內先進行一個判斷,判斷this是否為建構函式的例項物件。

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

當然這種寫法也有它的弊端,與call()函式一起使用時會出問題,所以要結合原型鏈繼承一起使用,形成我們的偽經典繼承。

       function Polygon(sides){
            if(this instanceof Polygon){
                this.sides = sides;
                this.getArea = function(){
                    return 0;
                }
            }else{
                return new Polygon();
            }
        }
        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();//沒有這句最後就console不出2
        var rect = new Rectangle(5,10);
        console.log(rect.sides);

函式繫結

函式可以在特定的this環境中以指定引數呼叫另一個函式。

可以通過建立閉包保持一個this,也可以通過bind、call、apply改變this指向。

       var handler = {
            message: "Event handled",
            handleClick: function(){
                console.log(this);
            }
        };
        var btn = document.getElementById("my-btn");
        btn.addEventListener("click",function(){
            handler.handleClick();//此時this指向handler,這是一個閉包
        },false);
        btn.addEventListener("click",handler.handleClick,false);
        //此時this指向btn
        btn.addEventListener("click",handler.handleClick.bind(handler),false);
        //此時this指向handler,bind返回一個更改了this的函式

常考的一道題是手寫個bind:

        function bind(fn,context){
            return function(){
                return fn.call(context);
            }
        };
        function f1(){
            return this;
        }
        console.log(f1());//Window
        console.log(bind(f1,[1,2,3])());//[1,2,3]

函式柯里化

基本方法和函式繫結是一樣的:使用一個閉包返回一個函式。

        function curry(fn){
            var args = Array.prototype.slice.call(arguments,1);
            //arguments的第0位是fn,即要進行柯里化的引數
            return function(){
                var innerArgs = Array.prototype.slice.call(arguments);//內部函式的陣列
                var finalArgs = args.concat(innerArgs);
                return fn.apply(null,finalArgs);
                // return fn.call(null,args[0],innerArgs[0]);也可以
            };
        };
        function add(num1,num2){
            return num1+num2;
        }
        var curriedAdd = curry(add,5);//args=5
        console.log(curriedAdd(3));//innerArgs=3

記得上一個“函式繫結”時我們寫的bind嗎?並沒有寫關於引數的問題(因為不需要寫),現在函式柯里化必須要對內外函式的引數進行處理了。

        var handler = {
            message: "Event handled",
            handleClick: function(name,event){
                console.log(this.message+":"+name+":"+event.type);
            }
        };
        //可以用於函式柯里化和函式繫結的更牛掰的bind
        function bind(fn,context){
            var args = Array.prototype.slice.call(arguments,2);//外部函式
            return function(){
                //內部函式
                var innerArgs = Array.prototype.slice.call(arguments,0);
                var finalArgs = args.concat(innerArgs);
                return fn.apply(context,finalArgs);
            }
        }
        var btn = document.getElementById("my-btn");
        btn.addEventListener("click",bind(handler.handleClick,handler,"my-btn"),false);//Event handled:my-btn:click

防篡改物件

有時候我們寫了一個js庫,裡面有一些核心物件,我們希望在開發過程中這個核心物件不被修改,這時候就要防止該物件被篡改。

①不可篡改

呼叫Object.preventExtensions(object)方法就不能給object新增新屬性和方法了,但是已有的成員還是可以修改或刪除的。

使用Object.isExtensible()方法確定物件是否可以擴充套件。

②密封

呼叫Object.isSealed()方法來密封物件,不能刪除和新增屬性和方法,但是可以修改屬性和方法。使用 Object.isSealed()方法確定物件是否被密封。用 Object.isExtensible()檢測密封的物件也會返回 false,表明被密封的物件不可擴充套件。

③凍結

呼叫Object.freeze()方法來凍結物件,不能刪除、新增和修改屬性和方法。使用Object.isFrozen()方法檢測凍結物件。用 Object.isExtensible()和 Object.isSealed()檢測凍結物件將分別返回 false 和 true,因為凍結=不可篡改+密封。

以上可以看出,防止篡改物件的嚴格優先順序從高到低依次是 :凍結>密封>擴充套件

自定義事件

事件是一種叫 觀察者 的設計模式,話說我第一次聽說這個詞還是在學vue的時候,這裡我也不展開了,學得太糙回來再補。

它是一種建立鬆散耦合程式碼的技術,定義物件之間一對多的依賴關係,一個主體改變狀態,其他所有觀察者通過訂閱事件都能獲得通知,就好比你寫作業的時候爸爸媽媽姥姥姥爺爺爺奶奶全盯著你!

例子:有一天你登陸了一個網站,網站的頭會顯示你的使用者名稱和頭像,下面的顯示也是與你的賬號有關,旁邊可能還有購物車。我們通過獲取ajax返回的資訊,觸發多個事件,而這些事件統統放到一個數組中。

我壓縮了一下大紅書上的程式碼:

        function EventTarget(){
            this.handlers = {};
        };
        EventTarget.prototype = {
            constructor:EventTarget,
            addHandler: function(type,handler){//新增事件處理程式
                if(!this.handlers[type]){
                    this.handlers[type] = new Array;
                }
                this.handlers[type].push(handler);
            },
            fire: function(event){//觸發
                if(this.handlers[event.type]){
                    for(var i=0; i<this.handlers[event.type].length; i++){
                        this.handlers[event.type][i](event);
                    }
                }
            },
            removeHandler: function(type,handler){//刪除事件處理程式
                if(this.handlers[type]){
                    for(var i=0; i<this.handlers[type].length; i++){
                        if(this.handlers[type][i] === handler){
                            break;
                        }
                    };
                    this.handlers[type].splice(i,1);
                }
            }
        }

其中EventTarget是一個事件管理器,handles用來儲存事件處理程式,每一項都是一個事件type,裡面的內容是這個事件type所對應的多個事件處理程式。

在網上看到一道筆試題,非常經典非常好!強烈推薦大家啥都不看也要做做這道題,查缺補漏~

[附加題] 請實現下面的自定義事件 Event 物件的介面,功能見註釋(測試1)
該 Event 物件的介面需要能被其他物件拓展複用(測試2)
// 測試1
Event.on('test', function (result) {
    console.log(result);
});
Event.on('test', function () {
    console.log('test');
});
Event.emit('test', 'hello world'); // 輸出 'hello world' 和 'test'
// 測試2
var person1 = {};
var person2 = {};
Object.assign(person1, Event);
Object.assign(person2, Event);
person1.on('call1', function () {
    console.log('person1');
});
person2.on('call2', function () {
    console.log('person2');
});
person1.emit('call1'); // 輸出 'person1'
person1.emit('call2'); // 沒有輸出
person2.emit('call1'); // 沒有輸出
person2.emit('call2'); // 輸出 'person2'
var Event = {
    // 通過on介面監聽事件eventName
    // 如果事件eventName被觸發,則執行callback回撥函式
    on: function (eventName, callback) {
        //你的程式碼
    },
    // 觸發事件 eventName
    emit: function (eventName) {
        //你的程式碼
    }
};

首先完成第一個測試題需要用到我們的觀察者模式啦~啥也不說直接上程式碼!

var Event = {
    // 通過on介面監聽事件eventName
    // 如果事件eventName被觸發,則執行callback回撥函式
    on: function (eventName, callback) {
        if(!this.handlers){
            this.handles={};
        }
        if(!this.handlers[eventName]){
            this.handlers[eventName] = new Array();
        };
        this.handlers[eventName].push(callback);
    },
    // 觸發事件 eventName
    emit: function (eventName) {
        if(this.handlers[eventName]){
            for(var i=0; i<this.handlers[eventName].length; i++){
                this.handlers[eventName][i](arguments[1]);
            }
        };
    }
};

當然可以輕鬆解決測試1,但測試2顯然不是這麼回事了,因為Object.assign是一種淺拷貝,也就是隻拷貝到物件的地址而不是地址中的內容,所以一榮俱榮一損俱損。

這時候用到的知識是學“物件”那一章的設定物件屬性,不知道大家想沒想起來,我反正是隻記得這麼個事情卻不太會使,導致這道題雖然知道坑在哪但沒想到要設定那裡。

var Event = {
    // 通過on介面監聽事件eventName
    // 如果事件eventName被觸發,則執行callback回撥函式
    on: function (eventName, callback) {
        if(!this.handlers){
            //this.handles={};
            Object.defineProperty(this, "handlers", {
                value: {},
                enumerable: false,
                configurable: true,
                writable: true
            })
        }
        if(!this.handlers[eventName]){
            this.handlers[eventName] = new Array();
        };
        this.handlers[eventName].push(callback);
    },
    // 觸發事件 eventName
    emit: function (eventName) {
        if(this.handlers[eventName]){
            for(var i=0; i<this.handlers[eventName].length; i++){
                this.handlers[eventName][i](arguments[1]);
            }
        };
    }
};

Object.defineProperty()方法需要三個引數,分別是:屬性所在物件,這裡是Event本身;屬性的名字,這裡是handlers,注意必須加引號;一個描述符物件,描述符包括configurable、enumerable、writable 和 value。所以我們會這樣設定:讓handlers為不可列舉的enumerable:false,由於Object.assign()對不可列舉的屬性沒有辦法,於是person1和person2呼叫他的時候生成的handlers就不是同一個了。

我們再來詳細說說這描述符物件:

 configurable : false 表示不能刪除屬性,而且一旦設定成false就沒法改成true了;

 writable : false 表示不可修改

enumerable : false 表示不可列舉,不能通過for-in遍歷得到。

拖放

本單元最後一部分內容,也是我認為最重要的一部分,涉及的東西很多。

在完成之前需要一段完善的過程,否則不能稱為一個好作品,程式碼亦是如此,不可能最開始就想的非常全面,伴隨著bug伴隨著越來越多的需求,程式碼也在不斷地完善。

①首先是實現簡單的拖拽,分為三個部分,當滑鼠按下的時候,滑鼠移動的時候,和滑鼠鬆開的時候。

滑鼠按下的時候要確認滑鼠的位置同你所移動的物體之間的距離diff;

滑鼠移動的時候將物體的位置設為 滑鼠位置-diff;

滑鼠鬆開的時候取消前兩步的監聽。

在這裡插一句話:mousedown->focus->mouseup->click的順序一定要記牢!

        var myDiv = document.getElementById("myDiv"),
            count = 0,
            diffX = 0,
            diffY = 0;
        function mousedown(event) {
            diffX = event.clientX - this.offsetLeft;
            diffY = event.clientY - this.offsetTop; 
            count = 1;
        }
        function mousemove(event) {
            if(count === 1){
                this.style.left = event.clientX - diffX + "px";
                this.style.top = event.clientY - diffY + "px"; 
            }
        }
        myDiv.addEventListener("mousedown",mousedown,false);
        myDiv.addEventListener("mousemove",mousemove,false);
        myDiv.addEventListener("mouseup",function(event){
            myDiv.removeEventListener("mousedown",mousedown(event));
            myDiv.removeEventListener("mousemove",mousedown(event));
            count = 0;
        },false);

②如果我們想在這三步的時候分別實現不同的事件處理程式呢?結合之前的自定義事件非常好實現。

        //自定義事件
        function EventTarget(){
            this.handlers = this.handlers||{};
            this.addHandler = function(type,handler){
                if(!this.handlers[type]){
                    this.handlers[type] = [];
                }
                this.handlers[type].push(handler);
            };
            this.fire = function(obj){
                for(var i=0; i<this.handlers[obj.type].length; i++){
                    this.handlers[obj.type][i](obj);
                }
            };
            this.removeHanlder = function(type,handler){
                for(var i=0; i<this.handlers[type].length; i++){
                    if(this.handlers[type][i]===handler){
                        this.handlers[type].splice(i,1);
                    }
                }
            };
        }
        var dragdrop = new EventTarget();
        var myDiv = document.getElementById("myDiv"),
            count = 0,
            diffX = 0,
            diffY = 0;
        function mousedown(event) {
            diffX = event.clientX - this.offsetLeft;
            diffY = event.clientY - this.offsetTop; 
            count = 1;
            dragdrop.fire({type:"dragstart",target:myDiv,x:event.clientX,y:event.clientY});
        }
        function mousemove(event) {
            if(count === 1){
                this.style.left = event.clientX - diffX + "px";
                this.style.top = event.clientY - diffY + "px";
                dragdrop.fire({type:"drag",target:myDiv,x:event.clientX,y:event.clientY}); 
            }
        }
        myDiv.addEventListener("mousedown",mousedown,false);
        myDiv.addEventListener("mousemove",mousemove,false);
        myDiv.addEventListener("mouseup",function(event){
            myDiv.removeEventListener("mousedown",mousedown);
            myDiv.removeEventListener("mousemove",mousemove);
            count = 0;
            dragdrop.fire({type:"dragend",target:myDiv,x:event.clientX,y:event.clientY});                        
        },false);
        //注意要加一個定時器,否則只能拖拽一次
        setInterval(function(){
            myDiv.addEventListener("mousedown",mousedown,false);
            myDiv.addEventListener("mousemove",mousemove,false);
        },300);
        
        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+")";
        });

定時器雖然能使我們無限次使用拖拽,但也會帶來卡頓等問題,所以真正應該使用的方法是定義一個物件,等滑鼠鬆開的時候將這個物件設為null而不是粗暴地將滑鼠按下和滑鼠移動的事件監聽取消。

③再上一步的基礎之上,我們採用模組化程式設計的方式。

        function EventTarget(){
            this.handlers = {};
        };
        EventTarget.prototype = {
            constructor:EventTarget,
            addHandler: function(type,handler){
                if(!this.handlers[type]){
                    this.handlers[type] = new Array;
                }
                this.handlers[type].push(handler);
            },
            fire: function(event){
                if(this.handlers[event.type]){
                    for(var i=0; i<this.handlers[event.type].length; i++){
                        this.handlers[event.type][i](event);
                    }
                }
            },
            removeHandler: function(type,handler){
                if(this.handlers[type]){
                    for(var i=0; i<this.handlers[type].length; i++){
                        if(this.handlers[type][i] === handler){
                            break;
                        }
                    };
                    this.handlers[type].splice(i,1);
                }
            }
        }
        var DragDrop = function(){
            var dragdrop = new EventTarget(),
                dragging = null,
                diffX = 0,
                diffY = 0;
            function handleEvent(event){
                event = event||window.event;
                var target = event.target||event.srcElement;

                //確定事件型別
                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){
                            //指定位置
                            dragging.style.left = event.clientX-diffX +"px";
                            dragging.style.top = event.clientY-diffY +"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);
                    document.addEventListener("mousemove",handleEvent);
                    document.addEventListener("mouseup",handleEvent);
                    
            };
            dragdrop.disable = function(){
                    document.removeEventListener("mousedown",handleEvent);
                    document.removeEventListener("mousemove",handleEvent);
                    document.removeEventListener("mouseup",handleEvent);
            };

            return dragdrop;
        }();

        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+")";
        });
        DragDrop.enable();

在這裡我們定義了dragging表示我們要拖拽的物件,DragDrop是拖拽模組,採用自執行函式的形式。

發現模組化設計也是一個需要好好研究的東西呢~