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是拖拽模块,采用自执行函数的形式。

发现模块化设计也是一个需要好好研究的东西呢~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值