广告
返回顶部
首页 > 资讯 > 前端开发 > JavaScript >详解JavaScript面向对象实战之封装拖拽对象
  • 217
分享到

详解JavaScript面向对象实战之封装拖拽对象

2024-04-02 19:04:59 217人浏览 泡泡鱼
摘要

目录概述1、如何让一个DOM元素动起来2、如何获取当前浏览器支持的transfORM兼容写法3、如何获取元素的初始位置5、我们需要用到哪些事件?6、拖拽的原理7、我又来推荐思维导图辅

概述

为了能够帮助大家了解更多的方式与进行对比,我会使用三种不同的方式来实现拖拽。

  • 不封装对象直接实现;
  • 利用原生javascript封装拖拽对象;
  • 通过扩展Jquery来实现拖拽对象。

拖拽的实现过程会涉及到非常多的实用小知识,因此为了巩固我自己的知识积累,也为了大家能够学到更多的知识,我会尽量详细的将一些细节分享出来,相信大家认真阅读之后,一定能学到一些东西。

1、如何让一个DOM元素动起来

我们常常会通过修改元素的top,left,translate来其的位置发生改变。在下面的例子中,每点击一次按钮,对应的元素就会移动5px。大家可点击查看。

点击查看一个让元素动起来的小例子

由于修改一个元素top/left值会引起页面重绘,而translate不会,因此从性能优化上来判断,我们会优先使用translate属性。

2、如何获取当前浏览器支持的transform兼容写法

transform是css3的属性,当我们使用它时就不得不面对兼容性的问题。不同版本浏览器的兼容写法大致有如下几种:

['transform', 'WEBkitTransform', 'MozTransform', 'msTransform', 'OTransform']

因此我们需要判断当前浏览器环境支持的transform属性是哪一种,方法如下:


// 获取当前浏览器支持的transform兼容写法
function getTransform() {
    var transform = '',
    divStyle = document.createElement('div').style,
    // 可能涉及到的几种兼容性写法,通过循环找出浏览器识别的那一个
    transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'],

    i = 0,
    len = transformArr.length;

    for(; i < len; i++)  {
        if(transformArr[i] in divStyle) {
            // 找到之后立即返回,结束函数
            return transform = transformArr[i];
        }
    }

    // 如果没有找到,就直接返回空字符串
    return transform;
}

该方法用于获取浏览器支持的transform属性。如果返回的为空字符串,则表示当前浏览器并不支持transform,这个时候我们就需要使用left,top值来改变元素的位置。如果支持,就改变transform的值。

3、如何获取元素的初始位置

我们首先需要获取到目标元素的初始位置,因此这里我们需要一个专门用来获取元素样式的功能函数。

但是获取元素样式在IE浏览器与其他浏览器有一些不同,因此我们需要一个兼容性的写法。


function getStyle(elem, property) {
    // ie通过currentStyle来获取元素的样式,其他浏览器通过getComputedStyle来获取
    return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(elem, false)[property] : elem.currentStyle[property];
}

有了这个方法之后,就可以开始动手写获取目标元素初始位置的方法了。


function getTargetPos(elem) {
    var pos = {x: 0, y: 0};
    var transform = getTransform();
    if(transform) {
        var transformValue = getStyle(elem, transform);
        if(transformValue == 'none') {
            elem.style[transform] = 'translate(0, 0)';
            return pos;
        } else {
            var temp = transformValue.match(/-?\d+/g);
            return pos = {
                x: parseInt(temp[4].trim()),
                y: parseInt(temp[5].trim())
            }
        }
    } else {
        if(getStyle(elem, 'position') == 'static') {
            elem.style.position = 'relative';
            return pos;
        } else {
            var x = parseInt(getStyle(elem, 'left') ? getStyle(elem, 'left') : 0);
            var y = parseInt(getStyle(elem, 'top') ? getStyle(elem, 'top') : 0);
            return pos = {
                x: x,
                y: y
            }
        }
    }
}

在拖拽过程中,我们需要不停的设置目标元素的新位置,这样它才会移动起来,因此我们需要一个设置目标元素位置的方法。


// pos = { x: 200, y: 100 }
function setTargetPos(elem, pos) {
    var transform = getTransform();
    if(transform) {
        elem.style[transform] = 'translate('+ pos.x +'px, '+ pos.y +'px)';
    } else {
        elem.style.left = pos.x + 'px';
        elem.style.top = pos.y + 'px';
    }
    return elem;
}

5、我们需要用到哪些事件?

在pc上的浏览器中,结合mousedown、mousemove、mouseup这三个事件可以帮助我们实现拖拽。

  • mousedown鼠标按下时触发
  • mousemove鼠标按下后拖动时触发
  • mouseup鼠标松开时触发

而在移动端,分别与之对应的则是touchstart、touchmove、touchend。

当我们将元素绑定这些事件时,有一个事件对象将会作为参数传递给回调函数,通过事件对象,我们可以获取到当前鼠标的精确位置,鼠标位置信息是实现拖拽的关键。

事件对象十分重要,其中包含了非常多的有用的信息,这里我就不扩展了,大家可以在函数中将事件对象打印出来查看其中的具体属性,这个方法对于记不清事件对象重要属性的童鞋非常有用。

6、拖拽的原理

当事件触发时,我们可以通过事件对象获取到鼠标的精切位置。这是实现拖拽的关键。当鼠标按下(mousedown触发)时,我们需要记住鼠标的初始位置与目标元素的初始位置,我们的目标就是实现当鼠标移动时,目标元素也跟着移动,根据常理我们可以得出如下关系:

移动后的鼠标位置 - 鼠标初始位置 = 移动后的目标元素位置 - 目标元素的初始位置

如果鼠标位置的差值我们用dis来表示,那么目标元素的位置就等于:

移动后目标元素的位置 = dis + 目标元素的初始位置

通过事件对象,我们可以精确的知道鼠标的当前位置,因此当鼠标拖动(mousemove)时,我们可以不停的计算出鼠标移动的差值,以此来求出目标元素的当前位置。这个过程,就实现了拖拽。

而在鼠标松开(mouseup)结束拖拽时,我们需要处理一些收尾工作。详情见代码。

7、 我又来推荐思维导图辅助写代码了

常常有新人朋友跑来问我,如果逻辑思维能力不强,能不能写代码做前端。我的答案是:能。因为借助思维导图,可以很轻松的弥补逻辑的短板。而且比在自己头脑中脑补逻辑更加清晰明了,不易出错。

上面第六点我介绍了原理,因此如何做就显得不是那么难了,而具体的步骤,则在下面的思维导图中明确给出,我们只需要按照这个步骤来写代码即可,试试看,一定很轻松。

使用思维导图清晰的表达出整个拖拽过程我们需要干的事情

8、代码实现

part1、准备工作


// 获取目标元素对象
var oElem = document.getElementById('target');

// 声明2个变量用来保存鼠标初始位置的x,y坐标
var startX = 0;
var startY = 0;

// 声明2个变量用来保存目标元素初始位置的x,y坐标
var sourceX = 0;
var sourceY = 0;

part2、功能函数

因为之前已经贴过代码,就不再重复


// 获取当前浏览器支持的transform兼容写法
function getTransform() {}

// 获取元素属性
function getStyle(elem, property) {}

// 获取元素的初始位置
function getTargetPos(elem) {}

// 设置元素的初始位置
function setTargetPos(elem, potions) {}

part3、声明三个事件的回调函数

这三个方法就是实现拖拽的核心所在,我将严格按照上面思维导图中的步骤来完成我们的代码。


// 绑定在mousedown上的回调,event为传入的事件对象
function start(event) {
    // 获取鼠标初始位置
    startX = event.pageX;
    startY = event.pageY;

    // 获取元素初始位置
    var pos = getTargetPos(oElem);

    sourceX = pos.x;
    sourceY = pos.y;

    // 绑定
    document.addEventListener('mousemove', move, false);
    document.addEventListener('mouseup', end, false);
}

function move(event) {
    // 获取鼠标当前位置
    var currentX = event.pageX;
    var currentY = event.pageY;

    // 计算差值
    var distanceX = currentX - startX;
    var distanceY = currentY - startY;

    // 计算并设置元素当前位置
    setTargetPos(oElem, {
        x: (sourceX + distanceX).toFixed(),
        y: (sourceY + distanceY).toFixed()
    })
}

function end(event) {
    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', end);
    // do other things
}

OK,一个简单的拖拽,就这样愉快的实现了。点击下面的链接,可以在线查看该例子的demo。

使用原生js实现拖拽

9、封装拖拽对象

我们来将上面实现的拖拽封装为一个拖拽对象。我们的目标是,只要我们声明一个拖拽实例,那么传入的目标元素将自动具备可以被拖拽的功能。

在实际开发中,一个对象我们常常会单独放在一个js文件中,这个js文件将单独作为一个模块,利用各种模块的方式组织起来使用。当然这里没有复杂的模块交互,因为这个例子,我们只需要一个模块即可。

为了避免变量污染,我们需要将模块放置于一个函数自执行方式模拟的块级作用域中。


(function() {
    ...
})();

在普通的模块组织中,我们只是单纯的将许多js文件压缩成为一个js文件,因此此处的第一个分号则是为了防止上一个模块的结尾不用分号导致报错。必不可少。当然在通过require或者es6模块等方式就不会出现这样的情况。

我们知道,在封装一个对象的时候,我们可以将属性与方法放置于构造函数或者原型中,而在增加了自执行函数之后,我们又可以将属性和方法防止与模块的内部作用域。这是闭包的知识。

那么我们面临的挑战就在于,如何合理的处理属性与方法的位置。

当然,每一个对象的情况都不一样,不能一概而论,我们需要清晰的知道这三种位置的特性才能做出最适合的决定。

  • 构造函数中: 属性与方法为当前实例单独拥有,只能被当前实例访问,并且每声明一个实例,其中的方法都会被重新创建一次。
  • 原型中: 属性与方法为所有实例共同拥有,可以被所有实例访问,新声明实例不会重复创建方法。
  • 模块作用域中:属性和方法不能被任何实例访问,但是能被内部方法访问,新声明的实例,不会重复创建相同的方法。

对于方法的判断比较简单。

因为在构造函数中的方法总会在声明一个新的实例时被重复创建,因此我们声明的方法都尽量避免出现在构造函数中。

而如果你的方法中需要用到构造函数中的变量,或者想要公开,那就需要放在原型中。

如果方法需要私有不被外界访问,那么就放置在模块作用域中。

对于属性放置于什么位置有的时候很难做出正确的判断,因此我很难给出一个准确的定义告诉你什么属性一定要放在什么位置,这需要在实际开发中不断的总结经验。但是总的来说,仍然要结合这三个位置的特性来做出最合适的判断。

如果属性值只能被实例单独拥有,比如person对象的name,只能属于某一个person实例,又比如这里拖拽对象中,某一个元素的初始位置,也仅仅只是这个元素的当前位置,这个属性,则适合放在构造函数中。

而如果一个属性仅仅供内部方法访问,这个属性就适合放在模块作用域中。

关于面向对象,上面的几点思考我认为是这篇文章最值得认真思考的精华。如果在封装时没有思考清楚,很可能会遇到很多你意想不到的bug,所以建议大家结合自己的开发经验,多多思考,总结出自己的观点。

根据这些思考,大家可以自己尝试封装一下。然后与我的做一些对比,看看我们的想法有什么不同,在下面例子的注释中,我将自己的想法表达出来。

点击查看已经封装好的demo

js 源码


(function() {
    // 这是一个私有属性,不需要被实例访问
    var transform = getTransform();

    function Drag(selector) {
        // 放在构造函数中的属性,都是属于每一个实例单独拥有
        this.elem = typeof selector == 'Object' ? selector : document.getElementById(selector);
        this.startX = 0;
        this.startY = 0;
        this.sourceX = 0;
        this.sourceY = 0;

        this.init();
    }


    // 原型
    Drag.prototype = {
        constructor: Drag,

        init: function() {
            // 初始时需要做些什么事情
            this.setDrag();
        },

        // 稍作改造,仅用于获取当前元素的属性,类似于getName
        getStyle: function(property) {
            return document.defaultView.getComputedStyle ? document.defaultView.getComputedStyle(this.elem, false)[property] : this.elem.currentStyle[property];
        },

        // 用来获取当前元素的位置信息,注意与之前的不同之处
        getPosition: function() {
            var pos = {x: 0, y: 0};
            if(transform) {
                var transformValue = this.getStyle(transform);
                if(transformValue == 'none') {
                    this.elem.style[transform] = 'translate(0, 0)';
                } else {
                    var temp = transformValue.match(/-?\d+/g);
                    pos = {
                        x: parseInt(temp[4].trim()),
                        y: parseInt(temp[5].trim())
                    }
                }
            } else {
                if(this.getStyle('position') == 'static') {
                    this.elem.style.position = 'relative';
                } else {
                    pos = {
                        x: parseInt(this.getStyle('left') ? this.getStyle('left') : 0),
                        y: parseInt(this.getStyle('top') ? this.getStyle('top') : 0)
                    }
                }
            }

            return pos;
        },

        // 用来设置当前元素的位置
        setPostion: function(pos) {
            if(transform) {
                this.elem.style[transform] = 'translate('+ pos.x +'px, '+ pos.y +'px)';
            } else {
                this.elem.style.left = pos.x + 'px';
                this.elem.style.top = pos.y + 'px';
            }
        },

        // 该方法用来绑定事件
        setDrag: function() {
            var self = this;
            this.elem.addEventListener('mousedown', start, false);
            function start(event) {
                self.startX = event.pageX;
                self.startY = event.pageY;

                var pos = self.getPosition();

                self.sourceX = pos.x;
                self.sourceY = pos.y;

                document.addEventListener('mousemove', move, false);
                document.addEventListener('mouseup', end, false);
            }

            function move(event) {
                var currentX = event.pageX;
                var currentY = event.pageY;

                var distanceX = currentX - self.startX;
                var distanceY = currentY - self.startY;

                self.setPostion({
                    x: (self.sourceX + distanceX).toFixed(),
                    y: (self.sourceY + distanceY).toFixed()
                })
            }

            function end(event) {
                document.removeEventListener('mousemove', move);
                document.removeEventListener('mouseup', end);
                // do other things
            }
        }
    }

    // 私有方法,仅仅用来获取transform的兼容写法
    function getTransform() {
        var transform = '',
            divStyle = document.createElement('div').style,
            transformArr = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'],

            i = 0,
            len = transformArr.length;

        for(; i < len; i++)  {
            if(transformArr[i] in divStyle) {
                return transform = transformArr[i];
            }
        }

        return transform;
    }

    // 一种对外暴露的方式
    window.Drag = Drag;
})();

// 使用:声明2个拖拽实例
new Drag('target');
new Drag('target2');

这样一个拖拽对象就封装完毕了。

建议大家根据我提供的思维方式,多多尝试封装一些组件。比如封装一个弹窗,封装一个循环轮播等。练得多了,面向对象就不再是问题了。这种思维方式,在未来任何时候都是能够用到的。

以上就是详解JavaScript面向对象实战之封装拖拽对象的详细内容,更多关于JS 面向对象 如何实现封装拖拽对象的资料请关注编程网其它相关文章!

--结束END--

本文标题: 详解JavaScript面向对象实战之封装拖拽对象

本文链接: https://www.lsjlt.com/news/127970.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 详解JavaScript面向对象实战之封装拖拽对象
    目录概述1、如何让一个DOM元素动起来2、如何获取当前浏览器支持的transform兼容写法3、如何获取元素的初始位置5、我们需要用到哪些事件?6、拖拽的原理7、我又来推荐思维导图辅...
    99+
    2022-11-12
  • python 面向对象之class和封装
    # 封装 # Python并没有真正的私有化支持,但可用下划线得到伪私有 访问私有变量:实例._类名__变量名 访问私有方法:实例._类名__方法名() class Wife02(...
    99+
    2022-11-12
  • Java 面向对象 之 封装方法
    转载于 : http://www.verejava.com/id=16992728331734 public class Encapsulation {public static&...
    99+
    2023-06-02
  • PHP面向对象之封装,继承与多态详解
    在普通的编程中,没有涉及架构或者良好的设计,绝大多数都是使用的面向过程的方式。 当编程逐步深入后,就需要合理使用面向对象的知识来设计程序,而不是简单地脑海里有了思路就去写代码来实现,...
    99+
    2022-11-13
  • Java全面分析面向对象之封装
    目录什么是封装呢封装的好处意义getter方法和setter方法toString方法面向对象封装之包自定义包什么是封装呢 封装就是一种将数据和操作数据的方法进行有机结合,一种函数抽象...
    99+
    2022-11-13
  • Python面向对象中的封装详情
    目录一 封装的概念二 _ 和__ 对属性和方法的私有化1. 单下划线_2. 双下划线__3. 子类中访问父类的私有属性和私有方法三 访问及修改类的私有属性和私有方法1. 自定义公有方...
    99+
    2022-11-13
  • C++深入讲解类与对象之OOP面向对象编程与封装
    目录1.面向对象编程2.面向过程性编程和面向对象编程3.类的引入4.类的定义4.1类的两种定义方式4.1.1声明和定义全部放在类体中4.2.2.声明和定义不放在类体中5.类的访问限定...
    99+
    2022-11-13
  • Python面向对象编程之类的封装
    目录1、封装的理解2、私有类属性、公开类属性、私有实例属性和公开实例属性2.1 公开类属性2.2 私有类属性2.3 公开实例属性2.4 私有实例属性2.5 私有属性不一定真的私有3、...
    99+
    2022-11-12
  • C++类和对象之封装详解
    目录封装的意义以及示例访问权限公共权限 public保护权限 protected私有权限 privatestruct 和 class的区别成员属性私有化案例1:设计立方体类案例2:点...
    99+
    2022-11-12
  • Java面向对象类和对象实例详解
    目录1 - Java面向对象学习的三条主线2 - 面向过程与面向对象3 - 面向对象的三大特征4 - 面向对象分析方法分析问题的思路和步骤5 - 面向对象的思想概述6 - 类和对象的...
    99+
    2022-11-13
  • 详细理解JAVA面向对象的封装,继承,多态,抽象
    目录类和对象的使用(面向对象思想落地的实现):子类对象实例化的全过程1.从结果上看:(继承性)2.从过程上来看:1.封装性2.继承性继承性的好处:3.多态性虚拟方法调用4.抽象性1....
    99+
    2022-11-12
  • 详解C语言面向对象编程中的封装
    目录前言一、面向对象基本概念什么是对象?对象与类面向对象的编程方式二、C语言实现面向对象面向对象的三大特征面向对象之封装简介代码实现–基础版代码实现-进阶版总结前言 面向...
    99+
    2022-11-13
  • Java面向对象的封装你了解吗
    目录面向对象思想之封装什么是封装呢?封装的好处意义getter方法和setter方法toString方法总结:面向对象思想之封装 或许大家都听说过java是纯面向对象语言,面向对象思...
    99+
    2022-11-13
  • Python面向对象之模块详解
    目录1.1 导入模块1.1.1 import 模块名1.1.2 from 模块名 import 功能名1.1.3 from …import*1.1.4 as 定义别名1.2 制作模块...
    99+
    2022-11-12
  • javascript面向对象三大特征之封装的示例分析
    这篇文章主要介绍了javascript面向对象三大特征之封装的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。具体如下:封装封装(En...
    99+
    2022-10-19
  • JavaScript面向对象中的封装和继承你了解吗
    目录1、面向对象1、封装2、原型对象3、继承总结1、面向对象 【三大显著特征】: 封装、继承、多态 1、封装 【解释】: 封装的本质就是将有关联的代码组合在一起。...
    99+
    2022-11-13
  • Java面向对象之内部类详解
    目录前言内部类概述内部类的分类成员内部类局部内部类前言 在 Java 中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。本篇博客将总结内部类的使用。 内部类概述...
    99+
    2022-11-13
  • JavaScript面向对象中的封装和继承怎么实现
    这篇“JavaScript面向对象中的封装和继承怎么实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“JavaScript面...
    99+
    2023-06-29
  • Python面向对象编程之封装的艺术你了解吗
    目录1.面向对象编程 1.1OOP特点1.2OOP基本概念2.Python实现OOP2.1分析问题2.2类设计语法2.3创建对象语法3.OOP的封装性3.1广义角度:无处不...
    99+
    2022-11-13
  • JavaScript面向对象编程详细讲解
    这篇文章主要介绍“JavaScript面向对象编程详细讲解”,在日常操作中,相信很多人在JavaScript面向对象编程详细讲解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”...
    99+
    2022-10-19
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作