• # IFE前端(2015春)-task2


    第一章 JavaScript数据类型及语言基础

    期望达成

    • 掌握JavaScript的各种数据类型概念、判断方法
    • 掌握JavaScript函数、对象的概念
    • 掌握字符串、数字、数组、日期等对象的方法
    • 了解JavaScript的作用域
    • 初步掌握正则表达式的写法

    1.1 实践判断各种数据类型的方法

    任务描述

    创建一个JavaScript文件,比如util.js;并在util.js中实现以下方法:

    // 判断arr是否为一个数组,返回一个bool值
    function isArray(arr) {
        // your implement
    }
    
    // 判断fn是否为一个函数,返回一个bool值
    function isFunction(fn) {
        // your implement
    }
    

    解决方案

    数组本来就有原生的方法Array.isArray(xxx),函数则可以使用typeof判断。

    // 判断arr是否为一个数组,返回一个bool值
    function isArray(arr) {
        // your implement
        return Array.isArray(arr);
    }
    
    // 判断fn是否为一个函数,返回一个bool值
    function isFunction(fn) {
        // your implement
        return typeof fn=='function';
    }
    

    1.2 数据类型的特性

    任务描述

    了解值类型和引用类型的区别,了解各种对象的读取、遍历方式,在util.js中实现以下方法:

    // 使用递归来实现一个深度克隆,可以复制一个目标对象,返回一个完整拷贝
    // 被复制的对象类型会被限制为数字、字符串、布尔、日期、数组、Object对象。不会包含函数、正则对象等
    function cloneObject(src) {
        // your implement
    }
    
    

    解决方案

    基本数据类型包括undefinedNull(typeof操作返回Object对象),BooleanNumberStringObject

    应当明确,引用数据类型(Object)是不可通过赋值的方法进行复制的。引用数据类型包括:对象,数组、日期和正则。实际上就是解决引用对象的复制的问题。既然不包括函数和正则,则可以使用typeof进行分类讨论。对与一般的Object对象,做一个遍历就可以了。

    Date对象如何判定呢?实际上还有更为底层的方法:Object.prototype.toString.call(xxx)

    关于Object.prototype.toString.call(xxx),可参考文章:http://www.cnblogs.com/ziyunfei/archive/2012/11/05/2754156.html

    // 使用递归来实现一个深度克隆,可以复制一个目标对象,返回一个完整拷贝
    // 被复制的对象类型会被限制为数字、字符串、布尔、日期、数组、Object对象。不会包含函数、正则对象等
    function cloneObject(src) {
        // your implement
        var clone=null;
        if(typeof src!=='object'){
            clone=src;
        }else{
            if(Array.isArray(src)){
                clone=src.slice();
            }else if(Object.prototype.toString.call(src)=='[object Date]'){
                clone=new Date(src.valueOf());
            }else{
                clone={};
                for(var attr in src){
                    clone[attr]=cloneObject(src[attr]);
                }
            }
        }
        return clone;
    }
    
    // 测试用例:
    var srcObj = {
        a: 1,
        b: {
            b1: ["hello", "hi"],
            b2: "JavaScript"
        }
    };
    var abObj = srcObj;
    var tarObj = cloneObject(srcObj);
    
    srcObj.a = 2;
    srcObj.b.b1[0] = "Hello";
    
    console.log(abObj.a);//2
    console.log(abObj.b.b1[0]);//"Hello"
    
    console.log(tarObj.a);      // 1
    console.log(tarObj.b.b1[0]);    // "hello"
    

    测试通过。

    1.3 数组、字符串、数字相关方法

    任务描述

    util.js中实现以下函数

    // 对数组进行去重操作,只考虑数组中元素为数字或字符串,返回一个去重后的数组
    function uniqArray(arr) {
        // your implement
    }
    
    
    // 实现一个简单的trim函数,用于去除一个字符串,头部和尾部的空白字符
    // 假定空白字符只有半角空格、Tab
    function simpleTrim(str) {
        // your implement
    }
    
    // 接下来,我们真正实现一个trim
    // 对字符串头尾进行空格字符的去除、包括全角半角空格、Tab等,返回一个字符串
    // 尝试使用一行简洁的正则表达式完成该题目
    function trim(str) {
        // your implement
    }
    
    
    
    // 实现一个遍历数组的方法,针对数组中每一个元素执行fn函数,并将数组索引和元素作为参数传递
    function each(arr, fn) {
        // your implement
    }
    
    
    // 获取一个对象里面第一层元素的数量,返回一个整数
    function getObjectLength(obj) {}
    
    

    解决方案

    数组去重

    数组去重无非是设置一个新数组,循环套循环判断,可以设置一个flag量。

    function uniqArray(arr) {
        var newArr=[];
        var check;
        for(var i=0;i<arr.length;i++){
            check=true;
            for(var j=0;j<newArr.length;j++){
                if(arr[i]==newArr[j]){
                    check=false;
                    break;
                }
            }
            if(check){
                newArr.push(arr[i]);
            }
        }
        return newArr;
    }
    
    // 使用示例
    var a = [1, 3, 5, 7, 5, 3];
    var b = uniqArray(a);
    console.log(b); // [1, 3, 5, 7]
    
    trim函数

    实际上ES5早已提供了trim方法。

    function simpleTrim(str) {
        // your implement
        return str.trim();
    }
    

    如果需要自己写,可以用正则匹配边界,或是做一个计数器,然后用循环查找字符串边界。

    function trim(str) {
        // 行首的所有空格和行尾的所有空格
        var re=/^s+|s+$/g;
        return str.replace(re,'');
    }
    
    // 使用示例
    var str = '   hi!  ';
    str = trim(str);
    console.log(str); // 'hi!'
    
    数组遍历

    这跟forEach方法是一样的。

    // 实现一个遍历数组的方法,针对数组中每一个元素执行fn函数,并将数组索引和元素作为参数传递
    function each(arr, fn) {
        // your implement
        for(var i=0;i<arr.length;i++){
            fn(arr[i],i);
        }
    }
    
    // 使用示例
    var arr = ['java', 'c', 'php', 'html'];
    function output(item, index) {
        console.log(index + ': ' + item);
    }
    each(arr, output);  // 0:java, 1:c, 2:php, 3:html
    
    找到属性的数量

    对象一般用for-in循环,循环次数就是这个属性的长度。

    // 获取一个对象里面第一层元素的数量,返回一个整数
    function getObjectLength(obj) {
        var count=0;
        for(var attr in obj){
            count++;
        }
        return count;
    }
    
    // 使用示例
    var obj = {
        a: 1,
        b: 2,
        c: {
            c1: 3,
            c2: 4
        }
    };
    console.log(getObjectLength(obj)); // 3
    

    1.4 正则表达式

    任务描述

    util.js完成以下代码

    // 判断是否为邮箱地址
    function isEmail(emailStr) {
        // your implement
    }
    
    // 判断是否为手机号
    function isMobilePhone(phone) {
        // your implement
    }
    

    解决方案

    邮箱和手机号是个很常用的判断。然而死记没有用,唯有多写。

    // 判断是否为邮箱地址
    function isEmail(emailStr) {
        // 开头必须以字母和数字跟着1一个“@”,后边是小写字母或数字,最后就是域名(2-4位)。
        var re=/^w+@[a-z0-9]+.[a-z]{2,4}$/;
        return re.test(emailStr);
    }
    //测试
    //console.log(isEmail('dangjingtao@ds.cc'));
    
    // 判断是否为手机号
    function isMobilePhone(phone) {
        //手机号必须以1开头,后面跟着9位数字
        var re=/^1d{10,10}$/;
        return re.test(phone);
    }
    
    //测试
    //console.log(isMobilePhone('15515515515'));
    

    第二章 DOM

    • 熟练掌握DOM的相关操作。

    注:所有的dom测试需要在window.onload下完成。

    2.1 DOM查询方法

    任务描述

    先来一些简单的,在你的util.js中完成以下任务:

    // 为element增加一个样式名为newClassName的新样式
    function addClass(element, newClassName) {
        // your implement
    }
    
    // 移除element中的样式oldClassName
    function removeClass(element, oldClassName) {
        // your implement
    }
    
    // 判断siblingNode和element是否为同一个父元素下的同一级的元素,返回bool值
    function isSiblingNode(element, siblingNode) {
        // your implement
    }
    
    

    解决方案

    addClass方法的实现

    classList 属性返回元素的类名,作为 DOMTokenList 对象。该属性用于在元素中添加,移除及切换 CSS 类。classList 属性是只读的,但你可以使用 add() 和 remove() 方法修改它。

    // 为element增加一个样式名为newClassName的新样式
    function addClass(element, newClassName) {
        element.classList.add(newClassName);
    }
    
    // 移除element中的样式oldClassName
    function removeClass(element, oldClassName) {
        element.classList.remove(oldClassName);
    }
    
    同辈方法
    // 判断siblingNode和element是否为同一个父元素下的同一级的元素,返回bool值
    function isSiblingNode(element, siblingNode) {
        // your implement
        return element.parentNode==siblingNode.parentNode;
    }
    

    2.2 基本选择器(mini $)

    任务描述

    接下来挑战一个mini $,它和之前的$是不兼容的,它应该是document.querySelector的功能子集,在不直接使用document.querySelector的情况下,在你的util.js中完成以下任务:

    // 实现一个简单的Query
    function $(selector) {
    
    }
    
    // 可以通过id获取DOM对象,通过#标示,例如
    $("#adom"); // 返回id为adom的DOM对象
    
    // 可以通过tagName获取DOM对象,例如
    $("a"); // 返回第一个<a>对象
    
    // 可以通过样式名称获取DOM对象,例如
    $(".classa"); // 返回第一个样式定义包含classa的对象
    
    // 可以通过attribute匹配获取DOM对象,例如
    $("[data-log]"); // 返回第一个包含属性data-log的对象
    
    $("[data-time=2015]"); // 返回第一个包含属性data-time且值为2015的对象
    
    // 可以通过简单的组合提高查询便利性,例如
    $("#adom .classa"); // 返回id为adom的DOM所包含的所有子节点中,第一个样式定义包含classa的对象
    

    解决方案

    迷你jQuery的实现就是判断传进来的字符串特征。先思考,选择器需要什么功能?

    • 当传入字符串时,查找选择器。

      • 选择器首先需要判断是否存在selector1 selector2的写法。后代选择器应该用数组方法split进行解析,并进行递归。
      • 如果不是后代选择器,那就看首字母的特征
    • 设置一个父级参数,当没有时这个父级就是document

    • 设置class选择器时,需要考虑class兼容性

      请出getByClass函数吧!

      function getByClass(oParent, sClass){
          if(oParent.getElementsByClassName){
              return oParent.getElementsByClassName(sClass);
          }else{
              var res = [];
              var re = new RegExp(' ' + sClass + ' ', 'i');
              var aEle = oParent.getElementsByTagName('*');
              for(var i = 0; i < aEle.length; i++){
                  if(re.test(' ' + aEle[i].className + ' ')){
                      res.push(aEle[i]);
                  }
             }
          return res;
          }
      }
      
    • 应该用面向对象的方法进行封装。把选择器放到$.obj中。

    方案如下

    function $d(selector,oParent) {
        //不写第二个参数时,oParent就是document
        oParent=oParent?oParent:document;
        //用来存放选择器对象。
        this.obj=null;
      
        // 如果没有空格
        if(!selector.match(/s/)){
            switch(selector[0]){
                case '#'://id选择器
                    this.obj=oParent.getElementById(selector.substring(1));
                break;
    
                case '.'://类选择器
                    this.obj=getByClass(oParent,selector.substring(1))[0];
                break;
    
                case '['://属性选择器
                    // 提取方括号内的属性名
                    var str=selector.replace(/[|]/g,'');
                   // 找出父级的对象集合,方便遍历
                    var all=oParent.getElementsByTagName('*');
                   // 存放找到的html元素
                    var arr=[];
                    for(var i=0;i<all.length;i++){
                        if(all[i].getAttribute(str)){
                            arr.push(all[i]);
                        }else if(str.indexOf('=')>0){
                            // 匹配等号又边
                            var value=str.replace(/[^...]+?(?==)|=|['"]/g,'');
                            // 匹配等号左边
                            var attr=str.match(/[^...]+?(?==)|=|['"]/g)[0];
                            if(all[i].getAttribute(attr)==value){
                                arr.push(all[i]);
                            }
                        }
                    }
                   // 如果查找不到,则返回空对象
                    this.obj=arr[0]?arr[0]:null;
                break;
    
                default:
                    this.obj=oParent.getElementsByTagName(selector)[0];
            }
        }else{// 后代选择器
            var nodeList=selector.split(' ');
            var parent=nodeList[0];
            var children=nodeList[1];
    
            this.obj=$(children,$(parent).obj).obj;
        }
    
    }
    

    既然用了面向对象的方法来写,那么这个函数就需要进化一下,把$变成$d构造函数的实例:

    function $(selector,oParent){
        return new $d(selector,oParent);
    }
    

    调用时可以用$('#div1').obj进行查询。

    另外,原生方法有querySelector方法,但是兼容性不强。


    第三章 事件

    • 熟悉DOM事件相关知识

    3.1 事件注册

    任务描述

    我们来继续用封装自己的小jQuery库来实现我们对于JavaScript事件的学习,还是在你的util.js,实现以下函数

    // 给一个element绑定一个针对event事件的响应,响应函数为listener
    function addEvent(element, event, listener) {
        // your implement
    }
    
    // 例如:
    function clicklistener(event) {
        ...
    }
    addEvent($("#doma"), "click", a);
    
    // 移除element对象对于event事件发生时执行listener的响应
    function removeEvent(element, event, listener) {
        // your implement
    }
    

    接下来我们实现一些方便的事件方法

    // 实现对click事件的绑定
    function addClickEvent(element, listener) {
        // your implement
    }
    
    // 实现对于按Enter键时的事件绑定
    function addEnterEvent(element, listener) {
        // your implement
    }
    

    接下来我们把上面几个函数和$对象的一些方法

    • addEvent(element, event, listener) -> $.on(element, event, listener);
    • removeEvent(element, event, listener) -> $.un(element, event, listener);
    • addClickEvent(element, listener) -> $.click(element, listener);
    • addEnterEvent(element, listener) -> $.enter(element, listener);

    解决方案

    注册事件有两种方法:addEventListenerattachEvent。两者用法类似,但是存在若干不同。

    • addEventListener

      适用于现代浏览器,此方法接受3个参数,事件名称,回调函数,是否事件捕获(默认为false,通常不写)。作为对应,还有一个removeEventListener。比如说,我要创建一个对某个按钮创建一个点击事件处理函数:

      function xxx(){
        //balabala
      }
      
      window.onload=function(){
        var oBtn=document.getElementById('btn');
        //注册
        oBtn.addEventListener('click',xxx);
        //移除
        oBtn.removeEventListener('click',xxx);
        
      };
      
    • attachEvent

      适用于IE远古浏览器家族,相比addEventListener,少了第三个参数,同时事件名需要加一个on在前面

      window.onload=function(){
        var oBtn=document.getElementById('btn');
        //注册
        oBtn.attachEvent('onclick',xxx);
        //移除
        oBtn.detachEvent('onclick',xxx);
        
      };
      
    • 为什么要注册事件?

      还是上面的例子,oBtn.onclick=function(){...}会把之前添加的的内容给覆盖掉。而使用注册后,允许你写多个不同的事件函数,按照注册事件发生。

    首先看下如何获取浏览器信息,用的是navigator.userAgent

    //判断是否为IE浏览器,返回-1或者版本号
    function isIE() {
        var info=navigator.userAgent;
        var re=/msie (d+.d+)/i;
        var reEdge=/rv:(d+.d+)/i;
    
        if(info.match(re)){
            return info.match(re)[1];
        }else if(info.match(reEdge)&&!info.match(/firefox/i)){
            // 兼容Edge浏览器
            return info.match(reEdge)[1];
        }else{
            return -1;
        }
    }
    
    

    对于IE8.0以下的版本,使用attachEvent方法。

    写事件总会遇到万恶的兼容性问题。难点在于兼容性和获取this

    绑定this到元素身上的策略是call方法

    detachEvent方法无法获取原本执行的效果函数,既然这样,就把这个效果函数设置为一个构造函数,存入实际要执行的对象,待需要解绑时,在调出来,最后删除这个属性

    // 给一个element绑定一个针对_event事件的响应,响应函数为listener
    function addEvent(element,_event,listener) {
    
        if(isIE()==-1||isIE()>=9){
            element.addEventListener(_event,listener);
        }else if(isIE()!==-1&&isIE()<9){
            // 如果函数的绑定信息没有,就创建一个
            if(!listener.prototype.bindEvents){
                listener.prototype.bindEvents=[];
            }
    
            var bindInfo={
                target:element,
                event:_event,
                fn:function(){
                    return listener.call(element);
                }
            };
    
            listener.prototype.bindEvents.push(bindInfo);
            element.attachEvent('on'+_event,bindInfo.fn);
        }
    }
    
    // 移除element对象对于event事件发生时执行listener的响应
    function removeEvent(element, _event, listener) {
        if(isIE()==-1||isIE()>=9){
            element.removeEventListener(_event,listener);
        }else if(isIE()!==-1&&isIE()<9){
            var events=listener.prototype.bindEvents;
            for(var i=0;i<events.length;i++){
                if(events[i].target==element&&events[i].event==_event){
                    //调用这个属性,然后删除
                    element.detachEvent('on'+_event,events[i].fn);
                    events.splice(i,1);
                }
            }
        }
    }
    

    有了这两个函数就可以做出各种事件了。

    $d.prototype.on=function(_event,listener){
        addEvent(this.obj,_event,listener);
    };
    
    $d.prototype.un=function(_event,listener){
        removeEvent(this.obj,_event,listener);
    };
    
    $d.prototype.click=function(listener){
        addEvent(this.obj,'click',listener);
    };
    
    $d.prototype.enter=function(listener){
        addEvent(this.obj,'keydown',function(ev){
            var e=ev||window.event;
            if(e.keyCode==13){
                return listener();
            }
        });
    };
    

    经测试兼容IE8。

    3.2 事件监听代理

    任务描述

    接下来考虑这样一个场景,我们需要对一个列表里所有的``增加点击事件的监听

    最笨的方法

    <ul id="list">
        <li id="item1">Simon</li>
        <li id="item2">Kenner</li>
        <li id="item3">Erik</li>
    </ul>
    
    function clickListener(event) {
        console.log(event);
    }
    
    $.click($("#item1"), clickListener);
    $.click($("#item2"), clickListener);
    $.click($("#item3"), clickListener);
    

    上面这段代码要针对每一个item去绑定事件,这样显然是一件很麻烦的事情。

    稍微好一些的

    <ul id="list">
        <li>Simon</li>
        <li>Kenner</li>
        <li>Erik</li>
    </ul>
    

    我们试图改造一下

    function clickListener(event) {
        console.log(event);
    }
    
    each($("#list").getElementsByTagName('li'), function(li) {
        addClickEvent(li, clickListener);
    });
    

    我们通过自己写的函数,取到id为list这个ul里面的所有li,然后通过遍历给他们绑定事件。这样我们就不需要一个一个去绑定了。但是看看以下代码:

    <ul id="list">
        <li id="item1">Simon</li>
        <li id="item2">Kenner</li>
        <li id="item3">Erik</li>
    </ul>
    <button id="btn">Change</button>
    
    function clickListener(event) {
        console.log(event);
    }
    
    function renderList() {
        $("#list").innerHTML = '<li>new item</li>';
    }
    
    function init() {
        each($("#list").getElementsByTagName('li'), function(item) {
            $.click(item, clickListener);
        });
    
        $.click($("#btn"), renderList);
    }
    init();
    

    我们增加了一个按钮,当点击按钮时,改变list里面的项目,这个时候你再点击一下li,绑定事件不再生效了。那是不是我们每次改变了DOM结构或者内容后,都需要重新绑定事件呢?当然不会这么笨,接下来学习一下事件代理,然后实现下面新的方法:

    // 先简单一些
    function delegateEvent(element, tag, eventName, listener) {
        // your implement
    }
    
    $.delegate = delegateEvent;
    
    // 使用示例
    // 还是上面那段HTML,实现对list这个ul里面所有li的click事件进行响应
    $.delegate($("#list"), "li", "click", clickHandle);
    

    估计有同学已经开始吐槽了,函数里面一堆$看着晕啊,那么接下来把我们的事件函数做如下封装改变:

    $.delegate(selector, tag, event, listener) {
        // your implement
    }
    
    // 使用示例:
    $.click("[data-log]", logListener);
    $.delegate('#list', "li", "click", liClicker);
    

    解决方案

    事件监听是利用冒泡的机制,当你点击ul中的某个li,默认触发ul的点击。然后一层一层向上冒泡,冒泡到具体的li时,添加监听函数。

    $d.prototype.delegate=function(tags,_event,listener){
        addEvent(this.obj,_event,function(ev){
            var e=ev||window.event;
            //console.log(e.target.nodeName);
            if(e.target.nodeName.toUpperCase()==tags.toUpperCase()){
                return listener.call(e.target);
            }
        });
    };
    //测试
    window.onload=function(){
      $('#list').delegate('li','click',function(){
        console.log(this);
      })
    }
    

    遗憾的是该方法的nodeName不支持IE8


    第四章 BOM

    了解BOM的基础知识

    4.1 元素定位

    任务描述

    获取element相对于浏览器窗口的位置

    // 获取element相对于浏览器窗口的位置,返回一个对象{x, y}
    function getPosition(element) {
        // your implement
    }
    // your implemen
    

    解决思路

    先看如何获取元素的绝对位置——不断累加元素和本身的offset值。直到不可再加。

    	function getElementLeft(element){
        var actualLeft = element.offsetLeft;
        var current = element.offsetParent;
        while (current !== null){
          actualLeft += current.offsetLeft;
          current = current.offsetParent;
        }
        return actualLeft;
      }
    
      function getElementTop(element){
        var actualTop = element.offsetTop;
        var current = element.offsetParent;
        while (current !== null){
          actualTop += current.offsetTop;
          current = current.offsetParent;
        }
        return actualTop;
      }
    

    有了绝对方法,只要将绝对坐标减去页面的滚动条滚动的距离就可以了。

    // 获取element相对于浏览器窗口的位置,返回一个对象{x, y}
    function getPosition(element) {
        // your implement
        function getElementLeft(ele){
        var actualLeft = ele.offsetLeft;
        var current = ele.offsetParent;
        while (current !== null){
          actualLeft += current.offsetLeft;
          current = current.offsetParent;
        }
        return actualLeft;
      }
    
      function getElementTop(ele){
        var actualTop = ele.offsetTop;
        var current = ele.offsetParent;
        while (current !== null){
          actualTop += current.offsetTop;
          current = current.offsetParent;
        }
        return actualTop;
      }
    
        var position={};
    
        var scrollTop=document.documentElement.scrollTop||document.body.scrollTop;
        var scrollLeft=document.documentElement.scrollLeft||document.body.scrollLeft;
        var left=getElementLeft(element)-scrollLeft;
        var top=getElementTop(element)-scrollTop;
        position.x=left;
        position.y=top;
    
        return position;
    }
    

    如果你想快速获得相对位置——

    // 获取element相对于浏览器窗口的位置,返回一个对象{x, y}
    function getPosition(element) {
        // your implement
      	var X= element.getBoundingClientRect().left;
    	var Y =element.getBoundingClientRect().top;
      	return {
          x:X,
          y:Y
      	}
    }
    

    这两个方法都兼容IE8。

    任务描述

    实现以下函数

    // 设置cookie
    function setCookie(cookieName, cookieValue, expiredays) {
        // your implement
    }
    
    // 获取cookie值
    function getCookie(cookieName) {
        // your implement
    }
    

    解决方案

    cookie的测试需要在FF或服务器环境下进行。

    js中的cookie是document下的一个属性,cookie没有指定,其寿命就是浏览器进程。

    document.cookie="user=dangjingtao";
    document.cookie="pass=123";
    alert(document.cookie);
    

    cookie本质是一个字符串。通过document.cookie进行读取。但是是有意义的字符串,各个键值对通过分号隔开。包括基本属性(自定义)和过期时间(expires)。

    如果你要删除cookie,直接把过期时间设置为前一天就可以了。

    //以下是封装好的三个cookie函数
    function setCookie(name,value,iDay){
        var oDate=new Date();
        oDate.setDate(oDate.getDate()+iDay);
        document.cookie=name+'='+value+';expires='+oDate;
    }
    
    
    function getCookie(name){
        // 对cookie字符串转化为一个数组,
        // 每个数组元素对应是一个单独的cookie
        var arr=document.cookie.split(';');
    
        for(var i=0;i<arr.length;i++){
            // 再对每个单独的cookie进一步细分,
            // arr2[0]代表名字,arr2[1]代表值
            var arr2=arr[i].split('=');
    
            if(arr2[0]==name){
                // 把这个cookie值传出去!
                return arr2[1];
            }
        }
        // 如果查找不到,返回空字符串
        return '';
    }
    
    function removeCookie(name){
        setCookie(name,'null',-1);
    }
    

    第五章 Ajax

    • 掌握Ajax的实现方式

    任务描述

    学习Ajax,并尝试自己封装一个Ajax方法。实现如下方法:

    function ajax(url, options) {
        // your implement
    }
    
    // 使用示例:
    ajax(
        'http://localhost:8080/server/ajaxtest', 
        {
            data: {
                name: 'simon',
                password: '123456'
            },
            onsuccess: function (responseText, xhr) {
                console.log(responseText);
            }
        }
    );
    

    options是一个对象,里面可以包括的参数为:

    • type: post或者get,可以有一个默认值
    • data: 发送的数据,为一个键值对象或者为一个用&连接的赋值字符串
    • onsuccess: 成功时的调用函数
    • onfail: 失败时的调用函数

    解决方案

    XMLHttpRequest对象是ajax技术的核心,在IE6中是ActiveXObject对象。因此创建XMLHttpRequest对象时需要兼容性处理。

    	if(window.XMLHttpRequest){
            oAjax=new XMLHttpRequest();
        }else{
            oAjax=new ActiveXObject("Microsoft.XMLHTTP");
        }
    

    ajax的get请求基本过程如下

    创建对象=>oAjax.open()=>oAjax.send()=>根据返回的状态响应

    对于post请求,通常还要带上请求的数据。

    oAjax.setRequestHeader('Content-Type','application/json');
    oAjax.send(content);
    

    oAjax.readyState一共5个状态码:

    1. 0=>open方法尚未调用
    2. 1=>open已经调用
    3. 2=>接收到头信息
    4. 3=>接收到响应主体
    5. 4=>响应完成
    $d.prototype.ajax=function(url,json){
    
        var content=json.content?json.content:null;
        var type=json.type;
        var fnSucc=json.success;
        var fnFaild=json.faild;
    
        var oAjax=null;
        if(window.XMLHttpRequest){
            oAjax=new XMLHttpRequest();
        }else{
            oAjax=new ActiveXObject("Microsoft.XMLHTTP");
        }
    
        if(type.toUpperCase()=='GET'){
            oAjax.open('GET',url,true);
            oAjax.send();
        }else if(type.toUpperCase()=='POST'){
            oAjax.setRequestHeader('Content-Type','application/json');
            oAjax.open('POST',url,true);
            oAjax.send(content);
        }
    
    
        oAjax.onreadystatechange=function(){
            if(oAjax.readyState==4){
    
                if(oAjax.status==200){
                    fnSucc(oAjax.responseText);
                }else{
    
                    if(fnFaild){
                        fnFaild(oAjax.status);
                    }
    
                }
           }
       };
    };
    
    /* 使用示例
    ajax('json.json',{
      type:"POST",
      content:{
        name:"dangjingtao",
        password:"123"
      },
      success:function(res){
        alert(res);
      },
      faild:{
        alert('出错!');
      }
    });
    */
    

    第六章 js库的完善

    想要让目前这个$d库写起来像真正的jQuery一样顺手,需要完善的还有很多很多很多。

    具体查看解释,可以参照仿照jQuery封装个人的js库。本章该系列文章的浓缩版。

    $d参数放什么

    最开始是放字符串选择器。但是随着功能的增加,$d的方法越来越多。原来只传字符串进去显然不能满足了。

    一个流畅使用的$d选择器,应当满足:

    • 允许css形式的选择器字符串
    • 允许用$(function(){。。。})替代window.onload
    • 允许直接传html对象。或者更广。

    所以$d的代码结构应该是:

    function $d(selector oParent){
        switch(typeof selector){
          case:'function':
            //执行addEvent方法,主体对象是window,事件是load
          break;
          
          case:'object':
            //直接把该对象放到this.obj里面
          break;
          
          case:'string':
            //执行选择器操作
          break;
        }
    }
    

    群组选择器改进

    按照当初要求设计这个mini 版$库时有些坑爹啊。返回的是第一个元素,而不是一个数组。

    我们已经用$d.obj做了很多事情,再改就不现实了。群组选择器的全部结果放到一个$d.objs里面好了,那么this.obj就是this.objs[0]。

    // function $d(...){
    。。。。。
      			case '.':
                    this.objs=getByClass(oParent,selector.substring(1));
                    this.obj=this.objs[0];
    //属性选择器,标签选择器也都这么做。
    

    方法支持群组选择器

    之前的任务中,写了一个each方法,以为addClass和removeClass为例:

    //添加删除css类
    $d.prototype.addClass=function(newClassName){
        each(this.objs,function(item,index){
            item.classList.add(newClassName);
        });
    };
    
    $d.prototype.removeClass=function(oldClassName){
        each(this.objs,function(item,index){
            item.classList.remove(oldClassName);
        });
    };
    

    其它支持群组选择器的统统使用群组遍历的形式添加方法。


    第七章 综合练习

    7.1 兴趣爱好列表

    任务描述

    task0002目录下创建一个task0002_1.html文件,以及一个js目录和css目录,在js目录中创建task0002_1.js,并将之前写的util.js也拷贝到js目录下。然后完成以下需求。

    第一阶段

    在页面中,有一个单行输入框,一个按钮,输入框中用来输入用户的兴趣爱好,允许用户用半角逗号来作为不同爱好的分隔。

    当点击按钮时,把用户输入的兴趣爱好,按照上面所说的分隔符分开后保存到一个数组,过滤掉空的、重复的爱好,在按钮下方创建一个段落显示处理后的爱好。

    第二阶段

    单行变成多行输入框,一个按钮,输入框中用来输入用户的兴趣爱好,允许用户用换行、空格(全角/半角)、逗号(全角/半角)、顿号、分号来作为不同爱好的分隔。

    当点击按钮时的行为同上

    第三阶段

    用户输入的爱好数量不能超过10个,也不能什么都不输入。当发生异常时,在按钮上方显示一段红色的错误提示文字,并且不继续执行后面的行为;当输入正确时,提示文字消失。

    同时,当点击按钮时,不再是输出到一个段落,而是每一个爱好输出成为一个checkbox,爱好内容作为checkbox的label。

    解决思路

    用面向对象的方法来写,构造一个Hobby对象,然后绑定点击方法。

    	<textarea id="text"></textarea>
        <button type="button" id="btn" name="button">获取</button>
    	<span id="validate></span>
        <ol id="list">
    
        </ol>
    
    • 第一阶段:用split转化为一个数组。之前的js库中,已经有了数组去重方法uniqArray(arr)。(参见第一章第三节)正好拿出来用。

      function Hobby(textId,btnId,showerId){
      	this.id={
      		textId:textId,
      		btnId:btnId,
      		showerId:showerId
      	};
      
      }
      
      Hobby.prototype.getHobby=function(){
      	var _this=this;
      	$(_this.id.btnId).click(function(){
      		_this.text=$(_this.id.textId).obj.value;
      		_this.hobbyList=_this.text.split(',');
      		_this.newHobbyList=uniqArray(_this.hobbyList).filter(function(item){
      			return item!=='';
      		});
      		var content='';
      		_this.newHobbyList.forEach(function(item,index){
      			content+='<li>'+item+'</li>';
      		});
      		$(_this.id.showerId).obj.innerHTML=content;
      	});
      };
      

      window.onload=function(){
      var hobby=new Hobby('#text','#btn','#list');
      hobby.getHobby();

      };

      
      
    • 第二阶段:添加规则

      这一步无非是添加了多一个正则

      	//换行、空格(全角/半角)、逗号(全角/半角)、顿号、分号
      	this.re=/
      |s|\,|,|、|;|;/g;
      

      text用replace转化为半角逗号,然后再处理

    • 第三阶段:表单验证

      要求表单验证是实时的,那么面向对象的优势就出来了。就用keyup事件来更新Hobby对象中的数据吧!然后把验证的结果存入hobby.check中,点击之后如果校验不通过,也不会进行下一步操作。点击时获取的数据就不用再写了

    最后的代码是

    function Hobby(textId,btnId,showerId){
    	this.id={
    		textId:textId,
    		btnId:btnId,
    		showerId:showerId
    	};
    	//换行、空格(全角/半角)、逗号(全角/半角)、顿号、分号
    	this.re=/
    |s|\,|,|、|;|;/g;
    	this.check=false;
    }
    
    Hobby.prototype.getHobby=function(){
    	var _this=this;
    	$(_this.id.btnId).click(function(){
    		var content='';
    
    		//点击表单校验
    		if(!this.check){
    			return false;
    		}else{
    			_this.newHobbyList.forEach(function(item,index){
    				content+='<li>'+item+'</li>';
    			});
    		}
    
    		$(_this.id.showerId).obj.innerHTML=content;
    
    	});
    };
    Hobby.prototype.validate=function(validateId){
    	this.id.validateId=validateId;
    	var _this=this;
    	//通过keyUp获取实时数据
    	$(this.id.textId).on('keyup',function(){
    		console.log(_this);
    		_this.text=$(_this.id.textId).obj.value;
    		_this.hobbyList=_this.text.replace(_this.re,',').split(',');
    		_this.newHobbyList=uniqArray(_this.hobbyList).filter(function(item){
    			return item!=='';
    		});
    
    		var tips='';
    		//表单校验
    		if(_this.newHobbyList.length>10||_this.newHobbyList.length===0){
    			tips='不合法的数据!';
    			$(_this.id.validateId).obj.style.color='red';
    			_this.check=false;
    		}else{
    			tips='';
    			_this.check=true;
    		}
    
    		$(_this.id.validateId).obj.innerText=tips;
    	});
    };
    
    window.onload=function(){
    	var hobby=new Hobby('#text','#btn','#list');
    	hobby.getHobby();
    	hobby.validate('#validate');
    };
    

    7.2 倒计时

    任务描述

    在和上一任务同一目录下面创建一个task0002_2.html文件,在js目录中创建task0002_2.js,并在其中编码,实现一个倒计时功能。

    • 界面首先有一个文本输入框,允许按照特定的格式YYYY-MM-DD输入年月日;
    • 输入框旁有一个按钮,点击按钮后,计算当前距离输入的日期的00:00:00有多少时间差
    • 在页面中显示,距离YYYY年MM月DD日还有XX天XX小时XX分XX秒
    • 每一秒钟更新倒计时上显示的数
    • 如果时差为0,则倒计时停止

    解决思路

    先把html写出来吧!

    	<input type="text" id="text"><button id="get">get</button><br>
        <span>距离 <span id="futrue"></span> 还有 <span id="day"></span> 天 <span id="hours"></span> 小时 <span id="min"></span> 分 <span id="sec"></span>秒</span>
    

    解决这个问题主要在于计算倒计时方法。

    第一个注意的地方是,设置未来时间时,月份需要在原基础上减去1。

    var 未来=new Date(年份,月份-1,日期);
    

    接下来创建一个现在的时间,用未来减去现在,令结果为countDown,它一个毫秒差值。

    	var day=Math.floor(countDown/1000/60/60/24);
    	var hr=Math.floor(countDown/1000/60/60)%24;
    	var min=Math.floor(countDown/1000/60)%60;
    	var sec=Math.floor(countDown/1000)%60;
    

    这样就算出来了。

    然后就是写定时器,每秒刷新一次。注意每次点击后第一件事就是清除定时器。

    function Countdown(futrue){
    	this.now=new Date();
    	var timeList=futrue.split('-');
    
    	this.futrue={
    		year:timeList[0],
    		month:timeList[1],
    		date:timeList[2]
    	};
    
    }
    
    Countdown.prototype.getFutrue=function(){
    	$('#futrue').obj.innerText=this.futrue.year+'年'+this.futrue.month+'月'+this.futrue.date+'日';
    };
    
    Countdown.prototype.getCount=function(){
    	var countDown=this.futrue.computedFutrue-this.now;
    	if(countDown<0){
    		$('#day').obj.innerHTML=0;
    		$('#hours').obj.innerHTML=0;
    		$('#min').obj.innerHTML=0;
    		$('#sec').obj.innerHTML=0;
    		return false;
    	}
    
    	this.countDown={
    		day:Math.floor(countDown/1000/60/60/24),
    		hr:Math.floor(countDown/1000/60/60)%24,
    		min:Math.floor(countDown/1000/60)%60,
    		sec:Math.floor(countDown/1000)%60
    	};
    
    	$('#day').obj.innerHTML = this.countDown.day;
    	$('#hours').obj.innerHTML = this.countDown.hr;
    	$('#min').obj.innerHTML = this.countDown.min;
    	$('#sec').obj.innerHTML = this.countDown.sec;
    };
    
    window.onload=function(){
    	$('#get').click(function(){
    		clearInterval(this.timer);
    		var futrue=$('#text').obj.value;
    
    		this.timer=setInterval(function(){
    			var countdown=new Countdown(futrue);
    			countdown.getFutrue();
    			countdown.getCount();
    		},1000);
    	});
    };
    
    

    再改改硬编码部分,那么任务就算完成了。

    7.3 轮播图

    任务描述

    在和上一任务同一目录下面创建一个task0002_3.html文件,在js目录中创建task0002_3.js,并在其中编码,实现一个轮播图的功能。

    • 图片数量及URL均在HTML中写好
    • 可以配置轮播的顺序(正序、逆序)、是否循环、间隔时长
    • 图片切换的动画要流畅
    • 在轮播图下方自动生成对应图片的小点,点击小点,轮播图自动动画切换到对应的图片

    效果示例:http://echarts.baidu.com/ 上面的轮播图(不需要做左右两个箭头)

    解决思路

    对此我只想感叹选项卡轮播图真乃DOM必做的范例。

    首先需要明确需求:

    • 动画切换看起来应该是说无缝滚动,那么需要写一个运动框架。
    • 轮播图需要一个index方法和eq方法。这是轮播图的核心
    • 点击时,可以使用事件代理
    • 配置自动播放的参数,因此最好是用面向对象的思路来写。
    运动框架

    先看运动框架,在之前的笔记里写了一个所谓完美运动框架,现在需要把它封装为$d的方法。

    function getStyle(obj,attr){
        if(obj.crrentStyle){
            return obj.currentStyle[attr];
            //兼容IE8以下
        }else{
            return getComputedStyle(obj,false)[attr];
            //参数false已废。照用就好
        }
    }
    
    $d.prototype.move=function(obj,json,fn){
        var obj=this.obj;
        //清理定时器
        if(obj.timer){
            clearInterval(obj.timer);
        }
    
        obj.timer=setInterval(function(){
            var bStop=false;//如果为false就停了定时器!
            var iCur=0;
            // 处理属性值
            for(var attr in json){
    
    
                if(attr=='opacity'){
                    iCur=parseInt(parseFloat(getStyle(obj,attr))*100);
                }else{
                    iCur=parseInt(getStyle(obj,attr));
                }
    
                //定义速度值
                var iSpeed=(json[attr]-iCur)/8;
                iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed);
    
                //检测停止:如果我发现某个值不等于目标点bStop就不能为true。
                if(iCur!==json[attr]){
                    bStop=false;
                }
    
                if(attr=='opacity'){
                    obj.style[attr]=(iCur+iSpeed)/100;
                    obj.style.filter='alpha(opacity:'+(iCur+iSpeed)+')';
                }else{
                    obj.style[attr]=iCur+iSpeed+'px';
                }
            }
    
            //检测是否停止,是的话关掉定时器
            if(bStop===true){
                if(iCur==json[attr]){
                    clearInterval(obj.timer);
                    if(fn){
                        fn();
                    }
                }
            }
            
        },30);
    }
    
    index方法

    接下来写一个$d的index方法。获取一组同辈元素内,某元素的索引值。

    $d.prototype.index=function(){
        var obj=this.obj;
        var aBrother=obj.parentNode.children;
        var i=0;
    
        for(i=0;i<aBrother.length;i++){
            if(aBrother[i]==obj){
                return i;
            }
        }
    };
    
    eq方法

    然后来写这个eq方法

    $d.prototype.eq=function(n){
        return $(this.objs[n]);
    };
    
    轮播图

    好了。准备工作搞定,就来写这个轮播图。

    *{
    	margin:0;
    	padding:0;
    }
    ul li{
    	list-style: none;
    }
    
    #tab{
    	 400px;
    	height: 300px;
    	margin:200px auto;
    	position: relative;
    }
    
    #list{
    	 400px;
    	height: 1204px;
    }
    #list li{
    	height: 300px;
    }
    #list img{
    	 400px;
    	height: 300px
    }
    
    #btns{
    	position: absolute;
    	left: 40px;
    	bottom:10px;
    	z-index: 999;
    }
    
    #btns li {
    	 30px;
    	height: 30px;
    	float: left;
    	margin-left: 20px;
    	border-radius: 50%;
    	background: rgba(0, 0, 0, 0.5);
    	cursor: pointer;
    
    }
    
    #btns .active{
    	background: red;
    }
    #tab{
    	position: relative;
    	 400px;
    	height: 300px;
    	overflow: hidden;
    }
    #list{
    	position: absolute;
    }
    
    

    html结构

    <div id="tab">
            <ul id="btns">
                <li></li>
                <li></li>
                <li></li>
                <li></li>
            </ul>
            <div id="wrap">
                <ul id="list">
                    <li class="imgs"><img src="images/1.jpg"></li>
                    <li class="imgs"><img src="images/2.jpg"></li>
                    <li class="imgs"><img src="images/3.jpg"></li>
                    <li class="imgs"><img src="images/4.jpg"></li>
                </ul>
            </div>
        </div>
    

    点击按钮,要求移动一个图片的高度。

    只做选项卡的话,很快就出来效果了:

    $(function(){
    	$('#btns').delegate('li','click',function(){
    		$('#btns li').removeClass('active');
    		$(this).addClass('active');
    
    		var index=$(this).index();
    		var height=parseInt(getStyle($('#list li').obj,"height"));
    
    		$('#list').move({
    			'top':-height*index,
    		});
    	});
    });
    

    但是我们要用面向对象的方法来做:

    
    
    $(function(){
    	function Tab(option){
    		//console.log(option.bLoop)
    		//设置顺序
    		if(option&&option.order=='-'){
    			this.order=1;
    			this.iNow=3;
    			this.start=3;
    			this.end=0;
    
    		}else{
    			this.order=-1;
    			this.iNow=0;
    			this.start=0;
    			this.end=3;
    		}
    
    		//设置延迟时间
    		if(option&&option.delay){
    			this.delay=option.delay;
    		}else{
    			this.delay=2000;
    		}
    
    		//循环设置
    		if(option&&option.bLoop=='false'){
    			this.bLoop=false;
    		}else{
    			this.bLoop=true;
    		}
    
    		this.timer=null;
    		this.count=0;
    		this.height=parseInt(getStyle($('#list li').obj,"height"));
    		//页面初始化设置
    		$('#btns li').eq(this.iNow).addClass('active');
    		$('#list').obj.style.top=-this.height*this.iNow+'px';
    
    	}
    
    
    	Tab.prototype.tab=function(){
    		var _this=this;
    		$('#btns li').removeClass('active');
    		$('#btns li').eq(_this.iNow).addClass('active');
    		$('#list').move({
    			'top':-_this.height*_this.iNow,
    		});
    	};
    
    
    
    	Tab.prototype.timerInner=function(){
    		this.iNow-=this.order;
    		if(this.iNow==this.end-this.order){
    			this.iNow=this.start;
    			this.tab();
    			if(!this.bLoop){
    				//不循环则停止定时器!
    				clearInterval(this.timer);
    			}
    		}else{
    			this.tab();
    		}
    	};
    
    	Tab.prototype.move=function(){
    		var _this=this;
    
    		$('#btns').delegate('li','click',function(){
    			_this.iNow=$(this).index();
    			_this.tab();
    		});
    
    		_this.timer=setInterval(function(){
    			return _this.timerInner();
    		},_this.delay);
    
    		$('#tab').on('mouseover',function(){
    			clearInterval(_this.timer);
    		});
    
    		$('#tab').on('mouseout',function(){
    			_this.timer=setInterval(function(){
    				return _this.timerInner();
    			},_this.delay);
    		});
    	};
    
    	var _tab=new Tab({
    		delay:1000,
    		order:'-',
    		bLoop:'false'
    	});
    
    	_tab.move();
    });
    
    

    放个效果吧:

    7.4 输入提示框

    任务需求

    在和上一任务同一目录下面创建一个task0002_4.html文件,在js目录中创建task0002_4.js,并在其中编码,实现一个类似百度搜索框的输入提示的功能。

    要求如下:

    • 允许使用鼠标点击选中提示栏中的某个选项

    • 允许使用键盘上下键来选中提示栏中的某个选项,回车确认选中

    • 选中后,提示内容变更到输入框中

    • 自己搭建一个后端Server,使用Ajax来获取提示数据

    示例:

    示例

    解决思路

    html结构:

    	<input type="text" id="text">
        <div id="tips">
    		<ul id=ul1></ul>
    	</div>
    

    通过ajax方法通过GET请求获取基本数据,然后根据输入内容在数据中查找。

    基本框架应该是:

    $(function(){
    	$().ajax('server.json',{
    		type:"GET",
    		faild:function(status){
    			console.log(status);
    		},
    		success:function(data){
    			//console.log(data);
    			//主要内容
    		}
    	});
    });
    

    然后在根文件夹下建立一个"server.json"文件夹,存放自己做出来的数据

    [
    	{
    		"id":1,
    		"content":"阿姆斯特朗回旋加速喷气式阿姆斯特朗炮"
    	},
    
    	{
    		"id":2,
    		"content":"阿森纳"
    	},
    
    	{
    		"id":3,
    		"content":"阿斯顿维拉"
    	},
    	{
    		"id":4,
    		"content":"阿姆斯特丹"
    	}
    ]
    

    在服务器环境下测试,可以拿到数据。

    但是拿到的是一个字符串,而不是数组。那就用eval方法转一下吧!

    success:function(data){
    			//console.log(data);
    			data=eval(data);
    
    			//主要内容
    			$('#text').on('keyup',function(){
    				var value=this.value;
    				var arr=[];
    				var str='';
    				data.forEach(function(item,index){
    					if(value!==''&&item.content.indexOf(value)!==-1){
    						str+='<li>'+item.content+'</li>';
    					}
    				});
    
    				$('#ul1').obj.innerHTML=str;
    			});
    
    		}
    

    那么这样基本功能就实现了。

    提示框点选发生keyup时,监控event的内容,比如按上,下时,可以点选提示框内容,注意,此处不是真的要让提示框的内容为focus状态。而是高亮显示就可以了。

    写一个success函数内的全局变量index。默认为0,当执行了点击上下方向键时。#ul内的li高亮显示。再点击回车时,高亮显示的li的内容被打印到文本框中。

    但是又有一个问题。当DOM结构改变时,index值应该初始化为0。DOMCharacterDataModified事件可以监听文本节点发生变化。实现想要的功能:

    $('#ul1').on('DOMCharacterDataModified',function(){
    	index=0;
    });
    

    但是那么好用的事件,居然被废弃了。文档提供了一个官方的对象MutationObserver()。本着简单问题简单处理的思路,只要判断#ul1的innerHTML是否变动就可以了。

    $(function(){
    	$().ajax('server.json',{
    		type:"GET",
    		faild:function(status){
    			console.log(status);
    		},
    		success:function(data){
    			//console.log(data);
    			data=eval(data);
    
    			var index=0;
    			var str='';
    
    			//主要内容
    			$('#text').on('keyup',function(ev){
    				var e=ev||window.event;
    				//console.log(e);
    				var value=this.value;
    				var arr=[];
    				var newStr='';
    
    				data.forEach(function(item,index){
    					if(value!==''&&item.content.indexOf(value)!==-1){
    						arr.push(item.content);
    						newStr+='<li>'+item.content+'</li>';
    					}
    				});
    
    
    				$('#ul1').obj.innerHTML=newStr;
    
    				// 如果不同,就把index设置为0.
    				if(str!==newStr){
    					index=0;
    					str=newStr;
    				}
    
    				// 先判断按下的键是什么,上下回车
    				if(e.code=='ArrowUp'&&$('#ul1 li').eq(index)){
    					index--;
    					if(index<0){
    						index=arr.length-1;
    					}
    
    				}
    				if(e.code=='ArrowDown'&&$('#ul1 li').eq(index)){
    					index++;
    					if(index>arr.length-1){
    						index=0;
    					}
    				}
    
    				if(e.code=='Enter'&&$('#ul1 li').eq(index)){
    					var selector=$('#ul li').eq(index).obj.innerText;
    					this.value=selector;
    					$('#ul1').obj.innerHTML='';
    					return;
    				}
    
    				$('#ul1 li').eq(index).addClass('active');
    			});
    		}
    	});
    });
    
    

    放一个效果吧:

    7.5 界面拖拽交互

    任务需求

    • 实现一个可拖拽交互的界面
    • 如示例图,左右两侧各有一个容器,里面的选项可以通过拖拽来左右移动
    • 被选择拖拽的容器在拖拽过程后,在原容器中消失,跟随鼠标移动
    • 注意拖拽释放后,要添加到准确的位置
    • 拖拽到什么位置认为是可以添加到新容器的规则自己定
    • 注意交互中良好的用户体验和使用引导

    示例

    解决思路

    还是尝试用js的语言来描述需求

    首先是要拖拽。像ps那样。

    其次是拖拽是个模糊位置

    拖的逻辑

    做的是一个绝对定位的元素。通过mousedown事件和mousemove事件实现。

    这是一种很流行的用户界面模式。对于这个效果,也可以考虑把它封装为$djs库的方法。

    $d.prototype.drag=function(){
        each(this.objs,function(item,index){
            drag(item);
        });
    
        function drag(oDiv){//拖拽函数
            oDiv.onmousedown=function (ev){
                var oEvent=ev||event;
                //鼠标位置减去偏移量是鼠标相对于html块级元素的位置
                var disX=oEvent.clientX-oDiv.offsetLeft;
                var disY=oEvent.clientY-oDiv.offsetTop;
    
                document.onmousemove=function (ev){
                    var oEvent=ev||event;
                    // 拖拽时,html实际位置就是鼠标拖拽的位置减去相对位置
                    oDiv.style.left=oEvent.clientX-disX+'px';
                    oDiv.style.top=oEvent.clientY-disY+'px';
                };
    
                document.onmouseup=function (){
                    document.onmousemove=null;
                    document.onmouseup=null;
                };
            };
        }
    };
    

    但是我们发现,需求中的拖拽不是完全自由的。而且完全自由的拖拽在网页中也是不现实的。

    在拖拽之前,它是应该不是绝对定位实现的,一个思路是当鼠标按下,它变为绝对定位,当鼠标松开时,又变为默认的static 定位。同时把之前给这个对象赋予的left和top值恢复到原来的样子(其实就是空字符串)。

    $d.prototype.drag=function(){
        each(this.objs,function(item,index){
            drag(item);
        });
    
        function drag(oDiv){//拖拽函数
            oDiv.onmousedown=function (ev){
                oDiv.style.position='absolute';
                var oEvent=ev||event;
    
                //鼠标位置减去偏移量是鼠标相对于html块级元素的位置
                var disX=oEvent.clientX-oDiv.offsetLeft;
                var disY=oEvent.clientY-oDiv.offsetTop;
    
                document.onmousemove=function (ev){
                    var oEvent=ev||event;
                    // 拖拽时,html实际位置就是鼠标拖拽的位置减去相对位置
                    oDiv.style.left=oEvent.clientX-disX+'px';
                    oDiv.style.top=oEvent.clientY-disY+'px';
                };
    
                document.onmouseup=function (){
                    oDiv.style.position='static';
                    oDiv.style.left='';
                    oDiv.style.top='';
    
                    document.onmousemove=null;
                    document.onmouseup=null;
                };
            };
        }
    };
    
    基本框架

    先把结构写出来。

    <ul id="ul1">
            <li>阿姆斯特朗炮</li>
            <li>阿姆斯特丹</li>
        </ul>
    
        <ul id="ul2">
            <li>阿姆</li>
            <li>阿姆斯壮</li>
        </ul>
    

    css样式

    *{
    	margin:0;
    	padding: 0;
    }
    ul li{
    	list-style: none;
    	font-size: 30px;
    	text-align: center;
    	color: #fff;
    }
    
    #ul1{
    	position: relative;
    	float: left;
    	 auto;
    	height: 400px;
    	border: 1px solid #ccc;
    }
    #ul1 li{
    	margin-bottom: 2px;
    	 200px;
    	height: 50px;
    	background: red;
    }
    
    #ul2{
    	position: relative;
    	float: left;
    	margin-left: 200px;
    	height: 400px;
    	border: 1px solid #ccc;
    }
    #ul2 li{
    	margin-bottom: 2px;
    	 200px;
    	height: 50px;
    	background: blue;
    }
    

    接下来js部分两行代码就搞定了:

    $(function(){
    	$('#ul1 li').drag();
    	$('#ul2 li').drag();
    });
    
    放的逻辑

    当鼠标指针进入到指定区域(比如从#ul1移动到#ul2)后,松开鼠标,立刻从原来的区域复制一个节点,添加到新的区域中,并从原来的区域删除该节点。

    既然有了拖放的目标,所以drag方法必须接受一个id字符串参数。比如$(#ul1 li).drag('#ul2')——这样当鼠标松开,clientX和clientY的坐标在#ul2的范围内时就触发DOM改变。

    document.onmouseup=function (ev){
                    var oEvent=ev||window.event;
                    var x=oEvent.clientX;
                    var y=oEvent.clientY;
                    var l=$(id).obj.offsetLeft;
                    var r=parseInt(getStyle($(id).obj,'width'))+l;
                    var t=$(id).obj.offsetTop;
                    var b=parseInt(getStyle($(id).obj,'height'))+t;
    
                    if(x>l&&x<r&&y>t&&y<b){
                        console.log(1);
                    }
                    oDiv.style.position='static';
                    oDiv.style.left='';
                    oDiv.style.top='';
    
                    document.onmousemove=null;
                    document.onmouseup=null;
                };
    
    DOM操作

    DOM操作及其简单:

    				if(x>l&&x<r&&y>t&&y<b){
                        //console.log(1);
                        $(id).obj.appendChild(oDiv);
                    }
    

    但是问题又来了。当拖过去的li再想拖回来,就不行了。

    证明用$(#ul1 li).drag('#ul2')写成的函数还是有问题。

    有两个思路,一个是把drag作为一个事件,添加事件代理。一个就是监听DOM变动,重新赋值,这里不用担心重复添加事件。在这里为了简单起见采用第二种方法。

    $(function(){
    	$('#ul1 li').drag('#ul2');
    	$('#ul2 li').drag('#ul1');
    
    	// Firefox和Chrome早期版本中带有前缀
    	var 		MutationObserver=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;
    	// 创建观察者对象
    	var observer=new MutationObserver(function(mutations) {
    		$('#ul1 li').drag('#ul2');
        	$('#ul2 li').drag('#ul1');
    	});
    
    	// 配置观察选项:
    	var config = { attributes: true, childList: true, characterData: true };
    	// 传入目标节点和观察选项
    	observer.observe($('#ul1').obj, config);
    
    });
    

    放一个效果吧:

    至此百度前端初级班任务完成。

  • 相关阅读:
    Codeforces Round #609 (Div. 1)
    Codeforces Round #607 (Div. 1)
    Codeforces Round #604 (Div. 1)
    网络流应用
    javaScript遍历数组总结
    JavaScript遍历对象的常见方法
    JS中的可枚举属性与不可枚举属性
    typeScript泛型
    ts中函数重载声明
    ts中的可选参数
  • 原文地址:https://www.cnblogs.com/djtao/p/6347372.html
Copyright © 2020-2023  润新知