前端面试押题笔记(1) - HTML,CSS,原生JavaScript

503 阅读11分钟

押题和准备前端面试的笔记,写一点自己的想法,答案或许不完全正确.

HTML

必考:你是如何理解 HTML 语义化的?

语义化也即使用语义化标签,比如 header, nav, article, aside, footer等标签,通俗来说是使用正确的标签做正确的事,使用语义化标签替代单纯的div标签。
语义化不仅方便编程者阅读和理解代码,也方便浏览器解析,尤其在SEO上有较大的作用,有助于搜索引擎的爬虫识别网页各部分的内容,确定关键词。

meta viewport 是做什么用的,怎么写?

<meta> 标签代表 HTML 元数据,这部分数据不会显示在浏览器的视窗之中,但会被浏览器解析。通常用于指定与网页相关的描述,作者,文档修改时间等。
viewport 是 meta name属性的一种值,代表这一元数据标签将规定与浏览器视窗相关的数据。meta viewport 用于设置视窗大小和缩放,主要用于针对移动端网页的优化。

以常见的举例 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

其中 content 有几个常用值:

  • width 即为视窗宽度,device-width 是设备最理想的视窗宽度。令视窗宽度等于设备宽度。

  • initial-scale 代表初始缩放,值为1等同于没有缩放。

  • maximum-scale 代表最大缩放,值为1等同于禁止缩放。

  • user-scalable 代表是否允许用户缩放,既可以接受 yes|no,也可以接受 1|0,值为0或no代表不允许用户进行缩放。

关于为何会有缩放: 屏幕的尺寸各不相同,像素数量也不一样,对于一个750px的屏幕而言,如果我们设定的width=375,那么原本设为1px的图像就会变为2px,反之亦同。

你用过哪些 HTML 5 标签?

比较常用的有新的语义化标签,诸如 <section><article><header><nav>,除此之外还有<video><audio><canvas>

Canvas

虽然目前大部分浏览器都已支持canvas,但一些浏览器的早期版本仍然是不支持的.

创建canvas
<canvas id="canvas"> Not Support Canvas</canvas>

倘若浏览器不支持canvas,则会在原处显示 Not Support Canvas

获取canvas
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
绘制长方形和设定填充颜色
ctx.fillStyle = 'green';
// 左上角定位于 (10,10),宽150,高100
ctx.fillRect(10, 10, 150, 100);
绘制线条
ctx.beginPath() // 开始路径绘制
ctx.moveTo(20, 30) // 设置路径起点,坐标为(20,30)
ctx.lineTo(200, 10) // 设置路径终点坐标为(200,20)
ctx.lineWidth = 10.0 // 设置线宽
ctx.strokeStyle = '#CC0000'; // 设置线的颜色
ctx.stroke() // 对线进行着色,这时整条线就变得可见
图像转绘至画布
let img = new Image()
img.onload = function () {
  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')
  ctx.width = image.width
  ctx.height = image.height
  ctx.drawImage(img, 0, 0) // (0 ,0 )代表图片在画布上的位置
}
img.src = 'image.png'
画布转为图像
let img = new Image()
// toDataURL: 路径,图像质量(仅在jepg和webp有效)
img.src = ctx.toDataURL('image/png', 0.92)
// toBlob
img.src = ctx.toBlob('','image/png', 0.92)

H5 是什么?

H5 也即 HTML5,但在通俗概念中 H5=移动端页面。

由于 HTML5 的诸多新特性,例如本地储存,设备兼容,网页多媒体等。移动端页面不仅“轻”,而且兼容性强,功能丰富,制作便捷,适用于各种场景。不需要固定于一个APP之内,仅靠浏览器就能完成任务

CSS

必考:两种盒模型分别说一下。

两种盒模型 -> CSS 的两种盒模型,分别为 border box 和 content box。

设置方法:CSS中设置

box-sizing: border-box;
box-sizing: content-box;

html 元素通常以“盒”的形式呈现,拥有 margin,border,padding,content。

content box width = content width

border box width = border + padding + content width

此处添加一条关于外边距重叠

当块级元素 (block) 的上外边距 (margin-top) 和下外边距 (margin-bottom) 同时都有设定时只会只会保留最大边距,这种行为称为边界折叠 (margin collapsing),有时也翻译为外边距重叠。

必考:如何垂直居中?

1. 利用 table

  <table class="parent">
    <tr>
      <td class="child">
          content
      </td>
    </tr>
  </table>

2. flex

注意:部分浏览器不支持 flex

<div class="parent">
   <div class="child">
       content
   </div>
</div>
.parent{
  display: flex;
  // 横向居中
  justify-content: center;
  // 竖直居中
  align-items: center;
}
.child{
  border: 3px solid green;
  width: 300px;
}

3. 负 margin

<div class="parent">
   <div class="child">
       content
   </div>
</div>
.parent{
  border: 1px solid red;
  height: 600px;
  
  position: relative;
}
.child{
  border: 1px solid green;
  
  width: 300px;
  height: 100px;
  
  position: absolute;
  top: 50%;
  left: 50%;
  margin-left: -15
  margin-top: -50px;
}
  • 父元素 position: relative
  • 子元素 position: absolute
  • 子元素 top: 50%; left: 50%;
  • 子元素 margin-leftmargin-top 分别为宽、高的一半

4.grid

<div class="grid-container">  
    <div class="placeholder"></div>  
    <div class="center">content</div>  
    <div class="placeholder"></div>
</div>
.grid-container {
    width300px;  
    height300px;  
    display: grid
}

将父容器等分成三块,content 置于中间。

必考:flex 怎么用,常用属性有哪些?

flex 布局

flex 布局也称为弹性盒子,其中元素可以自动膨胀填充额外空间,或收缩以适应更小的空间。

设置flex
display: flex;

display: inline-flex;

设置为弹性盒模型或内联弹性盒模型。

flex 布局有一条主轴和一条交叉轴,主轴代表 flex 元素的排列方向。

flex-direction 可用于指定主轴的方向,row | row-reverse | column | column-reverse

flex-wrap 折行控制,nowrap | warp | wrap-reverse

flex-grow 指定了该项占容器中剩余空间的多少

justify-content 主轴方向的元素排列

justify-content: flex-start; /* <-- 元素靠左排列 */
justify-content: flex-end; /* 元素靠右排列 --> */
justify-content: center; /* 居中排列 */
justify-content: space-between; /* 中间有间隔的均匀分布 */
justify-content: space-around; /* 包围式有间隔均匀分布 */

align-item 交叉轴方向的元素排列

align-item: stretch; /* 在无高度的时候根据内容高度自动拉伸 */
align-item: flex-start; /* 全部向上,贴在顶部*/
align-item: flex-end; /* 全部向下,贴在底部 */
align-item: center; /* 聚在中心 */

必考:BFC 是什么?

BFC(Block Formatting Context),即块格式化上下文。

MDN 的解释简直非人话。

块格式化上下文(Block Formatting Context,BFC) 是 Web 页面的可视化 CSS 渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

BFC 可以被理解为页面上的一个与外界隔离的独立容器,容器内的子元素不会影响容器外的元素。

什么情况下会创建BFC

  • float 不为 none,也即浮动元素

  • overflow 不为 visible 的块元素

  • position 为 absolute 或 fixed

  • display 为 inline-block 或 table-cell

  • display 为 flex/inline-flex(弹性元素) 的直接子元素

  • display 为 grid/inline-grid 的直接子元素

CSS 选择器优先级

内联 > id 选择器 > 类、属性、伪类选择器 > 标签元素、伪元素

-> 后写的会覆盖先写的

-> CSS = 层叠样式表,层叠也有优先级,比如 !important

层叠优先级:CSS 动画 > 页面作者 !important > 用户 !important > 用户代理 !important > CSS 过渡 > 页面作者 > 用户 > 用户代理

清除浮动说一下

当子元素为浮动元素时,创建 .clearfix,添加于父元素class中,清除浮动,使脱离文档流的子元素可被父元素包裹。

在子元素为浮动元素的情况下,父元素高度可能会坍塌,清除浮动可解决这一问题。

.clearfix:after{
     content: '';
     display: block; /*或者 table*/
     clear: both;
 }
 .clearfix{
     zoom: 1; /* IE 兼容*/
 }

原生 JS

必考:ES 6 语法知道哪些,分别怎么用?

作用域

  • 块级作用域,用一对{}声明,可用于组合语句。
  • let,声明块级作用域的本地变量
  • const,块级作用域常量

箭头函数

比传统的函数表达式简洁,没有this, argument和super。不能作为构造函数。

默认参数

函数默认参数允许在没有值或undefined被传入时使用默认形参。

模块: 导入 | 导出 | 默认导出

  • import: 静态的import 语句用于导入由另一个模块导出的绑定。无论是否声明了 strict mode导入的模块都运行在严格模式下
  • export: export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import语句使用它们
  • export default: 导出单个特性作为默认值

展开语法

展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。举例:使用展开语法替代apply将参数传给数组。

必考 Promise、Promise.all、Promise.race 分别怎么用?

Promise 本质是JS异步的另一种写法,用于替代callback令人不甚舒服的书写方式以及异常处理。

Promise 用法

     function fn(){
         return new Promise((resolve, reject)=>{
             成功时调用 resolve(数据)
             失败时调用 reject(错误)
         })
     }
     fn().then(success, fail).then(success2, fail2)

Promise.all 用法

     Promise.all([promise1, promise2]).then(success1, fail1)

promise1和promise2都成功才会调用success1
任意一个失败立即抛出错误。

背代码 Promise.race 用法

     Promise.race([promise1, promise2]).then(success1, fail1)

promise1和promise2只要有一个成功就会调用success1;
promise1和promise2只要有一个失败就会调用fail1;

必考:手写函数防抖和函数节流

函数防抖和函数节流是两种优化高频率执行js代码方法。

JS中的一些事件(浏览器resize, scroll;鼠标mousemove,mouseover; input box keypress)在触发时会反复调用绑在事件上的回调函数,浪费资源,降低性能。因而需要对此类事件进行调用次数的限制

函数防抖

触发事件后,在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。 举例:

  • 搜索框搜索输入。只需要用户最后一次输入完再发送请求
  • 手机号、邮箱格式的输入验证检测
  • 窗口大小的 resize 。只需窗口调整完成后,计算窗口的大小,防止重复渲染。
 // 防抖(一段时间会等,然后带着一起做了)
 function debounce(fn, delay){
     let timerId = null
     return function(){
        // 取debounce执行作用域的this
         const context = this
         if(timerId){window.clearTimeout(timerId)}
         timerId = setTimeout(()=>{
             fn.apply(context, arguments)
             timerId = null
         },delay)
     }
 }
 const debounced = debounce(()=>console.log('hi'))
 debounced()
 debounced()

函数节流

限制一个函数在一定时间内只能执行一次。 举例:

  • 滚动加载,加载更多或滚动到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交
  • 省市信息对应字母快速选择
// 节流(一段时间执行一次之后,就不执行第二次)
 function throttle(fn, delay){
     let canUse = true
     return function(){
         if(canUse){
             fn.apply(this, arguments)
             canUse = false
             setTimeout(()=>canUse = true, delay)
         }
     }
 }

 const throttled = throttle(()=>console.log('hi'))
 throttled()
 throttled()

必考:手写AJAX

 var request = new XMLHttpRequest()
 request.open('GET', '/a/b/c?name=ff', true);
 request.onreadystatechange = function () {
   if(request.readyState === 4 && request.status === 200) {
     console.log(request.responseText);
   }};
 request.send();

必考:这段代码里的 this 是什么?

  1. fn()
    this => window/global
  2. obj.fn()
    this => obj
  3. fn.call(xx)
    this => xx
  4. fn.apply(xx)
    this => xx
  5. fn.bind(xx)
    this => xx
  6. new Fn()
    this => 新的对象
  7. fn = ()=> {}
    this => 外面的 this

必考:闭包/立即执行函数是什么?

闭包 closure

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

简单来说,创造一个封闭的环境(一个结构体),其内部方法可令外层内容被环境之外的读取或操作。

立即执行函数:

字面意思,使用圆括号包裹函数,函数后跟(),形成闭包后立即执行,执行完毕销毁变量,可用于控制变量污染。 举例:

(function(i){
    console.log(i)
})(i)

必考:什么是 JSONP,什么是 CORS,什么是跨域?

JSONP 和 CORS 都是用于解决跨域问题的方法,而跨域本质是由同源策略带来的问题。

同源策略

同源:即同协议,同域名,同端口

浏览器处于安全考量仅允许同源下的接口互相交互。非同源端口想要交互,就需要跨域

JSONP

虽然直接使用不同源的接口会遇到跨域的问题,但<script>标签例外。所有带src的标签都是例外。

  1. JSONP是通过 script 标签加载数据的方式去获取数据当做 JS 代码来执行

  2. 提前在页面上声明一个函数,函数名通过接口传参的方式传给后台,后台解析到函数名后在原始数据上「包裹」这个函数名,发送给前端。换句话说,JSONP 需要对应接口的后端的配合才能实现。

后端接口:php实现

<?php 
//获取回调函数名
header('Content-type: application/json');  
$jsoncallback = htmlspecialchars($_REQUEST ['jsoncallback']); 
//json数据 
$json_data = '["customername1","customername2"]'; 
//输出jsonp格式的数据 
echo $jsoncallback . "(" . $json_data . ")"; 
?>

前端函数

function callbackFunction(result, methodName) { 
    var html = '<ul>'; 
    for(var i = 0; i < result.length; i++) { 
        html += '<li>' + result[i] + '</li>'; 
    } 
    html += '</ul>'; 
    document.getElementById('divCustomers').innerHTML = html; 
}

调用:

<script type="text/javascript" src="https://www.runoob.com/try/ajax/jsonp.php?jsoncallback=callbackFunction"></script>

真正返回到客户端的数据显示为: callbackFunction(["customername1","customername2"])。

CORS(Cross-origin resource sharing)

CORS 分两种请求模式。

简单请求

(1)使用下列方法之一:

  • head
  • get
  • post

(2)请求的Heder是

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。

请求头:

请求头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

响应头:

CORS请求设置的响应头字段,都以 Access-Control-开头:

1)Access-Control-Allow-Origin:必选
它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
2)Access-Control-Allow-Credentials:可选
它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
3)Access-Control-Expose-Headers:可选
CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

预检请求头

预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..
复制代码

1)Access-Control-Request-Method:必选
用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
2)Access-Control-Request-Headers:可选
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

预检响应头:

服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。
HTTP回应中,除了关键的是Access-Control-Allow-Origin字段,其他CORS相关字段如下:
1)Access-Control-Allow-Methods:必选
它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
3)Access-Control-Allow-Credentials:可选
该字段与简单请求时的含义相同。
4)Access-Control-Max-Age:可选
用来指定本次预检请求的有效期,单位为秒。

-> 如果前端是使用axios请求,并且没有自定义请求头则不需要设置任何代理就能正常请求了。如果你使用fetch还需要 mode: 'cors'来允许跨域。

常考:async/await 怎么用,如何捕获异常?

async 和 await

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}

常考:如何实现深拷贝?

粗暴深拷贝

function deepCopy(obj1){
    let _obj = JSON.stringify(obj1);
    let obj2 = JSON.parse(_obj);
    return obj2;
}
var a = [1, [1, 2], 3, 4];
var b = deepCopy(a);
b[1][0] = 2;
alert(a); // 1,1,2,3,4
alert(b); // 2,2,2,3,4

复杂深拷贝

/**
 * 判断是否是基本数据类型
 * @param value 
 */
function isPrimitive(value){
  return (typeof value === 'string' || 
  typeof value === 'number' || 
  typeof value === 'symbol' ||
  typeof value === 'boolean')
}

/**
 * 判断是否是一个js对象
 * @param value 
 */
function isObject(value){
  return Object.prototype.toString.call(value) === "[object Object]"
}

/**
 * 深拷贝一个值
 * @param value 
 */
function cloneDeep(value){

  // 记录被拷贝的值,避免循环引用的出现
  let memo = {};

  function baseClone(value){
    let res;
    // 如果是基本数据类型,则直接返回
    if(isPrimitive(value)){
      return value;
    // 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
    }else if(Array.isArray(value)){
      res = [...value];
    }else if(isObject(value)){
      res = {...value};
    }

    // 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
    Reflect.ownKeys(res).forEach(key=>{
      if(typeof res[key] === "object" && res[key]!== null){
        //此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
        if(memo[res[key]]){
          res[key] = memo[res[key]];
        }else{
          memo[res[key]] = res[key];
          res[key] = baseClone(res[key])
        }
      }
    })
    return res;  
  }

  return baseClone(value)
}

ES插件lodash

import lodash from 'lodash'

var objects = [1,{ 'a': 1 }, { 'b': 2 }]; 
var deep = lodash.cloneDeep(objects);
deep[0] = 2;
deep[1].a = 2;
console.log(objects[0]);//1
console.log(deep[0]);//2
console.log(objects[1].a);//1
console.log(objects[1].a);//2

常考:如何用正则实现 trim()?

String.prototype.trim = function(){
    return this.replace(/^\s+|\s+$/g, '')
}
//或者 
function trim(string){
    return string.replace(/^\s+|\s+$/g, '')
}

常考:不用 class 如何实现继承?用 class 又如何实现?

不用class

 function Animal(color){
     this.color = color
 }
 Animal.prototype.move = function(){} // 动物可以动
 function Dog(color, name){
     Animal.call(this, color) // 或者 Animal.apply(this, arguments)
     this.name = name
 }
 // 下面三行实现 Dog.prototype.__proto__ = Animal.prototype
 function temp(){}
 temp.prototype = Animal.prototype
 Dog.prototype = new temp()

 Dog.prototype.constuctor = Dog // 这行看不懂就算了,面试官也不问
 Dog.prototype.say = function(){ console.log('汪')}

 var dog = new Dog('黄色','阿黄')

用class

 class Animal{
     constructor(color){
         this.color = color
     }
     move(){}
 }
 class Dog extends Animal{
     constructor(color, name){
         super(color)
         this.name = name
     }
     say(){}
 }

常考:如何实现数组去重?

WeakMap

let ob1 = {key: 'key', value: 'value'}
let ob2 = {value: 'value', key: 'key'}
let arr = [ob1,ob1,ob2]
function unique(array) {
  let wm = new WeakMap()
  for(let i = 0; i < array.length; i++){
    if(!wm.has(array[i])){
      wm.set(array[i], i)
    }
  }
  return wm
}
unique(arr)

indexOf

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res
}

放弃:== 相关题目(反着答)

送命题:手写一个 Promise

function Promise(executor) {
    let self = this;
    self.status = 'pending'; //等待态
    self.value = undefined;  //成功的返回值
    self.reason = undefined; //失败的原因

    function resolve(value){
        if(self.status === 'pending'){
            self.status = 'resolved';
            self.value = value;
        }
    }
    function reject(reason) {
        if(self.status === 'pending') {
            self.status = 'rejected';
            self.reason = reason;
        }
    }
    try{
        executor(resolve, reject);
    }catch(e){
        reject(e);// 捕获时发生异常,就直接失败
    }
}
//onFufiled 成功的回调
//onRejected 失败的回调
Promise.prototype.then = function (onFufiled, onRejected) {
    let self = this;
    if(self.status === 'resolved'){
        onFufiled(self.value);
    }
    if(self.status === 'rejected'){
        onRejected(self.reason);
    }
}
module.exports = Promise;

DOM

必考:事件委托

事件模型是指分为三个阶段:

  • 捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件;
  • 目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上;
  • 冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点)
 function delegate(element, eventType, selector, fn) {
     element.addEventListener(eventType, e => {
       let el = e.target
       while (!el.matches(selector)) {
         if (element === el) {
           el = null
           break
         }
         el = el.parentNode
       }
       el && fn.call(el, e, el)
     })
     return element
 }

局限性

比如 focus、blur 之类的事件本身没有事件冒泡机制,所以无法委托;

mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的

曾考:用 mouse 事件写一个可拖曳的 div

<style>
div{
  border: 1px solid red;
  position: absolute;
  top: 0;
  left: 0;
  width: 100px;
  height: 100px;
}
</style>

<div id="xxx"></div>
var dragging = false
var position = null

xxx.addEventListener('mousedown',function(e){
  dragging = true
  position = [e.clientX, e.clientY]
})

document.addEventListener('mousemove', function(e){
  if(dragging === false){return}
  console.log('hi')
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]
  const deltaY = y - position[1]
  const left = parseInt(xxx.style.left || 0)
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function(e){
  dragging = false
})